Working POC created

This commit is contained in:
Roman Krček
2025-07-17 21:50:14 +02:00
parent 2072e57585
commit c77c96c1c7
2 changed files with 190 additions and 19 deletions

View File

@@ -1,6 +1,6 @@
<script lang="ts"> <script lang="ts">
import { onMount } from 'svelte'; import { onMount } from 'svelte';
import { columnMapping, filteredSheetData, currentStep } from '$lib/stores'; import { columnMapping, filteredSheetData, currentStep, pictures, cropRects } from '$lib/stores';
import { downloadDriveImage, isGoogleDriveUrl, createImageObjectUrl } from '$lib/google'; import { downloadDriveImage, isGoogleDriveUrl, createImageObjectUrl } from '$lib/google';
import PhotoCard from '../PhotoCard.svelte'; import PhotoCard from '../PhotoCard.svelte';
import * as tf from '@tensorflow/tfjs'; import * as tf from '@tensorflow/tfjs';
@@ -120,6 +120,39 @@
photo.objectUrl = objectUrl; photo.objectUrl = objectUrl;
photo.status = 'success'; photo.status = 'success';
console.log(`Photo loaded successfully: ${photo.name}`); console.log(`Photo loaded successfully: ${photo.name}`);
// Save to pictures store
if (isGoogleDriveUrl(photo.url)) {
// For Google Drive images, save the blob
const blob = await downloadDriveImage(photo.url);
pictures.update(pics => ({
...pics,
[photo.url]: {
id: photo.url,
blob: blob,
url: objectUrl,
downloaded: true,
faceDetected: false,
faceCount: 0
}
}));
} else {
// For direct URLs, convert to blob
const response = await fetch(photo.url);
const blob = await response.blob();
pictures.update(pics => ({
...pics,
[photo.url]: {
id: photo.url,
blob: blob,
url: objectUrl,
downloaded: true,
faceDetected: false,
faceCount: 0
}
}));
}
// Automatically run face detection to generate crop // Automatically run face detection to generate crop
await detectFaceForPhoto(index); await detectFaceForPhoto(index);
} catch (error) { } catch (error) {
@@ -173,6 +206,22 @@
}; };
photos[index].cropData = crop; photos[index].cropData = crop;
photos[index].faceDetectionStatus = 'completed'; photos[index].faceDetectionStatus = 'completed';
// Save crop data to store
cropRects.update(crops => ({
...crops,
[photos[index].url]: crop
}));
// Update pictures store with face detection info
pictures.update(pics => ({
...pics,
[photos[index].url]: {
...pics[photos[index].url],
faceDetected: true,
faceCount: predictions.length
}
}));
} else { } else {
photos[index].faceDetectionStatus = 'failed'; photos[index].faceDetectionStatus = 'failed';
} }
@@ -195,6 +244,13 @@
function handleCropUpdate(index: number, cropData: { x: number; y: number; width: number; height: number }) { function handleCropUpdate(index: number, cropData: { x: number; y: number; width: number; height: number }) {
photos[index].cropData = cropData; photos[index].cropData = cropData;
// Save updated crop data to store
cropRects.update(crops => ({
...crops,
[photos[index].url]: cropData
}));
// No need to reassign photos array with $state reactivity // No need to reassign photos array with $state reactivity
} }

View File

@@ -1,6 +1,6 @@
<script lang="ts"> <script lang="ts">
import { onMount } from 'svelte'; import { onMount } from 'svelte';
import { filteredSheetData, currentStep } from '$lib/stores'; import { filteredSheetData, currentStep, pictures, cropRects } from '$lib/stores';
import { PDFDocument, StandardFonts, rgb } from 'pdf-lib'; import { PDFDocument, StandardFonts, rgb } from 'pdf-lib';
import * as fontkit from 'fontkit'; import * as fontkit from 'fontkit';
import { import {
@@ -35,6 +35,47 @@
} }
} }
// Crop image using canvas
async function cropImage(imageBlob: Blob, crop: { x: number; y: number; width: number; height: number }): Promise<Uint8Array> {
return new Promise((resolve, reject) => {
const img = new Image();
const canvas = document.createElement('canvas');
const ctx = canvas.getContext('2d');
if (!ctx) {
reject(new Error('Could not get canvas context'));
return;
}
img.onload = () => {
// Set canvas size to crop dimensions
canvas.width = crop.width;
canvas.height = crop.height;
// Draw the cropped portion of the image
ctx.drawImage(
img,
crop.x, crop.y, crop.width, crop.height, // Source rectangle
0, 0, crop.width, crop.height // Destination rectangle
);
// Convert canvas to blob then to array buffer
canvas.toBlob((blob) => {
if (blob) {
blob.arrayBuffer().then(buffer => {
resolve(new Uint8Array(buffer));
}).catch(reject);
} else {
reject(new Error('Failed to create blob from canvas'));
}
}, 'image/jpeg', 0.9);
};
img.onerror = () => reject(new Error('Failed to load image'));
img.src = URL.createObjectURL(imageBlob);
});
}
// PDF generation function // PDF generation function
async function generatePDFs() { async function generatePDFs() {
if (isGenerating) return; if (isGenerating) return;
@@ -181,7 +222,7 @@
return await pdfDoc.save(); return await pdfDoc.save();
} }
async function generatePhotoPDF() { async function generatePhotoPDF() {
const pdfDoc = await PDFDocument.create(); const pdfDoc = await PDFDocument.create();
// Register fontkit to enable custom font embedding // Register fontkit to enable custom font embedding
@@ -215,24 +256,98 @@
PHOTO_FIELD_LAYOUT.photo PHOTO_FIELD_LAYOUT.photo
); );
// Draw photo placeholder rectangle // Try to get and embed the actual photo
page.drawRectangle({ // Use picture URL as the key to lookup data
x: photoDims.x, const pictureUrl = row.pictureUrl || row.picture_url || row.Picture || row.PictureUrl;
y: photoDims.y, const pictureInfo = $pictures[pictureUrl];
width: photoDims.width, const cropData = $cropRects[pictureUrl];
height: photoDims.height,
borderColor: rgb(BORDER_CONFIG.color.r, BORDER_CONFIG.color.g, BORDER_CONFIG.color.b), console.log(`Row ${i} (${row.name}):`, {
borderWidth: BORDER_CONFIG.width rowId: row.id,
pictureUrl: pictureUrl,
pictureInfo: pictureInfo ? 'Found' : 'Missing',
cropData: cropData ? cropData : 'Missing',
allPictureIds: Object.keys($pictures),
allCropIds: Object.keys($cropRects)
}); });
// Draw placeholder text if (pictureInfo && cropData) {
page.drawText(PLACEHOLDER_CONFIG.text, { try {
x: cellX + gridLayout.cellWidth / 2 - 40, console.log(`Cropping and embedding photo for ${row.name} ${row.surname}`);
y: cellY + gridLayout.cellHeight / 2,
size: PLACEHOLDER_CONFIG.size, // Crop the image
font: font, const croppedImageBytes = await cropImage(pictureInfo.blob, cropData);
color: rgb(PLACEHOLDER_CONFIG.color.r, PLACEHOLDER_CONFIG.color.g, PLACEHOLDER_CONFIG.color.b)
}); // Embed the cropped image in the PDF
const image = await pdfDoc.embedJpg(croppedImageBytes);
// Calculate image dimensions to fit within the photo area while maintaining aspect ratio
const imageAspectRatio = image.width / image.height;
const photoAspectRatio = photoDims.width / photoDims.height;
let imageWidth, imageHeight;
if (imageAspectRatio > photoAspectRatio) {
// Image is wider - fit to width
imageWidth = photoDims.width;
imageHeight = photoDims.width / imageAspectRatio;
} else {
// Image is taller - fit to height
imageHeight = photoDims.height;
imageWidth = photoDims.height * imageAspectRatio;
}
// Center the image within the photo area
const imageX = photoDims.x + (photoDims.width - imageWidth) / 2;
const imageY = photoDims.y + (photoDims.height - imageHeight) / 2;
// Draw the image
page.drawImage(image, {
x: imageX,
y: imageY,
width: imageWidth,
height: imageHeight
});
} catch (error) {
console.error(`Failed to embed photo for ${row.name}:`, error);
// Fall back to placeholder if photo embedding fails
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
});
page.drawText('Photo failed', {
x: cellX + gridLayout.cellWidth / 2 - 30,
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)
});
}
} else {
// No photo or crop data available - draw placeholder
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
});
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 // Get field values safely
const name = row.name || row.Name || ''; const name = row.name || row.Name || '';