From b90de68742a51feb54cc30c7a1d5221931f3a4fa Mon Sep 17 00:00:00 2001 From: EthanHealy01 Date: Thu, 13 Nov 2025 13:00:44 +0000 Subject: [PATCH] get all info on PDF, needs a few more tidy-ups --- .../tools/getPdfInfo/GetPdfInfoReportView.tsx | 127 +++++++++++ .../tools/getPdfInfo/GetPdfInfoResults.tsx | 79 +++++++ .../getPdfInfo/sections/KeyValueSection.tsx | 22 ++ .../getPdfInfo/sections/OtherSection.tsx | 82 ++++++++ .../getPdfInfo/sections/PerPageSection.tsx | 101 +++++++++ .../getPdfInfo/sections/SummarySection.tsx | 156 ++++++++++++++ .../sections/TableOfContentsSection.tsx | 22 ++ .../tools/getPdfInfo/shared/KeyValueList.tsx | 27 +++ .../tools/getPdfInfo/shared/SectionBlock.tsx | 22 ++ .../getPdfInfo/shared/SimpleArrayList.tsx | 26 +++ .../validateSignature/reportView/styles.css | 12 +- .../core/data/useTranslatedToolRegistry.tsx | 3 +- .../getPdfInfo/useGetPdfInfoOperation.ts | 199 ++++++++++++++++++ .../getPdfInfo/useGetPdfInfoParameters.ts | 15 ++ frontend/src/core/tools/GetPdfInfo.tsx | 187 ++++++++++++++++ frontend/src/core/types/getPdfInfo.ts | 28 +++ 16 files changed, 1101 insertions(+), 7 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/SectionBlock.tsx create mode 100644 frontend/src/core/components/tools/getPdfInfo/shared/SimpleArrayList.tsx 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/src/core/components/tools/getPdfInfo/GetPdfInfoReportView.tsx b/frontend/src/core/components/tools/getPdfInfo/GetPdfInfoReportView.tsx new file mode 100644 index 000000000..01e53eaf6 --- /dev/null +++ b/frontend/src/core/components/tools/getPdfInfo/GetPdfInfoReportView.tsx @@ -0,0 +1,127 @@ +import React, { useEffect, useMemo, useRef } from 'react'; +import { Badge, Group, Stack, Text } from '@mantine/core'; +import { useTranslation } from 'react-i18next'; +import type { PdfInfoReportData, PdfInfoReportEntry } from '@app/types/getPdfInfo'; +import '@app/components/tools/validateSignature/reportView/styles.css'; +import SummarySection from './sections/SummarySection'; +import KeyValueSection from './sections/KeyValueSection'; +import TableOfContentsSection from './sections/TableOfContentsSection'; +import OtherSection from './sections/OtherSection'; +import PerPageSection from './sections/PerPageSection'; + +interface GetPdfInfoReportViewProps { + data: PdfInfoReportData & { scrollTo?: string | null }; +} + +const GetPdfInfoReportView: React.FC = ({ data }) => { + const { t } = useTranslation(); + const containerRef = useRef(null); + const entry: PdfInfoReportEntry | null = data.entries[0] ?? null; + + useEffect(() => { + if (!data.scrollTo) return; + const idMap: Record = { + metadata: 'metadata', + formFields: 'formFields', + basicInfo: 'basicInfo', + documentInfo: 'documentInfo', + compliance: 'compliance', + encryption: 'encryption', + permissions: 'permissions', + toc: 'toc', + other: 'other', + perPage: 'perPage', + }; + const anchor = idMap[data.scrollTo]; + if (!anchor) return; + const el = containerRef.current?.querySelector(`#${anchor}`); + if (el) { + el.scrollIntoView({ behavior: 'smooth', block: 'start' }); + } + }, [data.scrollTo]); + + const sections = useMemo(() => { + const raw = entry?.data ?? {}; + return { + metadata: (raw as any)['Metadata'] as Record | 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 | undefined, + permissions: (raw as any)['Permissions'] as Record | undefined, + toc: (raw as any)['Bookmarks/Outline/TOC'] ?? (raw as any)['Table of Contents'], + other: (raw as any)['Other'] as Record | undefined, + perPage: (raw as any)['PerPageInfo'] ?? (raw as any)['Per Page Info'], + summaryData: (raw as any)['SummaryData'] as Record | undefined, + }; + }, [entry]); + + if (!entry) { + return ( +
+ + No Data + Run the tool to generate the report. + +
+ ); + } + + return ( +
+ + + + {t('getPdfInfo.report.title', 'PDF Information View')} + + + {t('getPdfInfo.report.generatedAt', 'Generated')}{' '} + {new Date(data.generatedAt).toLocaleString()} + + + +
+ + +
+ + {entry.fileName} + + + {t('getPdfInfo.report.entryLabel', 'Full information 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..02c606387 --- /dev/null +++ b/frontend/src/core/components/tools/getPdfInfo/sections/KeyValueSection.tsx @@ -0,0 +1,22 @@ +import React from 'react'; +import SectionBlock from '../shared/SectionBlock'; +import KeyValueList from '../shared/KeyValueList'; + +interface KeyValueSectionProps { + title: string; + anchorId: string; + obj?: Record | 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..aeb07c168 --- /dev/null +++ b/frontend/src/core/components/tools/getPdfInfo/sections/OtherSection.tsx @@ -0,0 +1,82 @@ +import React from 'react'; +import { Accordion, Code, Stack, Text } from '@mantine/core'; +import { useTranslation } from 'react-i18next'; +import SectionBlock from '../shared/SectionBlock'; +import SimpleArrayList from '../shared/SimpleArrayList'; + +interface OtherSectionProps { + anchorId: string; + other?: Record | null; +} + +const OtherSection: React.FC = ({ anchorId, other }) => { + const { t } = useTranslation(); + const panelBg = 'var(--bg-raised)'; + const panelText = 'var(--text-primary)'; + return ( + + + + {t('getPdfInfo.other.attachments', 'Attachments')} + + + + {t('getPdfInfo.other.embeddedFiles', 'Embedded Files')} + + + + {t('getPdfInfo.other.javaScript', 'JavaScript')} + + + + {t('getPdfInfo.other.layers', 'Layers')} + + + + + + {t('getPdfInfo.other.structureTree', 'StructureTree')} + + + {Array.isArray(other?.StructureTree) && other?.StructureTree.length > 0 + ? + {JSON.stringify(other?.StructureTree, null, 2)} + + : {t('getPdfInfo.noneDetected', 'None detected')}} + + + + + {t('getPdfInfo.other.xmp', 'XMPMetadata')} + + + {other?.XMPMetadata + ? + {String(other?.XMPMetadata)} + + : {t('getPdfInfo.noneDetected', 'None detected')}} + + + + + + ); +}; + +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..16bce3404 --- /dev/null +++ b/frontend/src/core/components/tools/getPdfInfo/sections/PerPageSection.tsx @@ -0,0 +1,101 @@ +import React from 'react'; +import { Accordion, Group, Stack, Text } from '@mantine/core'; +import { useTranslation } from 'react-i18next'; +import SectionBlock from '../shared/SectionBlock'; +import KeyValueList from '../shared/KeyValueList'; +import SimpleArrayList from '../shared/SimpleArrayList'; + +interface PerPageSectionProps { + anchorId: string; + perPage?: Record | null; +} + +const PerPageSection: React.FC = ({ anchorId, perPage }) => { + const { t } = useTranslation(); + const panelBg = 'var(--bg-raised)'; + const panelText = 'var(--text-primary)'; + + return ( + + {perPage && Object.keys(perPage as any).length > 0 ? ( + + {Object.entries(perPage as any).map(([pageLabel, pageInfo]: [string, any]) => ( + + + + {pageLabel} + + + +
+ + {pageInfo?.Size && ( + + {t('getPdfInfo.perPage.size', 'Size')} + + + )} + + {pageInfo?.Annotations && ( + + {t('getPdfInfo.perPage.annotations', 'Annotations')} + + + )} + + {t('getPdfInfo.perPage.images', 'Images')} + + + + {t('getPdfInfo.perPage.links', 'Links')} + + + + {t('getPdfInfo.perPage.fonts', 'Fonts')} + {Array.isArray(pageInfo?.Fonts) && pageInfo.Fonts.length > 0 + ? ( + + {pageInfo.Fonts.map((f: any, idx: number) => ( + + {`${f?.Name ?? 'Unknown'}${f?.IsEmbedded ? ' (embedded)' : ''}`} + + ))} + + ) + : {t('getPdfInfo.noneDetected', 'None detected')}} + + {pageInfo?.XObjectCounts && ( + + {t('getPdfInfo.perPage.xobjects', 'XObject Counts')} + + + )} + + {t('getPdfInfo.perPage.multimedia', 'Multimedia')} + + + +
+
+
+ ))} +
+ ) : ( + {t('getPdfInfo.noneDetected', 'None detected')} + )} +
+ ); +}; + +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..d9d8c8f38 --- /dev/null +++ b/frontend/src/core/components/tools/getPdfInfo/sections/SummarySection.tsx @@ -0,0 +1,156 @@ +import React, { useMemo } from 'react'; +import { Stack, Text } from '@mantine/core'; +import { useTranslation } from 'react-i18next'; +import SectionBlock from '../shared/SectionBlock'; +import KeyValueList from '../shared/KeyValueList'; + +type SectionsData = { + metadata?: Record; + formFields?: Record; + basicInfo?: Record; + documentInfo?: Record; + compliance?: Record; + encryption?: Record; + permissions?: Record; + toc?: any; + other?: Record; + perPage?: Record; + summaryData?: Record; +}; + +interface SummarySectionProps { + sections: SectionsData; +} + +const SummarySection: React.FC = ({ sections }) => { + const { t } = useTranslation(); + + const summaryBlocks = useMemo(() => { + const basic = (sections.basicInfo as any) || {}; + const docInfo = (sections.documentInfo as any) || {}; + const metadata = (sections.metadata as any) || {}; + const encryption = (sections.encryption as any) || {}; + const permissions = (sections.permissions as any) || {}; + const summary = (sections.summaryData as any) || {}; + + const pages = basic['Number of pages']; + const fileSizeBytes = basic['FileSizeInBytes']; + const pdfVersion = docInfo['PDF version']; + const language = basic['Language']; + + const basicInformation: Record = { + [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'], + }; + + let securityStatusText = ''; + if (encryption?.IsEncrypted) { + securityStatusText = t('getPdfInfo.summary.security.encrypted', 'Encrypted PDF - Password protection present'); + } else { + securityStatusText = t('getPdfInfo.summary.security.unencrypted', 'Unencrypted PDF - No password protection'); + } + const restrictedCount = typeof summary?.restrictedPermissionsCount === 'number' ? summary.restrictedPermissionsCount : 0; + const permissionsAllAllowed = Object.values(permissions || {}).every((v) => v === 'Allowed'); + const permSummary = permissionsAllAllowed + ? t('getPdfInfo.summary.permsAll', 'All Permissions Allowed') + : restrictedCount > 0 + ? t('getPdfInfo.summary.permsRestricted', '{{count}} restrictions', { count: restrictedCount }) + : t('getPdfInfo.summary.permsMixed', 'Some permissions restricted'); + + const complianceText = sections.compliance && Object.values(sections.compliance).some(Boolean) + ? t('getPdfInfo.summary.hasCompliance', 'Has compliance standards') + : t('getPdfInfo.summary.noCompliance', 'No Compliance Standards'); + + const technical: Record = { + [t('getPdfInfo.summary.tech.images', 'Images')]: (() => { + const total = basic['TotalImages']; + if (typeof total === 'number') return total === 0 ? 'None' : `${total}`; + return 'None'; + })(), + [t('getPdfInfo.summary.tech.fonts', 'Fonts')]: (() => { + const pages = sections.perPage as any; + const firstPage = pages ? pages['Page 1'] : undefined; + const fonts = Array.isArray(firstPage?.Fonts) ? firstPage.Fonts : []; + if (!fonts || fonts.length === 0) return 'None'; + const embedded = fonts.filter((f: any) => f?.IsEmbedded).length; + return `${fonts.length} (${embedded} embedded)`; + })(), + [t('getPdfInfo.summary.tech.formFields', 'Form Fields')]: sections.formFields && Object.keys(sections.formFields as any).length > 0 ? Object.keys(sections.formFields as any).length : 'None', + [t('getPdfInfo.summary.tech.embeddedFiles', 'Embedded Files')]: Array.isArray((sections.other as any)?.EmbeddedFiles) ? (sections.other as any).EmbeddedFiles.length : 'None', + [t('getPdfInfo.summary.tech.javaScript', 'JavaScript')]: Array.isArray((sections.other as any)?.JavaScript) ? (sections.other as any).JavaScript.length : 'None', + [t('getPdfInfo.summary.tech.layers', 'Layers')]: Array.isArray((sections.other as any)?.Layers) ? (sections.other as any).Layers.length : 'None', + [t('getPdfInfo.summary.tech.bookmarks', 'Bookmarks')]: Array.isArray(sections.toc as any[]) ? (sections.toc as any[]).length : 'None', + [t('getPdfInfo.summary.tech.multimedia', 'Multimedia')]: (() => { + const pages = sections.perPage as any; + const firstPage = pages ? pages['Page 1'] : undefined; + const media = Array.isArray(firstPage?.Multimedia) ? firstPage.Multimedia : []; + return media.length === 0 ? 'None' : `${media.length}`; + })(), + }; + + const overview = (() => { + const tTitle = metadata['Title'] ? `"${metadata['Title']}"` : t('getPdfInfo.summary.overview.untitled', 'an untitled document'); + const author = metadata['Author'] || t('getPdfInfo.summary.overview.unknown', 'Unknown Author'); + const pagesCount = typeof pages === 'number' ? pages : '?'; + const version = pdfVersion ?? '?'; + return t('getPdfInfo.summary.overview.text', 'This is a {{pages}}-page PDF titled {{title}} created by {{author}} (PDF version {{version}}).', { + pages: pagesCount, + title: tTitle, + author, + version, + }); + })(); + + return { + basicInformation, + documentInformation, + securityStatusText, + permSummary, + complianceText, + technical, + overview, + }; + }, [sections, t]); + + return ( + + + + {t('getPdfInfo.summary.basic', 'Basic Information')} + + + + {t('getPdfInfo.summary.documentInfo', 'Document Information')} + + + + {t('getPdfInfo.summary.security', 'Security Status')} + {summaryBlocks.securityStatusText} + {summaryBlocks.permSummary} + {summaryBlocks.complianceText} + + + {t('getPdfInfo.summary.technical', 'Technical')} + + + + {t('getPdfInfo.summary.overview', 'PDF Overview')} + {summaryBlocks.overview} + + + + ); +}; + +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..bbfacfe65 --- /dev/null +++ b/frontend/src/core/components/tools/getPdfInfo/sections/TableOfContentsSection.tsx @@ -0,0 +1,22 @@ +import React from 'react'; +import SectionBlock from '../shared/SectionBlock'; +import SimpleArrayList from '../shared/SimpleArrayList'; +import { useTranslation } from 'react-i18next'; + +interface TableOfContentsSectionProps { + anchorId: string; + tocArray: any[]; +} + +const TableOfContentsSection: React.FC = ({ anchorId, tocArray }) => { + const { t } = useTranslation(); + return ( + + + + ); +}; + +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..9ea2e25a1 --- /dev/null +++ b/frontend/src/core/components/tools/getPdfInfo/shared/KeyValueList.tsx @@ -0,0 +1,27 @@ +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/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/SimpleArrayList.tsx b/frontend/src/core/components/tools/getPdfInfo/shared/SimpleArrayList.tsx new file mode 100644 index 000000000..bde2d0d8a --- /dev/null +++ b/frontend/src/core/components/tools/getPdfInfo/shared/SimpleArrayList.tsx @@ -0,0 +1,26 @@ +import React from 'react'; +import { Stack, Text } from '@mantine/core'; + +interface SimpleArrayListProps { + arr?: any[] | null; + emptyLabel?: string; +} + +const SimpleArrayList: React.FC = ({ arr, emptyLabel }) => { + if (!arr || arr.length === 0) { + return {emptyLabel ?? 'None detected'}; + } + return ( + + {arr.map((item, idx) => ( + + {typeof item === 'string' ? item : JSON.stringify(item)} + + ))} + + ); +}; + +export default SimpleArrayList; + + diff --git a/frontend/src/core/components/tools/validateSignature/reportView/styles.css b/frontend/src/core/components/tools/validateSignature/reportView/styles.css index b2d01f224..94ac2a963 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 */ diff --git a/frontend/src/core/data/useTranslatedToolRegistry.tsx b/frontend/src/core/data/useTranslatedToolRegistry.tsx index bcd11478b..a4e63d6cd 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"; @@ -294,7 +295,7 @@ 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, 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..fffe65389 --- /dev/null +++ b/frontend/src/core/hooks/tools/getPdfInfo/useGetPdfInfoOperation.ts @@ -0,0 +1,199 @@ +import { useCallback, useEffect, useMemo, useRef, useState } from 'react'; +import { useTranslation } from 'react-i18next'; +import apiClient from '@app/services/apiClient'; +import { useFileContext } from '@app/contexts/file/fileHooks'; +import { ToolOperationHook } from '@app/hooks/tools/shared/useToolOperation'; +import type { StirlingFile } from '@app/types/fileContext'; +import { extractErrorMessage } from '@app/utils/toolErrorHandler'; +import { + PdfInfoReportEntry, + INFO_JSON_FILENAME, +} from '@app/types/getPdfInfo'; + +export interface GetPdfInfoParameters { + // Placeholder for future parameters +} + +export const defaultParameters: GetPdfInfoParameters = {}; + +export interface GetPdfInfoOperationHook extends ToolOperationHook { + 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..e9065ae88 --- /dev/null +++ b/frontend/src/core/hooks/tools/getPdfInfo/useGetPdfInfoParameters.ts @@ -0,0 +1,15 @@ +import { useBaseParameters, BaseParametersHook } from '@app/hooks/tools/shared/useBaseParameters'; +import type { GetPdfInfoParameters } from '@app/hooks/tools/getPdfInfo/useGetPdfInfoOperation'; + +export const defaultParameters: GetPdfInfoParameters = {}; + +export type GetPdfInfoParametersHook = BaseParametersHook; + +export const useGetPdfInfoParameters = (): GetPdfInfoParametersHook => { + return useBaseParameters({ + defaultParameters, + endpointName: 'get-info-on-pdf', + }); +}; + + diff --git a/frontend/src/core/tools/GetPdfInfo.tsx b/frontend/src/core/tools/GetPdfInfo.tsx new file mode 100644 index 000000000..228558971 --- /dev/null +++ b/frontend/src/core/tools/GetPdfInfo.tsx @@ -0,0 +1,187 @@ +import { useEffect, useMemo, useRef } from 'react'; +import { useTranslation } from 'react-i18next'; +import PictureAsPdfIcon from '@mui/icons-material/PictureAsPdf'; +import { Stack, Button, Group, Divider, Text, UnstyledButton } from '@mantine/core'; +import LinkIcon from '@mui/icons-material/Link'; +import { createToolFlow } from '@app/components/tools/shared/createToolFlow'; +import { useBaseTool } from '@app/hooks/tools/shared/useBaseTool'; +import { BaseToolProps, ToolComponent } from '@app/types/tool'; +import { useGetPdfInfoParameters, defaultParameters } from '@app/hooks/tools/getPdfInfo/useGetPdfInfoParameters'; +import GetPdfInfoResults from '@app/components/tools/getPdfInfo/GetPdfInfoResults'; +import { useGetPdfInfoOperation, GetPdfInfoOperationHook } from '@app/hooks/tools/getPdfInfo/useGetPdfInfoOperation'; +import GetPdfInfoReportView from '@app/components/tools/getPdfInfo/GetPdfInfoReportView'; +import { useToolWorkflow } from '@app/contexts/ToolWorkflowContext'; +import { useNavigationActions, useNavigationState } from '@app/contexts/NavigationContext'; +import type { PdfInfoReportData } from '@app/types/getPdfInfo'; + +const CHAPTERS = [ + { id: 'metadata', labelKey: 'getPdfInfo.sections.metadata', fallback: 'Metadata' }, + { id: 'formFields', labelKey: 'getPdfInfo.sections.formFields', fallback: 'Form Fields' }, + { id: 'basicInfo', labelKey: 'getPdfInfo.sections.basicInfo', fallback: 'Basic Info' }, + { id: 'documentInfo', labelKey: 'getPdfInfo.sections.documentInfo', fallback: 'Document Info' }, + { id: 'compliance', labelKey: 'getPdfInfo.sections.compliance', fallback: 'Compliance' }, + { id: 'encryption', labelKey: 'getPdfInfo.sections.encryption', fallback: 'Encryption' }, + { id: 'permissions', labelKey: 'getPdfInfo.sections.permissions', fallback: 'Permissions' }, + { id: 'toc', labelKey: 'getPdfInfo.sections.tableOfContents', fallback: 'Table of Contents' }, + { id: 'other', labelKey: 'getPdfInfo.sections.other', fallback: 'Other' }, + { id: 'perPage', labelKey: 'getPdfInfo.sections.perPageInfo', fallback: 'Per Page Info' }, +]; + +const GetPdfInfo = (props: BaseToolProps) => { + const { t } = useTranslation(); + const { actions: navigationActions } = useNavigationActions(); + const navigationState = useNavigationState(); + const { + registerCustomWorkbenchView, + unregisterCustomWorkbenchView, + setCustomWorkbenchViewData, + clearCustomWorkbenchViewData, + } = useToolWorkflow(); + + const REPORT_VIEW_ID = 'getPdfInfoReport'; + const REPORT_WORKBENCH_ID = 'custom:getPdfInfoReport' as const; + const reportIcon = useMemo(() => , []); + + 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..feaa56097 --- /dev/null +++ b/frontend/src/core/types/getPdfInfo.ts @@ -0,0 +1,28 @@ +export interface PdfInfoBackendData { + // Raw backend payload keyed by human-readable section names + // Example keys: "Metadata", "Form Fields", "Basic Info", "Document Info", + // "Compliance", "Encryption", "Permissions", "Table of Contents", + // "Other", "Per Page Info" + [sectionName: string]: unknown; +} + +export interface PdfInfoReportEntry { + fileId: string; + fileName: string; + fileSize: number | null; + lastModified: number | null; + thumbnailUrl?: string | null; + data: PdfInfoBackendData; + error: string | null; + summaryGeneratedAt?: number; +} + +export interface PdfInfoReportData { + generatedAt: number; + entries: PdfInfoReportEntry[]; +} + +export const INFO_JSON_FILENAME = 'response.json'; +export const INFO_PDF_FILENAME = 'pdf-information-report.pdf'; + +