Working POC created
This commit is contained in:
@@ -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
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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 || '';
|
||||||
|
|||||||
Reference in New Issue
Block a user