From f8dbf171e190c7d026f0cee7dea9245923f8a7e7 Mon Sep 17 00:00:00 2001 From: EthanHealy01 <80844253+EthanHealy01@users.noreply.github.com> Date: Wed, 3 Dec 2025 20:02:42 +0000 Subject: [PATCH] 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. --- .../public/locales/en-GB/translation.toml | 85 ++++++ .../tools/getPdfInfo/GetPdfInfoReportView.tsx | 128 ++++++++ .../tools/getPdfInfo/GetPdfInfoResults.tsx | 79 +++++ .../getPdfInfo/sections/KeyValueSection.tsx | 22 ++ .../getPdfInfo/sections/OtherSection.tsx | 84 ++++++ .../getPdfInfo/sections/PerPageSection.tsx | 122 ++++++++ .../getPdfInfo/sections/SummarySection.tsx | 148 ++++++++++ .../sections/TableOfContentsSection.tsx | 35 +++ .../tools/getPdfInfo/shared/KeyValueList.tsx | 29 ++ .../getPdfInfo/shared/ScrollableCodeBlock.tsx | 47 +++ .../tools/getPdfInfo/shared/SectionBlock.tsx | 22 ++ .../getPdfInfo/shared/accordionStyles.ts | 14 + .../validateSignature/reportView/styles.css | 37 ++- .../core/data/useTranslatedToolRegistry.tsx | 6 +- .../getPdfInfo/useGetPdfInfoOperation.ts | 194 +++++++++++++ .../getPdfInfo/useGetPdfInfoParameters.ts | 19 ++ frontend/src/core/styles/theme.css | 2 + frontend/src/core/tools/GetPdfInfo.tsx | 188 ++++++++++++ frontend/src/core/types/getPdfInfo.ts | 273 ++++++++++++++++++ 19 files changed, 1526 insertions(+), 8 deletions(-) create mode 100644 frontend/src/core/components/tools/getPdfInfo/GetPdfInfoReportView.tsx create mode 100644 frontend/src/core/components/tools/getPdfInfo/GetPdfInfoResults.tsx create mode 100644 frontend/src/core/components/tools/getPdfInfo/sections/KeyValueSection.tsx create mode 100644 frontend/src/core/components/tools/getPdfInfo/sections/OtherSection.tsx create mode 100644 frontend/src/core/components/tools/getPdfInfo/sections/PerPageSection.tsx create mode 100644 frontend/src/core/components/tools/getPdfInfo/sections/SummarySection.tsx create mode 100644 frontend/src/core/components/tools/getPdfInfo/sections/TableOfContentsSection.tsx create mode 100644 frontend/src/core/components/tools/getPdfInfo/shared/KeyValueList.tsx create mode 100644 frontend/src/core/components/tools/getPdfInfo/shared/ScrollableCodeBlock.tsx create mode 100644 frontend/src/core/components/tools/getPdfInfo/shared/SectionBlock.tsx create mode 100644 frontend/src/core/components/tools/getPdfInfo/shared/accordionStyles.ts create mode 100644 frontend/src/core/hooks/tools/getPdfInfo/useGetPdfInfoOperation.ts create mode 100644 frontend/src/core/hooks/tools/getPdfInfo/useGetPdfInfoParameters.ts create mode 100644 frontend/src/core/tools/GetPdfInfo.tsx create mode 100644 frontend/src/core/types/getPdfInfo.ts diff --git a/frontend/public/locales/en-GB/translation.toml b/frontend/public/locales/en-GB/translation.toml index a425ad3d5..dd30ca449 100644 --- a/frontend/public/locales/en-GB/translation.toml +++ b/frontend/public/locales/en-GB/translation.toml @@ -3036,6 +3036,91 @@ title = "Get Info on PDF" header = "Get Info on PDF" submit = "Get Info" 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] tags = "extract" diff --git a/frontend/src/core/components/tools/getPdfInfo/GetPdfInfoReportView.tsx b/frontend/src/core/components/tools/getPdfInfo/GetPdfInfoReportView.tsx new file mode 100644 index 000000000..ec5d3805e --- /dev/null +++ b/frontend/src/core/components/tools/getPdfInfo/GetPdfInfoReportView.tsx @@ -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 = ({ data }) => { + const { t } = useTranslation(); + const containerRef = useRef(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(`#${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 ( +
+ + No Data + Run the tool to generate the report. + +
+ ); + } + + return ( +
+ + +
+ + + + {entry.fileName} + - {t('getPdfInfo.summary.title', 'PDF Summary')} + + + + + + + + + + + + + + + + + + + + + + + + + + +
+
+
+ ); +}; + +export default GetPdfInfoReportView; + + diff --git a/frontend/src/core/components/tools/getPdfInfo/GetPdfInfoResults.tsx b/frontend/src/core/components/tools/getPdfInfo/GetPdfInfoResults.tsx new file mode 100644 index 000000000..5ee89db4a --- /dev/null +++ b/frontend/src/core/components/tools/getPdfInfo/GetPdfInfoResults.tsx @@ -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 ( + + + {t('getPdfInfo.processing', 'Extracting information...')} + + ); + } + + if (!isLoading && operation.results.length === 0) { + return ( + + {t('getPdfInfo.noResults', 'Run the tool to generate a report.')} + + ); + } + + return ( + + {/* No background post-processing once JSON is ready */} + {errorMessage && ( + + {errorMessage} + + )} + + + + {t('getPdfInfo.downloads', 'Downloads')} + + + + + ); +}; + +export default GetPdfInfoResults; + + diff --git a/frontend/src/core/components/tools/getPdfInfo/sections/KeyValueSection.tsx b/frontend/src/core/components/tools/getPdfInfo/sections/KeyValueSection.tsx new file mode 100644 index 000000000..a98a47568 --- /dev/null +++ b/frontend/src/core/components/tools/getPdfInfo/sections/KeyValueSection.tsx @@ -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 | null; + emptyLabel?: string; +} + +const KeyValueSection: React.FC = ({ title, anchorId, obj, emptyLabel }) => { + return ( + + + + ); +}; + +export default KeyValueSection; + + diff --git a/frontend/src/core/components/tools/getPdfInfo/sections/OtherSection.tsx b/frontend/src/core/components/tools/getPdfInfo/sections/OtherSection.tsx new file mode 100644 index 000000000..e7eb8d8b3 --- /dev/null +++ b/frontend/src/core/components/tools/getPdfInfo/sections/OtherSection.tsx @@ -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 {emptyText}; + return ( + + {arr.map((item, idx) => ( + + {typeof item === 'string' ? item : JSON.stringify(item)} + + ))} + + ); +}; + +const OtherSection: React.FC = ({ 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 ( + + + + {t('getPdfInfo.other.attachments', 'Attachments')} + {renderList(other?.Attachments, noneDetected)} + + + {t('getPdfInfo.other.embeddedFiles', 'Embedded Files')} + {renderList(other?.EmbeddedFiles, noneDetected)} + + + {t('getPdfInfo.other.javaScript', 'JavaScript')} + {renderList(other?.JavaScript, noneDetected)} + + + {t('getPdfInfo.other.layers', 'Layers')} + {renderList(other?.Layers, noneDetected)} + + + + + {t('getPdfInfo.other.structureTree', 'StructureTree')} + + + + + + + + {t('getPdfInfo.other.xmp', 'XMPMetadata')} + + + + + + + + + ); +}; + +export default OtherSection; + + diff --git a/frontend/src/core/components/tools/getPdfInfo/sections/PerPageSection.tsx b/frontend/src/core/components/tools/getPdfInfo/sections/PerPageSection.tsx new file mode 100644 index 000000000..4fd257cce --- /dev/null +++ b/frontend/src/core/components/tools/getPdfInfo/sections/PerPageSection.tsx @@ -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 {emptyText}; + return ( + + {arr.map((item, idx) => ( + + {typeof item === 'string' ? item : JSON.stringify(item)} + + ))} + + ); +}; + +const renderFontsList = (fonts: PdfFontInfo[] | undefined, emptyText: string) => { + if (!fonts || fonts.length === 0) return {emptyText}; + return ( + + {fonts.map((font, idx) => ( + + {`${font.Name ?? 'Unknown'}${font.IsEmbedded ? ' (embedded)' : ''}`} + + ))} + + ); +}; + +const PerPageSection: React.FC = ({ anchorId, perPage }) => { + const { t } = useTranslation(); + const noneDetected = t('getPdfInfo.noneDetected', 'None detected'); + + const hasPages = perPage && Object.keys(perPage).length > 0; + + return ( + + {hasPages ? ( + + {Object.entries(perPage).map(([pageLabel, pageInfo]: [string, PdfPageInfo]) => ( + + + {pageLabel} + + +
+ + {pageInfo?.Size && ( + + {t('getPdfInfo.perPage.size', 'Size')} + + + )} + + {pageInfo?.Annotations && ( + + {t('getPdfInfo.perPage.annotations', 'Annotations')} + + + )} + + {t('getPdfInfo.perPage.images', 'Images')} + {renderList(pageInfo?.Images, noneDetected)} + + + {t('getPdfInfo.perPage.links', 'Links')} + {renderList(pageInfo?.Links, noneDetected)} + + + {t('getPdfInfo.perPage.fonts', 'Fonts')} + {renderFontsList(pageInfo?.Fonts, noneDetected)} + + {pageInfo?.XObjectCounts && ( + + {t('getPdfInfo.perPage.xobjects', 'XObject Counts')} + + + )} + + {t('getPdfInfo.perPage.multimedia', 'Multimedia')} + {renderList(pageInfo?.Multimedia, noneDetected)} + + +
+
+
+ ))} +
+ ) : ( + {noneDetected} + )} +
+ ); +}; + +export default PerPageSection; + + diff --git a/frontend/src/core/components/tools/getPdfInfo/sections/SummarySection.tsx b/frontend/src/core/components/tools/getPdfInfo/sections/SummarySection.tsx new file mode 100644 index 000000000..680f4b64d --- /dev/null +++ b/frontend/src/core/components/tools/getPdfInfo/sections/SummarySection.tsx @@ -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 = ({ 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 = { + [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 = { + [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 = { + [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 = ( + + + {t('getPdfInfo.summary.basic', 'Basic Information')} + + + + {t('getPdfInfo.summary.documentInfo', 'Document Information')} + + + + {t('getPdfInfo.summary.securityTitle', 'Security Status')} + {summaryBlocks.securityStatusText} + {summaryBlocks.permSummary} + {summaryBlocks.complianceText} + + + {t('getPdfInfo.summary.technical', 'Technical')} + + + + {t('getPdfInfo.summary.overviewTitle', 'PDF Overview')} + {summaryBlocks.overview} + + + ); + + if (hideSectionTitle) { + return
{content}
; + } + + return ( + + {content} + + ); +}; + +export default SummarySection; + + diff --git a/frontend/src/core/components/tools/getPdfInfo/sections/TableOfContentsSection.tsx b/frontend/src/core/components/tools/getPdfInfo/sections/TableOfContentsSection.tsx new file mode 100644 index 000000000..57e4ac2fe --- /dev/null +++ b/frontend/src/core/components/tools/getPdfInfo/sections/TableOfContentsSection.tsx @@ -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 = ({ anchorId, tocArray }) => { + const { t } = useTranslation(); + const noneDetected = t('getPdfInfo.noneDetected', 'None detected'); + + return ( + + {!tocArray || tocArray.length === 0 ? ( + {noneDetected} + ) : ( + + {tocArray.map((item, idx) => ( + + {typeof item === 'string' ? item : JSON.stringify(item)} + + ))} + + )} + + ); +}; + +export default TableOfContentsSection; + + diff --git a/frontend/src/core/components/tools/getPdfInfo/shared/KeyValueList.tsx b/frontend/src/core/components/tools/getPdfInfo/shared/KeyValueList.tsx new file mode 100644 index 000000000..e0cd809cc --- /dev/null +++ b/frontend/src/core/components/tools/getPdfInfo/shared/KeyValueList.tsx @@ -0,0 +1,29 @@ +import React from 'react'; +import { Group, Stack, Text } from '@mantine/core'; + +interface KeyValueListProps { + obj?: Record | null; + emptyLabel?: string; +} + +const KeyValueList: React.FC = ({ obj, emptyLabel }) => { + if (!obj || Object.keys(obj).length === 0) { + return {emptyLabel ?? 'None detected'}; + } + return ( + + {Object.entries(obj).map(([k, v]) => ( + + {k} + + {v == null ? '' : String(v)} + + + ))} + + ); +}; + +export default KeyValueList; + + diff --git a/frontend/src/core/components/tools/getPdfInfo/shared/ScrollableCodeBlock.tsx b/frontend/src/core/components/tools/getPdfInfo/shared/ScrollableCodeBlock.tsx new file mode 100644 index 000000000..bf0426426 --- /dev/null +++ b/frontend/src/core/components/tools/getPdfInfo/shared/ScrollableCodeBlock.tsx @@ -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 = ({ + content, + maxHeight = '400px', + emptyMessage, +}) => { + const { t } = useTranslation(); + + if (!content) { + return ( + + {emptyMessage ?? t('getPdfInfo.noneDetected', 'None detected')} + + ); + } + + return ( + + {content} + + ); +}; + +export default ScrollableCodeBlock; + diff --git a/frontend/src/core/components/tools/getPdfInfo/shared/SectionBlock.tsx b/frontend/src/core/components/tools/getPdfInfo/shared/SectionBlock.tsx new file mode 100644 index 000000000..0faa993f6 --- /dev/null +++ b/frontend/src/core/components/tools/getPdfInfo/shared/SectionBlock.tsx @@ -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 = ({ title, anchorId, children }) => { + return ( + + {title} + + {children} + + ); +}; + +export default SectionBlock; + + diff --git a/frontend/src/core/components/tools/getPdfInfo/shared/accordionStyles.ts b/frontend/src/core/components/tools/getPdfInfo/shared/accordionStyles.ts new file mode 100644 index 000000000..e974c9845 --- /dev/null +++ b/frontend/src/core/components/tools/getPdfInfo/shared/accordionStyles.ts @@ -0,0 +1,14 @@ +import type { AccordionStylesNames } from '@mantine/core'; +import type { CSSProperties } from 'react'; + +type AccordionStyles = Partial>; + +export const pdfInfoAccordionStyles: AccordionStyles = { + item: { + backgroundColor: 'var(--accordion-item-bg)', + }, + control: { + backgroundColor: 'transparent', + }, +}; + diff --git a/frontend/src/core/components/tools/validateSignature/reportView/styles.css b/frontend/src/core/components/tools/validateSignature/reportView/styles.css index b2d01f224..27b1081d4 100644 --- a/frontend/src/core/components/tools/validateSignature/reportView/styles.css +++ b/frontend/src/core/components/tools/validateSignature/reportView/styles.css @@ -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; +} diff --git a/frontend/src/core/data/useTranslatedToolRegistry.tsx b/frontend/src/core/data/useTranslatedToolRegistry.tsx index 5d0bcd622..bc514d879 100644 --- a/frontend/src/core/data/useTranslatedToolRegistry.tsx +++ b/frontend/src/core/data/useTranslatedToolRegistry.tsx @@ -27,6 +27,7 @@ import AdjustContrastSingleStepSettings from "@app/components/tools/adjustContra import { adjustContrastOperationConfig } from "@app/hooks/tools/adjustContrast/useAdjustContrastOperation"; import { getSynonyms } from "@app/utils/toolSynonyms"; import { useProprietaryToolRegistry } from "@app/data/useProprietaryToolRegistry"; +import GetPdfInfo from "@app/tools/GetPdfInfo"; import AddWatermark from "@app/tools/AddWatermark"; import AddStamp from "@app/tools/AddStamp"; import AddAttachments from "@app/tools/AddAttachments"; @@ -324,14 +325,15 @@ export function useTranslatedToolCatalog(): TranslatedToolCatalog { getPdfInfo: { icon: , name: t("home.getPdfInfo.title", "Get ALL Info on PDF"), - component: null, + component: GetPdfInfo, description: t("home.getPdfInfo.desc", "Grabs any and all information possible on PDFs"), categoryId: ToolCategoryId.STANDARD_TOOLS, subcategoryId: SubcategoryId.VERIFICATION, endpoints: ["get-info-on-pdf"], synonyms: getSynonyms(t, "getPdfInfo"), supportsAutomate: false, - automationSettings: null + automationSettings: null, + maxFiles: 1, }, validateSignature: { icon: , diff --git a/frontend/src/core/hooks/tools/getPdfInfo/useGetPdfInfoOperation.ts b/frontend/src/core/hooks/tools/getPdfInfo/useGetPdfInfoOperation.ts new file mode 100644 index 000000000..019968bca --- /dev/null +++ b/frontend/src/core/hooks/tools/getPdfInfo/useGetPdfInfoOperation.ts @@ -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 { + 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(null); + const [files, setFiles] = useState([]); + const [downloadUrl, setDownloadUrl] = useState(null); + const [downloadFilename, setDownloadFilename] = useState(''); + const [results, setResults] = useState([]); + + const cancelRequested = useRef(false); + const previousUrl = useRef(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( + () => ({ + 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, + ] + ); +}; + + diff --git a/frontend/src/core/hooks/tools/getPdfInfo/useGetPdfInfoParameters.ts b/frontend/src/core/hooks/tools/getPdfInfo/useGetPdfInfoParameters.ts new file mode 100644 index 000000000..488484809 --- /dev/null +++ b/frontend/src/core/hooks/tools/getPdfInfo/useGetPdfInfoParameters.ts @@ -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; + +export const useGetPdfInfoParameters = (): GetPdfInfoParametersHook => { + return useBaseParameters({ + defaultParameters, + endpointName: 'get-info-on-pdf', + }); +}; + + diff --git a/frontend/src/core/styles/theme.css b/frontend/src/core/styles/theme.css index 30991cf30..8551735ed 100644 --- a/frontend/src/core/styles/theme.css +++ b/frontend/src/core/styles/theme.css @@ -256,6 +256,7 @@ --header-selected-bg: #1E88E5; /* light mode selected header matches dark */ --header-selected-fg: #FFFFFF; --file-card-bg: #FFFFFF; /* file card background (light/dark paired) */ + --accordion-item-bg: #E8EAED; /* accordion item background - more distinguishable */ /* shadows */ --drop-shadow-color: rgba(0, 0, 0, 0.08); @@ -519,6 +520,7 @@ --header-selected-fg: #FFFFFF; /* file card background (dark) */ --file-card-bg: #1F2329; + --accordion-item-bg: #373D45; /* accordion item background - more distinguishable */ /* shadows */ --drop-shadow-color: rgba(255, 255, 255, 0.08); diff --git a/frontend/src/core/tools/GetPdfInfo.tsx b/frontend/src/core/tools/GetPdfInfo.tsx new file mode 100644 index 000000000..aa35fe16b --- /dev/null +++ b/frontend/src/core/tools/GetPdfInfo.tsx @@ -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(() => , []); + + 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(() => { + 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(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: ( + + {CHAPTERS.map((c, idx) => ( + + { + 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' }} + > + + + + {t(c.labelKey, c.fallback)} + + + + {idx < CHAPTERS.length - 1 && } + + ))} + + ), + }, + { + title: t('getPdfInfo.results', 'Results'), + isVisible: showResultsStep, + isCollapsed: false, + content: ( + + ), + }, + ], + 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; + + diff --git a/frontend/src/core/types/getPdfInfo.ts b/frontend/src/core/types/getPdfInfo.ts new file mode 100644 index 000000000..f489cd99a --- /dev/null +++ b/frontend/src/core/types/getPdfInfo.ts @@ -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[]; +} + +/** 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; + +/** 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';