PDF text generation working so far
This commit is contained in:
121
package-lock.json
generated
121
package-lock.json
generated
@@ -16,6 +16,7 @@
|
||||
"@types/gapi.client.sheets": "^4.0.20201031",
|
||||
"@types/google.accounts": "^0.0.17",
|
||||
"@types/uuid": "^10.0.0",
|
||||
"fontkit": "^2.0.4",
|
||||
"idb": "^8.0.3",
|
||||
"pdf-lib": "^1.17.1",
|
||||
"uuid": "^11.1.0"
|
||||
@@ -1073,6 +1074,21 @@
|
||||
"vite": "^6.3.0 || ^7.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@swc/helpers": {
|
||||
"version": "0.5.17",
|
||||
"resolved": "https://registry.npmjs.org/@swc/helpers/-/helpers-0.5.17.tgz",
|
||||
"integrity": "sha512-5IKx/Y13RsYd+sauPb2x+U/xZikHjolzfuDgTAl/Tdf3Q8rslRvC19NKDLgAJQ6wsqADk10ntlv08nPFw/gO/A==",
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"tslib": "^2.8.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@swc/helpers/node_modules/tslib": {
|
||||
"version": "2.8.1",
|
||||
"resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz",
|
||||
"integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==",
|
||||
"license": "0BSD"
|
||||
},
|
||||
"node_modules/@tailwindcss/node": {
|
||||
"version": "4.1.11",
|
||||
"resolved": "https://registry.npmjs.org/@tailwindcss/node/-/node-4.1.11.tgz",
|
||||
@@ -1662,6 +1678,35 @@
|
||||
"node": ">= 0.4"
|
||||
}
|
||||
},
|
||||
"node_modules/base64-js": {
|
||||
"version": "1.5.1",
|
||||
"resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz",
|
||||
"integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==",
|
||||
"funding": [
|
||||
{
|
||||
"type": "github",
|
||||
"url": "https://github.com/sponsors/feross"
|
||||
},
|
||||
{
|
||||
"type": "patreon",
|
||||
"url": "https://www.patreon.com/feross"
|
||||
},
|
||||
{
|
||||
"type": "consulting",
|
||||
"url": "https://feross.org/support"
|
||||
}
|
||||
],
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/brotli": {
|
||||
"version": "1.3.3",
|
||||
"resolved": "https://registry.npmjs.org/brotli/-/brotli-1.3.3.tgz",
|
||||
"integrity": "sha512-oTKjJdShmDuGW94SyyaoQvAjf30dZaHnjJ8uAF+u2/vGJkJbJPJAT1gDiOJP5v1Zb6f9KEyW/1HpuaWIXtGHPg==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"base64-js": "^1.1.2"
|
||||
}
|
||||
},
|
||||
"node_modules/call-bind-apply-helpers": {
|
||||
"version": "1.0.2",
|
||||
"resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz",
|
||||
@@ -1728,6 +1773,15 @@
|
||||
"wrap-ansi": "^7.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/clone": {
|
||||
"version": "2.1.2",
|
||||
"resolved": "https://registry.npmjs.org/clone/-/clone-2.1.2.tgz",
|
||||
"integrity": "sha512-3Pe/CF1Nn94hyhIYpjtiLhdCoEoz0DqQ+988E9gmeEdQZlojxnOb74wctFyuwWQHzqyf9X7C7MG8juUpqBJT8w==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=0.8"
|
||||
}
|
||||
},
|
||||
"node_modules/clsx": {
|
||||
"version": "2.1.1",
|
||||
"resolved": "https://registry.npmjs.org/clsx/-/clsx-2.1.1.tgz",
|
||||
@@ -1850,6 +1904,12 @@
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/dfa": {
|
||||
"version": "1.2.0",
|
||||
"resolved": "https://registry.npmjs.org/dfa/-/dfa-1.2.0.tgz",
|
||||
"integrity": "sha512-ED3jP8saaweFTjeGX8HQPjeC1YYyZs98jGNZx6IiBvxW7JG5v492kamAQB3m2wop07CvU/RQmzcKr6bgcC5D/Q==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/dunder-proto": {
|
||||
"version": "1.0.1",
|
||||
"resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz",
|
||||
@@ -2004,6 +2064,12 @@
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/fast-deep-equal": {
|
||||
"version": "3.1.3",
|
||||
"resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz",
|
||||
"integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/fdir": {
|
||||
"version": "6.4.6",
|
||||
"resolved": "https://registry.npmjs.org/fdir/-/fdir-6.4.6.tgz",
|
||||
@@ -2019,6 +2085,23 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/fontkit": {
|
||||
"version": "2.0.4",
|
||||
"resolved": "https://registry.npmjs.org/fontkit/-/fontkit-2.0.4.tgz",
|
||||
"integrity": "sha512-syetQadaUEDNdxdugga9CpEYVaQIxOwk7GlwZWWZ19//qW4zE5bknOKeMBDYAASwnpaSHKJITRLMF9m1fp3s6g==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@swc/helpers": "^0.5.12",
|
||||
"brotli": "^1.3.2",
|
||||
"clone": "^2.1.2",
|
||||
"dfa": "^1.2.0",
|
||||
"fast-deep-equal": "^3.1.3",
|
||||
"restructure": "^3.0.0",
|
||||
"tiny-inflate": "^1.0.3",
|
||||
"unicode-properties": "^1.4.0",
|
||||
"unicode-trie": "^2.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/form-data": {
|
||||
"version": "4.0.4",
|
||||
"resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.4.tgz",
|
||||
@@ -2875,6 +2958,12 @@
|
||||
"url": "https://github.com/sponsors/ljharb"
|
||||
}
|
||||
},
|
||||
"node_modules/restructure": {
|
||||
"version": "3.0.2",
|
||||
"resolved": "https://registry.npmjs.org/restructure/-/restructure-3.0.2.tgz",
|
||||
"integrity": "sha512-gSfoiOEA0VPE6Tukkrr7I0RBdE0s7H1eFCDBk05l1KIQT1UIKNc5JZy6jdyW6eYH3aR3g5b3PuL77rq0hvwtAw==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/rollup": {
|
||||
"version": "4.45.1",
|
||||
"resolved": "https://registry.npmjs.org/rollup/-/rollup-4.45.1.tgz",
|
||||
@@ -3147,6 +3236,12 @@
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/tiny-inflate": {
|
||||
"version": "1.0.3",
|
||||
"resolved": "https://registry.npmjs.org/tiny-inflate/-/tiny-inflate-1.0.3.tgz",
|
||||
"integrity": "sha512-pkY1fj1cKHb2seWDy0B16HeWyczlJA9/WW3u3c4z/NiWDsO3DOU5D7nhTLE9CF0yXv/QZFY7sEJmj24dK+Rrqw==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/tinyglobby": {
|
||||
"version": "0.2.14",
|
||||
"resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.14.tgz",
|
||||
@@ -3206,6 +3301,32 @@
|
||||
"integrity": "sha512-9UJ2xGDvQ43tYyVMpuHlsgApydB8ZKfVYTsLDhXkFL/6gfkp+U8xTGdh8pMJv1SpZna0zxG1DwsKZsreLbXBxw==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/unicode-properties": {
|
||||
"version": "1.4.1",
|
||||
"resolved": "https://registry.npmjs.org/unicode-properties/-/unicode-properties-1.4.1.tgz",
|
||||
"integrity": "sha512-CLjCCLQ6UuMxWnbIylkisbRj31qxHPAurvena/0iwSVbQ2G1VY5/HjV0IRabOEbDHlzZlRdCrD4NhB0JtU40Pg==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"base64-js": "^1.3.0",
|
||||
"unicode-trie": "^2.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/unicode-trie": {
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/unicode-trie/-/unicode-trie-2.0.0.tgz",
|
||||
"integrity": "sha512-x7bc76x0bm4prf1VLg79uhAzKw8DVboClSN5VxJuQ+LKDOVEW9CdH+VY7SP+vX7xCYQqzzgQpFqz15zeLvAtZQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"pako": "^0.2.5",
|
||||
"tiny-inflate": "^1.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/unicode-trie/node_modules/pako": {
|
||||
"version": "0.2.9",
|
||||
"resolved": "https://registry.npmjs.org/pako/-/pako-0.2.9.tgz",
|
||||
"integrity": "sha512-NUcwaKxUxWrZLpDG+z/xZaCgQITkA/Dv4V/T6bw7VON6l1Xz/VnrBqrYjZQ12TamKHzITTfOEIYUj48y2KXImA==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/uuid": {
|
||||
"version": "11.1.0",
|
||||
"resolved": "https://registry.npmjs.org/uuid/-/uuid-11.1.0.tgz",
|
||||
|
||||
@@ -29,14 +29,15 @@
|
||||
"vite-plugin-devtools-json": "^0.2.0"
|
||||
},
|
||||
"dependencies": {
|
||||
"@tensorflow-models/blazeface": "^0.1.0",
|
||||
"@tensorflow/tfjs": "^4.22.0",
|
||||
"@tensorflow/tfjs-backend-webgl": "^4.22.0",
|
||||
"@tensorflow-models/blazeface": "^0.1.0",
|
||||
"@types/gapi": "^0.0.47",
|
||||
"@types/gapi.client.drive": "^3.0.15",
|
||||
"@types/gapi.client.sheets": "^4.0.20201031",
|
||||
"@types/google.accounts": "^0.0.17",
|
||||
"@types/uuid": "^10.0.0",
|
||||
"fontkit": "^2.0.4",
|
||||
"idb": "^8.0.3",
|
||||
"pdf-lib": "^1.17.1",
|
||||
"uuid": "^11.1.0"
|
||||
|
||||
@@ -5,14 +5,15 @@
|
||||
import StepColumnMap from './wizard/StepColumnMap.svelte';
|
||||
import StepRowFilter from './wizard/StepRowFilter.svelte';
|
||||
import StepGallery from './wizard/StepGallery.svelte';
|
||||
// Additional steps to be added as they are implemented
|
||||
import StepGenerate from './wizard/StepGenerate.svelte';
|
||||
|
||||
const steps = [
|
||||
StepAuth,
|
||||
StepSheetSearch,
|
||||
StepColumnMap,
|
||||
StepRowFilter,
|
||||
StepGallery
|
||||
StepGallery,
|
||||
StepGenerate
|
||||
];
|
||||
|
||||
const stepTitles = [
|
||||
@@ -20,7 +21,8 @@
|
||||
'Select Sheet',
|
||||
'Map Columns',
|
||||
'Filter Rows',
|
||||
'Review Photos'
|
||||
'Review Photos',
|
||||
'Generate PDFs'
|
||||
];
|
||||
</script>
|
||||
|
||||
|
||||
@@ -6,10 +6,10 @@
|
||||
import * as tf from '@tensorflow/tfjs';
|
||||
import * as blazeface from '@tensorflow-models/blazeface';
|
||||
|
||||
let photos: PhotoInfo[] = [];
|
||||
let isProcessing = false;
|
||||
let processedCount = 0;
|
||||
let totalCount = 0;
|
||||
let photos = $state<PhotoInfo[]>([]);
|
||||
let isProcessing = $state(false);
|
||||
let processedCount = $state(0);
|
||||
let totalCount = $state(0);
|
||||
let detector: blazeface.BlazeFaceModel;
|
||||
|
||||
interface PhotoInfo {
|
||||
@@ -90,7 +90,7 @@
|
||||
|
||||
if (!isRetry) {
|
||||
photo.status = 'loading';
|
||||
photos = [...photos]; // Trigger reactivity
|
||||
// No need to reassign photos array with $state reactivity
|
||||
}
|
||||
|
||||
try {
|
||||
@@ -127,7 +127,7 @@
|
||||
photo.status = 'error';
|
||||
}
|
||||
|
||||
photos = [...photos]; // Trigger reactivity
|
||||
// No need to reassign photos array with $state reactivity
|
||||
}
|
||||
|
||||
async function detectFaceForPhoto(index: number) {
|
||||
@@ -179,7 +179,7 @@
|
||||
} catch {
|
||||
photos[index].faceDetectionStatus = 'failed';
|
||||
}
|
||||
photos = [...photos];
|
||||
// No need to reassign photos array with $state reactivity
|
||||
}
|
||||
|
||||
async function retryPhoto(index: number) {
|
||||
@@ -195,16 +195,16 @@
|
||||
|
||||
function handleCropUpdate(index: number, cropData: { x: number; y: number; width: number; height: number }) {
|
||||
photos[index].cropData = cropData;
|
||||
photos = [...photos]; // Trigger reactivity
|
||||
// No need to reassign photos array with $state reactivity
|
||||
}
|
||||
|
||||
function canProceed() {
|
||||
const canProceed = $derived(() => {
|
||||
const hasPhotos = photos.length > 0;
|
||||
const allLoaded = photos.every(photo => photo.status === 'success');
|
||||
const allCropped = photos.every(photo => photo.cropData);
|
||||
|
||||
return hasPhotos && allLoaded && allCropped;
|
||||
}
|
||||
});
|
||||
|
||||
// Cleanup object URLs when component is destroyed
|
||||
function cleanupObjectUrls() {
|
||||
@@ -215,13 +215,12 @@
|
||||
});
|
||||
}
|
||||
|
||||
// Cleanup on unmount or when photos change
|
||||
$: {
|
||||
// This will run when photos array changes
|
||||
if (photos.length === 0) {
|
||||
// Cleanup on unmount using $effect
|
||||
$effect(() => {
|
||||
return () => {
|
||||
cleanupObjectUrls();
|
||||
}
|
||||
}
|
||||
};
|
||||
});
|
||||
</script>
|
||||
|
||||
<div class="p-6">
|
||||
@@ -396,7 +395,7 @@
|
||||
</button>
|
||||
|
||||
<button
|
||||
onclick={() => currentStep.set(5)}
|
||||
onclick={() => currentStep.set(6)}
|
||||
disabled={!canProceed()}
|
||||
class="px-4 py-2 bg-blue-600 text-white rounded-lg font-medium hover:bg-blue-700 disabled:bg-gray-400 disabled:cursor-not-allowed"
|
||||
>
|
||||
|
||||
@@ -1,4 +1,408 @@
|
||||
<script lang="ts">
|
||||
import { onMount } from 'svelte';
|
||||
import { filteredSheetData, currentStep } from '$lib/stores';
|
||||
import { PDFDocument, StandardFonts, rgb } from 'pdf-lib';
|
||||
import * as fontkit from 'fontkit';
|
||||
import {
|
||||
PDF_DIMENSIONS,
|
||||
TEXT_PDF_GRID,
|
||||
PHOTO_PDF_GRID,
|
||||
TEXT_FIELD_LAYOUT,
|
||||
PHOTO_FIELD_LAYOUT,
|
||||
BORDER_CONFIG,
|
||||
TEXT_CONFIG,
|
||||
PLACEHOLDER_CONFIG,
|
||||
calculateGridLayout,
|
||||
getAbsolutePosition,
|
||||
getAbsolutePhotoDimensions
|
||||
} from '$lib/pdfLayout';
|
||||
|
||||
let isGenerating = $state(false);
|
||||
let progress = $state({ stage: '', current: 0, total: 0 });
|
||||
let generatedFiles = $state<{ name: string; url: string; size: number }[]>([]);
|
||||
|
||||
// Load Roboto font
|
||||
async function loadRobotoFont() {
|
||||
try {
|
||||
const fontResponse = await fetch('/fonts/Roboto-Regular.ttf');
|
||||
if (!fontResponse.ok) {
|
||||
throw new Error('Failed to load Roboto font');
|
||||
}
|
||||
return await fontResponse.arrayBuffer();
|
||||
} catch (error) {
|
||||
console.warn('Could not load Roboto font, falling back to standard font:', error);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
// PDF generation function
|
||||
async function generatePDFs() {
|
||||
if (isGenerating) return;
|
||||
|
||||
isGenerating = true;
|
||||
generatedFiles = [];
|
||||
|
||||
try {
|
||||
console.log("starting PDF generation...");
|
||||
console.log("filteredSheetData:", $filteredSheetData);
|
||||
console.log("valid rows:", $filteredSheetData.filter(row => row._isValid));
|
||||
|
||||
// Generate text PDF
|
||||
progress = { stage: 'Generating text PDF...', current: 1, total: 3 };
|
||||
const textPdfBytes = await generateTextPDF();
|
||||
const textBlob = new Blob([textPdfBytes], { type: 'application/pdf' });
|
||||
const textUrl = URL.createObjectURL(textBlob);
|
||||
generatedFiles.push({
|
||||
name: 'people_data.pdf',
|
||||
url: textUrl,
|
||||
size: textPdfBytes.length
|
||||
});
|
||||
|
||||
console.log("Text PDF generated:", textUrl);
|
||||
|
||||
console.log("starting photo PDF generation...");
|
||||
|
||||
// Generate photo PDF
|
||||
progress = { stage: 'Generating photo PDF...', current: 2, total: 3 };
|
||||
const photoPdfBytes = await generatePhotoPDF();
|
||||
const photoBlob = new Blob([photoPdfBytes], { type: 'application/pdf' });
|
||||
const photoUrl = URL.createObjectURL(photoBlob);
|
||||
generatedFiles.push({
|
||||
name: 'people_photos.pdf',
|
||||
url: photoUrl,
|
||||
size: photoPdfBytes.length
|
||||
});
|
||||
|
||||
progress = { stage: 'Complete!', current: 3, total: 3 };
|
||||
} catch (error) {
|
||||
console.error('PDF generation failed:', error);
|
||||
console.error('Error stack:', error.stack);
|
||||
console.error('Error details:', {
|
||||
message: error.message,
|
||||
name: error.name,
|
||||
stack: error.stack
|
||||
});
|
||||
alert('Failed to generate PDFs: ' + error.message);
|
||||
} finally {
|
||||
isGenerating = false;
|
||||
console.log("PDF generation completed.");
|
||||
}
|
||||
}
|
||||
|
||||
async function generateTextPDF() {
|
||||
const pdfDoc = await PDFDocument.create();
|
||||
|
||||
// Register fontkit to enable custom font embedding
|
||||
pdfDoc.registerFontkit(fontkit);
|
||||
|
||||
// Load custom Roboto font or fallback to standard font
|
||||
const robotoFontBytes = await loadRobotoFont();
|
||||
const font = robotoFontBytes
|
||||
? await pdfDoc.embedFont(robotoFontBytes)
|
||||
: await pdfDoc.embedFont(StandardFonts.TimesRoman);
|
||||
|
||||
// Calculate grid layout using configuration
|
||||
const gridLayout = calculateGridLayout(PDF_DIMENSIONS, TEXT_PDF_GRID);
|
||||
|
||||
let page = pdfDoc.addPage([PDF_DIMENSIONS.pageWidth, PDF_DIMENSIONS.pageHeight]);
|
||||
let currentRow = 0;
|
||||
let currentCol = 0;
|
||||
|
||||
const validRows = $filteredSheetData.filter(row => row._isValid);
|
||||
|
||||
for (let i = 0; i < validRows.length; i++) {
|
||||
const row = validRows[i];
|
||||
|
||||
console.log(`Processing row ${i}:`, row);
|
||||
|
||||
// Calculate cell position
|
||||
const cellX = PDF_DIMENSIONS.margin + currentCol * gridLayout.cellWidth;
|
||||
const cellY = PDF_DIMENSIONS.pageHeight - PDF_DIMENSIONS.margin - (currentRow + 1) * gridLayout.cellHeight;
|
||||
|
||||
// Get field values safely
|
||||
const name = row.name || row.Name || '';
|
||||
const surname = row.surname || row.Surname || row.lastname || row.LastName || '';
|
||||
const nationality = row.nationality || row.Nationality || row.country || row.Country || '';
|
||||
const birthday = row.birthday || row.Birthday || row.birthdate || row.Birthdate || row.birth_date || '';
|
||||
|
||||
// Draw name using absolute positioning
|
||||
const namePos = getAbsolutePosition(cellX, cellY, gridLayout.cellHeight, TEXT_FIELD_LAYOUT.name);
|
||||
page.drawText(`${name} ${surname}`, {
|
||||
x: namePos.x,
|
||||
y: namePos.y,
|
||||
size: namePos.size,
|
||||
font: font,
|
||||
color: rgb(TEXT_CONFIG.color.r, TEXT_CONFIG.color.g, TEXT_CONFIG.color.b)
|
||||
});
|
||||
|
||||
// Draw nationality
|
||||
const nationalityPos = getAbsolutePosition(cellX, cellY, gridLayout.cellHeight, TEXT_FIELD_LAYOUT.nationality);
|
||||
page.drawText(`Nationality: ${nationality}`, {
|
||||
x: nationalityPos.x,
|
||||
y: nationalityPos.y,
|
||||
size: nationalityPos.size,
|
||||
font: font,
|
||||
color: rgb(TEXT_CONFIG.color.r, TEXT_CONFIG.color.g, TEXT_CONFIG.color.b)
|
||||
});
|
||||
|
||||
// Draw birthday
|
||||
const birthdayPos = getAbsolutePosition(cellX, cellY, gridLayout.cellHeight, TEXT_FIELD_LAYOUT.birthday);
|
||||
page.drawText(`Birthday: ${birthday}`, {
|
||||
x: birthdayPos.x,
|
||||
y: birthdayPos.y,
|
||||
size: birthdayPos.size,
|
||||
font: font,
|
||||
color: rgb(TEXT_CONFIG.color.r, TEXT_CONFIG.color.g, TEXT_CONFIG.color.b)
|
||||
});
|
||||
|
||||
// Draw cell border
|
||||
page.drawRectangle({
|
||||
x: cellX,
|
||||
y: cellY,
|
||||
width: gridLayout.cellWidth,
|
||||
height: gridLayout.cellHeight,
|
||||
borderColor: rgb(BORDER_CONFIG.color.r, BORDER_CONFIG.color.g, BORDER_CONFIG.color.b),
|
||||
borderWidth: BORDER_CONFIG.width
|
||||
});
|
||||
|
||||
// Move to next cell
|
||||
currentCol++;
|
||||
if (currentCol >= gridLayout.cols) {
|
||||
currentCol = 0;
|
||||
currentRow++;
|
||||
if (currentRow >= gridLayout.rows) {
|
||||
// Add new page
|
||||
page = pdfDoc.addPage([PDF_DIMENSIONS.pageWidth, PDF_DIMENSIONS.pageHeight]);
|
||||
currentRow = 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return await pdfDoc.save();
|
||||
}
|
||||
|
||||
async function generatePhotoPDF() {
|
||||
const pdfDoc = await PDFDocument.create();
|
||||
|
||||
// Register fontkit to enable custom font embedding
|
||||
pdfDoc.registerFontkit(fontkit);
|
||||
|
||||
// Load custom Roboto font or fallback to standard font
|
||||
const robotoFontBytes = await loadRobotoFont();
|
||||
const font = robotoFontBytes
|
||||
? await pdfDoc.embedFont(robotoFontBytes)
|
||||
: await pdfDoc.embedFont(StandardFonts.TimesRoman);
|
||||
|
||||
// Calculate grid layout using configuration
|
||||
const gridLayout = calculateGridLayout(PDF_DIMENSIONS, PHOTO_PDF_GRID);
|
||||
|
||||
let page = pdfDoc.addPage([PDF_DIMENSIONS.pageWidth, PDF_DIMENSIONS.pageHeight]);
|
||||
let currentRow = 0;
|
||||
let currentCol = 0;
|
||||
|
||||
const validRows = $filteredSheetData.filter(row => row._isValid);
|
||||
|
||||
for (let i = 0; i < validRows.length; i++) {
|
||||
const row = validRows[i];
|
||||
|
||||
// Calculate cell position
|
||||
const cellX = PDF_DIMENSIONS.margin + currentCol * gridLayout.cellWidth;
|
||||
const cellY = PDF_DIMENSIONS.pageHeight - PDF_DIMENSIONS.margin - (currentRow + 1) * gridLayout.cellHeight;
|
||||
|
||||
// Get photo dimensions using configuration
|
||||
const photoDims = getAbsolutePhotoDimensions(
|
||||
cellX, cellY, gridLayout.cellWidth, gridLayout.cellHeight,
|
||||
PHOTO_FIELD_LAYOUT.photo
|
||||
);
|
||||
|
||||
// Draw photo placeholder rectangle
|
||||
page.drawRectangle({
|
||||
x: photoDims.x,
|
||||
y: photoDims.y,
|
||||
width: photoDims.width,
|
||||
height: photoDims.height,
|
||||
borderColor: rgb(BORDER_CONFIG.color.r, BORDER_CONFIG.color.g, BORDER_CONFIG.color.b),
|
||||
borderWidth: BORDER_CONFIG.width
|
||||
});
|
||||
|
||||
// Draw placeholder text
|
||||
page.drawText(PLACEHOLDER_CONFIG.text, {
|
||||
x: cellX + gridLayout.cellWidth / 2 - 40,
|
||||
y: cellY + gridLayout.cellHeight / 2,
|
||||
size: PLACEHOLDER_CONFIG.size,
|
||||
font: font,
|
||||
color: rgb(PLACEHOLDER_CONFIG.color.r, PLACEHOLDER_CONFIG.color.g, PLACEHOLDER_CONFIG.color.b)
|
||||
});
|
||||
|
||||
// Get field values safely
|
||||
const name = row.name || row.Name || '';
|
||||
const surname = row.surname || row.Surname || row.lastname || row.LastName || '';
|
||||
|
||||
// Draw name using absolute positioning
|
||||
const namePos = getAbsolutePosition(cellX, cellY, gridLayout.cellHeight, PHOTO_FIELD_LAYOUT.name);
|
||||
page.drawText(`${name} ${surname}`, {
|
||||
x: namePos.x,
|
||||
y: namePos.y,
|
||||
size: namePos.size,
|
||||
font: font,
|
||||
color: rgb(TEXT_CONFIG.color.r, TEXT_CONFIG.color.g, TEXT_CONFIG.color.b)
|
||||
});
|
||||
|
||||
// Move to next cell
|
||||
currentCol++;
|
||||
if (currentCol >= gridLayout.cols) {
|
||||
currentCol = 0;
|
||||
currentRow++;
|
||||
if (currentRow >= gridLayout.rows) {
|
||||
// Add new page
|
||||
page = pdfDoc.addPage([PDF_DIMENSIONS.pageWidth, PDF_DIMENSIONS.pageHeight]);
|
||||
currentRow = 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return await pdfDoc.save();
|
||||
}
|
||||
|
||||
function downloadFile(file: { name: string; url: string }) {
|
||||
const link = document.createElement('a');
|
||||
link.href = file.url;
|
||||
link.download = file.name;
|
||||
document.body.appendChild(link);
|
||||
link.click();
|
||||
document.body.removeChild(link);
|
||||
}
|
||||
|
||||
function formatFileSize(bytes: number): string {
|
||||
if (bytes === 0) return '0 Bytes';
|
||||
const k = 1024;
|
||||
const sizes = ['Bytes', 'KB', 'MB', 'GB'];
|
||||
const i = Math.floor(Math.log(bytes) / Math.log(k));
|
||||
return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i];
|
||||
}
|
||||
</script>
|
||||
|
||||
<div class="p-6">
|
||||
<h2 class="text-xl font-semibold text-gray-900">Generate PDFs</h2>
|
||||
<p class="text-sm text-gray-700">PDF generation functionality will be implemented here.</p>
|
||||
<div class="max-w-4xl mx-auto">
|
||||
<div class="mb-6">
|
||||
<h2 class="text-xl font-semibold text-gray-900 mb-2">
|
||||
Generate PDFs
|
||||
</h2>
|
||||
|
||||
<p class="text-sm text-gray-700 mb-4">
|
||||
Create two PDF documents: one with text data and one with photos.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<!-- Generation Status -->
|
||||
{#if isGenerating}
|
||||
<div class="bg-blue-50 border border-blue-200 rounded-lg p-4 mb-6">
|
||||
<div class="flex items-center justify-between mb-3">
|
||||
<div class="flex items-center">
|
||||
<div class="w-5 h-5 border-2 border-blue-600 border-t-transparent rounded-full animate-spin mr-3"></div>
|
||||
<span class="text-sm text-blue-800">{progress.stage}</span>
|
||||
</div>
|
||||
<span class="text-sm text-blue-600">
|
||||
{progress.current} / {progress.total}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div class="w-full bg-blue-200 rounded-full h-2">
|
||||
<div
|
||||
class="bg-blue-600 h-2 rounded-full transition-all duration-300"
|
||||
style="width: {(progress.current / progress.total) * 100}%"
|
||||
></div>
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
<!-- Summary -->
|
||||
<div class="bg-gray-50 border border-gray-200 rounded-lg p-4 mb-6">
|
||||
<h3 class="text-sm font-medium text-gray-700 mb-3">Generation Summary</h3>
|
||||
|
||||
<div class="grid grid-cols-1 md:grid-cols-3 gap-4 text-sm">
|
||||
<div class="text-center">
|
||||
<div class="text-2xl font-bold text-gray-900">
|
||||
{$filteredSheetData.filter(row => row._isValid).length}
|
||||
</div>
|
||||
<div class="text-gray-600">Records to Process</div>
|
||||
</div>
|
||||
|
||||
<div class="text-center">
|
||||
<div class="text-2xl font-bold text-blue-600">2</div>
|
||||
<div class="text-gray-600">PDFs to Generate</div>
|
||||
</div>
|
||||
|
||||
<div class="text-center">
|
||||
<div class="text-2xl font-bold text-green-600">
|
||||
{generatedFiles.length}
|
||||
</div>
|
||||
<div class="text-gray-600">Files Ready</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Generate Button -->
|
||||
{#if !isGenerating && generatedFiles.length === 0}
|
||||
<div class="text-center mb-6">
|
||||
<button
|
||||
onclick={generatePDFs}
|
||||
class="px-6 py-3 bg-blue-600 text-white rounded-lg font-medium hover:bg-blue-700 focus:outline-none focus:ring-2 focus:ring-blue-500"
|
||||
>
|
||||
Generate PDFs
|
||||
</button>
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
<!-- Generated Files -->
|
||||
{#if generatedFiles.length > 0}
|
||||
<div class="bg-white border border-gray-200 rounded-lg overflow-hidden mb-6">
|
||||
<div class="p-4 border-b border-gray-200">
|
||||
<h3 class="text-lg font-medium text-gray-900">Generated Files</h3>
|
||||
</div>
|
||||
|
||||
<div class="divide-y divide-gray-200">
|
||||
{#each generatedFiles as file}
|
||||
<div class="p-4 flex items-center justify-between">
|
||||
<div class="flex items-center">
|
||||
<svg class="w-8 h-8 text-red-600 mr-3" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M7 21h10a2 2 0 002-2V9.414a1 1 0 00-.293-.707l-5.414-5.414A1 1 0 0012.586 3H7a2 2 0 00-2 2v14a2 2 0 002 2z"/>
|
||||
</svg>
|
||||
<div>
|
||||
<h4 class="text-sm font-medium text-gray-900">{file.name}</h4>
|
||||
<p class="text-xs text-gray-500">{formatFileSize(file.size)}</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<button
|
||||
onclick={() => downloadFile(file)}
|
||||
class="px-4 py-2 bg-blue-600 text-white rounded-md text-sm font-medium hover:bg-blue-700"
|
||||
>
|
||||
Download
|
||||
</button>
|
||||
</div>
|
||||
{/each}
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
<!-- Navigation -->
|
||||
<div class="flex justify-between">
|
||||
<button
|
||||
onclick={() => currentStep.set(5)}
|
||||
class="px-4 py-2 bg-gray-200 text-gray-700 rounded-lg font-medium hover:bg-gray-300"
|
||||
>
|
||||
← Back to Gallery
|
||||
</button>
|
||||
|
||||
{#if generatedFiles.length > 0}
|
||||
<button
|
||||
onclick={() => currentStep.set(0)}
|
||||
class="px-4 py-2 bg-green-600 text-white rounded-lg font-medium hover:bg-green-700"
|
||||
>
|
||||
Start Over
|
||||
</button>
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
157
src/lib/pdfLayout.ts
Normal file
157
src/lib/pdfLayout.ts
Normal file
@@ -0,0 +1,157 @@
|
||||
// PDF Layout Configuration Module
|
||||
// Centralized configuration for PDF generation layouts
|
||||
|
||||
export interface PDFDimensions {
|
||||
pageWidth: number;
|
||||
pageHeight: number;
|
||||
margin: number;
|
||||
}
|
||||
|
||||
export interface GridLayout {
|
||||
cols: number;
|
||||
rows: number;
|
||||
cellWidth: number;
|
||||
cellHeight: number;
|
||||
}
|
||||
|
||||
export interface TextPosition {
|
||||
x: number;
|
||||
y: number;
|
||||
size: number;
|
||||
}
|
||||
|
||||
export interface PhotoPosition {
|
||||
x: number;
|
||||
y: number;
|
||||
width: number;
|
||||
height: number;
|
||||
}
|
||||
|
||||
export interface TextFieldLayout {
|
||||
name: TextPosition;
|
||||
nationality: TextPosition;
|
||||
birthday: TextPosition;
|
||||
}
|
||||
|
||||
export interface PhotoFieldLayout {
|
||||
photo: PhotoPosition;
|
||||
name: TextPosition;
|
||||
}
|
||||
|
||||
// A4 dimensions in points
|
||||
export const PDF_DIMENSIONS: PDFDimensions = {
|
||||
pageWidth: 595.28,
|
||||
pageHeight: 841.89,
|
||||
margin: 40
|
||||
};
|
||||
|
||||
// Text PDF Layout (3x7 grid)
|
||||
export const TEXT_PDF_GRID = {
|
||||
cols: 3,
|
||||
rows: 7
|
||||
};
|
||||
|
||||
// Photo PDF Layout (3x5 grid)
|
||||
export const PHOTO_PDF_GRID = {
|
||||
cols: 3,
|
||||
rows: 5
|
||||
};
|
||||
|
||||
// Calculate grid layout
|
||||
export function calculateGridLayout(
|
||||
dimensions: PDFDimensions,
|
||||
grid: { cols: number; rows: number }
|
||||
): GridLayout {
|
||||
const cellWidth = (dimensions.pageWidth - 2 * dimensions.margin) / grid.cols;
|
||||
const cellHeight = (dimensions.pageHeight - 2 * dimensions.margin) / grid.rows;
|
||||
|
||||
return {
|
||||
cols: grid.cols,
|
||||
rows: grid.rows,
|
||||
cellWidth,
|
||||
cellHeight
|
||||
};
|
||||
}
|
||||
|
||||
// Text PDF Field Positions (relative to cell)
|
||||
export const TEXT_FIELD_LAYOUT: TextFieldLayout = {
|
||||
name: {
|
||||
x: 5, // 5pt from left edge of cell
|
||||
y: -15, // 15pt from top of cell (negative because PDF coords are bottom-up)
|
||||
size: 10
|
||||
},
|
||||
nationality: {
|
||||
x: 5, // 5pt from left edge of cell
|
||||
y: -29, // 29pt from top of cell (15 + 14 line height)
|
||||
size: 10
|
||||
},
|
||||
birthday: {
|
||||
x: 5, // 5pt from left edge of cell
|
||||
y: -43, // 43pt from top of cell (15 + 14 + 14 line height)
|
||||
size: 10
|
||||
}
|
||||
};
|
||||
|
||||
// Photo PDF Field Positions (relative to cell)
|
||||
export const PHOTO_FIELD_LAYOUT: PhotoFieldLayout = {
|
||||
photo: {
|
||||
x: 10, // 10pt from left edge of cell
|
||||
y: 40, // 40pt from bottom of cell
|
||||
width: -20, // cell width minus 20pt (10pt margin on each side)
|
||||
height: -60 // cell height minus 60pt (40pt bottom margin + 20pt top margin)
|
||||
},
|
||||
name: {
|
||||
x: 10, // 10pt from left edge of cell
|
||||
y: 20, // 20pt from bottom of cell
|
||||
size: 10
|
||||
}
|
||||
};
|
||||
|
||||
// Helper function to get absolute position within a cell
|
||||
export function getAbsolutePosition(
|
||||
cellX: number,
|
||||
cellY: number,
|
||||
cellHeight: number,
|
||||
relativePos: TextPosition
|
||||
): { x: number; y: number; size: number } {
|
||||
return {
|
||||
x: cellX + relativePos.x,
|
||||
y: cellY + cellHeight + relativePos.y, // Convert relative Y to absolute
|
||||
size: relativePos.size
|
||||
};
|
||||
}
|
||||
|
||||
// Helper function to get absolute photo dimensions
|
||||
export function getAbsolutePhotoDimensions(
|
||||
cellX: number,
|
||||
cellY: number,
|
||||
cellWidth: number,
|
||||
cellHeight: number,
|
||||
relativePhoto: PhotoPosition
|
||||
): { x: number; y: number; width: number; height: number } {
|
||||
return {
|
||||
x: cellX + relativePhoto.x,
|
||||
y: cellY + relativePhoto.y,
|
||||
width: relativePhoto.width < 0 ? cellWidth + relativePhoto.width : relativePhoto.width,
|
||||
height: relativePhoto.height < 0 ? cellHeight + relativePhoto.height : relativePhoto.height
|
||||
};
|
||||
}
|
||||
|
||||
// Border configuration
|
||||
export const BORDER_CONFIG = {
|
||||
color: { r: 0.8, g: 0.8, b: 0.8 },
|
||||
width: 1
|
||||
};
|
||||
|
||||
// Text configuration
|
||||
export const TEXT_CONFIG = {
|
||||
color: { r: 0, g: 0, b: 0 },
|
||||
lineHeight: 14
|
||||
};
|
||||
|
||||
// Placeholder text configuration
|
||||
export const PLACEHOLDER_CONFIG = {
|
||||
text: 'Photo placeholder',
|
||||
color: { r: 0.5, g: 0.5, b: 0.5 },
|
||||
size: 8
|
||||
};
|
||||
BIN
static/fonts/Roboto-Regular.ttf
Normal file
BIN
static/fonts/Roboto-Regular.ttf
Normal file
Binary file not shown.
Reference in New Issue
Block a user