adds agents.md, splits zip-processing.ts
This commit is contained in:
@@ -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 { parseXJustizMetadata, type XJustizMetadata } from './services/xml-reading.service';
|
||||
import { unzipArchive } from './services/zip-inflating.service';
|
||||
|
||||
export type ZipAttachmentKind = 'pdf' | 'image';
|
||||
|
||||
export type ZipAttachment = {
|
||||
@@ -15,12 +17,6 @@ export type ProcessedZipArchive = {
|
||||
attachments: ZipAttachment[];
|
||||
};
|
||||
|
||||
type XJustizMetadata = {
|
||||
sender: string;
|
||||
receiver: string;
|
||||
documentNames: string[];
|
||||
};
|
||||
|
||||
const IMAGE_EXTENSIONS = new Set([
|
||||
'.avif',
|
||||
'.bmp',
|
||||
@@ -36,22 +32,22 @@ const IMAGE_EXTENSIONS = new Set([
|
||||
]);
|
||||
|
||||
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 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 baseName = getBaseName(path).toLowerCase();
|
||||
const lastDotIndex = baseName.lastIndexOf('.');
|
||||
@@ -78,93 +74,6 @@ const isMacMetadataEntry = (path: string) => {
|
||||
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 buckets = new Map<string, ZipAttachment[]>();
|
||||
|
||||
@@ -216,61 +125,48 @@ const orderAttachmentsByDocumentNames = (attachments: ZipAttachment[], documentN
|
||||
return orderedAttachments;
|
||||
};
|
||||
|
||||
const extractZipEntries = async (
|
||||
archiveBytes: Uint8Array,
|
||||
_fallbackName: string
|
||||
): Promise<ProcessedZipArchive[]> => {
|
||||
const shouldSkipZipEntry = (path: string) => isMacMetadataEntry(path);
|
||||
|
||||
const isMetadataFile = (path: string) => getBaseName(path).toLowerCase() === ZIP_META_FILE_NAME;
|
||||
|
||||
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 attachments: ZipAttachment[] = [];
|
||||
const nestedArchives: ProcessedZipArchive[] = [];
|
||||
let xjustizNachrichtXml: Uint8Array | null = null;
|
||||
|
||||
for (const [path, data] of Object.entries(files)) {
|
||||
const baseName = getBaseName(path);
|
||||
|
||||
if (isMacMetadataEntry(path)) {
|
||||
if (shouldSkipZipEntry(path)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (baseName.toLowerCase() === ZIP_META_FILE_NAME) {
|
||||
if (isMetadataFile(path)) {
|
||||
xjustizNachrichtXml = data;
|
||||
continue;
|
||||
}
|
||||
|
||||
if (isZipArchive(path)) {
|
||||
const childArchives = await extractZipEntries(data, baseName);
|
||||
const childArchives = await extractZipEntries(data);
|
||||
nestedArchives.push(...childArchives);
|
||||
continue;
|
||||
}
|
||||
|
||||
if (isPdf(path)) {
|
||||
attachments.push({
|
||||
name: baseName,
|
||||
path,
|
||||
kind: 'pdf',
|
||||
data
|
||||
});
|
||||
continue;
|
||||
}
|
||||
|
||||
if (isImage(path)) {
|
||||
attachments.push({
|
||||
name: baseName,
|
||||
path,
|
||||
kind: 'image',
|
||||
data
|
||||
});
|
||||
if (isPdf(path) || isImage(path)) {
|
||||
attachments.push(createAttachment(path, data));
|
||||
}
|
||||
}
|
||||
|
||||
const metadata = xjustizNachrichtXml ? parseXJustizMetadata(xjustizNachrichtXml) : null;
|
||||
const documentNames = metadata?.documentNames ?? [];
|
||||
const orderedAttachments = orderAttachmentsByDocumentNames(attachments, documentNames);
|
||||
const now = new Date();
|
||||
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 archiveName = createArchiveName(new Date(), metadata);
|
||||
const archives: ProcessedZipArchive[] = [];
|
||||
|
||||
if (orderedAttachments.length > 0) {
|
||||
@@ -286,7 +182,7 @@ const extractZipEntries = async (
|
||||
export const extractZipArchives = async (file: File): Promise<ProcessedZipArchive[]> => {
|
||||
const archiveBytes = new Uint8Array(await file.arrayBuffer());
|
||||
|
||||
return extractZipEntries(archiveBytes, file.name);
|
||||
return extractZipEntries(archiveBytes);
|
||||
};
|
||||
|
||||
const loadImageElement = (src: string) =>
|
||||
@@ -361,9 +257,7 @@ const embedAttachmentImage = async (pdfDocument: PDFDocument, attachment: ZipAtt
|
||||
};
|
||||
|
||||
const mergeAttachmentIntoPdf = async (mergedPdf: PDFDocument, attachment: ZipAttachment) => {
|
||||
const canLoadAsPdf = attachment.kind === 'pdf'
|
||||
|
||||
if (canLoadAsPdf) {
|
||||
if (attachment.kind === 'pdf') {
|
||||
const sourcePdf = await PDFDocument.load(attachment.data, { ignoreEncryption: true });
|
||||
const copiedPages = await mergedPdf.copyPages(sourcePdf, sourcePdf.getPageIndices());
|
||||
|
||||
@@ -385,10 +279,6 @@ const mergeAttachmentIntoPdf = async (mergedPdf: PDFDocument, attachment: ZipAtt
|
||||
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;
|
||||
}
|
||||
};
|
||||
|
||||
@@ -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">
|
||||
<label class="flex flex-col gap-2">
|
||||
<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>
|
||||
</div>
|
||||
|
||||
|
||||
Reference in New Issue
Block a user