diff --git a/package-lock.json b/package-lock.json index 4ed9931..f8fdbbb 100644 --- a/package-lock.json +++ b/package-lock.json @@ -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", diff --git a/package.json b/package.json index 56af0b8..12d3599 100644 --- a/package.json +++ b/package.json @@ -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" diff --git a/src/lib/components/Wizard.svelte b/src/lib/components/Wizard.svelte index bab8fcc..019e323 100644 --- a/src/lib/components/Wizard.svelte +++ b/src/lib/components/Wizard.svelte @@ -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' ]; diff --git a/src/lib/components/wizard/StepGallery.svelte b/src/lib/components/wizard/StepGallery.svelte index f6088f2..d4a9f57 100644 --- a/src/lib/components/wizard/StepGallery.svelte +++ b/src/lib/components/wizard/StepGallery.svelte @@ -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([]); + 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(); - } - } + }; + });
@@ -396,7 +395,7 @@ +
+ {/if} + + + {#if generatedFiles.length > 0} +
+
+

Generated Files

+
+ +
+ {#each generatedFiles as file} +
+
+ + + +
+

{file.name}

+

{formatFileSize(file.size)}

+
+
+ + +
+ {/each} +
+
+ {/if} + + +
+ + + {#if generatedFiles.length > 0} + + {/if} +
+ diff --git a/src/lib/pdfLayout.ts b/src/lib/pdfLayout.ts new file mode 100644 index 0000000..b2e95da --- /dev/null +++ b/src/lib/pdfLayout.ts @@ -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 +}; diff --git a/static/fonts/Roboto-Regular.ttf b/static/fonts/Roboto-Regular.ttf new file mode 100644 index 0000000..7e3bb2f Binary files /dev/null and b/static/fonts/Roboto-Regular.ttf differ