1 parent
d71b25f834
commit
b7969b4c9e
7 files changed
+426
-10
No files matched your search
@@ -1,10 +1,10 @@
|
||||
import Root from "./card.svelte";
|
||||
import Content from "./card-content.svelte";
|
||||
import Description from "./card-description.svelte";
|
||||
import Footer from "./card-footer.svelte";
|
||||
import Header from "./card-header.svelte";
|
||||
import Title from "./card-title.svelte";
|
||||
import Action from "./card-action.svelte";
|
||||
import Root from './card.svelte';
|
||||
import Content from './card-content.svelte';
|
||||
import Description from './card-description.svelte';
|
||||
import Footer from './card-footer.svelte';
|
||||
import Header from './card-header.svelte';
|
||||
import Title from './card-title.svelte';
|
||||
import Action from './card-action.svelte';
|
||||
|
||||
export {
|
||||
Root,
|
||||
@@ -21,5 +21,5 @@ export {
|
||||
Footer as CardFooter,
|
||||
Header as CardHeader,
|
||||
Title as CardTitle,
|
||||
Action as CardAction,
|
||||
Action as CardAction
|
||||
};
|
||||
@@ -0,0 +1,108 @@
|
||||
import { PDFDocument } from 'pdf-lib';
|
||||
|
||||
export type PdfMetadataInfo = {
|
||||
title: string;
|
||||
author: string;
|
||||
subject: string;
|
||||
keywords: string[];
|
||||
creator: string;
|
||||
producer: string;
|
||||
creationDate: Date | null;
|
||||
modificationDate: Date | null;
|
||||
pageCount: number;
|
||||
embeddedFileSuspected: boolean;
|
||||
};
|
||||
|
||||
export type PdfMetadataResult =
|
||||
| { status: 'ok'; metadata: PdfMetadataInfo }
|
||||
| { status: 'encrypted' };
|
||||
|
||||
// The pdf-lib error class for encrypted documents is not exported from the
|
||||
// package root, so we identify it by its message instead.
|
||||
const isEncryptedPdfError = (error: unknown) =>
|
||||
error instanceof Error && error.message.includes('is encrypted');
|
||||
|
||||
const safeRead = <T>(read: () => T | undefined): T | undefined => {
|
||||
try {
|
||||
return read();
|
||||
} catch {
|
||||
return undefined;
|
||||
}
|
||||
};
|
||||
|
||||
const toDateOrNull = (value: Date | undefined): Date | null =>
|
||||
value && !Number.isNaN(value.getTime()) ? value : null;
|
||||
|
||||
// Finds an ASCII marker in raw bytes without decoding the whole file. The
|
||||
// search for '/EmbeddedFile' also matches '/EmbeddedFiles' as a prefix.
|
||||
const containsAsciiMarker = (bytes: Uint8Array, marker: string): boolean => {
|
||||
const firstByte = marker.charCodeAt(0);
|
||||
for (let index = 0; index <= bytes.length - marker.length; index += 1) {
|
||||
if (bytes[index] !== firstByte) continue;
|
||||
let matched = true;
|
||||
for (let offset = 1; offset < marker.length; offset += 1) {
|
||||
if (bytes[index + offset] !== marker.charCodeAt(offset)) {
|
||||
matched = false;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (matched) return true;
|
||||
}
|
||||
return false;
|
||||
};
|
||||
|
||||
export const hasEmbeddedFileMarkers = (bytes: Uint8Array): boolean =>
|
||||
containsAsciiMarker(bytes, '/EmbeddedFile');
|
||||
|
||||
export const readPdfMetadata = async (bytes: Uint8Array): Promise<PdfMetadataResult> => {
|
||||
const embeddedFileSuspected = hasEmbeddedFileMarkers(bytes);
|
||||
|
||||
let document: PDFDocument;
|
||||
try {
|
||||
document = await PDFDocument.load(bytes, { updateMetadata: false });
|
||||
} catch (error) {
|
||||
if (isEncryptedPdfError(error)) return { status: 'encrypted' };
|
||||
throw error;
|
||||
}
|
||||
|
||||
const keywords = safeRead(() => document.getKeywords());
|
||||
|
||||
return {
|
||||
status: 'ok',
|
||||
metadata: {
|
||||
title: safeRead(() => document.getTitle()) ?? '',
|
||||
author: safeRead(() => document.getAuthor()) ?? '',
|
||||
subject: safeRead(() => document.getSubject()) ?? '',
|
||||
// PDF keyword lists have no canonical separator; split only on
|
||||
// explicit punctuation so multi-word keywords survive.
|
||||
keywords: keywords
|
||||
? keywords
|
||||
.split(/[;,]/)
|
||||
.map((keyword) => keyword.trim())
|
||||
.filter(Boolean)
|
||||
: [],
|
||||
creator: safeRead(() => document.getCreator()) ?? '',
|
||||
producer: safeRead(() => document.getProducer()) ?? '',
|
||||
creationDate: toDateOrNull(safeRead(() => document.getCreationDate())),
|
||||
modificationDate: toDateOrNull(safeRead(() => document.getModificationDate())),
|
||||
pageCount: document.getPageCount(),
|
||||
embeddedFileSuspected
|
||||
}
|
||||
};
|
||||
};
|
||||
|
||||
export const stripPdfMetadata = async (bytes: Uint8Array): Promise<Uint8Array> => {
|
||||
const document = await PDFDocument.load(bytes, { updateMetadata: false });
|
||||
const now = new Date();
|
||||
|
||||
document.setTitle('');
|
||||
document.setAuthor('');
|
||||
document.setSubject('');
|
||||
document.setKeywords([]);
|
||||
document.setCreator('');
|
||||
document.setProducer('beA-Edit');
|
||||
document.setCreationDate(now);
|
||||
document.setModificationDate(now);
|
||||
|
||||
return document.save();
|
||||
};
|
||||
@@ -59,5 +59,11 @@ export const tools: Tool[] = [
|
||||
title: 'PDFs zusammenfügen',
|
||||
description: 'Fügen Sie mehrere PDFs zu einer Datei zusammen.',
|
||||
icon: 'merge'
|
||||
},
|
||||
{
|
||||
slug: 'metadata',
|
||||
title: 'Metadaten',
|
||||
description: 'Zeigen Sie Metadaten eines PDFs an und entfernen Sie sie.',
|
||||
icon: 'info'
|
||||
}
|
||||
];
|
||||
Reference in new issue
Block a user