adds agents.md, splits zip-processing.ts
This commit is contained in:
Generated
+16
-8
@@ -3,11 +3,8 @@
|
|||||||
<component name="ChangeListManager">
|
<component name="ChangeListManager">
|
||||||
<list default="true" id="89f9fc3a-63ad-41cb-8d25-f07df352eb98" name="Changes" comment="">
|
<list default="true" id="89f9fc3a-63ad-41cb-8d25-f07df352eb98" name="Changes" comment="">
|
||||||
<change beforePath="$PROJECT_DIR$/.idea/workspace.xml" beforeDir="false" afterPath="$PROJECT_DIR$/.idea/workspace.xml" afterDir="false" />
|
<change beforePath="$PROJECT_DIR$/.idea/workspace.xml" beforeDir="false" afterPath="$PROJECT_DIR$/.idea/workspace.xml" afterDir="false" />
|
||||||
<change beforePath="$PROJECT_DIR$/package.json" beforeDir="false" afterPath="$PROJECT_DIR$/package.json" afterDir="false" />
|
|
||||||
<change beforePath="$PROJECT_DIR$/pnpm-lock.yaml" beforeDir="false" afterPath="$PROJECT_DIR$/pnpm-lock.yaml" afterDir="false" />
|
|
||||||
<change beforePath="$PROJECT_DIR$/src/lib/components/AttachmentPreview.svelte" beforeDir="false" afterPath="$PROJECT_DIR$/src/lib/components/AttachmentPreview.svelte" afterDir="false" />
|
|
||||||
<change beforePath="$PROJECT_DIR$/src/lib/components/ProcessedZipArchiveEditor.svelte" beforeDir="false" afterPath="$PROJECT_DIR$/src/lib/components/ProcessedZipArchiveEditor.svelte" afterDir="false" />
|
|
||||||
<change beforePath="$PROJECT_DIR$/src/lib/zip-processing.ts" beforeDir="false" afterPath="$PROJECT_DIR$/src/lib/zip-processing.ts" afterDir="false" />
|
<change beforePath="$PROJECT_DIR$/src/lib/zip-processing.ts" beforeDir="false" afterPath="$PROJECT_DIR$/src/lib/zip-processing.ts" afterDir="false" />
|
||||||
|
<change beforePath="$PROJECT_DIR$/src/routes/+page.svelte" beforeDir="false" afterPath="$PROJECT_DIR$/src/routes/+page.svelte" afterDir="false" />
|
||||||
</list>
|
</list>
|
||||||
<option name="SHOW_DIALOG" value="false" />
|
<option name="SHOW_DIALOG" value="false" />
|
||||||
<option name="HIGHLIGHT_CONFLICTS" value="true" />
|
<option name="HIGHLIGHT_CONFLICTS" value="true" />
|
||||||
@@ -41,7 +38,7 @@
|
|||||||
"git-widget-placeholder": "features/zip-processing",
|
"git-widget-placeholder": "features/zip-processing",
|
||||||
"last_opened_file_path": "/home/rehlert/repos/juri-merger",
|
"last_opened_file_path": "/home/rehlert/repos/juri-merger",
|
||||||
"nodejs_package_manager_path": "pnpm",
|
"nodejs_package_manager_path": "pnpm",
|
||||||
"settings.editor.selected.configurable": "preferences.pluginManager",
|
"settings.editor.selected.configurable": "proofread",
|
||||||
"ts.external.directory.path": "/home/rehlert/repos/juri-merger/node_modules/typescript/lib",
|
"ts.external.directory.path": "/home/rehlert/repos/juri-merger/node_modules/typescript/lib",
|
||||||
"vue.rearranger.settings.migration": "true"
|
"vue.rearranger.settings.migration": "true"
|
||||||
}
|
}
|
||||||
@@ -55,7 +52,9 @@
|
|||||||
<updated>1781169139290</updated>
|
<updated>1781169139290</updated>
|
||||||
<workItem from="1781169141138" duration="309000" />
|
<workItem from="1781169141138" duration="309000" />
|
||||||
<workItem from="1781251694220" duration="22281000" />
|
<workItem from="1781251694220" duration="22281000" />
|
||||||
<workItem from="1781349431536" duration="774000" />
|
<workItem from="1781349431536" duration="3602000" />
|
||||||
|
<workItem from="1781378743498" duration="306000" />
|
||||||
|
<workItem from="1781534483559" duration="2103000" />
|
||||||
</task>
|
</task>
|
||||||
<task id="LOCAL-00001" summary="adapts styling of rvg.legal">
|
<task id="LOCAL-00001" summary="adapts styling of rvg.legal">
|
||||||
<option name="closed" value="true" />
|
<option name="closed" value="true" />
|
||||||
@@ -145,7 +144,15 @@
|
|||||||
<option name="project" value="LOCAL" />
|
<option name="project" value="LOCAL" />
|
||||||
<updated>1781268046478</updated>
|
<updated>1781268046478</updated>
|
||||||
</task>
|
</task>
|
||||||
<option name="localTasksCounter" value="12" />
|
<task id="LOCAL-00012" summary="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.">
|
||||||
|
<option name="closed" value="true" />
|
||||||
|
<created>1781350375505</created>
|
||||||
|
<option name="number" value="00012" />
|
||||||
|
<option name="presentableId" value="LOCAL-00012" />
|
||||||
|
<option name="project" value="LOCAL" />
|
||||||
|
<updated>1781350375505</updated>
|
||||||
|
</task>
|
||||||
|
<option name="localTasksCounter" value="13" />
|
||||||
<servers />
|
<servers />
|
||||||
</component>
|
</component>
|
||||||
<component name="TypeScriptGeneratedFilesManager">
|
<component name="TypeScriptGeneratedFilesManager">
|
||||||
@@ -166,7 +173,8 @@
|
|||||||
<MESSAGE value="handles zips in zip correctly" />
|
<MESSAGE value="handles zips in zip correctly" />
|
||||||
<MESSAGE value="range settings" />
|
<MESSAGE value="range settings" />
|
||||||
<MESSAGE value="name generation improvement" />
|
<MESSAGE value="name generation improvement" />
|
||||||
<option name="LAST_COMMIT_MESSAGE" value="name generation improvement" />
|
<MESSAGE value="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." />
|
||||||
|
<option name="LAST_COMMIT_MESSAGE" value="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." />
|
||||||
</component>
|
</component>
|
||||||
<component name="XDebuggerManager">
|
<component name="XDebuggerManager">
|
||||||
<breakpoint-manager>
|
<breakpoint-manager>
|
||||||
|
|||||||
@@ -0,0 +1,50 @@
|
|||||||
|
# Scout findings
|
||||||
|
|
||||||
|
## Project snapshot
|
||||||
|
- **Tech stack:** SvelteKit + TypeScript + Vite + Tailwind v4.
|
||||||
|
- **App name/branding:** `beA-Edit`.
|
||||||
|
- **Core purpose:** upload one or more ZIP files, extract relevant PDFs/images, preview them, optionally reorder attachments, and download a merged PDF.
|
||||||
|
|
||||||
|
## Main user flow
|
||||||
|
1. User adds ZIP files via drag/drop or file picker (`ZipDropzone`).
|
||||||
|
2. `+page.svelte` starts async extraction for each file.
|
||||||
|
3. Each ZIP is processed into one or more `ProcessedZipArchive` entries.
|
||||||
|
4. `ProcessedZipArchiveEditor` shows thumbnails, allows reordering, and can download the merged PDF.
|
||||||
|
5. `AttachmentPreview` renders image previews directly and PDF previews via `pdfjs-dist`.
|
||||||
|
|
||||||
|
## Important source files
|
||||||
|
- `src/routes/+page.svelte` — top-level page state, file selection, localStorage-backed thumbnail width setting, async ZIP processing.
|
||||||
|
- `src/lib/zip-processing.ts` — archive extraction, recursive nested ZIP handling, metadata ordering, PDF merge/export, image conversion.
|
||||||
|
- `src/lib/services/zip-inflating.service.ts` — thin wrapper around `fflate` unzip.
|
||||||
|
- `src/lib/services/xml-reading.service.ts` — parses XJustiz XML metadata (`sender`, `receiver`, `documentNames`).
|
||||||
|
- `src/lib/components/ZipDropzone.svelte` — file picker + drag/drop ZIP intake.
|
||||||
|
- `src/lib/components/ProcessedZipArchiveEditor.svelte` — archive editor, drag reordering, download button.
|
||||||
|
- `src/lib/components/AttachmentPreview.svelte` — preview generation for images/PDFs.
|
||||||
|
- `src/lib/components/Card.svelte` — generic page container.
|
||||||
|
|
||||||
|
## ZIP/PDF processing details
|
||||||
|
- ZIP entries are filtered to ignore macOS metadata (`__MACOSX`, `.DS_Store`, `._*`).
|
||||||
|
- Relevant attachments are PDFs and common image formats.
|
||||||
|
- Nested ZIP archives are processed recursively.
|
||||||
|
- `xjustiz_nachricht.xml` is used to derive archive naming and ordering.
|
||||||
|
- Ordering prefers document names from metadata before falling back to original attachment order.
|
||||||
|
- PDF merging uses `pdf-lib`; images are embedded as PNG/JPG, with browser-side canvas conversion for other image types.
|
||||||
|
|
||||||
|
## UI/state details
|
||||||
|
- Thumbnail width is stored in `localStorage` under `thumbnailWidth` and clamped to `100..400`.
|
||||||
|
- Processing progress is shown as a count of pending ZIP files.
|
||||||
|
- The editor supports manual attachment reordering via drag-and-drop.
|
||||||
|
|
||||||
|
## Dependencies worth noting
|
||||||
|
- `fflate` for ZIP extraction
|
||||||
|
- `pdf-lib` for PDF generation/merging
|
||||||
|
- `pdfjs-dist` for PDF thumbnail previews
|
||||||
|
- `@skeletonlabs/skeleton` / `@skeletonlabs/skeleton-svelte` for styling components
|
||||||
|
|
||||||
|
## Current working tree observations
|
||||||
|
- `agents.md` is newly created.
|
||||||
|
- Existing local changes are present in:
|
||||||
|
- `src/lib/zip-processing.ts`
|
||||||
|
- `src/routes/+page.svelte`
|
||||||
|
- New/untracked directory present:
|
||||||
|
- `src/lib/services/`
|
||||||
@@ -0,0 +1,93 @@
|
|||||||
|
export type XJustizMetadata = {
|
||||||
|
sender: string;
|
||||||
|
receiver: string;
|
||||||
|
documentNames: string[];
|
||||||
|
};
|
||||||
|
|
||||||
|
const XML_TEXT_DECODER = new TextDecoder();
|
||||||
|
|
||||||
|
const matchesElementName = (element: Element, requestedName: string) => {
|
||||||
|
const requestedLocalName = requestedName.split('.').pop() ?? requestedName;
|
||||||
|
|
||||||
|
return (
|
||||||
|
element.localName === requestedName ||
|
||||||
|
element.localName === requestedLocalName ||
|
||||||
|
element.tagName === requestedName ||
|
||||||
|
element.tagName === requestedLocalName ||
|
||||||
|
element.tagName.endsWith(`:${requestedLocalName}`)
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const getDirectChildByLocalName = (element: Element, localName: string) =>
|
||||||
|
Array.from(element.children).find((child) => matchesElementName(child, localName)) ?? null;
|
||||||
|
|
||||||
|
const getFirstDescendantByLocalName = (root: Document | Element, localName: string) =>
|
||||||
|
Array.from(root.querySelectorAll('*')).find((element) =>
|
||||||
|
matchesElementName(element, localName)
|
||||||
|
) ?? null;
|
||||||
|
|
||||||
|
const getTextContentByPath = (root: Element, path: string[]) => {
|
||||||
|
let current: Element | null = root;
|
||||||
|
|
||||||
|
for (const localName of path) {
|
||||||
|
current = current ? getDirectChildByLocalName(current, localName) : null;
|
||||||
|
|
||||||
|
if (!current) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const textContent = current.textContent?.trim();
|
||||||
|
|
||||||
|
return textContent ? textContent : null;
|
||||||
|
};
|
||||||
|
|
||||||
|
const getTextContentByLocalName = (root: Document | Element, localName: string) =>
|
||||||
|
getFirstDescendantByLocalName(root, localName)?.textContent?.trim() ?? null;
|
||||||
|
|
||||||
|
const parseXmlDocument = (xmlBytes: Uint8Array) => {
|
||||||
|
const parser = new DOMParser();
|
||||||
|
const document = parser.parseFromString(XML_TEXT_DECODER.decode(xmlBytes), 'application/xml');
|
||||||
|
|
||||||
|
if (document.querySelector('parsererror')) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return document;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const parseXJustizMetadata = (xmlBytes: Uint8Array): XJustizMetadata | null => {
|
||||||
|
const document = parseXmlDocument(xmlBytes);
|
||||||
|
|
||||||
|
if (!document) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const sender = getTextContentByLocalName(document, 'aktenzeichen.absender') ?? '';
|
||||||
|
const receiver = getTextContentByLocalName(document, 'aktenzeichen.empfaenger') ?? '';
|
||||||
|
const schriftgutobjekte = getFirstDescendantByLocalName(document, 'schriftgutobjekte');
|
||||||
|
|
||||||
|
if (!schriftgutobjekte) {
|
||||||
|
return {
|
||||||
|
sender,
|
||||||
|
receiver,
|
||||||
|
documentNames: []
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
const dokumentNodes = Array.from(schriftgutobjekte.children).filter((child) =>
|
||||||
|
matchesElementName(child, 'dokument')
|
||||||
|
);
|
||||||
|
|
||||||
|
const documentNames = dokumentNodes
|
||||||
|
.map((dokument) =>
|
||||||
|
getTextContentByPath(dokument, ['xjustiz.fachspezifischeDaten', 'datei', 'dateiname'])
|
||||||
|
)
|
||||||
|
.filter((name): name is string => Boolean(name));
|
||||||
|
|
||||||
|
return {
|
||||||
|
sender,
|
||||||
|
receiver,
|
||||||
|
documentNames
|
||||||
|
};
|
||||||
|
};
|
||||||
@@ -0,0 +1,13 @@
|
|||||||
|
import { unzip } from 'fflate';
|
||||||
|
|
||||||
|
export const unzipArchive = (data: Uint8Array) =>
|
||||||
|
new Promise<Record<string, Uint8Array>>((resolve, reject) => {
|
||||||
|
unzip(data, (error, files) => {
|
||||||
|
if (error) {
|
||||||
|
reject(error);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
resolve(files);
|
||||||
|
});
|
||||||
|
});
|
||||||
+36
-146
@@ -1,6 +1,8 @@
|
|||||||
import { unzip } from 'fflate';
|
|
||||||
import { PDFDocument } from 'pdf-lib';
|
import { PDFDocument } from 'pdf-lib';
|
||||||
|
|
||||||
|
import { parseXJustizMetadata, type XJustizMetadata } from './services/xml-reading.service';
|
||||||
|
import { unzipArchive } from './services/zip-inflating.service';
|
||||||
|
|
||||||
export type ZipAttachmentKind = 'pdf' | 'image';
|
export type ZipAttachmentKind = 'pdf' | 'image';
|
||||||
|
|
||||||
export type ZipAttachment = {
|
export type ZipAttachment = {
|
||||||
@@ -15,12 +17,6 @@ export type ProcessedZipArchive = {
|
|||||||
attachments: ZipAttachment[];
|
attachments: ZipAttachment[];
|
||||||
};
|
};
|
||||||
|
|
||||||
type XJustizMetadata = {
|
|
||||||
sender: string;
|
|
||||||
receiver: string;
|
|
||||||
documentNames: string[];
|
|
||||||
};
|
|
||||||
|
|
||||||
const IMAGE_EXTENSIONS = new Set([
|
const IMAGE_EXTENSIONS = new Set([
|
||||||
'.avif',
|
'.avif',
|
||||||
'.bmp',
|
'.bmp',
|
||||||
@@ -36,22 +32,22 @@ const IMAGE_EXTENSIONS = new Set([
|
|||||||
]);
|
]);
|
||||||
|
|
||||||
const ZIP_META_FILE_NAME = 'xjustiz_nachricht.xml';
|
const ZIP_META_FILE_NAME = 'xjustiz_nachricht.xml';
|
||||||
const XML_TEXT_DECODER = new TextDecoder();
|
|
||||||
|
|
||||||
const unzipArchive = (data: Uint8Array) =>
|
|
||||||
new Promise<Record<string, Uint8Array>>((resolve, reject) => {
|
|
||||||
unzip(data, (error, files) => {
|
|
||||||
if (error) {
|
|
||||||
reject(error);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
resolve(files);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
const getBaseName = (path: string) => path.split('/').pop() ?? path;
|
const getBaseName = (path: string) => path.split('/').pop() ?? path;
|
||||||
|
|
||||||
|
const getCurrentDateString = (date: Date) =>
|
||||||
|
`${date.getFullYear()}-${String(date.getMonth() + 1).padStart(2, '0')}-${String(date.getDate()).padStart(2, '0')}`;
|
||||||
|
|
||||||
|
const createArchiveName = (date: Date, metadata: XJustizMetadata | null) => {
|
||||||
|
const datePrefix = getCurrentDateString(date);
|
||||||
|
|
||||||
|
if (!metadata) {
|
||||||
|
return `${datePrefix}_unbekannt_unbekannt.pdf`;
|
||||||
|
}
|
||||||
|
|
||||||
|
return `${datePrefix}_${metadata.sender}_${metadata.receiver}.pdf`;
|
||||||
|
};
|
||||||
|
|
||||||
const getExtension = (path: string) => {
|
const getExtension = (path: string) => {
|
||||||
const baseName = getBaseName(path).toLowerCase();
|
const baseName = getBaseName(path).toLowerCase();
|
||||||
const lastDotIndex = baseName.lastIndexOf('.');
|
const lastDotIndex = baseName.lastIndexOf('.');
|
||||||
@@ -78,93 +74,6 @@ const isMacMetadataEntry = (path: string) => {
|
|||||||
baseName.startsWith('._')
|
baseName.startsWith('._')
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
const matchesElementName = (element: Element, requestedName: string) => {
|
|
||||||
const requestedLocalName = requestedName.split('.').pop() ?? requestedName;
|
|
||||||
|
|
||||||
return (
|
|
||||||
element.localName === requestedName ||
|
|
||||||
element.localName === requestedLocalName ||
|
|
||||||
element.tagName === requestedName ||
|
|
||||||
element.tagName === requestedLocalName ||
|
|
||||||
element.tagName.endsWith(`:${requestedLocalName}`)
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
const getDirectChildByLocalName = (element: Element, localName: string) =>
|
|
||||||
Array.from(element.children).find((child) => matchesElementName(child, localName)) ?? null;
|
|
||||||
|
|
||||||
const getFirstDescendantByLocalName = (root: Document | Element, localName: string) =>
|
|
||||||
Array.from(root.querySelectorAll('*')).find((element) =>
|
|
||||||
matchesElementName(element, localName)
|
|
||||||
) ?? null;
|
|
||||||
|
|
||||||
const getTextContentByPath = (root: Element, path: string[]) => {
|
|
||||||
let current: Element | null = root;
|
|
||||||
|
|
||||||
for (const localName of path) {
|
|
||||||
current = current ? getDirectChildByLocalName(current, localName) : null;
|
|
||||||
|
|
||||||
if (!current) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const textContent = current.textContent?.trim();
|
|
||||||
|
|
||||||
return textContent ? textContent : null;
|
|
||||||
};
|
|
||||||
|
|
||||||
const getTextContentByLocalName = (root: Document | Element, localName: string) =>
|
|
||||||
getFirstDescendantByLocalName(root, localName)?.textContent?.trim() ?? null;
|
|
||||||
|
|
||||||
const parseXmlDocument = (xmlBytes: Uint8Array) => {
|
|
||||||
const parser = new DOMParser();
|
|
||||||
const document = parser.parseFromString(XML_TEXT_DECODER.decode(xmlBytes), 'application/xml');
|
|
||||||
|
|
||||||
if (document.querySelector('parsererror')) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
return document;
|
|
||||||
};
|
|
||||||
|
|
||||||
const parseXJustizMetadata = (xmlBytes: Uint8Array): XJustizMetadata | null => {
|
|
||||||
const document = parseXmlDocument(xmlBytes);
|
|
||||||
|
|
||||||
if (!document) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
const sender = getTextContentByLocalName(document, 'aktenzeichen.absender') ?? '';
|
|
||||||
const receiver = getTextContentByLocalName(document, 'aktenzeichen.empfaenger') ?? '';
|
|
||||||
const schriftgutobjekte = getFirstDescendantByLocalName(document, 'schriftgutobjekte');
|
|
||||||
|
|
||||||
if (!schriftgutobjekte) {
|
|
||||||
return {
|
|
||||||
sender,
|
|
||||||
receiver,
|
|
||||||
documentNames: []
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
const dokumentNodes = Array.from(schriftgutobjekte.children).filter((child) =>
|
|
||||||
matchesElementName(child, 'dokument')
|
|
||||||
);
|
|
||||||
|
|
||||||
const documentNames = dokumentNodes
|
|
||||||
.map((dokument) =>
|
|
||||||
getTextContentByPath(dokument, ['xjustiz.fachspezifischeDaten', 'datei', 'dateiname'])
|
|
||||||
)
|
|
||||||
.filter((name): name is string => Boolean(name));
|
|
||||||
|
|
||||||
return {
|
|
||||||
sender,
|
|
||||||
receiver,
|
|
||||||
documentNames
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
const orderAttachmentsByDocumentNames = (attachments: ZipAttachment[], documentNames: string[]) => {
|
const orderAttachmentsByDocumentNames = (attachments: ZipAttachment[], documentNames: string[]) => {
|
||||||
const buckets = new Map<string, ZipAttachment[]>();
|
const buckets = new Map<string, ZipAttachment[]>();
|
||||||
|
|
||||||
@@ -216,61 +125,48 @@ const orderAttachmentsByDocumentNames = (attachments: ZipAttachment[], documentN
|
|||||||
return orderedAttachments;
|
return orderedAttachments;
|
||||||
};
|
};
|
||||||
|
|
||||||
const extractZipEntries = async (
|
const shouldSkipZipEntry = (path: string) => isMacMetadataEntry(path);
|
||||||
archiveBytes: Uint8Array,
|
|
||||||
_fallbackName: string
|
const isMetadataFile = (path: string) => getBaseName(path).toLowerCase() === ZIP_META_FILE_NAME;
|
||||||
): Promise<ProcessedZipArchive[]> => {
|
|
||||||
|
const createAttachment = (path: string, data: Uint8Array): ZipAttachment => ({
|
||||||
|
name: getBaseName(path),
|
||||||
|
path,
|
||||||
|
kind: isPdf(path) ? 'pdf' : 'image',
|
||||||
|
data
|
||||||
|
});
|
||||||
|
|
||||||
|
const extractZipEntries = async (archiveBytes: Uint8Array): Promise<ProcessedZipArchive[]> => {
|
||||||
const files = await unzipArchive(archiveBytes);
|
const files = await unzipArchive(archiveBytes);
|
||||||
const attachments: ZipAttachment[] = [];
|
const attachments: ZipAttachment[] = [];
|
||||||
const nestedArchives: ProcessedZipArchive[] = [];
|
const nestedArchives: ProcessedZipArchive[] = [];
|
||||||
let xjustizNachrichtXml: Uint8Array | null = null;
|
let xjustizNachrichtXml: Uint8Array | null = null;
|
||||||
|
|
||||||
for (const [path, data] of Object.entries(files)) {
|
for (const [path, data] of Object.entries(files)) {
|
||||||
const baseName = getBaseName(path);
|
if (shouldSkipZipEntry(path)) {
|
||||||
|
|
||||||
if (isMacMetadataEntry(path)) {
|
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (baseName.toLowerCase() === ZIP_META_FILE_NAME) {
|
if (isMetadataFile(path)) {
|
||||||
xjustizNachrichtXml = data;
|
xjustizNachrichtXml = data;
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (isZipArchive(path)) {
|
if (isZipArchive(path)) {
|
||||||
const childArchives = await extractZipEntries(data, baseName);
|
const childArchives = await extractZipEntries(data);
|
||||||
nestedArchives.push(...childArchives);
|
nestedArchives.push(...childArchives);
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (isPdf(path)) {
|
if (isPdf(path) || isImage(path)) {
|
||||||
attachments.push({
|
attachments.push(createAttachment(path, data));
|
||||||
name: baseName,
|
|
||||||
path,
|
|
||||||
kind: 'pdf',
|
|
||||||
data
|
|
||||||
});
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (isImage(path)) {
|
|
||||||
attachments.push({
|
|
||||||
name: baseName,
|
|
||||||
path,
|
|
||||||
kind: 'image',
|
|
||||||
data
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const metadata = xjustizNachrichtXml ? parseXJustizMetadata(xjustizNachrichtXml) : null;
|
const metadata = xjustizNachrichtXml ? parseXJustizMetadata(xjustizNachrichtXml) : null;
|
||||||
const documentNames = metadata?.documentNames ?? [];
|
const documentNames = metadata?.documentNames ?? [];
|
||||||
const orderedAttachments = orderAttachmentsByDocumentNames(attachments, documentNames);
|
const orderedAttachments = orderAttachmentsByDocumentNames(attachments, documentNames);
|
||||||
const now = new Date();
|
const archiveName = createArchiveName(new Date(), metadata);
|
||||||
const date = `${now.getFullYear()}-${String(now.getMonth() + 1).padStart(2, '0')}-${String(now.getDate()).padStart(2, '0')}`;
|
|
||||||
const archiveName = metadata
|
|
||||||
? `${date}_${metadata.sender}_${metadata.receiver}.pdf`
|
|
||||||
: `${date}_unbekannt_unbekannt.pdf`;
|
|
||||||
const archives: ProcessedZipArchive[] = [];
|
const archives: ProcessedZipArchive[] = [];
|
||||||
|
|
||||||
if (orderedAttachments.length > 0) {
|
if (orderedAttachments.length > 0) {
|
||||||
@@ -286,7 +182,7 @@ const extractZipEntries = async (
|
|||||||
export const extractZipArchives = async (file: File): Promise<ProcessedZipArchive[]> => {
|
export const extractZipArchives = async (file: File): Promise<ProcessedZipArchive[]> => {
|
||||||
const archiveBytes = new Uint8Array(await file.arrayBuffer());
|
const archiveBytes = new Uint8Array(await file.arrayBuffer());
|
||||||
|
|
||||||
return extractZipEntries(archiveBytes, file.name);
|
return extractZipEntries(archiveBytes);
|
||||||
};
|
};
|
||||||
|
|
||||||
const loadImageElement = (src: string) =>
|
const loadImageElement = (src: string) =>
|
||||||
@@ -361,9 +257,7 @@ const embedAttachmentImage = async (pdfDocument: PDFDocument, attachment: ZipAtt
|
|||||||
};
|
};
|
||||||
|
|
||||||
const mergeAttachmentIntoPdf = async (mergedPdf: PDFDocument, attachment: ZipAttachment) => {
|
const mergeAttachmentIntoPdf = async (mergedPdf: PDFDocument, attachment: ZipAttachment) => {
|
||||||
const canLoadAsPdf = attachment.kind === 'pdf'
|
if (attachment.kind === 'pdf') {
|
||||||
|
|
||||||
if (canLoadAsPdf) {
|
|
||||||
const sourcePdf = await PDFDocument.load(attachment.data, { ignoreEncryption: true });
|
const sourcePdf = await PDFDocument.load(attachment.data, { ignoreEncryption: true });
|
||||||
const copiedPages = await mergedPdf.copyPages(sourcePdf, sourcePdf.getPageIndices());
|
const copiedPages = await mergedPdf.copyPages(sourcePdf, sourcePdf.getPageIndices());
|
||||||
|
|
||||||
@@ -385,10 +279,6 @@ const mergeAttachmentIntoPdf = async (mergedPdf: PDFDocument, attachment: ZipAtt
|
|||||||
height: page.getHeight()
|
height: page.getHeight()
|
||||||
});
|
});
|
||||||
} catch (error) {
|
} 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;
|
throw error;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -107,7 +107,7 @@
|
|||||||
<div class="absolute right-0 z-10 mt-2 w-64 rounded-xl border border-primary-100 bg-white p-4 shadow-lg">
|
<div class="absolute right-0 z-10 mt-2 w-64 rounded-xl border border-primary-100 bg-white p-4 shadow-lg">
|
||||||
<label class="flex flex-col gap-2">
|
<label class="flex flex-col gap-2">
|
||||||
<div class="flex items-center justify-between gap-3 text-sm font-medium text-primary-900">
|
<div class="flex items-center justify-between gap-3 text-sm font-medium text-primary-900">
|
||||||
<span>Thumbnail width</span>
|
<span>Thumbnail Breite</span>
|
||||||
<span class="tabular-nums text-primary-700">{thumbnailWidth}px</span>
|
<span class="tabular-nums text-primary-700">{thumbnailWidth}px</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user