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
@@ -1,6 +1,6 @@
<script lang="ts">
import AttachmentPreview from "$lib/components/AttachmentPreview.svelte";
import type {ProcessedZipArchive, ZipAttachment} from "$lib/zip-processing";
import { mergeProcessedZipArchive, type ProcessedZipArchive, type ZipAttachment } from "$lib/zip-processing";
type Props = {
archive: ProcessedZipArchive;
@@ -19,6 +19,14 @@
let archiveName = $state("");
let dragIndex = $state<number | null>(null);
let dropIndex = $state<number | null>(null);
let isMerging = $state(false);
let mergeError = $state<string | null>(null);
const toArrayBuffer = (bytes: Uint8Array) => {
const copy = new Uint8Array(bytes.byteLength);
copy.set(bytes);
return copy.buffer;
};
$effect(() => {
archiveName = archive.name;
@@ -33,6 +41,37 @@
});
};
const downloadMergedPdf = async () => {
if (attachments.length === 0 || isMerging) {
return;
}
isMerging = true;
mergeError = null;
try {
const mergedBytes = await mergeProcessedZipArchive({
...archive,
name: archiveName,
attachments: attachments.map((attachment) => ({...attachment}))
});
const pdfBlob = new Blob([toArrayBuffer(mergedBytes)], {type: "application/pdf"});
const objectUrl = URL.createObjectURL(pdfBlob);
const downloadLink = document.createElement("a");
downloadLink.href = objectUrl;
downloadLink.download = archiveName.toLowerCase().endsWith(".pdf") ? archiveName : `${archiveName}.pdf`;
downloadLink.rel = "noopener";
downloadLink.click();
window.setTimeout(() => URL.revokeObjectURL(objectUrl), 1000);
} catch (error) {
console.error(`Failed to merge archive ${archive.name}`, error);
mergeError = "PDF konnte nicht erstellt werden.";
} finally {
isMerging = false;
}
};
const moveAttachment = (fromIndex: number, toIndex: number) => {
if (
fromIndex === toIndex ||
@@ -107,8 +146,23 @@
{attachments.length} relevante Datei{attachments.length === 1 ? "" : "en"}
</p>
</div>
<button
class="rounded-container whitespace-nowrap bg-primary-900 px-4 py-2 text-[14px] font-bold leading-6 text-white shadow-sm transition hover:bg-primary-800 disabled:cursor-not-allowed disabled:bg-primary-300"
type="button"
disabled={attachments.length === 0 || isMerging}
onclick={downloadMergedPdf}
>
{isMerging ? "PDF wird erstellt" : "PDF herunterladen"}
</button>
</header>
{#if mergeError}
<div class="rounded-container bg-red-50 px-4 py-3 text-[14px] font-medium text-red-900">
{mergeError}
</div>
{/if}
<ul class="flex gap-3 overflow-x-auto pb-1">
{#each attachments as attachment, index (attachment.path)}
<li