The git changes introduce functionality to merge attachments from a processed ZIP archive into a single PDF file. Here is a summary of the changes: ### New Features - PDF Merging: Added the ability to merge multiple attachments (PDFs and images) from a processed ZIP archive into a single PDF document. - Image Conversion: Implemented automatic conversion of non-PNG images (like JPGs) to PNG format before embedding them into the merged PDF. ### File Changes - Dependencies: Added pdf-lib to the project's dependencies. - src/lib/zip-processing.ts:

- Added mergeProcessedZipArchive to handle the core logic of creating a merged PDF.                         - Implemented helpers to embed PDF pages and convert/embed images.     - Added toArrayBuffer and convertImageBytesToPng utilities for handling binary data and canvas-based image processing.
     - src/lib/components/ProcessedZipArchiveEditor.svelte:
 - Added a "PDF herunterladen" (Download PDF) button.              - Added UI states to handle the merging process (loading state, error messages).
     - Integrated the mergeProcessedZipArchive function with the UI.
      - src/lib/components/AttachmentPreview.svelte:
 - Added a fix to ensure attachment.data is treated as a Uint8Array when loading PDF documents.
This commit is contained in:
2026-06-13 13:32:55 +02:00
parent e3518eaed0
commit ef96f523e3
6 changed files with 243 additions and 6 deletions
+119
View File
@@ -1,4 +1,5 @@
import { unzip } from 'fflate';
import { PDFDocument } from 'pdf-lib';
export type ZipAttachmentKind = 'pdf' | 'image';
@@ -287,3 +288,121 @@ export const extractZipArchives = async (file: File): Promise<ProcessedZipArchiv
return extractZipEntries(archiveBytes, file.name);
};
const loadImageElement = (src: string) =>
new Promise<HTMLImageElement>((resolve, reject) => {
const image = new Image();
image.decoding = 'async';
image.onload = () => resolve(image);
image.onerror = () => reject(new Error(`Unable to decode image asset: ${src}`));
image.src = src;
});
const toArrayBuffer = (bytes: Uint8Array) =>
(() => {
const copy = new Uint8Array(bytes.byteLength);
copy.set(bytes);
return copy.buffer;
})();
const convertImageBytesToPng = async (imageBytes: Uint8Array) => {
if (typeof document === 'undefined') {
throw new Error('Image conversion requires a browser environment.');
}
const objectUrl = URL.createObjectURL(new Blob([toArrayBuffer(imageBytes)]));
try {
const image = await loadImageElement(objectUrl);
const canvas = document.createElement('canvas');
canvas.width = image.naturalWidth || image.width;
canvas.height = image.naturalHeight || image.height;
const context = canvas.getContext('2d');
if (!context) {
throw new Error('Could not create a canvas context for image conversion.');
}
context.drawImage(image, 0, 0);
const pngBlob = await new Promise<Blob>((resolve, reject) => {
canvas.toBlob((blob) => {
if (blob) {
resolve(blob);
return;
}
reject(new Error('Could not encode image asset as PNG.'));
}, 'image/png');
});
return new Uint8Array(await pngBlob.arrayBuffer());
} finally {
URL.revokeObjectURL(objectUrl);
}
};
const embedAttachmentImage = async (pdfDocument: PDFDocument, attachment: ZipAttachment) => {
const extension = getExtension(attachment.path);
const imageData = attachment.data;
if (extension === '.jpg' || extension === '.jpeg') {
return pdfDocument.embedJpg(imageData);
}
if (extension === '.png') {
return pdfDocument.embedPng(imageData);
}
const pngBytes = await convertImageBytesToPng(imageData);
return pdfDocument.embedPng(pngBytes);
};
const mergeAttachmentIntoPdf = async (mergedPdf: PDFDocument, attachment: ZipAttachment) => {
const canLoadAsPdf = attachment.kind === 'pdf'
if (canLoadAsPdf) {
const sourcePdf = await PDFDocument.load(attachment.data, { ignoreEncryption: true });
const copiedPages = await mergedPdf.copyPages(sourcePdf, sourcePdf.getPageIndices());
for (const page of copiedPages) {
mergedPdf.addPage(page);
}
return;
}
try {
const embeddedImage = await embedAttachmentImage(mergedPdf, attachment);
const page = mergedPdf.addPage([embeddedImage.width, embeddedImage.height]);
page.drawImage(embeddedImage, {
x: 0,
y: 0,
width: page.getWidth(),
height: page.getHeight()
});
} catch (error) {
if (attachment.kind === 'pdf') {
throw new Error(`Attachment "${attachment.name}" is not a valid PDF and could not be rendered as an image.`);
}
throw error;
}
};
export const mergeProcessedZipArchive = async (archive: ProcessedZipArchive) => {
if (archive.attachments.length === 0) {
throw new Error('Cannot create a PDF from an empty archive.');
}
const mergedPdf = await PDFDocument.create();
for (const attachment of archive.attachments) {
await mergeAttachmentIntoPdf(mergedPdf, attachment);
}
return mergedPdf.save();
};