mirror of
https://github.com/arsvendg/Stirling-PDF.git
synced 2026-07-15 19:10:47 +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:
@@ -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 {
|
||||
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 */
|
||||
@@ -103,3 +103,28 @@
|
||||
color: rgb(var(--pdf-light-text-muted));
|
||||
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;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user