mirror of
https://github.com/arsvendg/Stirling-PDF.git
synced 2026-07-14 10:34:06 +02:00
Feature/v2/get all info on pdf (#5105)
# Description of Changes - Addition of the get all info on PDF tool --- ## Checklist ### General - [ ] I have read the [Contribution Guidelines](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/CONTRIBUTING.md) - [ ] I have read the [Stirling-PDF Developer Guide](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/DeveloperGuide.md) (if applicable) - [ ] I have read the [How to add new languages to Stirling-PDF](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/HowToAddNewLanguage.md) (if applicable) - [ ] I have performed a self-review of my own code - [ ] My changes generate no new warnings ### Documentation - [ ] I have updated relevant docs on [Stirling-PDF's doc repo](https://github.com/Stirling-Tools/Stirling-Tools.github.io/blob/main/docs/) (if functionality has heavily changed) - [ ] I have read the section [Add New Translation Tags](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/HowToAddNewLanguage.md#add-new-translation-tags) (for new translation tags only) ### Translations (if applicable) - [ ] I ran [`scripts/counter_translation.py`](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/docs/counter_translation.md) ### UI Changes (if applicable) - [ ] Screenshots or videos demonstrating the UI changes are attached (e.g., as comments or direct attachments in the PR) ### Testing (if applicable) - [ ] I have tested my changes locally. Refer to the [Testing Guide](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/DeveloperGuide.md#6-testing) for more details.
This commit is contained in:
@@ -3036,6 +3036,91 @@ title = "Get Info on PDF"
|
|||||||
header = "Get Info on PDF"
|
header = "Get Info on PDF"
|
||||||
submit = "Get Info"
|
submit = "Get Info"
|
||||||
downloadJson = "Download JSON"
|
downloadJson = "Download JSON"
|
||||||
|
processing = "Extracting information..."
|
||||||
|
results = "Results"
|
||||||
|
noResults = "Run the tool to generate a report."
|
||||||
|
downloads = "Downloads"
|
||||||
|
noneDetected = "None detected"
|
||||||
|
indexTitle = "Index"
|
||||||
|
|
||||||
|
[getPdfInfo.report]
|
||||||
|
entryLabel = "Full information summary"
|
||||||
|
shortTitle = "PDF Information"
|
||||||
|
|
||||||
|
[getPdfInfo.sections]
|
||||||
|
metadata = "Metadata"
|
||||||
|
formFields = "Form Fields"
|
||||||
|
basicInfo = "Basic Info"
|
||||||
|
documentInfo = "Document Info"
|
||||||
|
compliance = "Compliance"
|
||||||
|
encryption = "Encryption"
|
||||||
|
permissions = "Permissions"
|
||||||
|
other = "Other"
|
||||||
|
perPageInfo = "Per Page Info"
|
||||||
|
tableOfContents = "Table of Contents"
|
||||||
|
|
||||||
|
[getPdfInfo.other]
|
||||||
|
attachments = "Attachments"
|
||||||
|
embeddedFiles = "Embedded Files"
|
||||||
|
javaScript = "JavaScript"
|
||||||
|
layers = "Layers"
|
||||||
|
structureTree = "StructureTree"
|
||||||
|
xmp = "XMPMetadata"
|
||||||
|
|
||||||
|
[getPdfInfo.perPage]
|
||||||
|
size = "Size"
|
||||||
|
annotations = "Annotations"
|
||||||
|
images = "Images"
|
||||||
|
links = "Links"
|
||||||
|
fonts = "Fonts"
|
||||||
|
xobjects = "XObject Counts"
|
||||||
|
multimedia = "Multimedia"
|
||||||
|
|
||||||
|
[getPdfInfo.summary]
|
||||||
|
pages = "Pages"
|
||||||
|
fileSize = "File Size"
|
||||||
|
pdfVersion = "PDF Version"
|
||||||
|
language = "Language"
|
||||||
|
title = "PDF Summary"
|
||||||
|
author = "Author"
|
||||||
|
created = "Created"
|
||||||
|
modified = "Modified"
|
||||||
|
permsAll = "All Permissions Allowed"
|
||||||
|
permsRestricted = "{{count}} restrictions"
|
||||||
|
permsMixed = "Some permissions restricted"
|
||||||
|
hasCompliance = "Has compliance standards"
|
||||||
|
noCompliance = "No Compliance Standards"
|
||||||
|
basic = "Basic Information"
|
||||||
|
documentInfo = "Document Information"
|
||||||
|
securityTitle = "Security Status"
|
||||||
|
technical = "Technical"
|
||||||
|
overviewTitle = "PDF Overview"
|
||||||
|
|
||||||
|
[getPdfInfo.summary.security]
|
||||||
|
encrypted = "Encrypted PDF - Password protection present"
|
||||||
|
unencrypted = "Unencrypted PDF - No password protection"
|
||||||
|
|
||||||
|
[getPdfInfo.summary.tech]
|
||||||
|
images = "Images"
|
||||||
|
fonts = "Fonts"
|
||||||
|
formFields = "Form Fields"
|
||||||
|
embeddedFiles = "Embedded Files"
|
||||||
|
javaScript = "JavaScript"
|
||||||
|
layers = "Layers"
|
||||||
|
bookmarks = "Bookmarks"
|
||||||
|
multimedia = "Multimedia"
|
||||||
|
|
||||||
|
[getPdfInfo.summary.overview]
|
||||||
|
untitled = "an untitled document"
|
||||||
|
unknown = "Unknown Author"
|
||||||
|
text = "This is a {{pages}}-page PDF titled {{title}} created by {{author}} (PDF version {{version}})."
|
||||||
|
|
||||||
|
[getPdfInfo.error]
|
||||||
|
partial = "Some files could not be processed."
|
||||||
|
unexpected = "Unexpected error during extraction."
|
||||||
|
|
||||||
|
[getPdfInfo.status]
|
||||||
|
complete = "Extraction complete"
|
||||||
|
|
||||||
[extractPage]
|
[extractPage]
|
||||||
tags = "extract"
|
tags = "extract"
|
||||||
|
|||||||
@@ -0,0 +1,128 @@
|
|||||||
|
import React, { useEffect, useMemo, useRef } from 'react';
|
||||||
|
import { Badge, Divider, Stack, Text } from '@mantine/core';
|
||||||
|
import { useTranslation } from 'react-i18next';
|
||||||
|
import type {
|
||||||
|
PdfInfoReportData,
|
||||||
|
PdfInfoReportEntry,
|
||||||
|
PdfInfoBackendData,
|
||||||
|
ParsedPdfSections,
|
||||||
|
} from '@app/types/getPdfInfo';
|
||||||
|
import '@app/components/tools/validateSignature/reportView/styles.css';
|
||||||
|
import SummarySection from '@app/components/tools/getPdfInfo/sections/SummarySection';
|
||||||
|
import KeyValueSection from '@app/components/tools/getPdfInfo/sections/KeyValueSection';
|
||||||
|
import TableOfContentsSection from '@app/components/tools/getPdfInfo/sections/TableOfContentsSection';
|
||||||
|
import OtherSection from '@app/components/tools/getPdfInfo/sections/OtherSection';
|
||||||
|
import PerPageSection from '@app/components/tools/getPdfInfo/sections/PerPageSection';
|
||||||
|
|
||||||
|
|
||||||
|
/** Valid section anchor IDs for navigation */
|
||||||
|
const VALID_ANCHORS = new Set([
|
||||||
|
'summary', 'metadata', 'formFields', 'basicInfo', 'documentInfo',
|
||||||
|
'compliance', 'encryption', 'permissions', 'toc', 'other', 'perPage',
|
||||||
|
]);
|
||||||
|
|
||||||
|
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 || !VALID_ANCHORS.has(data.scrollTo)) return;
|
||||||
|
const anchor = data.scrollTo;
|
||||||
|
const container = containerRef.current;
|
||||||
|
const el = container?.querySelector<HTMLElement>(`#${anchor}`);
|
||||||
|
if (el && container) {
|
||||||
|
// Calculate scroll position with 4rem buffer from top
|
||||||
|
const bufferPx = parseFloat(getComputedStyle(document.documentElement).fontSize) * 4;
|
||||||
|
const elementTop = el.getBoundingClientRect().top;
|
||||||
|
const containerTop = container.getBoundingClientRect().top;
|
||||||
|
const currentScroll = container.scrollTop;
|
||||||
|
const targetScroll = currentScroll + (elementTop - containerTop) - bufferPx;
|
||||||
|
|
||||||
|
container.scrollTo({ top: Math.max(0, targetScroll), behavior: 'smooth' });
|
||||||
|
|
||||||
|
// Flash highlight the section
|
||||||
|
el.classList.remove('section-flash-highlight');
|
||||||
|
void el.offsetWidth; // Force reflow
|
||||||
|
el.classList.add('section-flash-highlight');
|
||||||
|
setTimeout(() => el.classList.remove('section-flash-highlight'), 1500);
|
||||||
|
}
|
||||||
|
}, [data.scrollTo]);
|
||||||
|
|
||||||
|
const sections = useMemo((): ParsedPdfSections => {
|
||||||
|
const raw: PdfInfoBackendData = entry?.data ?? {};
|
||||||
|
return {
|
||||||
|
metadata: raw.Metadata ?? null,
|
||||||
|
formFields: raw.FormFields ?? raw['Form Fields'] ?? null,
|
||||||
|
basicInfo: raw.BasicInfo ?? raw['Basic Info'] ?? null,
|
||||||
|
documentInfo: raw.DocumentInfo ?? raw['Document Info'] ?? null,
|
||||||
|
compliance: raw.Compliancy ?? raw.Compliance ?? null,
|
||||||
|
encryption: raw.Encryption ?? null,
|
||||||
|
permissions: raw.Permissions ?? null,
|
||||||
|
toc: raw['Bookmarks/Outline/TOC'] ?? raw['Table of Contents'] ?? null,
|
||||||
|
other: raw.Other ?? null,
|
||||||
|
perPage: raw.PerPageInfo ?? raw['Per Page Info'] ?? null,
|
||||||
|
summaryData: raw.SummaryData ?? null,
|
||||||
|
};
|
||||||
|
}, [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">
|
||||||
|
|
||||||
|
<div className="simulated-page">
|
||||||
|
<Stack gap="lg">
|
||||||
|
<Stack gap="xs">
|
||||||
|
<Text fw={700} size="xl" style={{ lineHeight: 1.3, wordBreak: 'break-word' }}>
|
||||||
|
{entry.fileName}
|
||||||
|
<Text component="span" fw={700}> - {t('getPdfInfo.summary.title', 'PDF Summary')}</Text>
|
||||||
|
</Text>
|
||||||
|
<Divider />
|
||||||
|
</Stack>
|
||||||
|
|
||||||
|
<SummarySection sections={sections} hideSectionTitle />
|
||||||
|
|
||||||
|
<KeyValueSection title={t('getPdfInfo.sections.metadata', 'Metadata')} anchorId="metadata" obj={sections.metadata} />
|
||||||
|
|
||||||
|
<KeyValueSection title={t('getPdfInfo.sections.formFields', 'Form Fields')} anchorId="formFields" obj={sections.formFields} />
|
||||||
|
|
||||||
|
<KeyValueSection title={t('getPdfInfo.sections.basicInfo', 'Basic Info')} anchorId="basicInfo" obj={sections.basicInfo} />
|
||||||
|
|
||||||
|
<KeyValueSection title={t('getPdfInfo.sections.documentInfo', 'Document Info')} anchorId="documentInfo" obj={sections.documentInfo} />
|
||||||
|
|
||||||
|
<KeyValueSection title={t('getPdfInfo.sections.compliance', 'Compliance')} anchorId="compliance" obj={sections.compliance} />
|
||||||
|
|
||||||
|
<KeyValueSection title={t('getPdfInfo.sections.encryption', 'Encryption')} anchorId="encryption" obj={sections.encryption} />
|
||||||
|
|
||||||
|
<KeyValueSection title={t('getPdfInfo.sections.permissions', 'Permissions')} anchorId="permissions" obj={sections.permissions} />
|
||||||
|
|
||||||
|
<TableOfContentsSection anchorId="toc" tocArray={sections.toc ?? []} />
|
||||||
|
|
||||||
|
<OtherSection anchorId="other" other={sections.other} />
|
||||||
|
|
||||||
|
<PerPageSection anchorId="perPage" perPage={sections.perPage} />
|
||||||
|
</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 '@app/components/tools/getPdfInfo/shared/SectionBlock';
|
||||||
|
import KeyValueList from '@app/components/tools/getPdfInfo/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,84 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import { Accordion, Stack, Text } from '@mantine/core';
|
||||||
|
import { useTranslation } from 'react-i18next';
|
||||||
|
import type { PdfOtherInfo } from '@app/types/getPdfInfo';
|
||||||
|
import SectionBlock from '@app/components/tools/getPdfInfo/shared/SectionBlock';
|
||||||
|
import ScrollableCodeBlock from '@app/components/tools/getPdfInfo/shared/ScrollableCodeBlock';
|
||||||
|
import { pdfInfoAccordionStyles } from '@app/components/tools/getPdfInfo/shared/accordionStyles';
|
||||||
|
|
||||||
|
interface OtherSectionProps {
|
||||||
|
anchorId: string;
|
||||||
|
other?: PdfOtherInfo | null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const renderList = (arr: unknown[] | undefined, emptyText: string) => {
|
||||||
|
if (!arr || arr.length === 0) return <Text size="sm" c="dimmed">{emptyText}</Text>;
|
||||||
|
return (
|
||||||
|
<Stack gap={4}>
|
||||||
|
{arr.map((item, idx) => (
|
||||||
|
<Text key={idx} size="sm" c="dimmed" style={{ wordBreak: 'break-word', overflowWrap: 'break-word' }}>
|
||||||
|
{typeof item === 'string' ? item : JSON.stringify(item)}
|
||||||
|
</Text>
|
||||||
|
))}
|
||||||
|
</Stack>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const OtherSection: React.FC<OtherSectionProps> = ({ anchorId, other }) => {
|
||||||
|
const { t } = useTranslation();
|
||||||
|
const noneDetected = t('getPdfInfo.noneDetected', 'None detected');
|
||||||
|
|
||||||
|
const structureTreeContent = Array.isArray(other?.StructureTree) && other.StructureTree.length > 0
|
||||||
|
? JSON.stringify(other.StructureTree, null, 2)
|
||||||
|
: null;
|
||||||
|
|
||||||
|
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>
|
||||||
|
{renderList(other?.Attachments, noneDetected)}
|
||||||
|
</Stack>
|
||||||
|
<Stack gap={6}>
|
||||||
|
<Text fw={600} size="sm">{t('getPdfInfo.other.embeddedFiles', 'Embedded Files')}</Text>
|
||||||
|
{renderList(other?.EmbeddedFiles, noneDetected)}
|
||||||
|
</Stack>
|
||||||
|
<Stack gap={6}>
|
||||||
|
<Text fw={600} size="sm">{t('getPdfInfo.other.javaScript', 'JavaScript')}</Text>
|
||||||
|
{renderList(other?.JavaScript, noneDetected)}
|
||||||
|
</Stack>
|
||||||
|
<Stack gap={6}>
|
||||||
|
<Text fw={600} size="sm">{t('getPdfInfo.other.layers', 'Layers')}</Text>
|
||||||
|
{renderList(other?.Layers, noneDetected)}
|
||||||
|
</Stack>
|
||||||
|
<Accordion
|
||||||
|
variant="separated"
|
||||||
|
radius="md"
|
||||||
|
defaultValue=""
|
||||||
|
styles={pdfInfoAccordionStyles}
|
||||||
|
>
|
||||||
|
<Accordion.Item value="structureTree">
|
||||||
|
<Accordion.Control>
|
||||||
|
<Text fw={600} size="sm">{t('getPdfInfo.other.structureTree', 'StructureTree')}</Text>
|
||||||
|
</Accordion.Control>
|
||||||
|
<Accordion.Panel>
|
||||||
|
<ScrollableCodeBlock content={structureTreeContent} maxHeight="20rem" />
|
||||||
|
</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>
|
||||||
|
<ScrollableCodeBlock content={other?.XMPMetadata} maxHeight="400px" />
|
||||||
|
</Accordion.Panel>
|
||||||
|
</Accordion.Item>
|
||||||
|
</Accordion>
|
||||||
|
</Stack>
|
||||||
|
</SectionBlock>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default OtherSection;
|
||||||
|
|
||||||
|
|
||||||
@@ -0,0 +1,122 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import { Accordion, Stack, Text } from '@mantine/core';
|
||||||
|
import { useTranslation } from 'react-i18next';
|
||||||
|
import type { PdfPerPageInfo, PdfPageInfo, PdfFontInfo } from '@app/types/getPdfInfo';
|
||||||
|
import SectionBlock from '@app/components/tools/getPdfInfo/shared/SectionBlock';
|
||||||
|
import KeyValueList from '@app/components/tools/getPdfInfo/shared/KeyValueList';
|
||||||
|
import { pdfInfoAccordionStyles } from '@app/components/tools/getPdfInfo/shared/accordionStyles';
|
||||||
|
|
||||||
|
interface PerPageSectionProps {
|
||||||
|
anchorId: string;
|
||||||
|
perPage?: PdfPerPageInfo | null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const renderList = (arr: unknown[] | undefined, emptyText: string) => {
|
||||||
|
if (!arr || arr.length === 0) return <Text size="sm" c="dimmed">{emptyText}</Text>;
|
||||||
|
return (
|
||||||
|
<Stack gap={4}>
|
||||||
|
{arr.map((item, idx) => (
|
||||||
|
<Text key={idx} size="sm" c="dimmed" style={{ wordBreak: 'break-word', overflowWrap: 'break-word' }}>
|
||||||
|
{typeof item === 'string' ? item : JSON.stringify(item)}
|
||||||
|
</Text>
|
||||||
|
))}
|
||||||
|
</Stack>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const renderFontsList = (fonts: PdfFontInfo[] | undefined, emptyText: string) => {
|
||||||
|
if (!fonts || fonts.length === 0) return <Text size="sm" c="dimmed">{emptyText}</Text>;
|
||||||
|
return (
|
||||||
|
<Stack gap={4}>
|
||||||
|
{fonts.map((font, idx) => (
|
||||||
|
<Text key={idx} size="sm" c="dimmed" style={{ wordBreak: 'break-word', overflowWrap: 'break-word' }}>
|
||||||
|
{`${font.Name ?? 'Unknown'}${font.IsEmbedded ? ' (embedded)' : ''}`}
|
||||||
|
</Text>
|
||||||
|
))}
|
||||||
|
</Stack>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const PerPageSection: React.FC<PerPageSectionProps> = ({ anchorId, perPage }) => {
|
||||||
|
const { t } = useTranslation();
|
||||||
|
const noneDetected = t('getPdfInfo.noneDetected', 'None detected');
|
||||||
|
|
||||||
|
const hasPages = perPage && Object.keys(perPage).length > 0;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<SectionBlock title={t('getPdfInfo.sections.perPageInfo', 'Per Page Info')} anchorId={anchorId}>
|
||||||
|
{hasPages ? (
|
||||||
|
<Accordion
|
||||||
|
variant="separated"
|
||||||
|
radius="md"
|
||||||
|
defaultValue=""
|
||||||
|
styles={pdfInfoAccordionStyles}
|
||||||
|
>
|
||||||
|
{Object.entries(perPage).map(([pageLabel, pageInfo]: [string, PdfPageInfo]) => (
|
||||||
|
<Accordion.Item key={pageLabel} value={pageLabel}>
|
||||||
|
<Accordion.Control>
|
||||||
|
<Text fw={600} size="sm">{pageLabel}</Text>
|
||||||
|
</Accordion.Control>
|
||||||
|
<Accordion.Panel>
|
||||||
|
<div style={{ backgroundColor: 'var(--bg-raised)', color: 'var(--text-primary)', borderRadius: 8, padding: 12 }}>
|
||||||
|
<Stack gap="sm">
|
||||||
|
{pageInfo?.Size && (
|
||||||
|
<Stack gap={4}>
|
||||||
|
<Text fw={600} size="sm">{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 fw={600} size="sm">{t('getPdfInfo.perPage.annotations', 'Annotations')}</Text>
|
||||||
|
<KeyValueList obj={pageInfo.Annotations} />
|
||||||
|
</Stack>
|
||||||
|
)}
|
||||||
|
<Stack gap={4}>
|
||||||
|
<Text fw={600} size="sm">{t('getPdfInfo.perPage.images', 'Images')}</Text>
|
||||||
|
{renderList(pageInfo?.Images, noneDetected)}
|
||||||
|
</Stack>
|
||||||
|
<Stack gap={4}>
|
||||||
|
<Text fw={600} size="sm">{t('getPdfInfo.perPage.links', 'Links')}</Text>
|
||||||
|
{renderList(pageInfo?.Links, noneDetected)}
|
||||||
|
</Stack>
|
||||||
|
<Stack gap={4}>
|
||||||
|
<Text fw={600} size="sm">{t('getPdfInfo.perPage.fonts', 'Fonts')}</Text>
|
||||||
|
{renderFontsList(pageInfo?.Fonts, noneDetected)}
|
||||||
|
</Stack>
|
||||||
|
{pageInfo?.XObjectCounts && (
|
||||||
|
<Stack gap={4}>
|
||||||
|
<Text fw={600} size="sm">{t('getPdfInfo.perPage.xobjects', 'XObject Counts')}</Text>
|
||||||
|
<KeyValueList obj={pageInfo.XObjectCounts} />
|
||||||
|
</Stack>
|
||||||
|
)}
|
||||||
|
<Stack gap={4}>
|
||||||
|
<Text fw={600} size="sm">{t('getPdfInfo.perPage.multimedia', 'Multimedia')}</Text>
|
||||||
|
{renderList(pageInfo?.Multimedia, noneDetected)}
|
||||||
|
</Stack>
|
||||||
|
</Stack>
|
||||||
|
</div>
|
||||||
|
</Accordion.Panel>
|
||||||
|
</Accordion.Item>
|
||||||
|
))}
|
||||||
|
</Accordion>
|
||||||
|
) : (
|
||||||
|
<Text size="sm" c="dimmed">{noneDetected}</Text>
|
||||||
|
)}
|
||||||
|
</SectionBlock>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default PerPageSection;
|
||||||
|
|
||||||
|
|
||||||
@@ -0,0 +1,148 @@
|
|||||||
|
import React, { useMemo } from 'react';
|
||||||
|
import { Stack, Text } from '@mantine/core';
|
||||||
|
import { useTranslation } from 'react-i18next';
|
||||||
|
import type { ParsedPdfSections, PdfFontInfo } from '@app/types/getPdfInfo';
|
||||||
|
import SectionBlock from '@app/components/tools/getPdfInfo/shared/SectionBlock';
|
||||||
|
import KeyValueList from '@app/components/tools/getPdfInfo/shared/KeyValueList';
|
||||||
|
|
||||||
|
interface SummarySectionProps {
|
||||||
|
sections: ParsedPdfSections;
|
||||||
|
hideSectionTitle?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
const SummarySection: React.FC<SummarySectionProps> = ({ sections, hideSectionTitle = false }) => {
|
||||||
|
const { t } = useTranslation();
|
||||||
|
|
||||||
|
const summaryBlocks = useMemo(() => {
|
||||||
|
const basic = sections.basicInfo ?? {};
|
||||||
|
const docInfo = sections.documentInfo ?? {};
|
||||||
|
const metadata = sections.metadata ?? {};
|
||||||
|
const encryption = sections.encryption ?? {};
|
||||||
|
const permissions = sections.permissions ?? {};
|
||||||
|
const summary = sections.summaryData ?? {};
|
||||||
|
const other = sections.other ?? {};
|
||||||
|
const perPage = sections.perPage ?? {};
|
||||||
|
|
||||||
|
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,
|
||||||
|
};
|
||||||
|
|
||||||
|
const securityStatusText = encryption.IsEncrypted
|
||||||
|
? t('getPdfInfo.summary.security.encrypted', 'Encrypted PDF - Password protection present')
|
||||||
|
: t('getPdfInfo.summary.security.unencrypted', 'Unencrypted PDF - No password protection');
|
||||||
|
|
||||||
|
const restrictedCount = 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');
|
||||||
|
|
||||||
|
// Helper to get first page data
|
||||||
|
const firstPage = perPage['Page 1'];
|
||||||
|
const firstPageFonts: PdfFontInfo[] = firstPage?.Fonts ?? [];
|
||||||
|
|
||||||
|
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')]: (() => {
|
||||||
|
if (firstPageFonts.length === 0) return 'None';
|
||||||
|
const embedded = firstPageFonts.filter((f) => f.IsEmbedded).length;
|
||||||
|
return `${firstPageFonts.length} (${embedded} embedded)`;
|
||||||
|
})(),
|
||||||
|
[t('getPdfInfo.summary.tech.formFields', 'Form Fields')]: sections.formFields && Object.keys(sections.formFields).length > 0 ? Object.keys(sections.formFields).length : 'None',
|
||||||
|
[t('getPdfInfo.summary.tech.embeddedFiles', 'Embedded Files')]: other.EmbeddedFiles?.length ?? 'None',
|
||||||
|
[t('getPdfInfo.summary.tech.javaScript', 'JavaScript')]: other.JavaScript?.length ?? 'None',
|
||||||
|
[t('getPdfInfo.summary.tech.layers', 'Layers')]: other.Layers?.length ?? 'None',
|
||||||
|
[t('getPdfInfo.summary.tech.bookmarks', 'Bookmarks')]: sections.toc?.length ?? 'None',
|
||||||
|
[t('getPdfInfo.summary.tech.multimedia', 'Multimedia')]: firstPage?.Multimedia?.length ?? 'None',
|
||||||
|
};
|
||||||
|
|
||||||
|
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]);
|
||||||
|
|
||||||
|
const content = (
|
||||||
|
<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.securityTitle', '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} />
|
||||||
|
</Stack>
|
||||||
|
<Stack gap={6}>
|
||||||
|
<Text fw={600} size="sm">{t('getPdfInfo.summary.overviewTitle', 'PDF Overview')}</Text>
|
||||||
|
<Text size="sm" c="dimmed">{summaryBlocks.overview}</Text>
|
||||||
|
</Stack>
|
||||||
|
</Stack>
|
||||||
|
);
|
||||||
|
|
||||||
|
if (hideSectionTitle) {
|
||||||
|
return <div id="summary">{content}</div>;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<SectionBlock title={t('getPdfInfo.summary.title', 'PDF Summary')} anchorId="summary">
|
||||||
|
{content}
|
||||||
|
</SectionBlock>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default SummarySection;
|
||||||
|
|
||||||
|
|
||||||
@@ -0,0 +1,35 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import { Stack, Text } from '@mantine/core';
|
||||||
|
import { useTranslation } from 'react-i18next';
|
||||||
|
import type { PdfTocEntry } from '@app/types/getPdfInfo';
|
||||||
|
import SectionBlock from '@app/components/tools/getPdfInfo/shared/SectionBlock';
|
||||||
|
|
||||||
|
interface TableOfContentsSectionProps {
|
||||||
|
anchorId: string;
|
||||||
|
tocArray: PdfTocEntry[];
|
||||||
|
}
|
||||||
|
|
||||||
|
const TableOfContentsSection: React.FC<TableOfContentsSectionProps> = ({ anchorId, tocArray }) => {
|
||||||
|
const { t } = useTranslation();
|
||||||
|
const noneDetected = t('getPdfInfo.noneDetected', 'None detected');
|
||||||
|
|
||||||
|
return (
|
||||||
|
<SectionBlock title={t('getPdfInfo.sections.tableOfContents', 'Table of Contents')} anchorId={anchorId}>
|
||||||
|
{!tocArray || tocArray.length === 0 ? (
|
||||||
|
<Text size="sm" c="dimmed">{noneDetected}</Text>
|
||||||
|
) : (
|
||||||
|
<Stack gap={4}>
|
||||||
|
{tocArray.map((item, idx) => (
|
||||||
|
<Text key={idx} size="sm" c="dimmed">
|
||||||
|
{typeof item === 'string' ? item : JSON.stringify(item)}
|
||||||
|
</Text>
|
||||||
|
))}
|
||||||
|
</Stack>
|
||||||
|
)}
|
||||||
|
</SectionBlock>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default TableOfContentsSection;
|
||||||
|
|
||||||
|
|
||||||
@@ -0,0 +1,29 @@
|
|||||||
|
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} wrap="nowrap" align="flex-start" style={{ width: '100%' }}>
|
||||||
|
<Text size="sm" style={{ minWidth: 180, maxWidth: 180, flexShrink: 0 }}>{k}</Text>
|
||||||
|
<Text size="sm" c="dimmed" style={{ wordBreak: 'break-word', overflowWrap: 'break-word', flex: 1 }}>
|
||||||
|
{v == null ? '' : String(v)}
|
||||||
|
</Text>
|
||||||
|
</Group>
|
||||||
|
))}
|
||||||
|
</Stack>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default KeyValueList;
|
||||||
|
|
||||||
|
|
||||||
@@ -0,0 +1,47 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import { Code, Text } from '@mantine/core';
|
||||||
|
import { useTranslation } from 'react-i18next';
|
||||||
|
|
||||||
|
interface ScrollableCodeBlockProps {
|
||||||
|
content: string | null | undefined;
|
||||||
|
maxHeight?: string;
|
||||||
|
emptyMessage?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A reusable scrollable code block component with consistent styling.
|
||||||
|
* Used for displaying large text content like XMP metadata or structure trees.
|
||||||
|
*/
|
||||||
|
const ScrollableCodeBlock: React.FC<ScrollableCodeBlockProps> = ({
|
||||||
|
content,
|
||||||
|
maxHeight = '400px',
|
||||||
|
emptyMessage,
|
||||||
|
}) => {
|
||||||
|
const { t } = useTranslation();
|
||||||
|
|
||||||
|
if (!content) {
|
||||||
|
return (
|
||||||
|
<Text size="sm" c="dimmed">
|
||||||
|
{emptyMessage ?? t('getPdfInfo.noneDetected', 'None detected')}
|
||||||
|
</Text>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Code
|
||||||
|
block
|
||||||
|
style={{
|
||||||
|
whiteSpace: 'pre-wrap',
|
||||||
|
backgroundColor: 'var(--bg-raised)',
|
||||||
|
color: 'var(--text-primary)',
|
||||||
|
maxHeight,
|
||||||
|
overflowY: 'auto',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{content}
|
||||||
|
</Code>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default ScrollableCodeBlock;
|
||||||
|
|
||||||
@@ -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,14 @@
|
|||||||
|
import type { AccordionStylesNames } from '@mantine/core';
|
||||||
|
import type { CSSProperties } from 'react';
|
||||||
|
|
||||||
|
type AccordionStyles = Partial<Record<AccordionStylesNames, CSSProperties>>;
|
||||||
|
|
||||||
|
export const pdfInfoAccordionStyles: AccordionStyles = {
|
||||||
|
item: {
|
||||||
|
backgroundColor: 'var(--accordion-item-bg)',
|
||||||
|
},
|
||||||
|
control: {
|
||||||
|
backgroundColor: 'transparent',
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
@@ -44,15 +44,15 @@
|
|||||||
.simulated-page {
|
.simulated-page {
|
||||||
width: min(820px, 100%);
|
width: min(820px, 100%);
|
||||||
min-height: 1040px;
|
min-height: 1040px;
|
||||||
background-color: rgb(var(--pdf-light-simulated-page-bg)) !important;
|
background-color: var(--bg-raised) !important;
|
||||||
box-shadow: 0 12px 32px rgba(var(--pdf-light-simulated-page-text), 0.12) !important;
|
box-shadow: 0 12px 32px var(--shadow-color) !important;
|
||||||
border-radius: 12px !important;
|
border-radius: 12px !important;
|
||||||
padding: 48px 56px !important;
|
padding: 48px 56px !important;
|
||||||
position: relative;
|
position: relative;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
color: rgb(var(--pdf-light-simulated-page-text)) !important;
|
color: var(--text-primary) !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Container for the interactive report view */
|
/* Container for the interactive report view */
|
||||||
@@ -67,12 +67,12 @@
|
|||||||
|
|
||||||
/* Keep field blocks stable colors across themes */
|
/* Keep field blocks stable colors across themes */
|
||||||
.field-value {
|
.field-value {
|
||||||
border: 1px solid rgb(var(--pdf-light-box-border)) !important;
|
border: 1px solid var(--border-default) !important;
|
||||||
background-color: rgb(var(--pdf-light-box-bg)) !important;
|
background-color: var(--bg-raised) !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
.field-container {
|
.field-container {
|
||||||
color: rgb(var(--pdf-light-simulated-page-text)) !important;
|
color: var(--text-primary) !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Thumbnail preview styles */
|
/* Thumbnail preview styles */
|
||||||
@@ -103,3 +103,28 @@
|
|||||||
color: rgb(var(--pdf-light-text-muted));
|
color: rgb(var(--pdf-light-text-muted));
|
||||||
background: linear-gradient(145deg, var(--mantine-color-gray-1) 0%, var(--mantine-color-gray-0) 100%);
|
background: linear-gradient(145deg, var(--mantine-color-gray-1) 0%, var(--mantine-color-gray-0) 100%);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Flash highlight animation for section navigation */
|
||||||
|
@keyframes section-flash {
|
||||||
|
0% {
|
||||||
|
background-color: rgba(255, 235, 59, 0);
|
||||||
|
box-shadow: none;
|
||||||
|
}
|
||||||
|
20% {
|
||||||
|
background-color: rgba(255, 235, 59, 0.35);
|
||||||
|
box-shadow: 0 0 20px rgba(255, 235, 59, 0.5);
|
||||||
|
}
|
||||||
|
50% {
|
||||||
|
background-color: rgba(255, 235, 59, 0.25);
|
||||||
|
box-shadow: 0 0 15px rgba(255, 235, 59, 0.4);
|
||||||
|
}
|
||||||
|
100% {
|
||||||
|
background-color: rgba(255, 235, 59, 0);
|
||||||
|
box-shadow: none;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.section-flash-highlight {
|
||||||
|
animation: section-flash 1.5s ease-out;
|
||||||
|
border-radius: 8px;
|
||||||
|
}
|
||||||
|
|||||||
@@ -27,6 +27,7 @@ import AdjustContrastSingleStepSettings from "@app/components/tools/adjustContra
|
|||||||
import { adjustContrastOperationConfig } from "@app/hooks/tools/adjustContrast/useAdjustContrastOperation";
|
import { adjustContrastOperationConfig } from "@app/hooks/tools/adjustContrast/useAdjustContrastOperation";
|
||||||
import { getSynonyms } from "@app/utils/toolSynonyms";
|
import { getSynonyms } from "@app/utils/toolSynonyms";
|
||||||
import { useProprietaryToolRegistry } from "@app/data/useProprietaryToolRegistry";
|
import { useProprietaryToolRegistry } from "@app/data/useProprietaryToolRegistry";
|
||||||
|
import GetPdfInfo from "@app/tools/GetPdfInfo";
|
||||||
import AddWatermark from "@app/tools/AddWatermark";
|
import AddWatermark from "@app/tools/AddWatermark";
|
||||||
import AddStamp from "@app/tools/AddStamp";
|
import AddStamp from "@app/tools/AddStamp";
|
||||||
import AddAttachments from "@app/tools/AddAttachments";
|
import AddAttachments from "@app/tools/AddAttachments";
|
||||||
@@ -324,14 +325,15 @@ export function useTranslatedToolCatalog(): TranslatedToolCatalog {
|
|||||||
getPdfInfo: {
|
getPdfInfo: {
|
||||||
icon: <LocalIcon icon="fact-check-rounded" width="1.5rem" height="1.5rem" />,
|
icon: <LocalIcon icon="fact-check-rounded" width="1.5rem" height="1.5rem" />,
|
||||||
name: t("home.getPdfInfo.title", "Get ALL Info on PDF"),
|
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"),
|
description: t("home.getPdfInfo.desc", "Grabs any and all information possible on PDFs"),
|
||||||
categoryId: ToolCategoryId.STANDARD_TOOLS,
|
categoryId: ToolCategoryId.STANDARD_TOOLS,
|
||||||
subcategoryId: SubcategoryId.VERIFICATION,
|
subcategoryId: SubcategoryId.VERIFICATION,
|
||||||
endpoints: ["get-info-on-pdf"],
|
endpoints: ["get-info-on-pdf"],
|
||||||
synonyms: getSynonyms(t, "getPdfInfo"),
|
synonyms: getSynonyms(t, "getPdfInfo"),
|
||||||
supportsAutomate: false,
|
supportsAutomate: false,
|
||||||
automationSettings: null
|
automationSettings: null,
|
||||||
|
maxFiles: 1,
|
||||||
},
|
},
|
||||||
validateSignature: {
|
validateSignature: {
|
||||||
icon: <LocalIcon icon="verified-rounded" width="1.5rem" height="1.5rem" />,
|
icon: <LocalIcon icon="verified-rounded" width="1.5rem" height="1.5rem" />,
|
||||||
|
|||||||
@@ -0,0 +1,194 @@
|
|||||||
|
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';
|
||||||
|
import type { GetPdfInfoParameters } from '@app/hooks/tools/getPdfInfo/useGetPdfInfoParameters';
|
||||||
|
|
||||||
|
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,19 @@
|
|||||||
|
import { BaseParameters } from '@app/types/parameters';
|
||||||
|
import { useBaseParameters, BaseParametersHook } from '@app/hooks/tools/shared/useBaseParameters';
|
||||||
|
|
||||||
|
export interface GetPdfInfoParameters extends BaseParameters {
|
||||||
|
// No parameters needed
|
||||||
|
}
|
||||||
|
|
||||||
|
export const defaultParameters: GetPdfInfoParameters = {};
|
||||||
|
|
||||||
|
export type GetPdfInfoParametersHook = BaseParametersHook<GetPdfInfoParameters>;
|
||||||
|
|
||||||
|
export const useGetPdfInfoParameters = (): GetPdfInfoParametersHook => {
|
||||||
|
return useBaseParameters({
|
||||||
|
defaultParameters,
|
||||||
|
endpointName: 'get-info-on-pdf',
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
@@ -256,6 +256,7 @@
|
|||||||
--header-selected-bg: #1E88E5; /* light mode selected header matches dark */
|
--header-selected-bg: #1E88E5; /* light mode selected header matches dark */
|
||||||
--header-selected-fg: #FFFFFF;
|
--header-selected-fg: #FFFFFF;
|
||||||
--file-card-bg: #FFFFFF; /* file card background (light/dark paired) */
|
--file-card-bg: #FFFFFF; /* file card background (light/dark paired) */
|
||||||
|
--accordion-item-bg: #E8EAED; /* accordion item background - more distinguishable */
|
||||||
|
|
||||||
/* shadows */
|
/* shadows */
|
||||||
--drop-shadow-color: rgba(0, 0, 0, 0.08);
|
--drop-shadow-color: rgba(0, 0, 0, 0.08);
|
||||||
@@ -519,6 +520,7 @@
|
|||||||
--header-selected-fg: #FFFFFF;
|
--header-selected-fg: #FFFFFF;
|
||||||
/* file card background (dark) */
|
/* file card background (dark) */
|
||||||
--file-card-bg: #1F2329;
|
--file-card-bg: #1F2329;
|
||||||
|
--accordion-item-bg: #373D45; /* accordion item background - more distinguishable */
|
||||||
|
|
||||||
/* shadows */
|
/* shadows */
|
||||||
--drop-shadow-color: rgba(255, 255, 255, 0.08);
|
--drop-shadow-color: rgba(255, 255, 255, 0.08);
|
||||||
|
|||||||
@@ -0,0 +1,188 @@
|
|||||||
|
import { useEffect, useMemo, useRef } from 'react';
|
||||||
|
import { useTranslation } from 'react-i18next';
|
||||||
|
import PictureAsPdfIcon from '@mui/icons-material/PictureAsPdf';
|
||||||
|
import LinkIcon from '@mui/icons-material/Link';
|
||||||
|
import { Stack, Group, Divider, Text, UnstyledButton } from '@mantine/core';
|
||||||
|
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: 'summary', labelKey: 'getPdfInfo.summary.title', fallback: 'PDF Summary' },
|
||||||
|
{ 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;
|
||||||
|
|
||||||
|
|
||||||
@@ -0,0 +1,273 @@
|
|||||||
|
/** Metadata section from PDF */
|
||||||
|
export interface PdfMetadata {
|
||||||
|
Title?: string | null;
|
||||||
|
Author?: string | null;
|
||||||
|
Subject?: string | null;
|
||||||
|
Keywords?: string | null;
|
||||||
|
Creator?: string | null;
|
||||||
|
Producer?: string | null;
|
||||||
|
CreationDate?: string | null;
|
||||||
|
ModificationDate?: string | null;
|
||||||
|
[key: string]: unknown;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Basic info section */
|
||||||
|
export interface PdfBasicInfo {
|
||||||
|
FileSizeInBytes?: number;
|
||||||
|
WordCount?: number;
|
||||||
|
ParagraphCount?: number;
|
||||||
|
CharacterCount?: number;
|
||||||
|
Compression?: boolean;
|
||||||
|
CompressionType?: string;
|
||||||
|
Language?: string | null;
|
||||||
|
'Number of pages'?: number;
|
||||||
|
TotalImages?: number;
|
||||||
|
[key: string]: unknown;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Document info section */
|
||||||
|
export interface PdfDocumentInfo {
|
||||||
|
'PDF version'?: string;
|
||||||
|
Trapped?: string | null;
|
||||||
|
'Page Mode'?: string;
|
||||||
|
[key: string]: unknown;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Encryption section */
|
||||||
|
export interface PdfEncryption {
|
||||||
|
IsEncrypted?: boolean;
|
||||||
|
EncryptionAlgorithm?: string;
|
||||||
|
KeyLength?: number;
|
||||||
|
[key: string]: unknown;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Permissions section - values are "Allowed" or "Not Allowed" */
|
||||||
|
export interface PdfPermissions {
|
||||||
|
'Document Assembly'?: 'Allowed' | 'Not Allowed';
|
||||||
|
'Extracting Content'?: 'Allowed' | 'Not Allowed';
|
||||||
|
'Extracting for accessibility'?: 'Allowed' | 'Not Allowed';
|
||||||
|
'Form Filling'?: 'Allowed' | 'Not Allowed';
|
||||||
|
'Modifying'?: 'Allowed' | 'Not Allowed';
|
||||||
|
'Modifying annotations'?: 'Allowed' | 'Not Allowed';
|
||||||
|
'Printing'?: 'Allowed' | 'Not Allowed';
|
||||||
|
[key: string]: 'Allowed' | 'Not Allowed' | undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Compliance section */
|
||||||
|
export interface PdfCompliance {
|
||||||
|
'IsPDF/ACompliant'?: boolean;
|
||||||
|
'PDF/AConformanceLevel'?: string;
|
||||||
|
'IsPDF/AValidated'?: boolean;
|
||||||
|
'IsPDF/XCompliant'?: boolean;
|
||||||
|
'IsPDF/ECompliant'?: boolean;
|
||||||
|
'IsPDF/VTCompliant'?: boolean;
|
||||||
|
'IsPDF/UACompliant'?: boolean;
|
||||||
|
'IsPDF/BCompliant'?: boolean;
|
||||||
|
'IsPDF/SECCompliant'?: boolean;
|
||||||
|
[key: string]: unknown;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Font info within a page */
|
||||||
|
export interface PdfFontInfo {
|
||||||
|
Name?: string;
|
||||||
|
IsEmbedded?: boolean;
|
||||||
|
Subtype?: string;
|
||||||
|
ItalicAngle?: number;
|
||||||
|
IsItalic?: boolean;
|
||||||
|
IsBold?: boolean;
|
||||||
|
IsFixedPitch?: boolean;
|
||||||
|
IsSerif?: boolean;
|
||||||
|
IsSymbolic?: boolean;
|
||||||
|
IsScript?: boolean;
|
||||||
|
IsNonsymbolic?: boolean;
|
||||||
|
FontFamily?: string;
|
||||||
|
FontWeight?: number;
|
||||||
|
Count?: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Image info within a page */
|
||||||
|
export interface PdfImageInfo {
|
||||||
|
Width?: number;
|
||||||
|
Height?: number;
|
||||||
|
Name?: string;
|
||||||
|
ColorSpace?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Link info within a page */
|
||||||
|
export interface PdfLinkInfo {
|
||||||
|
URI?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Annotations info within a page */
|
||||||
|
export interface PdfAnnotationsInfo {
|
||||||
|
AnnotationsCount?: number;
|
||||||
|
SubtypeCount?: number;
|
||||||
|
ContentsCount?: number;
|
||||||
|
[key: string]: unknown;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Size/dimensions info within a page */
|
||||||
|
export interface PdfSizeInfo {
|
||||||
|
'Width (px)'?: string;
|
||||||
|
'Height (px)'?: string;
|
||||||
|
'Width (in)'?: string;
|
||||||
|
'Height (in)'?: string;
|
||||||
|
'Width (cm)'?: string;
|
||||||
|
'Height (cm)'?: string;
|
||||||
|
'Standard Page'?: string;
|
||||||
|
[key: string]: unknown;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** XObject counts within a page */
|
||||||
|
export interface PdfXObjectCounts {
|
||||||
|
Image?: number;
|
||||||
|
Form?: number;
|
||||||
|
Other?: number;
|
||||||
|
[key: string]: unknown;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** ICC Profile info */
|
||||||
|
export interface PdfICCProfile {
|
||||||
|
'ICC Profile Length'?: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Page-level information */
|
||||||
|
export interface PdfPageInfo {
|
||||||
|
Size?: PdfSizeInfo;
|
||||||
|
Rotation?: number;
|
||||||
|
'Page Orientation'?: string;
|
||||||
|
MediaBox?: string;
|
||||||
|
CropBox?: string;
|
||||||
|
BleedBox?: string;
|
||||||
|
TrimBox?: string;
|
||||||
|
ArtBox?: string;
|
||||||
|
'Text Characters Count'?: number;
|
||||||
|
Annotations?: PdfAnnotationsInfo;
|
||||||
|
Images?: PdfImageInfo[];
|
||||||
|
Links?: PdfLinkInfo[];
|
||||||
|
Fonts?: PdfFontInfo[];
|
||||||
|
'Color Spaces & ICC Profiles'?: PdfICCProfile[];
|
||||||
|
XObjectCounts?: PdfXObjectCounts;
|
||||||
|
Multimedia?: Record<string, unknown>[];
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Per-page info section (keyed by "Page 1", "Page 2", etc.) */
|
||||||
|
export interface PdfPerPageInfo {
|
||||||
|
[pageLabel: string]: PdfPageInfo;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Embedded file info */
|
||||||
|
export interface PdfEmbeddedFileInfo {
|
||||||
|
Name?: string;
|
||||||
|
FileSize?: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Attachment info */
|
||||||
|
export interface PdfAttachmentInfo {
|
||||||
|
Name?: string;
|
||||||
|
Description?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** JavaScript info */
|
||||||
|
export interface PdfJavaScriptInfo {
|
||||||
|
'JS Name'?: string;
|
||||||
|
'JS Script Length'?: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Layer info */
|
||||||
|
export interface PdfLayerInfo {
|
||||||
|
Name?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Structure tree element */
|
||||||
|
export interface PdfStructureTreeElement {
|
||||||
|
Type?: string;
|
||||||
|
Content?: string;
|
||||||
|
Children?: PdfStructureTreeElement[];
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Other section with miscellaneous data */
|
||||||
|
export interface PdfOtherInfo {
|
||||||
|
Attachments?: PdfAttachmentInfo[];
|
||||||
|
EmbeddedFiles?: PdfEmbeddedFileInfo[];
|
||||||
|
JavaScript?: PdfJavaScriptInfo[];
|
||||||
|
Layers?: PdfLayerInfo[];
|
||||||
|
StructureTree?: PdfStructureTreeElement[];
|
||||||
|
'Bookmarks/Outline/TOC'?: PdfTocEntry[];
|
||||||
|
XMPMetadata?: string | null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Table of contents bookmark entry */
|
||||||
|
export interface PdfTocEntry {
|
||||||
|
Title?: string;
|
||||||
|
[key: string]: unknown;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Summary data section */
|
||||||
|
export interface PdfSummaryData {
|
||||||
|
encrypted?: boolean;
|
||||||
|
restrictedPermissions?: string[];
|
||||||
|
restrictedPermissionsCount?: number;
|
||||||
|
standardCompliance?: string;
|
||||||
|
standardPurpose?: string;
|
||||||
|
standardValidationPassed?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Form fields section */
|
||||||
|
export type PdfFormFields = Record<string, string>;
|
||||||
|
|
||||||
|
/** Parsed sections with normalized keys for frontend use */
|
||||||
|
export interface ParsedPdfSections {
|
||||||
|
metadata?: PdfMetadata | null;
|
||||||
|
formFields?: PdfFormFields | null;
|
||||||
|
basicInfo?: PdfBasicInfo | null;
|
||||||
|
documentInfo?: PdfDocumentInfo | null;
|
||||||
|
compliance?: PdfCompliance | null;
|
||||||
|
encryption?: PdfEncryption | null;
|
||||||
|
permissions?: PdfPermissions | null;
|
||||||
|
toc?: PdfTocEntry[] | null;
|
||||||
|
other?: PdfOtherInfo | null;
|
||||||
|
perPage?: PdfPerPageInfo | null;
|
||||||
|
summaryData?: PdfSummaryData | null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Raw backend response structure */
|
||||||
|
export interface PdfInfoBackendData {
|
||||||
|
Metadata?: PdfMetadata;
|
||||||
|
FormFields?: PdfFormFields;
|
||||||
|
BasicInfo?: PdfBasicInfo;
|
||||||
|
DocumentInfo?: PdfDocumentInfo;
|
||||||
|
Compliancy?: PdfCompliance;
|
||||||
|
Encryption?: PdfEncryption;
|
||||||
|
Permissions?: PdfPermissions;
|
||||||
|
Other?: PdfOtherInfo;
|
||||||
|
PerPageInfo?: PdfPerPageInfo;
|
||||||
|
SummaryData?: PdfSummaryData;
|
||||||
|
// Legacy/alternative keys for backwards compatibility
|
||||||
|
'Form Fields'?: PdfFormFields;
|
||||||
|
'Basic Info'?: PdfBasicInfo;
|
||||||
|
'Document Info'?: PdfDocumentInfo;
|
||||||
|
Compliance?: PdfCompliance;
|
||||||
|
'Bookmarks/Outline/TOC'?: PdfTocEntry[];
|
||||||
|
'Table of Contents'?: PdfTocEntry[];
|
||||||
|
'Per Page Info'?: PdfPerPageInfo;
|
||||||
|
}
|
||||||
|
|
||||||
|
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