mirror of
https://github.com/Stirling-Tools/Stirling-PDF.git
synced 2025-09-24 12:36:13 +00:00
Compare commits
No commits in common. "668c47d5a09b8910875116cd7086f5d04a9bbefc" and "55b8455b66cbc40b30da36258e21f2241530b420" have entirely different histories.
668c47d5a0
...
55b8455b66
@ -5,6 +5,7 @@ import { useMetadataExtraction } from "../../../hooks/tools/changeMetadata/useMe
|
|||||||
import DeleteAllStep from "./steps/DeleteAllStep";
|
import DeleteAllStep from "./steps/DeleteAllStep";
|
||||||
import StandardMetadataStep from "./steps/StandardMetadataStep";
|
import StandardMetadataStep from "./steps/StandardMetadataStep";
|
||||||
import DocumentDatesStep from "./steps/DocumentDatesStep";
|
import DocumentDatesStep from "./steps/DocumentDatesStep";
|
||||||
|
import CustomMetadataStep from "./steps/CustomMetadataStep";
|
||||||
import AdvancedOptionsStep from "./steps/AdvancedOptionsStep";
|
import AdvancedOptionsStep from "./steps/AdvancedOptionsStep";
|
||||||
|
|
||||||
interface ChangeMetadataSingleStepProps {
|
interface ChangeMetadataSingleStepProps {
|
||||||
@ -26,10 +27,17 @@ const ChangeMetadataSingleStep = ({
|
|||||||
}: ChangeMetadataSingleStepProps) => {
|
}: ChangeMetadataSingleStepProps) => {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
|
|
||||||
// Extract metadata from uploaded files
|
// Create a params object that matches the hook interface
|
||||||
const { isExtractingMetadata } = useMetadataExtraction({
|
const paramsHook = {
|
||||||
|
parameters,
|
||||||
updateParameter: onParameterChange,
|
updateParameter: onParameterChange,
|
||||||
});
|
addCustomMetadata,
|
||||||
|
removeCustomMetadata,
|
||||||
|
updateCustomMetadata,
|
||||||
|
};
|
||||||
|
|
||||||
|
// Extract metadata from uploaded files
|
||||||
|
const { isExtractingMetadata } = useMetadataExtraction(paramsHook);
|
||||||
|
|
||||||
const isDeleteAllEnabled = parameters.deleteAll;
|
const isDeleteAllEnabled = parameters.deleteAll;
|
||||||
const fieldsDisabled = disabled || isDeleteAllEnabled || isExtractingMetadata;
|
const fieldsDisabled = disabled || isDeleteAllEnabled || isExtractingMetadata;
|
||||||
@ -78,6 +86,23 @@ const ChangeMetadataSingleStep = ({
|
|||||||
|
|
||||||
<Divider />
|
<Divider />
|
||||||
|
|
||||||
|
{/* Custom Metadata */}
|
||||||
|
<Stack gap="md">
|
||||||
|
<Text size="sm" fw={500}>
|
||||||
|
{t('changeMetadata.customFields.title', 'Custom Metadata')}
|
||||||
|
</Text>
|
||||||
|
<CustomMetadataStep
|
||||||
|
parameters={parameters}
|
||||||
|
onParameterChange={onParameterChange}
|
||||||
|
disabled={fieldsDisabled}
|
||||||
|
addCustomMetadata={addCustomMetadata}
|
||||||
|
removeCustomMetadata={removeCustomMetadata}
|
||||||
|
updateCustomMetadata={updateCustomMetadata}
|
||||||
|
/>
|
||||||
|
</Stack>
|
||||||
|
|
||||||
|
<Divider />
|
||||||
|
|
||||||
{/* Advanced Options */}
|
{/* Advanced Options */}
|
||||||
<Stack gap="md">
|
<Stack gap="md">
|
||||||
<Text size="sm" fw={500}>
|
<Text size="sm" fw={500}>
|
||||||
@ -87,9 +112,6 @@ const ChangeMetadataSingleStep = ({
|
|||||||
parameters={parameters}
|
parameters={parameters}
|
||||||
onParameterChange={onParameterChange}
|
onParameterChange={onParameterChange}
|
||||||
disabled={fieldsDisabled}
|
disabled={fieldsDisabled}
|
||||||
addCustomMetadata={addCustomMetadata}
|
|
||||||
removeCustomMetadata={removeCustomMetadata}
|
|
||||||
updateCustomMetadata={updateCustomMetadata}
|
|
||||||
/>
|
/>
|
||||||
</Stack>
|
</Stack>
|
||||||
</Stack>
|
</Stack>
|
||||||
|
@ -1,60 +1,38 @@
|
|||||||
import { Stack, Select, Divider } from "@mantine/core";
|
import { Select } from "@mantine/core";
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
import { ChangeMetadataParameters } from "../../../../hooks/tools/changeMetadata/useChangeMetadataParameters";
|
import { ChangeMetadataParameters } from "../../../../hooks/tools/changeMetadata/useChangeMetadataParameters";
|
||||||
import { TrappedStatus } from "../../../../types/metadata";
|
import { TrappedStatus } from "../../../../types/metadata";
|
||||||
import CustomMetadataStep from "./CustomMetadataStep";
|
|
||||||
|
|
||||||
interface AdvancedOptionsStepProps {
|
interface AdvancedOptionsStepProps {
|
||||||
parameters: ChangeMetadataParameters;
|
parameters: ChangeMetadataParameters;
|
||||||
onParameterChange: <K extends keyof ChangeMetadataParameters>(key: K, value: ChangeMetadataParameters[K]) => void;
|
onParameterChange: <K extends keyof ChangeMetadataParameters>(key: K, value: ChangeMetadataParameters[K]) => void;
|
||||||
disabled?: boolean;
|
disabled?: boolean;
|
||||||
addCustomMetadata: (key?: string, value?: string) => void;
|
|
||||||
removeCustomMetadata: (id: string) => void;
|
|
||||||
updateCustomMetadata: (id: string, key: string, value: string) => void;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const AdvancedOptionsStep = ({
|
const AdvancedOptionsStep = ({
|
||||||
parameters,
|
parameters,
|
||||||
onParameterChange,
|
onParameterChange,
|
||||||
disabled = false,
|
disabled = false
|
||||||
addCustomMetadata,
|
|
||||||
removeCustomMetadata,
|
|
||||||
updateCustomMetadata
|
|
||||||
}: AdvancedOptionsStepProps) => {
|
}: AdvancedOptionsStepProps) => {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Stack gap="md">
|
<Select
|
||||||
{/* Trapped Status */}
|
label={t('changeMetadata.trapped.label', 'Trapped Status')}
|
||||||
<Select
|
description={t('changeMetadata.trapped.description', 'Indicates whether the document has been trapped for high-quality printing')}
|
||||||
label={t('changeMetadata.trapped.label', 'Trapped Status')}
|
value={parameters.trapped}
|
||||||
description={t('changeMetadata.trapped.description', 'Indicates whether the document has been trapped for high-quality printing')}
|
onChange={(value) => {
|
||||||
value={parameters.trapped}
|
if (value) {
|
||||||
onChange={(value) => {
|
onParameterChange('trapped', value as TrappedStatus);
|
||||||
if (value) {
|
}
|
||||||
onParameterChange('trapped', value as TrappedStatus);
|
}}
|
||||||
}
|
disabled={disabled || parameters.deleteAll}
|
||||||
}}
|
data={[
|
||||||
disabled={disabled || parameters.deleteAll}
|
{ value: TrappedStatus.UNKNOWN, label: t('changeMetadata.trapped.unknown', 'Unknown') },
|
||||||
data={[
|
{ value: TrappedStatus.TRUE, label: t('changeMetadata.trapped.true', 'True') },
|
||||||
{ value: TrappedStatus.UNKNOWN, label: t('changeMetadata.trapped.unknown', 'Unknown') },
|
{ value: TrappedStatus.FALSE, label: t('changeMetadata.trapped.false', 'False') }
|
||||||
{ value: TrappedStatus.TRUE, label: t('changeMetadata.trapped.true', 'True') },
|
]}
|
||||||
{ value: TrappedStatus.FALSE, label: t('changeMetadata.trapped.false', 'False') }
|
/>
|
||||||
]}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<Divider />
|
|
||||||
|
|
||||||
{/* Custom Metadata */}
|
|
||||||
<CustomMetadataStep
|
|
||||||
parameters={parameters}
|
|
||||||
onParameterChange={onParameterChange}
|
|
||||||
disabled={disabled}
|
|
||||||
addCustomMetadata={addCustomMetadata}
|
|
||||||
removeCustomMetadata={removeCustomMetadata}
|
|
||||||
updateCustomMetadata={updateCustomMetadata}
|
|
||||||
/>
|
|
||||||
</Stack>
|
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -98,15 +98,6 @@ export const useAdvancedOptionsTips = (): TooltipContent => {
|
|||||||
t("changeMetadata.tooltip.advanced.trapped.bullet2", "False: Document has not been trapped"),
|
t("changeMetadata.tooltip.advanced.trapped.bullet2", "False: Document has not been trapped"),
|
||||||
t("changeMetadata.tooltip.advanced.trapped.bullet3", "Unknown: Trapped status is not specified")
|
t("changeMetadata.tooltip.advanced.trapped.bullet3", "Unknown: Trapped status is not specified")
|
||||||
]
|
]
|
||||||
},
|
|
||||||
{
|
|
||||||
title: t("changeMetadata.tooltip.customFields.title", "Custom Metadata"),
|
|
||||||
description: t("changeMetadata.tooltip.customFields.text", "Add your own custom key-value metadata pairs."),
|
|
||||||
bullets: [
|
|
||||||
t("changeMetadata.tooltip.customFields.bullet1", "Add any custom fields relevant to your document"),
|
|
||||||
t("changeMetadata.tooltip.customFields.bullet2", "Examples: Department, Project, Version, Status"),
|
|
||||||
t("changeMetadata.tooltip.customFields.bullet3", "Both key and value are required for each entry")
|
|
||||||
]
|
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
};
|
};
|
||||||
|
@ -1,63 +1,53 @@
|
|||||||
import { useState, useEffect, useRef } from "react";
|
import { useState, useEffect } from "react";
|
||||||
import { extractPDFMetadata } from "../../../services/pdfMetadataService";
|
import { PDFMetadataService } from "../../../services/pdfMetadataService";
|
||||||
import { useSelectedFiles } from "../../../contexts/file/fileHooks";
|
import { useSelectedFiles } from "../../../contexts/file/fileHooks";
|
||||||
import { ChangeMetadataParameters } from "./useChangeMetadataParameters";
|
import { ChangeMetadataParametersHook } from "./useChangeMetadataParameters";
|
||||||
|
|
||||||
interface MetadataExtractionParams {
|
export const useMetadataExtraction = (params: ChangeMetadataParametersHook) => {
|
||||||
updateParameter: <K extends keyof ChangeMetadataParameters>(key: K, value: ChangeMetadataParameters[K]) => void;
|
|
||||||
}
|
|
||||||
|
|
||||||
export const useMetadataExtraction = (params: MetadataExtractionParams) => {
|
|
||||||
const { selectedFiles } = useSelectedFiles();
|
const { selectedFiles } = useSelectedFiles();
|
||||||
const [isExtractingMetadata, setIsExtractingMetadata] = useState(false);
|
const [isExtractingMetadata, setIsExtractingMetadata] = useState(false);
|
||||||
const [hasExtractedMetadata, setHasExtractedMetadata] = useState(false);
|
const [hasExtractedMetadata, setHasExtractedMetadata] = useState(false);
|
||||||
const previousFileCountRef = useRef(0);
|
|
||||||
|
|
||||||
// Reset extraction state only when files are cleared (length goes to 0)
|
|
||||||
useEffect(() => {
|
|
||||||
if (previousFileCountRef.current > 0 && selectedFiles.length === 0) {
|
|
||||||
setHasExtractedMetadata(false);
|
|
||||||
}
|
|
||||||
previousFileCountRef.current = selectedFiles.length;
|
|
||||||
}, [selectedFiles]);
|
|
||||||
|
|
||||||
// Extract metadata from first file when files change
|
// Extract metadata from first file when files change
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const extractMetadata = async () => {
|
const extractMetadata = async () => {
|
||||||
if (selectedFiles.length === 0) {
|
if (selectedFiles.length === 0 || hasExtractedMetadata) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const firstFile = selectedFiles[0];
|
|
||||||
|
|
||||||
if (hasExtractedMetadata) {
|
const firstFile = selectedFiles[0];
|
||||||
|
if (!firstFile) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
setIsExtractingMetadata(true);
|
setIsExtractingMetadata(true);
|
||||||
|
try {
|
||||||
|
const result = await PDFMetadataService.extractMetadata(firstFile);
|
||||||
|
|
||||||
const result = await extractPDFMetadata(firstFile);
|
if (result.success) {
|
||||||
|
const metadata = result.metadata;
|
||||||
|
|
||||||
if (result.success) {
|
// Pre-populate all fields with extracted metadata
|
||||||
const metadata = result.metadata;
|
params.updateParameter('title', metadata.title);
|
||||||
|
params.updateParameter('author', metadata.author);
|
||||||
|
params.updateParameter('subject', metadata.subject);
|
||||||
|
params.updateParameter('keywords', metadata.keywords);
|
||||||
|
params.updateParameter('creator', metadata.creator);
|
||||||
|
params.updateParameter('producer', metadata.producer);
|
||||||
|
params.updateParameter('creationDate', metadata.creationDate);
|
||||||
|
params.updateParameter('modificationDate', metadata.modificationDate);
|
||||||
|
params.updateParameter('trapped', metadata.trapped);
|
||||||
|
|
||||||
// Pre-populate all fields with extracted metadata
|
// Set custom metadata entries directly to avoid state update timing issues
|
||||||
params.updateParameter('title', metadata.title);
|
params.updateParameter('customMetadata', metadata.customMetadata);
|
||||||
params.updateParameter('author', metadata.author);
|
|
||||||
params.updateParameter('subject', metadata.subject);
|
|
||||||
params.updateParameter('keywords', metadata.keywords);
|
|
||||||
params.updateParameter('creator', metadata.creator);
|
|
||||||
params.updateParameter('producer', metadata.producer);
|
|
||||||
params.updateParameter('creationDate', metadata.creationDate);
|
|
||||||
params.updateParameter('modificationDate', metadata.modificationDate);
|
|
||||||
params.updateParameter('trapped', metadata.trapped);
|
|
||||||
params.updateParameter('customMetadata', metadata.customMetadata);
|
|
||||||
|
|
||||||
setHasExtractedMetadata(true);
|
setHasExtractedMetadata(true);
|
||||||
} else {
|
}
|
||||||
console.warn('Failed to extract metadata:', result.error);
|
} catch (error) {
|
||||||
|
console.warn('Failed to extract metadata:', error);
|
||||||
|
} finally {
|
||||||
|
setIsExtractingMetadata(false);
|
||||||
}
|
}
|
||||||
|
|
||||||
setIsExtractingMetadata(false);
|
|
||||||
};
|
};
|
||||||
|
|
||||||
extractMetadata();
|
extractMetadata();
|
||||||
|
@ -1,7 +1,6 @@
|
|||||||
import { pdfWorkerManager } from './pdfWorkerManager';
|
import { pdfWorkerManager } from './pdfWorkerManager';
|
||||||
import { FileAnalyzer } from './fileAnalyzer';
|
import { FileAnalyzer } from './fileAnalyzer';
|
||||||
import { TrappedStatus, CustomMetadataEntry, ExtractedPDFMetadata } from '../types/metadata';
|
import { TrappedStatus, CustomMetadataEntry, ExtractedPDFMetadata } from '../types/metadata';
|
||||||
import { PDFDocumentProxy } from 'pdfjs-dist/types/src/display/api';
|
|
||||||
|
|
||||||
export interface MetadataExtractionResult {
|
export interface MetadataExtractionResult {
|
||||||
success: true;
|
success: true;
|
||||||
@ -19,45 +18,49 @@ export type MetadataExtractionResponse = MetadataExtractionResult | MetadataExtr
|
|||||||
* Utility to format PDF date strings to required format (yyyy/MM/dd HH:mm:ss)
|
* Utility to format PDF date strings to required format (yyyy/MM/dd HH:mm:ss)
|
||||||
* Handles PDF date format: "D:YYYYMMDDHHmmSSOHH'mm'" or standard date strings
|
* Handles PDF date format: "D:YYYYMMDDHHmmSSOHH'mm'" or standard date strings
|
||||||
*/
|
*/
|
||||||
function formatPDFDate(dateString: string): string {
|
function formatPDFDate(dateString: unknown): string {
|
||||||
if (!dateString) {
|
if (!dateString || typeof dateString !== 'string') {
|
||||||
return '';
|
return '';
|
||||||
}
|
}
|
||||||
|
|
||||||
let date: Date;
|
try {
|
||||||
|
let date: Date;
|
||||||
|
|
||||||
// Check if it's a PDF date format (starts with "D:")
|
// Check if it's a PDF date format (starts with "D:")
|
||||||
if (dateString.startsWith('D:')) {
|
if (dateString.startsWith('D:')) {
|
||||||
// Parse PDF date format: D:YYYYMMDDHHmmSSOHH'mm'
|
// Parse PDF date format: D:YYYYMMDDHHmmSSOHH'mm'
|
||||||
const dateStr = dateString.substring(2); // Remove "D:"
|
const dateStr = dateString.substring(2); // Remove "D:"
|
||||||
|
|
||||||
// Extract date parts
|
// Extract date parts
|
||||||
const year = parseInt(dateStr.substring(0, 4));
|
const year = parseInt(dateStr.substring(0, 4));
|
||||||
const month = parseInt(dateStr.substring(4, 6));
|
const month = parseInt(dateStr.substring(4, 6));
|
||||||
const day = parseInt(dateStr.substring(6, 8));
|
const day = parseInt(dateStr.substring(6, 8));
|
||||||
const hour = parseInt(dateStr.substring(8, 10)) || 0;
|
const hour = parseInt(dateStr.substring(8, 10)) || 0;
|
||||||
const minute = parseInt(dateStr.substring(10, 12)) || 0;
|
const minute = parseInt(dateStr.substring(10, 12)) || 0;
|
||||||
const second = parseInt(dateStr.substring(12, 14)) || 0;
|
const second = parseInt(dateStr.substring(12, 14)) || 0;
|
||||||
|
|
||||||
// Create date object (month is 0-indexed)
|
// Create date object (month is 0-indexed)
|
||||||
date = new Date(year, month - 1, day, hour, minute, second);
|
date = new Date(year, month - 1, day, hour, minute, second);
|
||||||
} else {
|
} else {
|
||||||
// Try parsing as regular date string
|
// Try parsing as regular date string
|
||||||
date = new Date(dateString);
|
date = new Date(dateString);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (isNaN(date.getTime())) {
|
if (isNaN(date.getTime())) {
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
|
||||||
|
const year = date.getFullYear();
|
||||||
|
const month = String(date.getMonth() + 1).padStart(2, '0');
|
||||||
|
const day = String(date.getDate()).padStart(2, '0');
|
||||||
|
const hours = String(date.getHours()).padStart(2, '0');
|
||||||
|
const minutes = String(date.getMinutes()).padStart(2, '0');
|
||||||
|
const seconds = String(date.getSeconds()).padStart(2, '0');
|
||||||
|
|
||||||
|
return `${year}/${month}/${day} ${hours}:${minutes}:${seconds}`;
|
||||||
|
} catch {
|
||||||
return '';
|
return '';
|
||||||
}
|
}
|
||||||
|
|
||||||
const year = date.getFullYear();
|
|
||||||
const month = String(date.getMonth() + 1).padStart(2, '0');
|
|
||||||
const day = String(date.getDate()).padStart(2, '0');
|
|
||||||
const hours = String(date.getHours()).padStart(2, '0');
|
|
||||||
const minutes = String(date.getMinutes()).padStart(2, '0');
|
|
||||||
const seconds = String(date.getSeconds()).padStart(2, '0');
|
|
||||||
|
|
||||||
return `${year}/${month}/${day} ${hours}:${minutes}:${seconds}`;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -77,14 +80,14 @@ function convertTrappedStatus(trapped: unknown): TrappedStatus {
|
|||||||
* Extract custom metadata fields from PDF.js info object
|
* Extract custom metadata fields from PDF.js info object
|
||||||
* Custom metadata is nested under the "Custom" key
|
* Custom metadata is nested under the "Custom" key
|
||||||
*/
|
*/
|
||||||
function extractCustomMetadata(custom: unknown): CustomMetadataEntry[] {
|
function extractCustomMetadata(info: Record<string, unknown>): CustomMetadataEntry[] {
|
||||||
const customMetadata: CustomMetadataEntry[] = [];
|
const customMetadata: CustomMetadataEntry[] = [];
|
||||||
let customIdCounter = 1;
|
let customIdCounter = 1;
|
||||||
|
|
||||||
|
|
||||||
// Check if there's a Custom object containing the custom metadata
|
// Check if there's a Custom object containing the custom metadata
|
||||||
if (typeof custom === 'object' && custom !== null) {
|
if (info.Custom && typeof info.Custom === 'object' && info.Custom !== null) {
|
||||||
const customObj = custom as Record<string, unknown>;
|
const customObj = info.Custom as Record<string, unknown>;
|
||||||
|
|
||||||
Object.entries(customObj).forEach(([key, value]) => {
|
Object.entries(customObj).forEach(([key, value]) => {
|
||||||
if (value != null && value !== '') {
|
if (value != null && value !== '') {
|
||||||
@ -102,80 +105,71 @@ function extractCustomMetadata(custom: unknown): CustomMetadataEntry[] {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Safely cleanup PDF document with error handling
|
* Service to extract metadata from PDF files using PDF.js
|
||||||
*/
|
*/
|
||||||
function cleanupPdfDocument(pdfDoc: PDFDocumentProxy | null): void {
|
export class PDFMetadataService {
|
||||||
if (pdfDoc) {
|
/**
|
||||||
|
* Extract all metadata from a PDF file
|
||||||
|
* Returns a result object with success/error state
|
||||||
|
*/
|
||||||
|
static async extractMetadata(file: File): Promise<MetadataExtractionResponse> {
|
||||||
|
// Use existing PDF validation
|
||||||
|
const isValidPDF = await FileAnalyzer.isValidPDF(file);
|
||||||
|
if (!isValidPDF) {
|
||||||
|
return {
|
||||||
|
success: false,
|
||||||
|
error: 'File is not a valid PDF'
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
let pdfDoc: any = null;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
pdfWorkerManager.destroyDocument(pdfDoc);
|
const arrayBuffer = await file.arrayBuffer();
|
||||||
} catch (cleanupError) {
|
pdfDoc = await pdfWorkerManager.createDocument(arrayBuffer, {
|
||||||
console.warn('Failed to cleanup PDF document:', cleanupError);
|
disableAutoFetch: true,
|
||||||
|
disableStream: true
|
||||||
|
});
|
||||||
|
|
||||||
|
const metadata = await pdfDoc.getMetadata();
|
||||||
|
const info = metadata.info || {};
|
||||||
|
|
||||||
|
// Safely extract metadata with proper type checking
|
||||||
|
const extractedMetadata: ExtractedPDFMetadata = {
|
||||||
|
title: typeof info.Title === 'string' ? info.Title : '',
|
||||||
|
author: typeof info.Author === 'string' ? info.Author : '',
|
||||||
|
subject: typeof info.Subject === 'string' ? info.Subject : '',
|
||||||
|
keywords: typeof info.Keywords === 'string' ? info.Keywords : '',
|
||||||
|
creator: typeof info.Creator === 'string' ? info.Creator : '',
|
||||||
|
producer: typeof info.Producer === 'string' ? info.Producer : '',
|
||||||
|
creationDate: formatPDFDate(info.CreationDate),
|
||||||
|
modificationDate: formatPDFDate(info.ModDate),
|
||||||
|
trapped: convertTrappedStatus(info.Trapped),
|
||||||
|
customMetadata: extractCustomMetadata(info)
|
||||||
|
};
|
||||||
|
|
||||||
|
return {
|
||||||
|
success: true,
|
||||||
|
metadata: extractedMetadata
|
||||||
|
};
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
const errorMessage = error instanceof Error ? error.message : 'Unknown error';
|
||||||
|
|
||||||
|
return {
|
||||||
|
success: false,
|
||||||
|
error: `Failed to extract PDF metadata: ${errorMessage}`
|
||||||
|
};
|
||||||
|
|
||||||
|
} finally {
|
||||||
|
// Ensure cleanup even if extraction fails
|
||||||
|
if (pdfDoc) {
|
||||||
|
try {
|
||||||
|
pdfWorkerManager.destroyDocument(pdfDoc);
|
||||||
|
} catch (cleanupError) {
|
||||||
|
console.warn('Failed to cleanup PDF document:', cleanupError);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function getStringMetadata(info: Record<string, unknown>, key: string): string {
|
|
||||||
if (typeof info[key] === 'string') {
|
|
||||||
return info[key];
|
|
||||||
} else {
|
|
||||||
return '';
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Extract all metadata from a PDF file
|
|
||||||
* Returns a result object with success/error state
|
|
||||||
*/
|
|
||||||
export async function extractPDFMetadata(file: File): Promise<MetadataExtractionResponse> {
|
|
||||||
// Use existing PDF validation
|
|
||||||
const isValidPDF = await FileAnalyzer.isValidPDF(file);
|
|
||||||
if (!isValidPDF) {
|
|
||||||
return {
|
|
||||||
success: false,
|
|
||||||
error: 'File is not a valid PDF'
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
let pdfDoc: PDFDocumentProxy | null = null;
|
|
||||||
let arrayBuffer: ArrayBuffer;
|
|
||||||
let metadata;
|
|
||||||
|
|
||||||
try {
|
|
||||||
arrayBuffer = await file.arrayBuffer();
|
|
||||||
pdfDoc = await pdfWorkerManager.createDocument(arrayBuffer, {
|
|
||||||
disableAutoFetch: true,
|
|
||||||
disableStream: true
|
|
||||||
});
|
|
||||||
metadata = await pdfDoc.getMetadata();
|
|
||||||
} catch (error) {
|
|
||||||
const errorMessage = error instanceof Error ? error.message : 'Unknown error';
|
|
||||||
cleanupPdfDocument(pdfDoc);
|
|
||||||
return {
|
|
||||||
success: false,
|
|
||||||
error: `Failed to read PDF: ${errorMessage}`
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
const info = metadata.info as Record<string, unknown>;
|
|
||||||
|
|
||||||
// Safely extract metadata with proper type checking
|
|
||||||
const extractedMetadata: ExtractedPDFMetadata = {
|
|
||||||
title: getStringMetadata(info, 'Title'),
|
|
||||||
author: getStringMetadata(info, 'Author'),
|
|
||||||
subject: getStringMetadata(info, 'Subject'),
|
|
||||||
keywords: getStringMetadata(info, 'Keywords'),
|
|
||||||
creator: getStringMetadata(info, 'Creator'),
|
|
||||||
producer: getStringMetadata(info, 'Producer'),
|
|
||||||
creationDate: formatPDFDate(getStringMetadata(info, 'CreationDate')),
|
|
||||||
modificationDate: formatPDFDate(getStringMetadata(info, 'ModDate')),
|
|
||||||
trapped: convertTrappedStatus(info.Trapped),
|
|
||||||
customMetadata: extractCustomMetadata(info.Custom),
|
|
||||||
};
|
|
||||||
|
|
||||||
cleanupPdfDocument(pdfDoc);
|
|
||||||
|
|
||||||
return {
|
|
||||||
success: true,
|
|
||||||
metadata: extractedMetadata
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
@ -6,12 +6,11 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
import * as pdfjsLib from 'pdfjs-dist';
|
import * as pdfjsLib from 'pdfjs-dist';
|
||||||
import { PDFDocumentProxy } from 'pdfjs-dist/types/src/display/api';
|
|
||||||
const { getDocument, GlobalWorkerOptions } = pdfjsLib;
|
const { getDocument, GlobalWorkerOptions } = pdfjsLib;
|
||||||
|
|
||||||
class PDFWorkerManager {
|
class PDFWorkerManager {
|
||||||
private static instance: PDFWorkerManager;
|
private static instance: PDFWorkerManager;
|
||||||
private activeDocuments = new Set<PDFDocumentProxy>();
|
private activeDocuments = new Set<any>();
|
||||||
private workerCount = 0;
|
private workerCount = 0;
|
||||||
private maxWorkers = 10; // Limit concurrent workers
|
private maxWorkers = 10; // Limit concurrent workers
|
||||||
private isInitialized = false;
|
private isInitialized = false;
|
||||||
@ -49,7 +48,7 @@ class PDFWorkerManager {
|
|||||||
stopAtErrors?: boolean;
|
stopAtErrors?: boolean;
|
||||||
verbosity?: number;
|
verbosity?: number;
|
||||||
} = {}
|
} = {}
|
||||||
): Promise<PDFDocumentProxy> {
|
): Promise<any> {
|
||||||
// Wait if we've hit the worker limit
|
// Wait if we've hit the worker limit
|
||||||
if (this.activeDocuments.size >= this.maxWorkers) {
|
if (this.activeDocuments.size >= this.maxWorkers) {
|
||||||
await this.waitForAvailableWorker();
|
await this.waitForAvailableWorker();
|
||||||
@ -105,7 +104,7 @@ class PDFWorkerManager {
|
|||||||
/**
|
/**
|
||||||
* Properly destroy a PDF document and clean up resources
|
* Properly destroy a PDF document and clean up resources
|
||||||
*/
|
*/
|
||||||
destroyDocument(pdf: PDFDocumentProxy): void {
|
destroyDocument(pdf: any): void {
|
||||||
if (this.activeDocuments.has(pdf)) {
|
if (this.activeDocuments.has(pdf)) {
|
||||||
try {
|
try {
|
||||||
pdf.destroy();
|
pdf.destroy();
|
||||||
|
@ -1,9 +1,10 @@
|
|||||||
import { useState } from "react";
|
import { useState, useEffect } from "react";
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
import { createToolFlow } from "../components/tools/shared/createToolFlow";
|
import { createToolFlow } from "../components/tools/shared/createToolFlow";
|
||||||
import DeleteAllStep from "../components/tools/changeMetadata/steps/DeleteAllStep";
|
import DeleteAllStep from "../components/tools/changeMetadata/steps/DeleteAllStep";
|
||||||
import StandardMetadataStep from "../components/tools/changeMetadata/steps/StandardMetadataStep";
|
import StandardMetadataStep from "../components/tools/changeMetadata/steps/StandardMetadataStep";
|
||||||
import DocumentDatesStep from "../components/tools/changeMetadata/steps/DocumentDatesStep";
|
import DocumentDatesStep from "../components/tools/changeMetadata/steps/DocumentDatesStep";
|
||||||
|
import CustomMetadataStep from "../components/tools/changeMetadata/steps/CustomMetadataStep";
|
||||||
import AdvancedOptionsStep from "../components/tools/changeMetadata/steps/AdvancedOptionsStep";
|
import AdvancedOptionsStep from "../components/tools/changeMetadata/steps/AdvancedOptionsStep";
|
||||||
import { useChangeMetadataParameters } from "../hooks/tools/changeMetadata/useChangeMetadataParameters";
|
import { useChangeMetadataParameters } from "../hooks/tools/changeMetadata/useChangeMetadataParameters";
|
||||||
import { useChangeMetadataOperation } from "../hooks/tools/changeMetadata/useChangeMetadataOperation";
|
import { useChangeMetadataOperation } from "../hooks/tools/changeMetadata/useChangeMetadataOperation";
|
||||||
@ -14,6 +15,7 @@ import {
|
|||||||
useDeleteAllTips,
|
useDeleteAllTips,
|
||||||
useStandardMetadataTips,
|
useStandardMetadataTips,
|
||||||
useDocumentDatesTips,
|
useDocumentDatesTips,
|
||||||
|
useCustomMetadataTips,
|
||||||
useAdvancedOptionsTips
|
useAdvancedOptionsTips
|
||||||
} from "../components/tooltips/useChangeMetadataTips";
|
} from "../components/tooltips/useChangeMetadataTips";
|
||||||
|
|
||||||
@ -24,12 +26,14 @@ const ChangeMetadata = (props: BaseToolProps) => {
|
|||||||
const deleteAllTips = useDeleteAllTips();
|
const deleteAllTips = useDeleteAllTips();
|
||||||
const standardMetadataTips = useStandardMetadataTips();
|
const standardMetadataTips = useStandardMetadataTips();
|
||||||
const documentDatesTips = useDocumentDatesTips();
|
const documentDatesTips = useDocumentDatesTips();
|
||||||
|
const customMetadataTips = useCustomMetadataTips();
|
||||||
const advancedOptionsTips = useAdvancedOptionsTips();
|
const advancedOptionsTips = useAdvancedOptionsTips();
|
||||||
|
|
||||||
// Individual step collapse states
|
// Individual step collapse states
|
||||||
const [deleteAllCollapsed, setDeleteAllCollapsed] = useState(false);
|
const [deleteAllCollapsed, setDeleteAllCollapsed] = useState(false);
|
||||||
const [standardMetadataCollapsed, setStandardMetadataCollapsed] = useState(false);
|
const [standardMetadataCollapsed, setStandardMetadataCollapsed] = useState(false);
|
||||||
const [documentDatesCollapsed, setDocumentDatesCollapsed] = useState(true);
|
const [documentDatesCollapsed, setDocumentDatesCollapsed] = useState(true);
|
||||||
|
const [customMetadataCollapsed, setCustomMetadataCollapsed] = useState(true);
|
||||||
const [advancedOptionsCollapsed, setAdvancedOptionsCollapsed] = useState(true);
|
const [advancedOptionsCollapsed, setAdvancedOptionsCollapsed] = useState(true);
|
||||||
|
|
||||||
const base = useBaseTool(
|
const base = useBaseTool(
|
||||||
@ -98,6 +102,24 @@ const ChangeMetadata = (props: BaseToolProps) => {
|
|||||||
/>
|
/>
|
||||||
),
|
),
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
title: t("changeMetadata.customFields.title", "Custom Metadata"),
|
||||||
|
isCollapsed: getActualCollapsedState(customMetadataCollapsed),
|
||||||
|
onCollapsedClick: base.hasResults
|
||||||
|
? (base.settingsCollapsed ? base.handleSettingsReset : undefined)
|
||||||
|
: () => setCustomMetadataCollapsed(!customMetadataCollapsed),
|
||||||
|
tooltip: customMetadataTips,
|
||||||
|
content: (
|
||||||
|
<CustomMetadataStep
|
||||||
|
parameters={base.params.parameters}
|
||||||
|
onParameterChange={base.params.updateParameter}
|
||||||
|
disabled={base.endpointLoading || base.params.parameters.deleteAll || isExtractingMetadata}
|
||||||
|
addCustomMetadata={base.params.addCustomMetadata}
|
||||||
|
removeCustomMetadata={base.params.removeCustomMetadata}
|
||||||
|
updateCustomMetadata={base.params.updateCustomMetadata}
|
||||||
|
/>
|
||||||
|
),
|
||||||
|
},
|
||||||
{
|
{
|
||||||
title: t("changeMetadata.advanced.title", "Advanced Options"),
|
title: t("changeMetadata.advanced.title", "Advanced Options"),
|
||||||
isCollapsed: getActualCollapsedState(advancedOptionsCollapsed),
|
isCollapsed: getActualCollapsedState(advancedOptionsCollapsed),
|
||||||
@ -110,9 +132,6 @@ const ChangeMetadata = (props: BaseToolProps) => {
|
|||||||
parameters={base.params.parameters}
|
parameters={base.params.parameters}
|
||||||
onParameterChange={base.params.updateParameter}
|
onParameterChange={base.params.updateParameter}
|
||||||
disabled={base.endpointLoading || isExtractingMetadata}
|
disabled={base.endpointLoading || isExtractingMetadata}
|
||||||
addCustomMetadata={base.params.addCustomMetadata}
|
|
||||||
removeCustomMetadata={base.params.removeCustomMetadata}
|
|
||||||
updateCustomMetadata={base.params.updateCustomMetadata}
|
|
||||||
/>
|
/>
|
||||||
),
|
),
|
||||||
},
|
},
|
||||||
|
Loading…
x
Reference in New Issue
Block a user