lint and tests

This commit is contained in:
EthanHealy01
2025-12-01 14:20:07 +00:00
parent 12462080e0
commit bc74f43717
11 changed files with 573 additions and 221 deletions

View File

@@ -1,13 +1,25 @@
import React, { useEffect, useMemo, useRef } from 'react';
import { Badge, Group, Stack, Text } from '@mantine/core';
import { useTranslation } from 'react-i18next';
import type { PdfInfoReportData, PdfInfoReportEntry } from '@app/types/getPdfInfo';
import type {
PdfInfoReportData,
PdfInfoReportEntry,
PdfInfoBackendData,
ParsedPdfSections,
} from '@app/types/getPdfInfo';
import '@app/components/tools/validateSignature/reportView/styles.css';
import SummarySection from './sections/SummarySection';
import KeyValueSection from './sections/KeyValueSection';
import TableOfContentsSection from './sections/TableOfContentsSection';
import OtherSection from './sections/OtherSection';
import PerPageSection from './sections/PerPageSection';
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 };
@@ -19,22 +31,8 @@ const GetPdfInfoReportView: React.FC<GetPdfInfoReportViewProps> = ({ data }) =>
const entry: PdfInfoReportEntry | null = data.entries[0] ?? null;
useEffect(() => {
if (!data.scrollTo) return;
const idMap: Record<string, string> = {
summary: 'summary',
metadata: 'metadata',
formFields: 'formFields',
basicInfo: 'basicInfo',
documentInfo: 'documentInfo',
compliance: 'compliance',
encryption: 'encryption',
permissions: 'permissions',
toc: 'toc',
other: 'other',
perPage: 'perPage',
};
const anchor = idMap[data.scrollTo];
if (!anchor) return;
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) {
@@ -55,20 +53,20 @@ const GetPdfInfoReportView: React.FC<GetPdfInfoReportViewProps> = ({ data }) =>
}
}, [data.scrollTo]);
const sections = useMemo(() => {
const raw = entry?.data ?? {};
const sections = useMemo((): ParsedPdfSections => {
const raw: PdfInfoBackendData = entry?.data ?? {};
return {
metadata: (raw as any)['Metadata'] as Record<string, unknown> | undefined,
formFields: (raw as any)['FormFields'] ?? (raw as any)['Form Fields'],
basicInfo: (raw as any)['BasicInfo'] ?? (raw as any)['Basic Info'],
documentInfo: (raw as any)['DocumentInfo'] ?? (raw as any)['Document Info'],
compliance: (raw as any)['Compliancy'] ?? (raw as any)['Compliance'],
encryption: (raw as any)['Encryption'] as Record<string, unknown> | undefined,
permissions: (raw as any)['Permissions'] as Record<string, unknown> | undefined,
toc: (raw as any)['Bookmarks/Outline/TOC'] ?? (raw as any)['Table of Contents'],
other: (raw as any)['Other'] as Record<string, unknown> | undefined,
perPage: (raw as any)['PerPageInfo'] ?? (raw as any)['Per Page Info'],
summaryData: (raw as any)['SummaryData'] as Record<string, unknown> | undefined,
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]);
@@ -100,27 +98,27 @@ const GetPdfInfoReportView: React.FC<GetPdfInfoReportViewProps> = ({ data }) =>
</div>
</Group>
<SummarySection sections={sections as any} />
<SummarySection sections={sections} />
<KeyValueSection title={t('getPdfInfo.sections.metadata', 'Metadata')} anchorId="metadata" obj={sections.metadata ?? null} />
<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 as any} />
<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 ?? null} />
<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 ?? null} />
<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 ?? null} />
<KeyValueSection title={t('getPdfInfo.sections.compliance', 'Compliance')} anchorId="compliance" obj={sections.compliance} />
<KeyValueSection title={t('getPdfInfo.sections.encryption', 'Encryption')} anchorId="encryption" obj={sections.encryption ?? null} />
<KeyValueSection title={t('getPdfInfo.sections.encryption', 'Encryption')} anchorId="encryption" obj={sections.encryption} />
<KeyValueSection title={t('getPdfInfo.sections.permissions', 'Permissions')} anchorId="permissions" obj={sections.permissions ?? null} />
<KeyValueSection title={t('getPdfInfo.sections.permissions', 'Permissions')} anchorId="permissions" obj={sections.permissions} />
<TableOfContentsSection anchorId="toc" tocArray={Array.isArray(sections.toc) ? (sections.toc as any[]) : []} />
<TableOfContentsSection anchorId="toc" tocArray={sections.toc ?? []} />
<OtherSection anchorId="other" other={sections.other as any} />
<OtherSection anchorId="other" other={sections.other} />
<PerPageSection anchorId="perPage" perPage={sections.perPage as any} />
<PerPageSection anchorId="perPage" perPage={sections.perPage} />
</Stack>
</div>
</Stack>

View File

@@ -1,6 +1,6 @@
import React from 'react';
import SectionBlock from '../shared/SectionBlock';
import KeyValueList from '../shared/KeyValueList';
import SectionBlock from '@app/components/tools/getPdfInfo/shared/SectionBlock';
import KeyValueList from '@app/components/tools/getPdfInfo/shared/KeyValueList';
interface KeyValueSectionProps {
title: string;

View File

@@ -1,37 +1,55 @@
import React from 'react';
import { Accordion, Code, Stack, Text } from '@mantine/core';
import { Accordion, Stack, Text } from '@mantine/core';
import { useTranslation } from 'react-i18next';
import SectionBlock from '../shared/SectionBlock';
import SimpleArrayList from '../shared/SimpleArrayList';
import { pdfInfoAccordionStyles } from '../shared/accordionStyles';
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?: Record<string, any> | null;
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">
{typeof item === 'string' ? item : JSON.stringify(item)}
</Text>
))}
</Stack>
);
};
const OtherSection: React.FC<OtherSectionProps> = ({ anchorId, other }) => {
const { t } = useTranslation();
const panelBg = 'var(--bg-raised)';
const panelText = 'var(--text-primary)';
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>
<SimpleArrayList arr={Array.isArray(other?.Attachments) ? other?.Attachments : []} />
{renderList(other?.Attachments, noneDetected)}
</Stack>
<Stack gap={6}>
<Text fw={600} size="sm">{t('getPdfInfo.other.embeddedFiles', 'Embedded Files')}</Text>
<SimpleArrayList arr={Array.isArray(other?.EmbeddedFiles) ? other?.EmbeddedFiles : []} />
{renderList(other?.EmbeddedFiles, noneDetected)}
</Stack>
<Stack gap={6}>
<Text fw={600} size="sm">{t('getPdfInfo.other.javaScript', 'JavaScript')}</Text>
<SimpleArrayList arr={Array.isArray(other?.JavaScript) ? other?.JavaScript : []} />
{renderList(other?.JavaScript, noneDetected)}
</Stack>
<Stack gap={6}>
<Text fw={600} size="sm">{t('getPdfInfo.other.layers', 'Layers')}</Text>
<SimpleArrayList arr={Array.isArray(other?.Layers) ? other?.Layers : []} />
{renderList(other?.Layers, noneDetected)}
</Stack>
<Accordion
variant="separated"
@@ -44,20 +62,7 @@ const OtherSection: React.FC<OtherSectionProps> = ({ anchorId, other }) => {
<Text fw={600} size="sm">{t('getPdfInfo.other.structureTree', 'StructureTree')}</Text>
</Accordion.Control>
<Accordion.Panel>
{Array.isArray(other?.StructureTree) && other?.StructureTree.length > 0
? <Code
block
style={{
whiteSpace: 'pre-wrap',
backgroundColor: panelBg,
color: panelText,
maxHeight: '20rem',
overflowY: 'auto'
}}
>
{JSON.stringify(other?.StructureTree, null, 2)}
</Code>
: <Text size="sm" c="dimmed">{t('getPdfInfo.noneDetected', 'None detected')}</Text>}
<ScrollableCodeBlock content={structureTreeContent} maxHeight="20rem" />
</Accordion.Panel>
</Accordion.Item>
<Accordion.Item value="xmp">
@@ -65,20 +70,7 @@ const OtherSection: React.FC<OtherSectionProps> = ({ anchorId, other }) => {
<Text fw={600} size="sm">{t('getPdfInfo.other.xmp', 'XMPMetadata')}</Text>
</Accordion.Control>
<Accordion.Panel>
{other?.XMPMetadata
? <Code
block
style={{
whiteSpace: 'pre-wrap',
backgroundColor: panelBg,
color: panelText,
maxHeight: '400px',
overflowY: 'auto'
}}
>
{String(other?.XMPMetadata)}
</Code>
: <Text size="sm" c="dimmed">{t('getPdfInfo.noneDetected', 'None detected')}</Text>}
<ScrollableCodeBlock content={other?.XMPMetadata} maxHeight="400px" />
</Accordion.Panel>
</Accordion.Item>
</Accordion>

View File

@@ -1,43 +1,68 @@
import React from 'react';
import { Accordion, Group, Stack, Text } from '@mantine/core';
import { Accordion, Stack, Text } from '@mantine/core';
import { useTranslation } from 'react-i18next';
import SectionBlock from '../shared/SectionBlock';
import KeyValueList from '../shared/KeyValueList';
import SimpleArrayList from '../shared/SimpleArrayList';
import { pdfInfoAccordionStyles } from '../shared/accordionStyles';
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?: Record<string, any> | null;
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">
{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">
{`${font.Name ?? 'Unknown'}${font.IsEmbedded ? ' (embedded)' : ''}`}
</Text>
))}
</Stack>
);
};
const PerPageSection: React.FC<PerPageSectionProps> = ({ anchorId, perPage }) => {
const { t } = useTranslation();
const panelBg = 'var(--bg-raised)';
const panelText = 'var(--text-primary)';
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}>
{perPage && Object.keys(perPage as any).length > 0 ? (
{hasPages ? (
<Accordion
variant="separated"
radius="md"
defaultValue=""
styles={pdfInfoAccordionStyles}
>
{Object.entries(perPage as any).map(([pageLabel, pageInfo]: [string, any]) => (
{Object.entries(perPage).map(([pageLabel, pageInfo]: [string, PdfPageInfo]) => (
<Accordion.Item key={pageLabel} value={pageLabel}>
<Accordion.Control>
<Group justify="space-between" w="100%" gap="xs">
<Text fw={600} size="sm">{pageLabel}</Text>
</Group>
<Text fw={600} size="sm">{pageLabel}</Text>
</Accordion.Control>
<Accordion.Panel>
<div style={{ backgroundColor: panelBg, color: panelText, borderRadius: 8, padding: 12 }}>
<div style={{ backgroundColor: 'var(--bg-raised)', color: 'var(--text-primary)', borderRadius: 8, padding: 12 }}>
<Stack gap="sm">
{pageInfo?.Size && (
<Stack gap={4}>
<Text size="sm" fw={600}>{t('getPdfInfo.perPage.size', 'Size')}</Text>
<Text fw={600} size="sm">{t('getPdfInfo.perPage.size', 'Size')}</Text>
<KeyValueList obj={pageInfo.Size} />
</Stack>
)}
@@ -53,41 +78,31 @@ const PerPageSection: React.FC<PerPageSectionProps> = ({ anchorId, perPage }) =>
}} />
{pageInfo?.Annotations && (
<Stack gap={4}>
<Text size="sm" fw={600}>{t('getPdfInfo.perPage.annotations', 'Annotations')}</Text>
<Text fw={600} size="sm">{t('getPdfInfo.perPage.annotations', 'Annotations')}</Text>
<KeyValueList obj={pageInfo.Annotations} />
</Stack>
)}
<Stack gap={4}>
<Text size="sm" fw={600}>{t('getPdfInfo.perPage.images', 'Images')}</Text>
<SimpleArrayList arr={Array.isArray(pageInfo?.Images) ? pageInfo.Images : []} />
<Text fw={600} size="sm">{t('getPdfInfo.perPage.images', 'Images')}</Text>
{renderList(pageInfo?.Images, noneDetected)}
</Stack>
<Stack gap={4}>
<Text size="sm" fw={600}>{t('getPdfInfo.perPage.links', 'Links')}</Text>
<SimpleArrayList arr={Array.isArray(pageInfo?.Links) ? pageInfo.Links : []} />
<Text fw={600} size="sm">{t('getPdfInfo.perPage.links', 'Links')}</Text>
{renderList(pageInfo?.Links, noneDetected)}
</Stack>
<Stack gap={4}>
<Text size="sm" fw={600}>{t('getPdfInfo.perPage.fonts', 'Fonts')}</Text>
{Array.isArray(pageInfo?.Fonts) && pageInfo.Fonts.length > 0
? (
<Stack gap={4}>
{pageInfo.Fonts.map((f: any, idx: number) => (
<Text key={idx} size="sm" c="dimmed">
{`${f?.Name ?? 'Unknown'}${f?.IsEmbedded ? ' (embedded)' : ''}`}
</Text>
))}
</Stack>
)
: <Text size="sm" c="dimmed">{t('getPdfInfo.noneDetected', 'None detected')}</Text>}
<Text fw={600} size="sm">{t('getPdfInfo.perPage.fonts', 'Fonts')}</Text>
{renderFontsList(pageInfo?.Fonts, noneDetected)}
</Stack>
{pageInfo?.XObjectCounts && (
<Stack gap={4}>
<Text size="sm" fw={600}>{t('getPdfInfo.perPage.xobjects', 'XObject Counts')}</Text>
<Text fw={600} size="sm">{t('getPdfInfo.perPage.xobjects', 'XObject Counts')}</Text>
<KeyValueList obj={pageInfo.XObjectCounts} />
</Stack>
)}
<Stack gap={4}>
<Text size="sm" fw={600}>{t('getPdfInfo.perPage.multimedia', 'Multimedia')}</Text>
<SimpleArrayList arr={Array.isArray(pageInfo?.Multimedia) ? pageInfo.Multimedia : []} />
<Text fw={600} size="sm">{t('getPdfInfo.perPage.multimedia', 'Multimedia')}</Text>
{renderList(pageInfo?.Multimedia, noneDetected)}
</Stack>
</Stack>
</div>
@@ -96,7 +111,7 @@ const PerPageSection: React.FC<PerPageSectionProps> = ({ anchorId, perPage }) =>
))}
</Accordion>
) : (
<Text size="sm" c="dimmed">{t('getPdfInfo.noneDetected', 'None detected')}</Text>
<Text size="sm" c="dimmed">{noneDetected}</Text>
)}
</SectionBlock>
);

View File

@@ -1,42 +1,31 @@
import React, { useMemo } from 'react';
import { Stack, Text } from '@mantine/core';
import { useTranslation } from 'react-i18next';
import SectionBlock from '../shared/SectionBlock';
import KeyValueList from '../shared/KeyValueList';
type SectionsData = {
metadata?: Record<string, unknown>;
formFields?: Record<string, unknown>;
basicInfo?: Record<string, any>;
documentInfo?: Record<string, any>;
compliance?: Record<string, any>;
encryption?: Record<string, any>;
permissions?: Record<string, any>;
toc?: any;
other?: Record<string, any>;
perPage?: Record<string, any>;
summaryData?: Record<string, any>;
};
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: SectionsData;
sections: ParsedPdfSections;
}
const SummarySection: React.FC<SummarySectionProps> = ({ sections }) => {
const { t } = useTranslation();
const summaryBlocks = useMemo(() => {
const basic = (sections.basicInfo as any) || {};
const docInfo = (sections.documentInfo as any) || {};
const metadata = (sections.metadata as any) || {};
const encryption = (sections.encryption as any) || {};
const permissions = (sections.permissions as any) || {};
const summary = (sections.summaryData as any) || {};
const 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 fileSizeBytes = basic.FileSizeInBytes;
const pdfVersion = docInfo['PDF version'];
const language = basic['Language'];
const language = basic.Language;
const basicInformation: Record<string, unknown> = {
[t('getPdfInfo.summary.pages', 'Pages')]: pages,
@@ -46,20 +35,18 @@ const SummarySection: React.FC<SummarySectionProps> = ({ sections }) => {
};
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'],
[t('getPdfInfo.summary.title', 'Title')]: metadata.Title,
[t('getPdfInfo.summary.author', 'Author')]: metadata.Author,
[t('getPdfInfo.summary.created', 'Created')]: metadata.CreationDate,
[t('getPdfInfo.summary.modified', 'Modified')]: metadata.ModificationDate,
};
let securityStatusText = '';
if (encryption?.IsEncrypted) {
securityStatusText = t('getPdfInfo.summary.security.encrypted', 'Encrypted PDF - Password protection present');
} else {
securityStatusText = t('getPdfInfo.summary.security.unencrypted', 'Unencrypted PDF - No password protection');
}
const restrictedCount = typeof summary?.restrictedPermissionsCount === 'number' ? summary.restrictedPermissionsCount : 0;
const permissionsAllAllowed = Object.values(permissions || {}).every((v) => v === 'Allowed');
const 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
@@ -70,36 +57,32 @@ const SummarySection: React.FC<SummarySectionProps> = ({ sections }) => {
? 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'];
const total = basic.TotalImages;
if (typeof total === 'number') return total === 0 ? 'None' : `${total}`;
return 'None';
})(),
[t('getPdfInfo.summary.tech.fonts', 'Fonts')]: (() => {
const pages = sections.perPage as any;
const firstPage = pages ? pages['Page 1'] : undefined;
const fonts = Array.isArray(firstPage?.Fonts) ? firstPage.Fonts : [];
if (!fonts || fonts.length === 0) return 'None';
const embedded = fonts.filter((f: any) => f?.IsEmbedded).length;
return `${fonts.length} (${embedded} embedded)`;
})(),
[t('getPdfInfo.summary.tech.formFields', 'Form Fields')]: sections.formFields && Object.keys(sections.formFields as any).length > 0 ? Object.keys(sections.formFields as any).length : 'None',
[t('getPdfInfo.summary.tech.embeddedFiles', 'Embedded Files')]: Array.isArray((sections.other as any)?.EmbeddedFiles) ? (sections.other as any).EmbeddedFiles.length : 'None',
[t('getPdfInfo.summary.tech.javaScript', 'JavaScript')]: Array.isArray((sections.other as any)?.JavaScript) ? (sections.other as any).JavaScript.length : 'None',
[t('getPdfInfo.summary.tech.layers', 'Layers')]: Array.isArray((sections.other as any)?.Layers) ? (sections.other as any).Layers.length : 'None',
[t('getPdfInfo.summary.tech.bookmarks', 'Bookmarks')]: Array.isArray(sections.toc as any[]) ? (sections.toc as any[]).length : 'None',
[t('getPdfInfo.summary.tech.multimedia', 'Multimedia')]: (() => {
const pages = sections.perPage as any;
const firstPage = pages ? pages['Page 1'] : undefined;
const media = Array.isArray(firstPage?.Multimedia) ? firstPage.Multimedia : [];
return media.length === 0 ? 'None' : `${media.length}`;
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 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}}).', {
@@ -133,17 +116,17 @@ const SummarySection: React.FC<SummarySectionProps> = ({ sections }) => {
<KeyValueList obj={summaryBlocks.documentInformation} />
</Stack>
<Stack gap={6}>
<Text fw={600} size="sm">{t('getPdfInfo.summary.security', 'Security Status')}</Text>
<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 as any} />
<KeyValueList obj={summaryBlocks.technical} />
</Stack>
<Stack gap={6}>
<Text fw={600} size="sm">{t('getPdfInfo.summary.overview', 'PDF Overview')}</Text>
<Text fw={600} size="sm">{t('getPdfInfo.summary.overviewTitle', 'PDF Overview')}</Text>
<Text size="sm" c="dimmed">{summaryBlocks.overview}</Text>
</Stack>
</Stack>

View File

@@ -1,18 +1,31 @@
import React from 'react';
import SectionBlock from '../shared/SectionBlock';
import SimpleArrayList from '../shared/SimpleArrayList';
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: any[];
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}>
<SimpleArrayList arr={Array.isArray(tocArray) ? tocArray : []} />
{!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>
);
};

View File

@@ -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;

View File

@@ -1,26 +0,0 @@
import React from 'react';
import { Stack, Text } from '@mantine/core';
interface SimpleArrayListProps {
arr?: any[] | null;
emptyLabel?: string;
}
const SimpleArrayList: React.FC<SimpleArrayListProps> = ({ arr, emptyLabel }) => {
if (!arr || arr.length === 0) {
return <Text size="sm" c="dimmed">{emptyLabel ?? 'None detected'}</Text>;
}
return (
<Stack gap={4}>
{arr.map((item, idx) => (
<Text key={idx} size="sm" c="dimmed">
{typeof item === 'string' ? item : JSON.stringify(item)}
</Text>
))}
</Stack>
);
};
export default SimpleArrayList;

View File

@@ -1,8 +1,8 @@
import { useEffect, useMemo, useRef } from 'react';
import { useTranslation } from 'react-i18next';
import PictureAsPdfIcon from '@mui/icons-material/PictureAsPdf';
import { Stack, Button, Group, Divider, Text, UnstyledButton } from '@mantine/core';
import LinkIcon from '@mui/icons-material/Link';
import { 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';

View File

@@ -1,9 +1,256 @@
/** 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 {
// Raw backend payload keyed by human-readable section names
// Example keys: "Metadata", "Form Fields", "Basic Info", "Document Info",
// "Compliance", "Encryption", "Permissions", "Table of Contents",
// "Other", "Per Page Info"
[sectionName: string]: unknown;
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 {
@@ -24,5 +271,3 @@ export interface PdfInfoReportData {
export const INFO_JSON_FILENAME = 'response.json';
export const INFO_PDF_FILENAME = 'pdf-information-report.pdf';