mirror of
https://github.com/Frooodle/Stirling-PDF.git
synced 2026-02-17 13:52:14 +01:00
get all info on PDF, needs a few more tidy-ups
This commit is contained in:
@@ -0,0 +1,127 @@
|
||||
import React, { useEffect, useMemo, useRef } from 'react';
|
||||
import { Badge, Group, Stack, Text } from '@mantine/core';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import type { PdfInfoReportData, PdfInfoReportEntry } from '@app/types/getPdfInfo';
|
||||
import '@app/components/tools/validateSignature/reportView/styles.css';
|
||||
import SummarySection from './sections/SummarySection';
|
||||
import KeyValueSection from './sections/KeyValueSection';
|
||||
import TableOfContentsSection from './sections/TableOfContentsSection';
|
||||
import OtherSection from './sections/OtherSection';
|
||||
import PerPageSection from './sections/PerPageSection';
|
||||
|
||||
interface GetPdfInfoReportViewProps {
|
||||
data: PdfInfoReportData & { scrollTo?: string | null };
|
||||
}
|
||||
|
||||
const GetPdfInfoReportView: React.FC<GetPdfInfoReportViewProps> = ({ data }) => {
|
||||
const { t } = useTranslation();
|
||||
const containerRef = useRef<HTMLDivElement>(null);
|
||||
const entry: PdfInfoReportEntry | null = data.entries[0] ?? null;
|
||||
|
||||
useEffect(() => {
|
||||
if (!data.scrollTo) return;
|
||||
const idMap: Record<string, string> = {
|
||||
metadata: 'metadata',
|
||||
formFields: 'formFields',
|
||||
basicInfo: 'basicInfo',
|
||||
documentInfo: 'documentInfo',
|
||||
compliance: 'compliance',
|
||||
encryption: 'encryption',
|
||||
permissions: 'permissions',
|
||||
toc: 'toc',
|
||||
other: 'other',
|
||||
perPage: 'perPage',
|
||||
};
|
||||
const anchor = idMap[data.scrollTo];
|
||||
if (!anchor) return;
|
||||
const el = containerRef.current?.querySelector<HTMLElement>(`#${anchor}`);
|
||||
if (el) {
|
||||
el.scrollIntoView({ behavior: 'smooth', block: 'start' });
|
||||
}
|
||||
}, [data.scrollTo]);
|
||||
|
||||
const sections = useMemo(() => {
|
||||
const raw = entry?.data ?? {};
|
||||
return {
|
||||
metadata: (raw as any)['Metadata'] as Record<string, unknown> | undefined,
|
||||
formFields: (raw as any)['FormFields'] ?? (raw as any)['Form Fields'],
|
||||
basicInfo: (raw as any)['BasicInfo'] ?? (raw as any)['Basic Info'],
|
||||
documentInfo: (raw as any)['DocumentInfo'] ?? (raw as any)['Document Info'],
|
||||
compliance: (raw as any)['Compliancy'] ?? (raw as any)['Compliance'],
|
||||
encryption: (raw as any)['Encryption'] as Record<string, unknown> | undefined,
|
||||
permissions: (raw as any)['Permissions'] as Record<string, unknown> | undefined,
|
||||
toc: (raw as any)['Bookmarks/Outline/TOC'] ?? (raw as any)['Table of Contents'],
|
||||
other: (raw as any)['Other'] as Record<string, unknown> | undefined,
|
||||
perPage: (raw as any)['PerPageInfo'] ?? (raw as any)['Per Page Info'],
|
||||
summaryData: (raw as any)['SummaryData'] as Record<string, unknown> | undefined,
|
||||
};
|
||||
}, [entry]);
|
||||
|
||||
if (!entry) {
|
||||
return (
|
||||
<div className="report-container">
|
||||
<Stack gap="md" align="center">
|
||||
<Badge color="gray" variant="light">No Data</Badge>
|
||||
<Text size="sm" c="dimmed">Run the tool to generate the report.</Text>
|
||||
</Stack>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="report-container" ref={containerRef}>
|
||||
<Stack gap="xl" align="center">
|
||||
<Stack gap="xs" align="center">
|
||||
<Badge size="lg" color="blue" variant="light">
|
||||
{t('getPdfInfo.report.title', 'PDF Information View')}
|
||||
</Badge>
|
||||
<Text size="sm" c="dimmed">
|
||||
{t('getPdfInfo.report.generatedAt', 'Generated')}{' '}
|
||||
{new Date(data.generatedAt).toLocaleString()}
|
||||
</Text>
|
||||
</Stack>
|
||||
|
||||
<div className="simulated-page">
|
||||
<Stack gap="lg">
|
||||
<Group justify="space-between" align="flex-start">
|
||||
<div>
|
||||
<Text fw={700} size="xl" style={{ lineHeight: 1.1 }}>
|
||||
{entry.fileName}
|
||||
</Text>
|
||||
<Text size="sm" c="dimmed">
|
||||
{t('getPdfInfo.report.entryLabel', 'Full information summary')}
|
||||
</Text>
|
||||
</div>
|
||||
</Group>
|
||||
|
||||
<SummarySection sections={sections as any} />
|
||||
|
||||
<KeyValueSection title={t('getPdfInfo.sections.metadata', 'Metadata')} anchorId="metadata" obj={sections.metadata ?? null} />
|
||||
|
||||
<KeyValueSection title={t('getPdfInfo.sections.formFields', 'Form Fields')} anchorId="formFields" obj={sections.formFields as any} />
|
||||
|
||||
<KeyValueSection title={t('getPdfInfo.sections.basicInfo', 'Basic Info')} anchorId="basicInfo" obj={sections.basicInfo ?? null} />
|
||||
|
||||
<KeyValueSection title={t('getPdfInfo.sections.documentInfo', 'Document Info')} anchorId="documentInfo" obj={sections.documentInfo ?? null} />
|
||||
|
||||
<KeyValueSection title={t('getPdfInfo.sections.compliance', 'Compliance')} anchorId="compliance" obj={sections.compliance ?? null} />
|
||||
|
||||
<KeyValueSection title={t('getPdfInfo.sections.encryption', 'Encryption')} anchorId="encryption" obj={sections.encryption ?? null} />
|
||||
|
||||
<KeyValueSection title={t('getPdfInfo.sections.permissions', 'Permissions')} anchorId="permissions" obj={sections.permissions ?? null} />
|
||||
|
||||
<TableOfContentsSection anchorId="toc" tocArray={Array.isArray(sections.toc) ? (sections.toc as any[]) : []} />
|
||||
|
||||
<OtherSection anchorId="other" other={sections.other as any} />
|
||||
|
||||
<PerPageSection anchorId="perPage" perPage={sections.perPage as any} />
|
||||
</Stack>
|
||||
</div>
|
||||
</Stack>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default GetPdfInfoReportView;
|
||||
|
||||
|
||||
@@ -0,0 +1,79 @@
|
||||
import { useCallback, useMemo } from 'react';
|
||||
import { Alert, Button, Group, Loader, Stack, Text } from '@mantine/core';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import type { GetPdfInfoOperationHook } from '@app/hooks/tools/getPdfInfo/useGetPdfInfoOperation';
|
||||
|
||||
interface GetPdfInfoResultsProps {
|
||||
operation: GetPdfInfoOperationHook;
|
||||
isLoading: boolean;
|
||||
errorMessage: string | null;
|
||||
}
|
||||
|
||||
const findFileByExtension = (files: File[], extension: string) => {
|
||||
return files.find((file) => file.name.toLowerCase().endsWith(extension));
|
||||
};
|
||||
|
||||
const GetPdfInfoResults = ({ operation, isLoading, errorMessage }: GetPdfInfoResultsProps) => {
|
||||
const { t } = useTranslation();
|
||||
|
||||
const jsonFile = useMemo(() => findFileByExtension(operation.files, '.json'), [operation.files]);
|
||||
const selectedFile = useMemo(() => jsonFile ?? null, [jsonFile]);
|
||||
const selectedDownloadLabel = useMemo(() => t('getPdfInfo.downloadJson', 'Download JSON'), [t]);
|
||||
|
||||
const handleDownload = useCallback((file: File) => {
|
||||
const blobUrl = URL.createObjectURL(file);
|
||||
const link = document.createElement('a');
|
||||
link.href = blobUrl;
|
||||
link.download = file.name;
|
||||
document.body.appendChild(link);
|
||||
link.click();
|
||||
document.body.removeChild(link);
|
||||
URL.revokeObjectURL(blobUrl);
|
||||
}, []);
|
||||
|
||||
if (isLoading && operation.results.length === 0) {
|
||||
return (
|
||||
<Group justify="center" gap="sm" py="md">
|
||||
<Loader size="sm" />
|
||||
<Text>{t('getPdfInfo.processing', 'Extracting information...')}</Text>
|
||||
</Group>
|
||||
);
|
||||
}
|
||||
|
||||
if (!isLoading && operation.results.length === 0) {
|
||||
return (
|
||||
<Alert color="gray" variant="light" title={t('getPdfInfo.results', 'Results')}>
|
||||
<Text size="sm">{t('getPdfInfo.noResults', 'Run the tool to generate a report.')}</Text>
|
||||
</Alert>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<Stack gap="md">
|
||||
{/* No background post-processing once JSON is ready */}
|
||||
{errorMessage && (
|
||||
<Alert color="yellow" variant="light">
|
||||
<Text size="sm">{errorMessage}</Text>
|
||||
</Alert>
|
||||
)}
|
||||
|
||||
<Stack gap="xs">
|
||||
<Text size="sm" fw={600}>
|
||||
{t('getPdfInfo.downloads', 'Downloads')}
|
||||
</Text>
|
||||
<Button
|
||||
color="blue"
|
||||
onClick={() => selectedFile && handleDownload(selectedFile)}
|
||||
disabled={!selectedFile}
|
||||
fullWidth
|
||||
>
|
||||
{selectedDownloadLabel}
|
||||
</Button>
|
||||
</Stack>
|
||||
</Stack>
|
||||
);
|
||||
};
|
||||
|
||||
export default GetPdfInfoResults;
|
||||
|
||||
|
||||
@@ -0,0 +1,22 @@
|
||||
import React from 'react';
|
||||
import SectionBlock from '../shared/SectionBlock';
|
||||
import KeyValueList from '../shared/KeyValueList';
|
||||
|
||||
interface KeyValueSectionProps {
|
||||
title: string;
|
||||
anchorId: string;
|
||||
obj?: Record<string, unknown> | null;
|
||||
emptyLabel?: string;
|
||||
}
|
||||
|
||||
const KeyValueSection: React.FC<KeyValueSectionProps> = ({ title, anchorId, obj, emptyLabel }) => {
|
||||
return (
|
||||
<SectionBlock title={title} anchorId={anchorId}>
|
||||
<KeyValueList obj={obj} emptyLabel={emptyLabel} />
|
||||
</SectionBlock>
|
||||
);
|
||||
};
|
||||
|
||||
export default KeyValueSection;
|
||||
|
||||
|
||||
@@ -0,0 +1,82 @@
|
||||
import React from 'react';
|
||||
import { Accordion, Code, Stack, Text } from '@mantine/core';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import SectionBlock from '../shared/SectionBlock';
|
||||
import SimpleArrayList from '../shared/SimpleArrayList';
|
||||
|
||||
interface OtherSectionProps {
|
||||
anchorId: string;
|
||||
other?: Record<string, any> | null;
|
||||
}
|
||||
|
||||
const OtherSection: React.FC<OtherSectionProps> = ({ anchorId, other }) => {
|
||||
const { t } = useTranslation();
|
||||
const panelBg = 'var(--bg-raised)';
|
||||
const panelText = 'var(--text-primary)';
|
||||
return (
|
||||
<SectionBlock title={t('getPdfInfo.sections.other', 'Other')} anchorId={anchorId}>
|
||||
<Stack gap="sm">
|
||||
<Stack gap={6}>
|
||||
<Text fw={600} size="sm">{t('getPdfInfo.other.attachments', 'Attachments')}</Text>
|
||||
<SimpleArrayList arr={Array.isArray(other?.Attachments) ? other?.Attachments : []} />
|
||||
</Stack>
|
||||
<Stack gap={6}>
|
||||
<Text fw={600} size="sm">{t('getPdfInfo.other.embeddedFiles', 'Embedded Files')}</Text>
|
||||
<SimpleArrayList arr={Array.isArray(other?.EmbeddedFiles) ? other?.EmbeddedFiles : []} />
|
||||
</Stack>
|
||||
<Stack gap={6}>
|
||||
<Text fw={600} size="sm">{t('getPdfInfo.other.javaScript', 'JavaScript')}</Text>
|
||||
<SimpleArrayList arr={Array.isArray(other?.JavaScript) ? other?.JavaScript : []} />
|
||||
</Stack>
|
||||
<Stack gap={6}>
|
||||
<Text fw={600} size="sm">{t('getPdfInfo.other.layers', 'Layers')}</Text>
|
||||
<SimpleArrayList arr={Array.isArray(other?.Layers) ? other?.Layers : []} />
|
||||
</Stack>
|
||||
<Accordion variant="separated" radius="md" defaultValue="">
|
||||
<Accordion.Item value="structureTree">
|
||||
<Accordion.Control>
|
||||
<Text fw={600} size="sm">{t('getPdfInfo.other.structureTree', 'StructureTree')}</Text>
|
||||
</Accordion.Control>
|
||||
<Accordion.Panel>
|
||||
{Array.isArray(other?.StructureTree) && other?.StructureTree.length > 0
|
||||
? <Code
|
||||
block
|
||||
style={{
|
||||
whiteSpace: 'pre-wrap',
|
||||
backgroundColor: panelBg,
|
||||
color: panelText
|
||||
}}
|
||||
>
|
||||
{JSON.stringify(other?.StructureTree, null, 2)}
|
||||
</Code>
|
||||
: <Text size="sm" c="dimmed">{t('getPdfInfo.noneDetected', 'None detected')}</Text>}
|
||||
</Accordion.Panel>
|
||||
</Accordion.Item>
|
||||
<Accordion.Item value="xmp">
|
||||
<Accordion.Control>
|
||||
<Text fw={600} size="sm">{t('getPdfInfo.other.xmp', 'XMPMetadata')}</Text>
|
||||
</Accordion.Control>
|
||||
<Accordion.Panel>
|
||||
{other?.XMPMetadata
|
||||
? <Code
|
||||
block
|
||||
style={{
|
||||
whiteSpace: 'pre-wrap',
|
||||
backgroundColor: panelBg,
|
||||
color: panelText
|
||||
}}
|
||||
>
|
||||
{String(other?.XMPMetadata)}
|
||||
</Code>
|
||||
: <Text size="sm" c="dimmed">{t('getPdfInfo.noneDetected', 'None detected')}</Text>}
|
||||
</Accordion.Panel>
|
||||
</Accordion.Item>
|
||||
</Accordion>
|
||||
</Stack>
|
||||
</SectionBlock>
|
||||
);
|
||||
};
|
||||
|
||||
export default OtherSection;
|
||||
|
||||
|
||||
@@ -0,0 +1,101 @@
|
||||
import React from 'react';
|
||||
import { Accordion, Group, Stack, Text } from '@mantine/core';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import SectionBlock from '../shared/SectionBlock';
|
||||
import KeyValueList from '../shared/KeyValueList';
|
||||
import SimpleArrayList from '../shared/SimpleArrayList';
|
||||
|
||||
interface PerPageSectionProps {
|
||||
anchorId: string;
|
||||
perPage?: Record<string, any> | null;
|
||||
}
|
||||
|
||||
const PerPageSection: React.FC<PerPageSectionProps> = ({ anchorId, perPage }) => {
|
||||
const { t } = useTranslation();
|
||||
const panelBg = 'var(--bg-raised)';
|
||||
const panelText = 'var(--text-primary)';
|
||||
|
||||
return (
|
||||
<SectionBlock title={t('getPdfInfo.sections.perPageInfo', 'Per Page Info')} anchorId={anchorId}>
|
||||
{perPage && Object.keys(perPage as any).length > 0 ? (
|
||||
<Accordion variant="separated" radius="md" defaultValue="">
|
||||
{Object.entries(perPage as any).map(([pageLabel, pageInfo]: [string, any]) => (
|
||||
<Accordion.Item key={pageLabel} value={pageLabel}>
|
||||
<Accordion.Control>
|
||||
<Group justify="space-between" w="100%" gap="xs">
|
||||
<Text fw={600} size="sm">{pageLabel}</Text>
|
||||
</Group>
|
||||
</Accordion.Control>
|
||||
<Accordion.Panel>
|
||||
<div style={{ backgroundColor: panelBg, color: panelText, borderRadius: 8, padding: 12 }}>
|
||||
<Stack gap="sm">
|
||||
{pageInfo?.Size && (
|
||||
<Stack gap={4}>
|
||||
<Text size="sm" fw={600}>{t('getPdfInfo.perPage.size', 'Size')}</Text>
|
||||
<KeyValueList obj={pageInfo.Size} />
|
||||
</Stack>
|
||||
)}
|
||||
<KeyValueList obj={{
|
||||
'Rotation': pageInfo?.Rotation,
|
||||
'Page Orientation': pageInfo?.['Page Orientation'],
|
||||
'MediaBox': pageInfo?.MediaBox,
|
||||
'CropBox': pageInfo?.CropBox,
|
||||
'BleedBox': pageInfo?.BleedBox,
|
||||
'TrimBox': pageInfo?.TrimBox,
|
||||
'ArtBox': pageInfo?.ArtBox,
|
||||
'Text Characters Count': pageInfo?.['Text Characters Count'],
|
||||
}} />
|
||||
{pageInfo?.Annotations && (
|
||||
<Stack gap={4}>
|
||||
<Text size="sm" fw={600}>{t('getPdfInfo.perPage.annotations', 'Annotations')}</Text>
|
||||
<KeyValueList obj={pageInfo.Annotations} />
|
||||
</Stack>
|
||||
)}
|
||||
<Stack gap={4}>
|
||||
<Text size="sm" fw={600}>{t('getPdfInfo.perPage.images', 'Images')}</Text>
|
||||
<SimpleArrayList arr={Array.isArray(pageInfo?.Images) ? pageInfo.Images : []} />
|
||||
</Stack>
|
||||
<Stack gap={4}>
|
||||
<Text size="sm" fw={600}>{t('getPdfInfo.perPage.links', 'Links')}</Text>
|
||||
<SimpleArrayList arr={Array.isArray(pageInfo?.Links) ? pageInfo.Links : []} />
|
||||
</Stack>
|
||||
<Stack gap={4}>
|
||||
<Text size="sm" fw={600}>{t('getPdfInfo.perPage.fonts', 'Fonts')}</Text>
|
||||
{Array.isArray(pageInfo?.Fonts) && pageInfo.Fonts.length > 0
|
||||
? (
|
||||
<Stack gap={4}>
|
||||
{pageInfo.Fonts.map((f: any, idx: number) => (
|
||||
<Text key={idx} size="sm" c="dimmed">
|
||||
{`${f?.Name ?? 'Unknown'}${f?.IsEmbedded ? ' (embedded)' : ''}`}
|
||||
</Text>
|
||||
))}
|
||||
</Stack>
|
||||
)
|
||||
: <Text size="sm" c="dimmed">{t('getPdfInfo.noneDetected', 'None detected')}</Text>}
|
||||
</Stack>
|
||||
{pageInfo?.XObjectCounts && (
|
||||
<Stack gap={4}>
|
||||
<Text size="sm" fw={600}>{t('getPdfInfo.perPage.xobjects', 'XObject Counts')}</Text>
|
||||
<KeyValueList obj={pageInfo.XObjectCounts} />
|
||||
</Stack>
|
||||
)}
|
||||
<Stack gap={4}>
|
||||
<Text size="sm" fw={600}>{t('getPdfInfo.perPage.multimedia', 'Multimedia')}</Text>
|
||||
<SimpleArrayList arr={Array.isArray(pageInfo?.Multimedia) ? pageInfo.Multimedia : []} />
|
||||
</Stack>
|
||||
</Stack>
|
||||
</div>
|
||||
</Accordion.Panel>
|
||||
</Accordion.Item>
|
||||
))}
|
||||
</Accordion>
|
||||
) : (
|
||||
<Text size="sm" c="dimmed">{t('getPdfInfo.noneDetected', 'None detected')}</Text>
|
||||
)}
|
||||
</SectionBlock>
|
||||
);
|
||||
};
|
||||
|
||||
export default PerPageSection;
|
||||
|
||||
|
||||
@@ -0,0 +1,156 @@
|
||||
import React, { useMemo } from 'react';
|
||||
import { Stack, Text } from '@mantine/core';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import SectionBlock from '../shared/SectionBlock';
|
||||
import KeyValueList from '../shared/KeyValueList';
|
||||
|
||||
type SectionsData = {
|
||||
metadata?: Record<string, unknown>;
|
||||
formFields?: Record<string, unknown>;
|
||||
basicInfo?: Record<string, any>;
|
||||
documentInfo?: Record<string, any>;
|
||||
compliance?: Record<string, any>;
|
||||
encryption?: Record<string, any>;
|
||||
permissions?: Record<string, any>;
|
||||
toc?: any;
|
||||
other?: Record<string, any>;
|
||||
perPage?: Record<string, any>;
|
||||
summaryData?: Record<string, any>;
|
||||
};
|
||||
|
||||
interface SummarySectionProps {
|
||||
sections: SectionsData;
|
||||
}
|
||||
|
||||
const SummarySection: React.FC<SummarySectionProps> = ({ sections }) => {
|
||||
const { t } = useTranslation();
|
||||
|
||||
const summaryBlocks = useMemo(() => {
|
||||
const basic = (sections.basicInfo as any) || {};
|
||||
const docInfo = (sections.documentInfo as any) || {};
|
||||
const metadata = (sections.metadata as any) || {};
|
||||
const encryption = (sections.encryption as any) || {};
|
||||
const permissions = (sections.permissions as any) || {};
|
||||
const summary = (sections.summaryData as any) || {};
|
||||
|
||||
const pages = basic['Number of pages'];
|
||||
const fileSizeBytes = basic['FileSizeInBytes'];
|
||||
const pdfVersion = docInfo['PDF version'];
|
||||
const language = basic['Language'];
|
||||
|
||||
const basicInformation: Record<string, unknown> = {
|
||||
[t('getPdfInfo.summary.pages', 'Pages')]: pages,
|
||||
[t('getPdfInfo.summary.fileSize', 'File Size')]: typeof fileSizeBytes === 'number' ? `${(fileSizeBytes / 1024).toFixed(2)} KB` : fileSizeBytes,
|
||||
[t('getPdfInfo.summary.pdfVersion', 'PDF Version')]: pdfVersion,
|
||||
[t('getPdfInfo.summary.language', 'Language')]: language,
|
||||
};
|
||||
|
||||
const documentInformation: Record<string, unknown> = {
|
||||
[t('getPdfInfo.summary.title', 'Title')]: metadata['Title'],
|
||||
[t('getPdfInfo.summary.author', 'Author')]: metadata['Author'],
|
||||
[t('getPdfInfo.summary.created', 'Created')]: metadata['CreationDate'],
|
||||
[t('getPdfInfo.summary.modified', 'Modified')]: metadata['ModificationDate'],
|
||||
};
|
||||
|
||||
let securityStatusText = '';
|
||||
if (encryption?.IsEncrypted) {
|
||||
securityStatusText = t('getPdfInfo.summary.security.encrypted', 'Encrypted PDF - Password protection present');
|
||||
} else {
|
||||
securityStatusText = t('getPdfInfo.summary.security.unencrypted', 'Unencrypted PDF - No password protection');
|
||||
}
|
||||
const restrictedCount = typeof summary?.restrictedPermissionsCount === 'number' ? summary.restrictedPermissionsCount : 0;
|
||||
const permissionsAllAllowed = Object.values(permissions || {}).every((v) => v === 'Allowed');
|
||||
const permSummary = permissionsAllAllowed
|
||||
? t('getPdfInfo.summary.permsAll', 'All Permissions Allowed')
|
||||
: restrictedCount > 0
|
||||
? t('getPdfInfo.summary.permsRestricted', '{{count}} restrictions', { count: restrictedCount })
|
||||
: t('getPdfInfo.summary.permsMixed', 'Some permissions restricted');
|
||||
|
||||
const complianceText = sections.compliance && Object.values(sections.compliance).some(Boolean)
|
||||
? t('getPdfInfo.summary.hasCompliance', 'Has compliance standards')
|
||||
: t('getPdfInfo.summary.noCompliance', 'No Compliance Standards');
|
||||
|
||||
const technical: Record<string, unknown> = {
|
||||
[t('getPdfInfo.summary.tech.images', 'Images')]: (() => {
|
||||
const total = basic['TotalImages'];
|
||||
if (typeof total === 'number') return total === 0 ? 'None' : `${total}`;
|
||||
return 'None';
|
||||
})(),
|
||||
[t('getPdfInfo.summary.tech.fonts', 'Fonts')]: (() => {
|
||||
const pages = sections.perPage as any;
|
||||
const firstPage = pages ? pages['Page 1'] : undefined;
|
||||
const fonts = Array.isArray(firstPage?.Fonts) ? firstPage.Fonts : [];
|
||||
if (!fonts || fonts.length === 0) return 'None';
|
||||
const embedded = fonts.filter((f: any) => f?.IsEmbedded).length;
|
||||
return `${fonts.length} (${embedded} embedded)`;
|
||||
})(),
|
||||
[t('getPdfInfo.summary.tech.formFields', 'Form Fields')]: sections.formFields && Object.keys(sections.formFields as any).length > 0 ? Object.keys(sections.formFields as any).length : 'None',
|
||||
[t('getPdfInfo.summary.tech.embeddedFiles', 'Embedded Files')]: Array.isArray((sections.other as any)?.EmbeddedFiles) ? (sections.other as any).EmbeddedFiles.length : 'None',
|
||||
[t('getPdfInfo.summary.tech.javaScript', 'JavaScript')]: Array.isArray((sections.other as any)?.JavaScript) ? (sections.other as any).JavaScript.length : 'None',
|
||||
[t('getPdfInfo.summary.tech.layers', 'Layers')]: Array.isArray((sections.other as any)?.Layers) ? (sections.other as any).Layers.length : 'None',
|
||||
[t('getPdfInfo.summary.tech.bookmarks', 'Bookmarks')]: Array.isArray(sections.toc as any[]) ? (sections.toc as any[]).length : 'None',
|
||||
[t('getPdfInfo.summary.tech.multimedia', 'Multimedia')]: (() => {
|
||||
const pages = sections.perPage as any;
|
||||
const firstPage = pages ? pages['Page 1'] : undefined;
|
||||
const media = Array.isArray(firstPage?.Multimedia) ? firstPage.Multimedia : [];
|
||||
return media.length === 0 ? 'None' : `${media.length}`;
|
||||
})(),
|
||||
};
|
||||
|
||||
const overview = (() => {
|
||||
const tTitle = metadata['Title'] ? `"${metadata['Title']}"` : t('getPdfInfo.summary.overview.untitled', 'an untitled document');
|
||||
const author = metadata['Author'] || t('getPdfInfo.summary.overview.unknown', 'Unknown Author');
|
||||
const pagesCount = typeof pages === 'number' ? pages : '?';
|
||||
const version = pdfVersion ?? '?';
|
||||
return t('getPdfInfo.summary.overview.text', 'This is a {{pages}}-page PDF titled {{title}} created by {{author}} (PDF version {{version}}).', {
|
||||
pages: pagesCount,
|
||||
title: tTitle,
|
||||
author,
|
||||
version,
|
||||
});
|
||||
})();
|
||||
|
||||
return {
|
||||
basicInformation,
|
||||
documentInformation,
|
||||
securityStatusText,
|
||||
permSummary,
|
||||
complianceText,
|
||||
technical,
|
||||
overview,
|
||||
};
|
||||
}, [sections, t]);
|
||||
|
||||
return (
|
||||
<SectionBlock title={t('getPdfInfo.summary.title', 'PDF Summary')} anchorId="summary">
|
||||
<Stack gap="md">
|
||||
<Stack gap={6}>
|
||||
<Text fw={600} size="sm">{t('getPdfInfo.summary.basic', 'Basic Information')}</Text>
|
||||
<KeyValueList obj={summaryBlocks.basicInformation} />
|
||||
</Stack>
|
||||
<Stack gap={6}>
|
||||
<Text fw={600} size="sm">{t('getPdfInfo.summary.documentInfo', 'Document Information')}</Text>
|
||||
<KeyValueList obj={summaryBlocks.documentInformation} />
|
||||
</Stack>
|
||||
<Stack gap={6}>
|
||||
<Text fw={600} size="sm">{t('getPdfInfo.summary.security', 'Security Status')}</Text>
|
||||
<Text size="sm" c="dimmed">{summaryBlocks.securityStatusText}</Text>
|
||||
<Text size="sm" c="dimmed">{summaryBlocks.permSummary}</Text>
|
||||
<Text size="sm" c="dimmed">{summaryBlocks.complianceText}</Text>
|
||||
</Stack>
|
||||
<Stack gap={6}>
|
||||
<Text fw={600} size="sm">{t('getPdfInfo.summary.technical', 'Technical')}</Text>
|
||||
<KeyValueList obj={summaryBlocks.technical as any} />
|
||||
</Stack>
|
||||
<Stack gap={6}>
|
||||
<Text fw={600} size="sm">{t('getPdfInfo.summary.overview', 'PDF Overview')}</Text>
|
||||
<Text size="sm" c="dimmed">{summaryBlocks.overview}</Text>
|
||||
</Stack>
|
||||
</Stack>
|
||||
</SectionBlock>
|
||||
);
|
||||
};
|
||||
|
||||
export default SummarySection;
|
||||
|
||||
|
||||
@@ -0,0 +1,22 @@
|
||||
import React from 'react';
|
||||
import SectionBlock from '../shared/SectionBlock';
|
||||
import SimpleArrayList from '../shared/SimpleArrayList';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
interface TableOfContentsSectionProps {
|
||||
anchorId: string;
|
||||
tocArray: any[];
|
||||
}
|
||||
|
||||
const TableOfContentsSection: React.FC<TableOfContentsSectionProps> = ({ anchorId, tocArray }) => {
|
||||
const { t } = useTranslation();
|
||||
return (
|
||||
<SectionBlock title={t('getPdfInfo.sections.tableOfContents', 'Table of Contents')} anchorId={anchorId}>
|
||||
<SimpleArrayList arr={Array.isArray(tocArray) ? tocArray : []} />
|
||||
</SectionBlock>
|
||||
);
|
||||
};
|
||||
|
||||
export default TableOfContentsSection;
|
||||
|
||||
|
||||
@@ -0,0 +1,27 @@
|
||||
import React from 'react';
|
||||
import { Group, Stack, Text } from '@mantine/core';
|
||||
|
||||
interface KeyValueListProps {
|
||||
obj?: Record<string, unknown> | null;
|
||||
emptyLabel?: string;
|
||||
}
|
||||
|
||||
const KeyValueList: React.FC<KeyValueListProps> = ({ obj, emptyLabel }) => {
|
||||
if (!obj || Object.keys(obj).length === 0) {
|
||||
return <Text size="sm" c="dimmed">{emptyLabel ?? 'None detected'}</Text>;
|
||||
}
|
||||
return (
|
||||
<Stack gap={6}>
|
||||
{Object.entries(obj).map(([k, v]) => (
|
||||
<Group key={k} grow wrap="nowrap">
|
||||
<Text size="sm" style={{ minWidth: 180 }}>{k}</Text>
|
||||
<Text size="sm" c="dimmed">{v == null ? '' : String(v)}</Text>
|
||||
</Group>
|
||||
))}
|
||||
</Stack>
|
||||
);
|
||||
};
|
||||
|
||||
export default KeyValueList;
|
||||
|
||||
|
||||
@@ -0,0 +1,22 @@
|
||||
import React from 'react';
|
||||
import { Stack, Text, Divider } from '@mantine/core';
|
||||
|
||||
interface SectionBlockProps {
|
||||
title: string;
|
||||
anchorId: string;
|
||||
children: React.ReactNode;
|
||||
}
|
||||
|
||||
const SectionBlock: React.FC<SectionBlockProps> = ({ title, anchorId, children }) => {
|
||||
return (
|
||||
<Stack gap="sm" id={anchorId}>
|
||||
<Text fw={700} size="lg">{title}</Text>
|
||||
<Divider />
|
||||
{children}
|
||||
</Stack>
|
||||
);
|
||||
};
|
||||
|
||||
export default SectionBlock;
|
||||
|
||||
|
||||
@@ -0,0 +1,26 @@
|
||||
import React from 'react';
|
||||
import { Stack, Text } from '@mantine/core';
|
||||
|
||||
interface SimpleArrayListProps {
|
||||
arr?: any[] | null;
|
||||
emptyLabel?: string;
|
||||
}
|
||||
|
||||
const SimpleArrayList: React.FC<SimpleArrayListProps> = ({ arr, emptyLabel }) => {
|
||||
if (!arr || arr.length === 0) {
|
||||
return <Text size="sm" c="dimmed">{emptyLabel ?? 'None detected'}</Text>;
|
||||
}
|
||||
return (
|
||||
<Stack gap={4}>
|
||||
{arr.map((item, idx) => (
|
||||
<Text key={idx} size="sm" c="dimmed">
|
||||
{typeof item === 'string' ? item : JSON.stringify(item)}
|
||||
</Text>
|
||||
))}
|
||||
</Stack>
|
||||
);
|
||||
};
|
||||
|
||||
export default SimpleArrayList;
|
||||
|
||||
|
||||
@@ -44,15 +44,15 @@
|
||||
.simulated-page {
|
||||
width: min(820px, 100%);
|
||||
min-height: 1040px;
|
||||
background-color: rgb(var(--pdf-light-simulated-page-bg)) !important;
|
||||
box-shadow: 0 12px 32px rgba(var(--pdf-light-simulated-page-text), 0.12) !important;
|
||||
background-color: var(--bg-raised) !important;
|
||||
box-shadow: 0 12px 32px var(--shadow-color) !important;
|
||||
border-radius: 12px !important;
|
||||
padding: 48px 56px !important;
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
color: rgb(var(--pdf-light-simulated-page-text)) !important;
|
||||
color: var(--text-primary) !important;
|
||||
}
|
||||
|
||||
/* Container for the interactive report view */
|
||||
@@ -67,12 +67,12 @@
|
||||
|
||||
/* Keep field blocks stable colors across themes */
|
||||
.field-value {
|
||||
border: 1px solid rgb(var(--pdf-light-box-border)) !important;
|
||||
background-color: rgb(var(--pdf-light-box-bg)) !important;
|
||||
border: 1px solid var(--border-default) !important;
|
||||
background-color: var(--bg-raised) !important;
|
||||
}
|
||||
|
||||
.field-container {
|
||||
color: rgb(var(--pdf-light-simulated-page-text)) !important;
|
||||
color: var(--text-primary) !important;
|
||||
}
|
||||
|
||||
/* Thumbnail preview styles */
|
||||
|
||||
@@ -27,6 +27,7 @@ import AdjustContrastSingleStepSettings from "@app/components/tools/adjustContra
|
||||
import { adjustContrastOperationConfig } from "@app/hooks/tools/adjustContrast/useAdjustContrastOperation";
|
||||
import { getSynonyms } from "@app/utils/toolSynonyms";
|
||||
import { useProprietaryToolRegistry } from "@app/data/useProprietaryToolRegistry";
|
||||
import GetPdfInfo from "@app/tools/GetPdfInfo";
|
||||
import AddWatermark from "@app/tools/AddWatermark";
|
||||
import AddStamp from "@app/tools/AddStamp";
|
||||
import AddAttachments from "@app/tools/AddAttachments";
|
||||
@@ -294,7 +295,7 @@ export function useTranslatedToolCatalog(): TranslatedToolCatalog {
|
||||
getPdfInfo: {
|
||||
icon: <LocalIcon icon="fact-check-rounded" width="1.5rem" height="1.5rem" />,
|
||||
name: t("home.getPdfInfo.title", "Get ALL Info on PDF"),
|
||||
component: null,
|
||||
component: GetPdfInfo,
|
||||
description: t("home.getPdfInfo.desc", "Grabs any and all information possible on PDFs"),
|
||||
categoryId: ToolCategoryId.STANDARD_TOOLS,
|
||||
subcategoryId: SubcategoryId.VERIFICATION,
|
||||
|
||||
@@ -0,0 +1,199 @@
|
||||
import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import apiClient from '@app/services/apiClient';
|
||||
import { useFileContext } from '@app/contexts/file/fileHooks';
|
||||
import { ToolOperationHook } from '@app/hooks/tools/shared/useToolOperation';
|
||||
import type { StirlingFile } from '@app/types/fileContext';
|
||||
import { extractErrorMessage } from '@app/utils/toolErrorHandler';
|
||||
import {
|
||||
PdfInfoReportEntry,
|
||||
INFO_JSON_FILENAME,
|
||||
} from '@app/types/getPdfInfo';
|
||||
|
||||
export interface GetPdfInfoParameters {
|
||||
// Placeholder for future parameters
|
||||
}
|
||||
|
||||
export const defaultParameters: GetPdfInfoParameters = {};
|
||||
|
||||
export interface GetPdfInfoOperationHook extends ToolOperationHook<GetPdfInfoParameters> {
|
||||
results: PdfInfoReportEntry[];
|
||||
}
|
||||
|
||||
export const useGetPdfInfoOperation = (): GetPdfInfoOperationHook => {
|
||||
const { t } = useTranslation();
|
||||
const { selectors } = useFileContext();
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
const [status, setStatus] = useState('');
|
||||
const [errorMessage, setErrorMessage] = useState<string | null>(null);
|
||||
const [files, setFiles] = useState<File[]>([]);
|
||||
const [downloadUrl, setDownloadUrl] = useState<string | null>(null);
|
||||
const [downloadFilename, setDownloadFilename] = useState('');
|
||||
const [results, setResults] = useState<PdfInfoReportEntry[]>([]);
|
||||
|
||||
const cancelRequested = useRef(false);
|
||||
const previousUrl = useRef<string | null>(null);
|
||||
|
||||
const cleanupDownloadUrl = useCallback(() => {
|
||||
if (previousUrl.current) {
|
||||
URL.revokeObjectURL(previousUrl.current);
|
||||
previousUrl.current = null;
|
||||
}
|
||||
}, []);
|
||||
|
||||
const resetResults = useCallback(() => {
|
||||
cancelRequested.current = false;
|
||||
setResults([]);
|
||||
setFiles([]);
|
||||
cleanupDownloadUrl();
|
||||
setDownloadUrl(null);
|
||||
setDownloadFilename('');
|
||||
setStatus('');
|
||||
setErrorMessage(null);
|
||||
}, [cleanupDownloadUrl]);
|
||||
|
||||
const clearError = useCallback(() => {
|
||||
setErrorMessage(null);
|
||||
}, []);
|
||||
|
||||
const executeOperation = useCallback(
|
||||
async (_params: GetPdfInfoParameters, selectedFiles: StirlingFile[]) => {
|
||||
if (selectedFiles.length === 0) {
|
||||
setErrorMessage(t('noFileSelected', 'No files selected'));
|
||||
return;
|
||||
}
|
||||
|
||||
cancelRequested.current = false;
|
||||
setIsLoading(true);
|
||||
setStatus(t('getPdfInfo.processing', 'Extracting information...'));
|
||||
setErrorMessage(null);
|
||||
setResults([]);
|
||||
setFiles([]);
|
||||
cleanupDownloadUrl();
|
||||
setDownloadUrl(null);
|
||||
setDownloadFilename('');
|
||||
|
||||
try {
|
||||
const aggregated: PdfInfoReportEntry[] = [];
|
||||
const generatedAt = Date.now();
|
||||
|
||||
for (const file of selectedFiles) {
|
||||
if (cancelRequested.current) break;
|
||||
|
||||
const formData = new FormData();
|
||||
formData.append('fileInput', file);
|
||||
|
||||
try {
|
||||
const response = await apiClient.post('/api/v1/security/get-info-on-pdf', formData, {
|
||||
headers: { 'Content-Type': 'multipart/form-data' },
|
||||
});
|
||||
|
||||
const stub = selectors.getStirlingFileStub(file.fileId);
|
||||
const entry: PdfInfoReportEntry = {
|
||||
fileId: file.fileId,
|
||||
fileName: file.name,
|
||||
fileSize: file.size ?? null,
|
||||
lastModified: file.lastModified ?? null,
|
||||
thumbnailUrl: stub?.thumbnailUrl ?? null,
|
||||
data: response.data ?? {},
|
||||
error: null,
|
||||
summaryGeneratedAt: generatedAt,
|
||||
};
|
||||
aggregated.push(entry);
|
||||
} catch (error) {
|
||||
const stub = selectors.getStirlingFileStub(file.fileId);
|
||||
aggregated.push({
|
||||
fileId: file.fileId,
|
||||
fileName: file.name,
|
||||
fileSize: file.size ?? null,
|
||||
lastModified: file.lastModified ?? null,
|
||||
thumbnailUrl: stub?.thumbnailUrl ?? null,
|
||||
data: {},
|
||||
error: extractErrorMessage(error),
|
||||
summaryGeneratedAt: generatedAt,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
if (!cancelRequested.current) {
|
||||
setResults(aggregated);
|
||||
if (aggregated.length > 0) {
|
||||
// Build V1-compatible JSON: use backend payloads directly.
|
||||
const payloads = aggregated
|
||||
.filter((e) => !e.error)
|
||||
.map((e) => e.data);
|
||||
const content = payloads.length === 1 ? payloads[0] : payloads;
|
||||
const json = JSON.stringify(content, null, 2);
|
||||
const resultFile = new File([json], INFO_JSON_FILENAME, { type: 'application/json' });
|
||||
setFiles([resultFile]);
|
||||
}
|
||||
|
||||
const anyError = aggregated.some((item) => item.error);
|
||||
if (anyError) {
|
||||
setErrorMessage(t('getPdfInfo.error.partial', 'Some files could not be processed.'));
|
||||
}
|
||||
setStatus(t('getPdfInfo.status.complete', 'Extraction complete'));
|
||||
}
|
||||
} catch (e) {
|
||||
console.error('[getPdfInfo] unexpected failure', e);
|
||||
setErrorMessage(t('getPdfInfo.error.unexpected', 'Unexpected error during extraction.'));
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
}
|
||||
},
|
||||
[cleanupDownloadUrl, selectors, t]
|
||||
);
|
||||
|
||||
const cancelOperation = useCallback(() => {
|
||||
if (isLoading) {
|
||||
cancelRequested.current = true;
|
||||
setIsLoading(false);
|
||||
setStatus(t('operationCancelled', 'Operation cancelled'));
|
||||
}
|
||||
}, [isLoading, t]);
|
||||
|
||||
const undoOperation = useCallback(async () => {
|
||||
resetResults();
|
||||
}, [resetResults]);
|
||||
|
||||
useEffect(() => {
|
||||
return () => {
|
||||
cleanupDownloadUrl();
|
||||
};
|
||||
}, [cleanupDownloadUrl]);
|
||||
|
||||
return useMemo<GetPdfInfoOperationHook>(
|
||||
() => ({
|
||||
files,
|
||||
thumbnails: [],
|
||||
isGeneratingThumbnails: false,
|
||||
downloadUrl,
|
||||
downloadFilename,
|
||||
isLoading,
|
||||
status,
|
||||
errorMessage,
|
||||
progress: null,
|
||||
executeOperation,
|
||||
resetResults,
|
||||
clearError,
|
||||
cancelOperation,
|
||||
undoOperation,
|
||||
results,
|
||||
}),
|
||||
[
|
||||
cancelOperation,
|
||||
clearError,
|
||||
downloadFilename,
|
||||
downloadUrl,
|
||||
errorMessage,
|
||||
executeOperation,
|
||||
files,
|
||||
isLoading,
|
||||
resetResults,
|
||||
results,
|
||||
status,
|
||||
]
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
@@ -0,0 +1,15 @@
|
||||
import { useBaseParameters, BaseParametersHook } from '@app/hooks/tools/shared/useBaseParameters';
|
||||
import type { GetPdfInfoParameters } from '@app/hooks/tools/getPdfInfo/useGetPdfInfoOperation';
|
||||
|
||||
export const defaultParameters: GetPdfInfoParameters = {};
|
||||
|
||||
export type GetPdfInfoParametersHook = BaseParametersHook<GetPdfInfoParameters>;
|
||||
|
||||
export const useGetPdfInfoParameters = (): GetPdfInfoParametersHook => {
|
||||
return useBaseParameters({
|
||||
defaultParameters,
|
||||
endpointName: 'get-info-on-pdf',
|
||||
});
|
||||
};
|
||||
|
||||
|
||||
187
frontend/src/core/tools/GetPdfInfo.tsx
Normal file
187
frontend/src/core/tools/GetPdfInfo.tsx
Normal file
@@ -0,0 +1,187 @@
|
||||
import { useEffect, useMemo, useRef } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import PictureAsPdfIcon from '@mui/icons-material/PictureAsPdf';
|
||||
import { Stack, Button, Group, Divider, Text, UnstyledButton } from '@mantine/core';
|
||||
import LinkIcon from '@mui/icons-material/Link';
|
||||
import { createToolFlow } from '@app/components/tools/shared/createToolFlow';
|
||||
import { useBaseTool } from '@app/hooks/tools/shared/useBaseTool';
|
||||
import { BaseToolProps, ToolComponent } from '@app/types/tool';
|
||||
import { useGetPdfInfoParameters, defaultParameters } from '@app/hooks/tools/getPdfInfo/useGetPdfInfoParameters';
|
||||
import GetPdfInfoResults from '@app/components/tools/getPdfInfo/GetPdfInfoResults';
|
||||
import { useGetPdfInfoOperation, GetPdfInfoOperationHook } from '@app/hooks/tools/getPdfInfo/useGetPdfInfoOperation';
|
||||
import GetPdfInfoReportView from '@app/components/tools/getPdfInfo/GetPdfInfoReportView';
|
||||
import { useToolWorkflow } from '@app/contexts/ToolWorkflowContext';
|
||||
import { useNavigationActions, useNavigationState } from '@app/contexts/NavigationContext';
|
||||
import type { PdfInfoReportData } from '@app/types/getPdfInfo';
|
||||
|
||||
const CHAPTERS = [
|
||||
{ id: 'metadata', labelKey: 'getPdfInfo.sections.metadata', fallback: 'Metadata' },
|
||||
{ id: 'formFields', labelKey: 'getPdfInfo.sections.formFields', fallback: 'Form Fields' },
|
||||
{ id: 'basicInfo', labelKey: 'getPdfInfo.sections.basicInfo', fallback: 'Basic Info' },
|
||||
{ id: 'documentInfo', labelKey: 'getPdfInfo.sections.documentInfo', fallback: 'Document Info' },
|
||||
{ id: 'compliance', labelKey: 'getPdfInfo.sections.compliance', fallback: 'Compliance' },
|
||||
{ id: 'encryption', labelKey: 'getPdfInfo.sections.encryption', fallback: 'Encryption' },
|
||||
{ id: 'permissions', labelKey: 'getPdfInfo.sections.permissions', fallback: 'Permissions' },
|
||||
{ id: 'toc', labelKey: 'getPdfInfo.sections.tableOfContents', fallback: 'Table of Contents' },
|
||||
{ id: 'other', labelKey: 'getPdfInfo.sections.other', fallback: 'Other' },
|
||||
{ id: 'perPage', labelKey: 'getPdfInfo.sections.perPageInfo', fallback: 'Per Page Info' },
|
||||
];
|
||||
|
||||
const GetPdfInfo = (props: BaseToolProps) => {
|
||||
const { t } = useTranslation();
|
||||
const { actions: navigationActions } = useNavigationActions();
|
||||
const navigationState = useNavigationState();
|
||||
const {
|
||||
registerCustomWorkbenchView,
|
||||
unregisterCustomWorkbenchView,
|
||||
setCustomWorkbenchViewData,
|
||||
clearCustomWorkbenchViewData,
|
||||
} = useToolWorkflow();
|
||||
|
||||
const REPORT_VIEW_ID = 'getPdfInfoReport';
|
||||
const REPORT_WORKBENCH_ID = 'custom:getPdfInfoReport' as const;
|
||||
const reportIcon = useMemo(() => <PictureAsPdfIcon fontSize="small" />, []);
|
||||
|
||||
const base = useBaseTool(
|
||||
'getPdfInfo',
|
||||
useGetPdfInfoParameters,
|
||||
useGetPdfInfoOperation,
|
||||
props
|
||||
);
|
||||
|
||||
const operation = base.operation as GetPdfInfoOperationHook;
|
||||
const hasResults = operation.results.length > 0;
|
||||
const showResultsStep = hasResults || base.operation.isLoading || !!base.operation.errorMessage;
|
||||
|
||||
useEffect(() => {
|
||||
registerCustomWorkbenchView({
|
||||
id: REPORT_VIEW_ID,
|
||||
workbenchId: REPORT_WORKBENCH_ID,
|
||||
label: t('getPdfInfo.report.shortTitle', 'PDF Information'),
|
||||
icon: reportIcon,
|
||||
component: GetPdfInfoReportView,
|
||||
});
|
||||
|
||||
return () => {
|
||||
clearCustomWorkbenchViewData(REPORT_VIEW_ID);
|
||||
unregisterCustomWorkbenchView(REPORT_VIEW_ID);
|
||||
};
|
||||
}, [
|
||||
clearCustomWorkbenchViewData,
|
||||
registerCustomWorkbenchView,
|
||||
reportIcon,
|
||||
t,
|
||||
unregisterCustomWorkbenchView,
|
||||
]);
|
||||
|
||||
const reportData = useMemo<PdfInfoReportData | null>(() => {
|
||||
if (operation.results.length === 0) return null;
|
||||
const generatedAt = operation.results[0].summaryGeneratedAt ?? Date.now();
|
||||
return {
|
||||
generatedAt,
|
||||
entries: operation.results,
|
||||
};
|
||||
}, [operation.results]);
|
||||
|
||||
const lastReportGeneratedAtRef = useRef<number | null>(null);
|
||||
useEffect(() => {
|
||||
if (reportData) {
|
||||
setCustomWorkbenchViewData(REPORT_VIEW_ID, reportData);
|
||||
const generatedAt = reportData.generatedAt ?? null;
|
||||
const isNewReport = generatedAt && generatedAt !== lastReportGeneratedAtRef.current;
|
||||
if (isNewReport) {
|
||||
lastReportGeneratedAtRef.current = generatedAt;
|
||||
if (navigationState.selectedTool === 'getPdfInfo' && navigationState.workbench !== REPORT_WORKBENCH_ID) {
|
||||
navigationActions.setWorkbench(REPORT_WORKBENCH_ID);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
clearCustomWorkbenchViewData(REPORT_VIEW_ID);
|
||||
lastReportGeneratedAtRef.current = null;
|
||||
}
|
||||
}, [
|
||||
clearCustomWorkbenchViewData,
|
||||
navigationActions,
|
||||
navigationState.selectedTool,
|
||||
navigationState.workbench,
|
||||
reportData,
|
||||
setCustomWorkbenchViewData,
|
||||
]);
|
||||
|
||||
return createToolFlow({
|
||||
files: {
|
||||
selectedFiles: base.selectedFiles,
|
||||
isCollapsed: hasResults,
|
||||
},
|
||||
steps: [
|
||||
{
|
||||
title: t('getPdfInfo.indexTitle', 'Index'),
|
||||
isVisible: Boolean(reportData),
|
||||
isCollapsed: false,
|
||||
content: (
|
||||
<Stack gap={0}>
|
||||
{CHAPTERS.map((c, idx) => (
|
||||
<Stack key={c.id} gap={0}>
|
||||
<UnstyledButton
|
||||
onClick={() => {
|
||||
if (!reportData) return;
|
||||
setCustomWorkbenchViewData(REPORT_VIEW_ID, { ...reportData, scrollTo: c.id });
|
||||
if (navigationState.workbench !== REPORT_WORKBENCH_ID) {
|
||||
navigationActions.setWorkbench(REPORT_WORKBENCH_ID);
|
||||
}
|
||||
}}
|
||||
style={{ width: '100%', textAlign: 'left', padding: '8px 4px' }}
|
||||
>
|
||||
<Group justify="flex-start" gap="sm">
|
||||
<LinkIcon fontSize="small" style={{ opacity: 0.7 }} />
|
||||
<Text size="md" c="dimmed">
|
||||
{t(c.labelKey, c.fallback)}
|
||||
</Text>
|
||||
</Group>
|
||||
</UnstyledButton>
|
||||
{idx < CHAPTERS.length - 1 && <Divider my={6} />}
|
||||
</Stack>
|
||||
))}
|
||||
</Stack>
|
||||
),
|
||||
},
|
||||
{
|
||||
title: t('getPdfInfo.results', 'Results'),
|
||||
isVisible: showResultsStep,
|
||||
isCollapsed: false,
|
||||
content: (
|
||||
<GetPdfInfoResults
|
||||
operation={operation}
|
||||
isLoading={base.operation.isLoading}
|
||||
errorMessage={base.operation.errorMessage}
|
||||
/>
|
||||
),
|
||||
},
|
||||
],
|
||||
executeButton: {
|
||||
text: t('getPdfInfo.submit', 'Generate'),
|
||||
loadingText: t('loading', 'Loading...'),
|
||||
onClick: base.handleExecute,
|
||||
disabled:
|
||||
!base.params.validateParameters() ||
|
||||
!base.hasFiles ||
|
||||
base.operation.isLoading ||
|
||||
!base.endpointEnabled,
|
||||
isVisible: true,
|
||||
},
|
||||
review: {
|
||||
isVisible: false,
|
||||
operation: base.operation,
|
||||
title: t('getPdfInfo.results', 'Results'),
|
||||
onUndo: base.handleUndo,
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
const GetPdfInfoTool = GetPdfInfo as ToolComponent;
|
||||
GetPdfInfoTool.tool = () => useGetPdfInfoOperation;
|
||||
GetPdfInfoTool.getDefaultParameters = () => ({ ...defaultParameters });
|
||||
|
||||
export default GetPdfInfoTool;
|
||||
|
||||
|
||||
28
frontend/src/core/types/getPdfInfo.ts
Normal file
28
frontend/src/core/types/getPdfInfo.ts
Normal file
@@ -0,0 +1,28 @@
|
||||
export interface PdfInfoBackendData {
|
||||
// Raw backend payload keyed by human-readable section names
|
||||
// Example keys: "Metadata", "Form Fields", "Basic Info", "Document Info",
|
||||
// "Compliance", "Encryption", "Permissions", "Table of Contents",
|
||||
// "Other", "Per Page Info"
|
||||
[sectionName: string]: unknown;
|
||||
}
|
||||
|
||||
export interface PdfInfoReportEntry {
|
||||
fileId: string;
|
||||
fileName: string;
|
||||
fileSize: number | null;
|
||||
lastModified: number | null;
|
||||
thumbnailUrl?: string | null;
|
||||
data: PdfInfoBackendData;
|
||||
error: string | null;
|
||||
summaryGeneratedAt?: number;
|
||||
}
|
||||
|
||||
export interface PdfInfoReportData {
|
||||
generatedAt: number;
|
||||
entries: PdfInfoReportEntry[];
|
||||
}
|
||||
|
||||
export const INFO_JSON_FILENAME = 'response.json';
|
||||
export const INFO_PDF_FILENAME = 'pdf-information-report.pdf';
|
||||
|
||||
|
||||
Reference in New Issue
Block a user