From 9d0b68e18c24e180202e79f960b60595fb52d8ed Mon Sep 17 00:00:00 2001 From: alam00000 Date: Tue, 31 Mar 2026 17:59:49 +0530 Subject: [PATCH] Refactor and enhance type safety across various modules - Updated function parameters and return types in `page-preview.ts`, `pdf-decrypt.ts`, and `pymupdf-loader.ts` for improved type safety. - Introduced type definitions for `CpdfInstance`, `PyMuPDFInstance`, and other related types to ensure better type checking. - Enhanced error handling in `sanitize.ts` by creating a utility function for error messages. - Removed unnecessary type assertions and improved type inference in `editor.ts`, `serialization.ts`, and `tools.test.ts`. - Added type definitions for markdown-it plugins to improve compatibility and type safety. - Enforced stricter TypeScript settings by enabling `noImplicitAny` in `tsconfig.json`. - Cleaned up test files by refining type assertions and ensuring consistency in type usage. --- eslint.config.mjs | 104 +++-- src/js/canvasEditor.ts | 191 +++++---- src/js/compare/engine/compare.worker.ts | 3 - src/js/handlers/fileHandler.ts | 58 ++- src/js/logic/add-page-labels-page.ts | 31 +- src/js/logic/add-stamps.ts | 27 +- src/js/logic/add-watermark-page.ts | 7 +- src/js/logic/bookmark-pdf.ts | 8 +- src/js/logic/cbz-to-pdf-page.ts | 13 +- src/js/logic/change-permissions-page.ts | 21 +- src/js/logic/compress-pdf-page.ts | 15 +- src/js/logic/crop-pdf-page.ts | 22 +- src/js/logic/cropper.ts | 61 ++- src/js/logic/csv-to-pdf-page.ts | 442 +++++++++++---------- src/js/logic/decrypt-pdf-page.ts | 11 +- src/js/logic/delete-pages-page.ts | 2 +- src/js/logic/deskew-pdf-page.ts | 13 +- src/js/logic/digital-sign-pdf-page.ts | 2 +- src/js/logic/duplicate-organize.ts | 21 +- src/js/logic/edit-attachments-page.ts | 6 +- src/js/logic/edit-pdf-page.ts | 51 ++- src/js/logic/email-to-pdf-page.ts | 22 +- src/js/logic/email-to-pdf.ts | 23 +- src/js/logic/encrypt-pdf-page.ts | 17 +- src/js/logic/epub-to-pdf-page.ts | 8 +- src/js/logic/excel-to-pdf-page.ts | 394 +++++++++--------- src/js/logic/extract-images-page.ts | 12 +- src/js/logic/extract-pages-page.ts | 2 +- src/js/logic/extract-tables-page.ts | 4 +- src/js/logic/fb2-to-pdf-page.ts | 8 +- src/js/logic/form-creator.ts | 19 +- src/js/logic/image-to-pdf-page.ts | 17 +- src/js/logic/index.ts | 79 +--- src/js/logic/jpg-to-pdf-page.ts | 11 +- src/js/logic/linearize-pdf-page.ts | 10 +- src/js/logic/merge-pdf-page.ts | 18 +- src/js/logic/mobi-to-pdf-page.ts | 8 +- src/js/logic/odt-to-pdf-page.ts | 394 +++++++++--------- src/js/logic/organize-pdf-page.ts | 8 +- src/js/logic/pages-to-pdf-page.ts | 4 +- src/js/logic/pdf-booklet-page.ts | 4 +- src/js/logic/pdf-layers-page.ts | 66 ++- src/js/logic/pdf-multi-tool.ts | 8 +- src/js/logic/pdf-to-csv-page.ts | 5 +- src/js/logic/pdf-to-docx-page.ts | 10 +- src/js/logic/pdf-to-excel-page.ts | 4 +- src/js/logic/pdf-to-markdown-page.ts | 10 +- src/js/logic/pdf-to-markdown.ts | 10 +- src/js/logic/pdf-to-pdfa-page.ts | 7 +- src/js/logic/pdf-to-svg-page.ts | 6 +- src/js/logic/pdf-to-text-page.ts | 15 +- src/js/logic/pdf-workflow-page.ts | 11 +- src/js/logic/png-to-pdf-page.ts | 12 +- src/js/logic/powerpoint-to-pdf-page.ts | 10 +- src/js/logic/prepare-pdf-for-ai-page.ts | 13 +- src/js/logic/psd-to-pdf-page.ts | 11 +- src/js/logic/rasterize-pdf-page.ts | 27 +- src/js/logic/redact.ts | 8 +- src/js/logic/remove-blank-pages-page.ts | 10 +- src/js/logic/remove-restrictions-page.ts | 23 +- src/js/logic/repair-pdf.ts | 3 +- src/js/logic/rtf-to-pdf-page.ts | 10 +- src/js/logic/shortcuts.ts | 354 +++++++++-------- src/js/logic/split-pdf-page.ts | 35 +- src/js/logic/svg-to-pdf-page.ts | 10 +- src/js/logic/table-of-contents.ts | 9 - src/js/logic/txt-to-pdf-page.ts | 11 +- src/js/logic/validate-signature-pdf.ts | 16 +- src/js/logic/webp-to-pdf-page.ts | 12 +- src/js/logic/word-to-pdf-page.ts | 15 +- src/js/logic/word-to-pdf.ts | 32 +- src/js/logic/wpd-to-pdf-page.ts | 4 +- src/js/logic/wps-to-pdf-page.ts | 4 +- src/js/logic/xps-to-pdf-page.ts | 8 +- src/js/main.ts | 6 +- src/js/state.ts | 10 +- src/js/types/alternate-merge-page-type.ts | 10 +- src/js/types/canvas-editor-type.ts | 7 + src/js/types/crop-pdf-type.ts | 24 +- src/js/types/delete-pages-type.ts | 13 +- src/js/types/edit-pdf-type.ts | 15 + src/js/types/index.ts | 9 + src/js/types/markdown-editor-type.ts | 28 ++ src/js/types/merge-pdf-type.ts | 8 +- src/js/types/merge-worker-type.ts | 32 ++ src/js/types/redact-type.ts | 7 + src/js/types/sanitize-type.ts | 8 + src/js/types/shortcuts-type.ts | 5 + src/js/types/ui-type.ts | 5 + src/js/types/utils-types.ts | 235 +++++++++++ src/js/ui.ts | 37 +- src/js/utils/compress.ts | 12 +- src/js/utils/cpdf-helper.ts | 7 +- src/js/utils/disabled-tools.ts | 4 +- src/js/utils/ghostscript-dynamic-loader.ts | 31 +- src/js/utils/helpers.ts | 47 ++- src/js/utils/libreoffice-loader.ts | 264 ++++++------ src/js/utils/markdown-editor.ts | 194 +++++---- src/js/utils/page-preview.ts | 2 +- src/js/utils/pdf-decrypt.ts | 7 +- src/js/utils/pymupdf-loader.ts | 28 +- src/js/utils/sanitize.ts | 296 ++++++++------ src/js/utils/wasm-preloader.ts | 3 +- src/js/workflow/editor.ts | 7 +- src/js/workflow/serialization.ts | 41 +- src/tests/form-creator-extraction.test.ts | 2 +- src/tests/hocr-transform.test.ts | 5 +- src/tests/image-effects.test.ts | 2 +- src/tests/state.test.ts | 4 +- src/tests/timestamp-pdf-page.test.ts | 2 +- src/tests/tools.test.ts | 8 +- src/tests/watermark.test.ts | 4 +- src/types/markdown-it-plugins.d.ts | 54 +++ tsconfig.json | 2 +- 114 files changed, 2577 insertions(+), 1868 deletions(-) create mode 100644 src/js/types/canvas-editor-type.ts create mode 100644 src/js/types/edit-pdf-type.ts create mode 100644 src/js/types/markdown-editor-type.ts create mode 100644 src/js/types/merge-worker-type.ts create mode 100644 src/js/types/redact-type.ts create mode 100644 src/js/types/sanitize-type.ts create mode 100644 src/js/types/shortcuts-type.ts create mode 100644 src/js/types/ui-type.ts create mode 100644 src/js/types/utils-types.ts create mode 100644 src/types/markdown-it-plugins.d.ts diff --git a/eslint.config.mjs b/eslint.config.mjs index d1130f8..add1932 100644 --- a/eslint.config.mjs +++ b/eslint.config.mjs @@ -1,47 +1,63 @@ -import globals from "globals"; -import pluginJs from "@eslint/js"; -import tseslint from "typescript-eslint"; -import eslintConfigPrettier from "eslint-config-prettier"; +import globals from 'globals'; +import pluginJs from '@eslint/js'; +import tseslint from 'typescript-eslint'; +import eslintConfigPrettier from 'eslint-config-prettier'; export default [ - { files: ["**/*.{js,mjs,cjs,ts}"] }, - { ignores: ["dist/**", "coverage/**", "node_modules/**", "**/.vitepress/cache/**", "public/**/*.min.js"] }, - { languageOptions: { globals: { ...globals.browser, ...globals.node } } }, - pluginJs.configs.recommended, - ...tseslint.configs.recommended, - eslintConfigPrettier, - { - rules: { - "@typescript-eslint/no-explicit-any": "warn", - "@typescript-eslint/no-unused-vars": ["warn", { "argsIgnorePattern": "^_" }], - "@typescript-eslint/no-unused-expressions": "warn", - "no-redeclare": "warn", - "no-constant-condition": "warn", - "no-ex-assign": "warn", - "no-empty": "warn", - "no-case-declarations": "warn", - "@typescript-eslint/no-this-alias": "warn", - "no-func-assign": "warn", - "no-fallthrough": "warn", - "no-cond-assign": "warn", - "no-irregular-whitespace": "warn", - "no-prototype-builtins": "warn", - "no-undef": "warn", - "no-useless-escape": "warn", - "no-unsafe-finally": "warn", - "no-self-assign": "warn", - "no-control-regex": "warn", - "@typescript-eslint/no-require-imports": "warn", - "getter-return": "warn", - "no-constant-binary-expression": "warn", - "@typescript-eslint/ban-ts-comment": "warn", - "no-unused-private-class-members": "warn", - "no-unreachable": "warn", - "no-setter-return": "warn", - "no-useless-catch": "warn", - "valid-typeof": "warn", - "no-sparse-arrays": "warn", - "no-misleading-character-class": "warn" - } - } + { files: ['**/*.{js,mjs,cjs,ts}'] }, + { + ignores: [ + 'dist/**', + 'coverage/**', + 'node_modules/**', + '**/.vitepress/cache/**', + 'public/**/*.min.js', + ], + }, + { languageOptions: { globals: { ...globals.browser, ...globals.node } } }, + pluginJs.configs.recommended, + ...tseslint.configs.recommended, + eslintConfigPrettier, + { + rules: { + '@typescript-eslint/no-explicit-any': 'warn', + '@typescript-eslint/no-unused-vars': [ + 'warn', + { + argsIgnorePattern: '^_', + varsIgnorePattern: '^_', + caughtErrorsIgnorePattern: '^_', + destructuredArrayIgnorePattern: '^_', + }, + ], + '@typescript-eslint/no-unused-expressions': 'warn', + 'no-redeclare': 'warn', + 'no-constant-condition': 'warn', + 'no-ex-assign': 'warn', + 'no-empty': 'warn', + 'no-case-declarations': 'warn', + '@typescript-eslint/no-this-alias': 'warn', + 'no-func-assign': 'warn', + 'no-fallthrough': 'warn', + 'no-cond-assign': 'warn', + 'no-irregular-whitespace': 'warn', + 'no-prototype-builtins': 'warn', + 'no-undef': 'warn', + 'no-useless-escape': 'warn', + 'no-unsafe-finally': 'warn', + 'no-self-assign': 'warn', + 'no-control-regex': 'warn', + '@typescript-eslint/no-require-imports': 'warn', + 'getter-return': 'warn', + 'no-constant-binary-expression': 'warn', + '@typescript-eslint/ban-ts-comment': 'warn', + 'no-unused-private-class-members': 'warn', + 'no-unreachable': 'warn', + 'no-setter-return': 'warn', + 'no-useless-catch': 'warn', + 'valid-typeof': 'warn', + 'no-sparse-arrays': 'warn', + 'no-misleading-character-class': 'warn', + }, + }, ]; diff --git a/src/js/canvasEditor.ts b/src/js/canvasEditor.ts index 144878b..d8cd83e 100644 --- a/src/js/canvasEditor.ts +++ b/src/js/canvasEditor.ts @@ -4,25 +4,34 @@ import { state } from './state.js'; import { toolLogic } from './logic/index.js'; import { icons, createIcons } from 'lucide'; import * as pdfjsLib from 'pdfjs-dist'; +import type { PDFDocumentProxy, PDFPageProxy } from 'pdfjs-dist'; +import type { CropBox } from '@/types'; -pdfjsLib.GlobalWorkerOptions.workerSrc = new URL('pdfjs-dist/build/pdf.worker.min.mjs', import.meta.url).toString(); - +pdfjsLib.GlobalWorkerOptions.workerSrc = new URL( + 'pdfjs-dist/build/pdf.worker.min.mjs', + import.meta.url +).toString(); const editorState: { - pdf: any; - canvas: any; - context: any; - container: any; + pdf: PDFDocumentProxy | null; + canvas: HTMLCanvasElement | null; + context: CanvasRenderingContext2D | null; + container: HTMLElement | null; currentPageNum: number; pageRendering: boolean; pageNumPending: number | null; scale: number | 'fit'; - pageSnapshot: any; + pageSnapshot: ImageData | null; isDrawing: boolean; startX: number; startY: number; - cropBoxes: Record; - lastInteractionRect: { x: number; y: number; width: number; height: number } | null; + cropBoxes: Record; + lastInteractionRect: { + x: number; + y: number; + width: number; + height: number; + } | null; } = { pdf: null, canvas: null, @@ -37,15 +46,15 @@ const editorState: { startX: 0, startY: 0, cropBoxes: {}, - lastInteractionRect: null, // Used to store the rectangle from the last move event + lastInteractionRect: null, }; /** * Calculates the best scale to fit the page within the container. * @param {PDFPageProxy} page - The PDF.js page object. */ -function calculateFitScale(page: any) { - const containerWidth = editorState.container.clientWidth; +function calculateFitScale(page: PDFPageProxy) { + const containerWidth = editorState.container!.clientWidth; const viewport = page.getViewport({ scale: 1.0 }); return containerWidth / viewport.width; } @@ -54,33 +63,34 @@ function calculateFitScale(page: any) { * Renders a specific page of the PDF onto the canvas. * @param {number} num The page number to render. */ -async function renderPage(num: any) { +async function renderPage(num: number) { editorState.pageRendering = true; showLoader(`Loading page ${num}...`); try { - const page = await editorState.pdf.getPage(num); + const page = await editorState.pdf!.getPage(num); if (editorState.scale === 'fit') { editorState.scale = calculateFitScale(page); } - const viewport = page.getViewport({ scale: editorState.scale }); - editorState.canvas.height = viewport.height; - editorState.canvas.width = viewport.width; + const viewport = page.getViewport({ scale: editorState.scale as number }); + editorState.canvas!.height = viewport.height; + editorState.canvas!.width = viewport.width; const renderContext = { - canvasContext: editorState.context, + canvas: editorState.canvas, + canvasContext: editorState.context!, viewport: viewport, }; await page.render(renderContext).promise; - editorState.pageSnapshot = editorState.context.getImageData( + editorState.pageSnapshot = editorState.context!.getImageData( 0, 0, - editorState.canvas.width, - editorState.canvas.height + editorState.canvas!.width, + editorState.canvas!.height ); redrawShapes(); } catch (error) { @@ -90,12 +100,11 @@ async function renderPage(num: any) { editorState.pageRendering = false; hideLoader(); - document.getElementById('current-page-display').textContent = num; - // @ts-expect-error TS(2339) FIXME: Property 'disabled' does not exist on type 'HTMLEl... Remove this comment to see the full error message - document.getElementById('prev-page').disabled = num <= 1; - // @ts-expect-error TS(2339) FIXME: Property 'disabled' does not exist on type 'HTMLEl... Remove this comment to see the full error message - document.getElementById('next-page').disabled = - num >= editorState.pdf.numPages; + document.getElementById('current-page-display')!.textContent = String(num); + (document.getElementById('prev-page') as HTMLButtonElement).disabled = + num <= 1; + (document.getElementById('next-page') as HTMLButtonElement).disabled = + num >= editorState.pdf!.numPages; if (editorState.pageNumPending !== null) { const pendingPage = editorState.pageNumPending; @@ -105,7 +114,7 @@ async function renderPage(num: any) { } } -function queueRenderPage(num: any) { +function queueRenderPage(num: number) { if (editorState.pageRendering) { editorState.pageNumPending = num; } else { @@ -116,36 +125,37 @@ function queueRenderPage(num: any) { function redrawShapes() { if (editorState.pageSnapshot) { - editorState.context.putImageData(editorState.pageSnapshot, 0, 0); + editorState.context!.putImageData(editorState.pageSnapshot, 0, 0); } const currentCropBox = editorState.cropBoxes[editorState.currentPageNum - 1]; if (currentCropBox) { - editorState.context.strokeStyle = 'rgba(79, 70, 229, 0.9)'; - editorState.context.lineWidth = 2; - editorState.context.setLineDash([8, 4]); - editorState.context.strokeRect( + editorState.context!.strokeStyle = 'rgba(79, 70, 229, 0.9)'; + editorState.context!.lineWidth = 2; + editorState.context!.setLineDash([8, 4]); + editorState.context!.strokeRect( currentCropBox.x, currentCropBox.y, currentCropBox.width, currentCropBox.height ); - editorState.context.setLineDash([]); + editorState.context!.setLineDash([]); } } -function getEventCoordinates(e: any) { - const rect = editorState.canvas.getBoundingClientRect(); - const touch = e.touches ? e.touches[0] : e; - const scaleX = editorState.canvas.width / rect.width; - const scaleY = editorState.canvas.height / rect.height; +function getEventCoordinates(e: MouseEvent | TouchEvent) { + const rect = editorState.canvas!.getBoundingClientRect(); + const touch = + 'touches' in e && e.touches.length > 0 ? e.touches[0] : (e as MouseEvent); + const scaleX = editorState.canvas!.width / rect.width; + const scaleY = editorState.canvas!.height / rect.height; return { x: (touch.clientX - rect.left) * scaleX, y: (touch.clientY - rect.top) * scaleY, }; } -function handleInteractionStart(e: any) { +function handleInteractionStart(e: MouseEvent | TouchEvent) { e.preventDefault(); const coords = getEventCoordinates(e); editorState.isDrawing = true; @@ -153,7 +163,7 @@ function handleInteractionStart(e: any) { editorState.startY = coords.y; } -function handleInteractionMove(e: any) { +function handleInteractionMove(e: MouseEvent | TouchEvent) { if (!editorState.isDrawing) return; e.preventDefault(); @@ -165,13 +175,12 @@ function handleInteractionMove(e: any) { const width = Math.abs(editorState.startX - coords.x); const height = Math.abs(editorState.startY - coords.y); - editorState.context.strokeStyle = 'rgba(79, 70, 229, 0.9)'; - editorState.context.lineWidth = 2; - editorState.context.setLineDash([8, 4]); - editorState.context.strokeRect(x, y, width, height); - editorState.context.setLineDash([]); + editorState.context!.strokeStyle = 'rgba(79, 70, 229, 0.9)'; + editorState.context!.lineWidth = 2; + editorState.context!.setLineDash([8, 4]); + editorState.context!.strokeRect(x, y, width, height); + editorState.context!.setLineDash([]); - // Store the last valid rectangle drawn during the move event editorState.lastInteractionRect = { x, y, width, height }; } @@ -192,12 +201,14 @@ function handleInteractionEnd() { scale: editorState.scale, }; - editorState.lastInteractionRect = null; // Reset for the next drawing action + editorState.lastInteractionRect = null; redrawShapes(); } -export async function setupCanvasEditor(toolId: any) { - editorState.canvas = document.getElementById('canvas-editor'); +export async function setupCanvasEditor(toolId: string) { + editorState.canvas = document.getElementById( + 'canvas-editor' + ) as HTMLCanvasElement | null; if (!editorState.canvas) return; editorState.container = document.getElementById('canvas-container'); editorState.context = editorState.canvas.getContext('2d'); @@ -210,7 +221,7 @@ export async function setupCanvasEditor(toolId: any) { editorState.currentPageNum = 1; editorState.scale = 'fit'; - pageNav.textContent = ''; + pageNav!.textContent = ''; const prevButton = document.createElement('button'); prevButton.id = 'prev-page'; @@ -237,22 +248,21 @@ export async function setupCanvasEditor(toolId: any) { 'btn p-2 rounded-full bg-gray-700 hover:bg-gray-600 disabled:opacity-50'; nextButton.innerHTML = ''; - pageNav.append(prevButton, pageInfo, nextButton); + pageNav!.append(prevButton, pageInfo, nextButton); createIcons({ icons }); - document.getElementById('prev-page').addEventListener('click', () => { + document.getElementById('prev-page')!.addEventListener('click', () => { if (editorState.currentPageNum > 1) queueRenderPage(editorState.currentPageNum - 1); }); - document.getElementById('next-page').addEventListener('click', () => { - if (editorState.currentPageNum < editorState.pdf.numPages) + document.getElementById('next-page')!.addEventListener('click', () => { + if (editorState.currentPageNum < editorState.pdf!.numPages) queueRenderPage(editorState.currentPageNum + 1); }); - // To prevent stacking multiple listeners, we replace the canvas element with a clone - const newCanvas = editorState.canvas.cloneNode(true); - editorState.canvas.parentNode.replaceChild(newCanvas, editorState.canvas); + const newCanvas = editorState.canvas.cloneNode(true) as HTMLCanvasElement; + editorState.canvas.parentNode!.replaceChild(newCanvas, editorState.canvas); editorState.canvas = newCanvas; editorState.context = newCanvas.getContext('2d'); @@ -272,50 +282,55 @@ export async function setupCanvasEditor(toolId: any) { editorState.canvas.addEventListener('touchend', handleInteractionEnd); if (toolId === 'crop') { - document.getElementById('zoom-in-btn').onclick = () => { + (document.getElementById('zoom-in-btn') as HTMLElement).onclick = () => { if (typeof editorState.scale === 'number') { editorState.scale += 0.25; } renderPage(editorState.currentPageNum); }; - document.getElementById('zoom-out-btn').onclick = () => { + (document.getElementById('zoom-out-btn') as HTMLElement).onclick = () => { if (typeof editorState.scale === 'number' && editorState.scale > 0.25) { editorState.scale -= 0.25; renderPage(editorState.currentPageNum); } }; - document.getElementById('fit-page-btn').onclick = async () => { - const page = await editorState.pdf.getPage(editorState.currentPageNum); - editorState.scale = calculateFitScale(page); - renderPage(editorState.currentPageNum); - }; - document.getElementById('clear-crop-btn').onclick = () => { + (document.getElementById('fit-page-btn') as HTMLElement).onclick = + async () => { + const page = await editorState.pdf!.getPage(editorState.currentPageNum); + editorState.scale = calculateFitScale(page); + renderPage(editorState.currentPageNum); + }; + (document.getElementById('clear-crop-btn') as HTMLElement).onclick = () => { delete editorState.cropBoxes[editorState.currentPageNum - 1]; redrawShapes(); }; - document.getElementById('clear-all-crops-btn').onclick = () => { - editorState.cropBoxes = {}; - redrawShapes(); - }; + (document.getElementById('clear-all-crops-btn') as HTMLElement).onclick = + () => { + editorState.cropBoxes = {}; + redrawShapes(); + }; - document.getElementById('process-btn').onclick = async () => { - if (Object.keys(editorState.cropBoxes).length === 0) { - showAlert( - 'No Area Selected', - 'Please draw a rectangle on at least one page to select the crop area.' - ); - return; - } - const success = await toolLogic['crop-pdf'].process( - editorState.cropBoxes - ); - if (success) { - showAlert( - 'Success!', - 'Your PDF has been cropped and the download has started.' - ); - } - }; + (document.getElementById('process-btn') as HTMLElement).onclick = + async () => { + if (Object.keys(editorState.cropBoxes).length === 0) { + showAlert( + 'No Area Selected', + 'Please draw a rectangle on at least one page to select the crop area.' + ); + return; + } + const cropLogic = toolLogic['crop-pdf']; + const success = + typeof cropLogic === 'object' && cropLogic.process + ? await cropLogic.process(editorState.cropBoxes) + : false; + if (success) { + showAlert( + 'Success!', + 'Your PDF has been cropped and the download has started.' + ); + } + }; } queueRenderPage(1); diff --git a/src/js/compare/engine/compare.worker.ts b/src/js/compare/engine/compare.worker.ts index 21bfc56..9c485e3 100644 --- a/src/js/compare/engine/compare.worker.ts +++ b/src/js/compare/engine/compare.worker.ts @@ -4,7 +4,6 @@ import type { CompareTextItem, ComparePageSignature, ComparePagePair, - ComparePageResult, CompareChangeSummary, CompareTextChange, } from '../types.ts'; @@ -44,8 +43,6 @@ interface ErrorResult { message: string; } -type WorkerResult = DiffResult | PairResult | ErrorResult; - self.onmessage = function (e: MessageEvent) { const msg = e.data; try { diff --git a/src/js/handlers/fileHandler.ts b/src/js/handlers/fileHandler.ts index 06628fe..bd0e094 100644 --- a/src/js/handlers/fileHandler.ts +++ b/src/js/handlers/fileHandler.ts @@ -58,13 +58,13 @@ async function handleSinglePdfUpload(toolId: string, file: File) { const processBtn = document.getElementById('process-btn'); if (processBtn) { const logic = toolLogic[toolId]; - if (logic && logic.process) { + if (logic && typeof logic === 'object' && logic.process) { processBtn.onclick = logic.process; } } const logic = toolLogic[toolId]; - if (logic && logic.setup) { + if (logic && typeof logic === 'object' && logic.setup) { await logic.setup(); } return; @@ -102,8 +102,12 @@ async function handleSinglePdfUpload(toolId: string, file: File) { const logic = toolLogic[toolId]; if (logic) { const func = - typeof logic.process === 'function' ? logic.process : logic; - processBtn.onclick = func; + typeof logic === 'object' && typeof logic.process === 'function' + ? logic.process + : typeof logic === 'function' + ? logic + : null; + if (func) processBtn.onclick = func; } } @@ -281,13 +285,17 @@ async function handleSinglePdfUpload(toolId: string, file: File) { const infoSection = createSection('Info Dictionary'); if (info && Object.keys(info).length > 0) { for (const key in info) { - const value = info[key]; - let displayValue; + const value = (info as Record)[key]; + let displayValue: string; if (value === null || typeof value === 'undefined') { displayValue = '- Not Set -'; - } else if (typeof value === 'object' && value.name) { - displayValue = value.name; + } else if ( + typeof value === 'object' && + 'name' in (value as object) && + (value as Record).name + ) { + displayValue = String((value as Record).name); } else if (typeof value === 'object') { try { displayValue = JSON.stringify(value); @@ -524,7 +532,8 @@ async function handleSinglePdfUpload(toolId: string, file: File) { } if (toolId === 'page-dimensions') { - toolLogic['page-dimensions'](); + const pageDimLogic = toolLogic['page-dimensions']; + if (typeof pageDimLogic === 'function') pageDimLogic(); } if (toolId === 'pdf-to-jpg') { @@ -569,8 +578,13 @@ async function handleSinglePdfUpload(toolId: string, file: File) { } } - if (toolLogic[toolId] && typeof toolLogic[toolId].setup === 'function') { - toolLogic[toolId].setup(); + const setupLogic = toolLogic[toolId]; + if ( + setupLogic && + typeof setupLogic === 'object' && + typeof setupLogic.setup === 'function' + ) { + setupLogic.setup(); } } catch (e) { hideLoader(); @@ -656,13 +670,19 @@ async function handleMultiFileUpload(toolId: string) { (processBtn as HTMLButtonElement).disabled = false; const logic = toolLogic[toolId]; if (logic) { - const func = typeof logic.process === 'function' ? logic.process : logic; - processBtn.onclick = func; + const func = + typeof logic === 'object' && typeof logic.process === 'function' + ? logic.process + : typeof logic === 'function' + ? logic + : null; + if (func) processBtn.onclick = func; } } if (toolId === 'alternate-merge') { - toolLogic['alternate-merge'].setup(); + const altMerge = toolLogic['alternate-merge']; + if (typeof altMerge === 'object' && altMerge.setup) altMerge.setup(); } else if (toolId === 'image-to-pdf') { const imageList = document.getElementById('image-list'); @@ -869,9 +889,8 @@ export function setupFileInputHandler(toolId: string) { processBtn.onclick = () => { const logic = toolLogic[toolId]; if (logic) { - const func = - typeof logic.process === 'function' ? logic.process : logic; - func(); + if (typeof logic === 'function') logic(); + else if (logic.process) logic.process(); } }; } @@ -893,9 +912,8 @@ export function setupFileInputHandler(toolId: string) { processBtn.onclick = () => { const logic = toolLogic[toolId]; if (logic) { - const func = - typeof logic.process === 'function' ? logic.process : logic; - func(); + if (typeof logic === 'function') logic(); + else if (logic.process) logic.process(); } }; } diff --git a/src/js/logic/add-page-labels-page.ts b/src/js/logic/add-page-labels-page.ts index 96002cc..f7d30b1 100644 --- a/src/js/logic/add-page-labels-page.ts +++ b/src/js/logic/add-page-labels-page.ts @@ -3,6 +3,7 @@ import type { AddPageLabelsState, LabelRule, PageLabelStyleName, + CpdfInstance, } from '@/types'; import { showAlert, showLoader, hideLoader } from '../ui.js'; import { t } from '../i18n/index.js'; @@ -20,30 +21,6 @@ import { } from '../utils/page-labels.js'; import { loadPdfDocument } from '../utils/load-pdf-document.js'; -type AddPageLabelsCpdf = { - setSlow?: () => void; - fromMemory(data: Uint8Array, userpw: string): CoherentPdf; - removePageLabels(pdf: CoherentPdf): void; - parsePagespec(pdf: CoherentPdf, pagespec: string): CpdfPageRange; - all(pdf: CoherentPdf): CpdfPageRange; - addPageLabels( - pdf: CoherentPdf, - style: CpdfLabelStyle, - prefix: string, - offset: number, - range: CpdfPageRange, - progress: boolean - ): void; - toMemory(pdf: CoherentPdf, linearize: boolean, makeId: boolean): Uint8Array; - deletePdf(pdf: CoherentPdf): void; - decimalArabic: CpdfLabelStyle; - lowercaseRoman: CpdfLabelStyle; - uppercaseRoman: CpdfLabelStyle; - lowercaseLetters: CpdfLabelStyle; - uppercaseLetters: CpdfLabelStyle; - noLabelPrefixOnly?: CpdfLabelStyle; -}; - let labelRuleCounter = 0; const translate = ( @@ -464,8 +441,8 @@ async function addPageLabels() { ) as HTMLInputElement | null )?.checked ?? true; - let cpdf: AddPageLabelsCpdf | null = null; - let pdf: CoherentPdf | null = null; + let cpdf: CpdfInstance | null = null; + let pdf: unknown = null; try { cpdf = await getCpdf(); @@ -482,7 +459,7 @@ async function addPageLabels() { const rule = pageState.rules[index]; const trimmedRange = rule.pageRange.trim(); - let range: CpdfPageRange; + let range: unknown; try { range = trimmedRange ? cpdf.parsePagespec(pdf, trimmedRange) diff --git a/src/js/logic/add-stamps.ts b/src/js/logic/add-stamps.ts index e99f280..236cf28 100644 --- a/src/js/logic/add-stamps.ts +++ b/src/js/logic/add-stamps.ts @@ -9,7 +9,6 @@ import { loadPdfWithPasswordPrompt } from '../utils/password-prompt.js'; let selectedFile: File | null = null; let viewerIframe: HTMLIFrameElement | null = null; -let viewerReady = false; let currentBlobUrl: string | null = null; const pdfInput = document.getElementById('pdfFile') as HTMLInputElement; @@ -47,7 +46,7 @@ function resetState() { viewerContainer.removeChild(viewerIframe); } viewerIframe = null; - viewerReady = false; + if (viewerCard) viewerCard.classList.add('hidden'); if (saveStampedBtn) saveStampedBtn.classList.add('hidden'); @@ -157,7 +156,6 @@ async function loadPdfInViewer(file: File) { currentBlobUrl = null; } viewerIframe = null; - viewerReady = false; // Calculate and apply dynamic height await adjustViewerHeight(file); @@ -169,7 +167,7 @@ async function loadPdfInViewer(file: File) { try { const existingPrefsRaw = localStorage.getItem('pdfjs.preferences'); const existingPrefs = existingPrefsRaw ? JSON.parse(existingPrefsRaw) : {}; - delete (existingPrefs as any).annotationEditorMode; + delete (existingPrefs as Record).annotationEditorMode; const newPrefs = { ...existingPrefs, enablePermissions: false, @@ -204,7 +202,14 @@ async function loadPdfInViewer(file: File) { function setupAnnotationViewer(iframe: HTMLIFrameElement) { try { - const win = iframe.contentWindow as any; + const win = iframe.contentWindow as + | (Window & { + PDFViewerApplication?: { + initializedPromise?: Promise; + eventBus?: { _on?: (event: string, callback: () => void) => void }; + }; + }) + | null; const doc = win?.document as Document | null; if (!win || !doc) return; @@ -238,8 +243,6 @@ function setupAnnotationViewer(iframe: HTMLIFrameElement) { if (root) { root.classList.add('PdfjsAnnotationExtension_Comment_hidden'); } - - viewerReady = true; } catch (e) { console.error( 'Failed to initialize annotation viewer for Add Stamps:', @@ -304,8 +307,14 @@ if (saveStampedBtn) { } try { - const win = viewerIframe.contentWindow as any; - const extensionInstance = win?.pdfjsAnnotationExtensionInstance as any; + const win = viewerIframe.contentWindow as + | (Window & { + pdfjsAnnotationExtensionInstance?: { + exportPdf?: () => Promise; + }; + }) + | null; + const extensionInstance = win?.pdfjsAnnotationExtensionInstance; if ( extensionInstance && diff --git a/src/js/logic/add-watermark-page.ts b/src/js/logic/add-watermark-page.ts index fdb1c11..b61200c 100644 --- a/src/js/logic/add-watermark-page.ts +++ b/src/js/logic/add-watermark-page.ts @@ -1013,9 +1013,12 @@ async function applyWatermark() { 'watermarked.pdf' ); showAlert('Success', 'Watermark added successfully!', 'success'); - } catch (e: any) { + } catch (e: unknown) { console.error(e); - showAlert('Error', e.message || 'Could not add the watermark.'); + showAlert( + 'Error', + e instanceof Error ? e.message : 'Could not add the watermark.' + ); } finally { hideLoader(); } diff --git a/src/js/logic/bookmark-pdf.ts b/src/js/logic/bookmark-pdf.ts index 080f99e..19d2146 100644 --- a/src/js/logic/bookmark-pdf.ts +++ b/src/js/logic/bookmark-pdf.ts @@ -7,7 +7,6 @@ import '../../css/bookmark.css'; import { initializeGlobalShortcuts } from '../utils/shortcuts-init.js'; import { truncateFilename, - getPDFDocument, formatBytes, downloadFile, escapeHtml, @@ -46,7 +45,6 @@ let isPickingDestination = false; let currentPickingCallback: DestinationCallback | null = null; let destinationMarker: HTMLDivElement | null = null; let savedModalOverlay: HTMLDivElement | null = null; -let savedModal: HTMLDivElement | null = null; let currentViewport: PageViewport | null = null; let currentZoom = 1.0; const fileInput = document.getElementById( @@ -463,7 +461,6 @@ class="w-full px-2 py-1 border border-gray-300 rounded text-sm text-gray-900" /> if (pickDestBtn) { pickDestBtn.addEventListener('click', () => { savedModalOverlay = overlay; - savedModal = modal; overlay.style.display = 'none'; startDestinationPicking((page: number, pdfX: number, pdfY: number) => { @@ -699,7 +696,6 @@ function cancelDestinationPicking(): void { if (savedModalOverlay) { savedModalOverlay.style.display = ''; savedModalOverlay = null; - savedModal = null; } } @@ -1317,7 +1313,7 @@ jsonInput?.addEventListener('change', async (e: Event) => { 'JSON Loaded', 'Loaded bookmarks from JSON. Now upload your PDF.' ); - } catch (err) { + } catch { await showAlertModal('Error', 'Invalid JSON format'); } }); @@ -2018,7 +2014,7 @@ jsonImportHidden?.addEventListener('change', async (e: Event) => { saveState(); renderBookmarkTree(); await showAlertModal('Success', 'Bookmarks imported from JSON!'); - } catch (err) { + } catch { await showAlertModal('Error', 'Invalid JSON format'); } diff --git a/src/js/logic/cbz-to-pdf-page.ts b/src/js/logic/cbz-to-pdf-page.ts index 1573af4..f8d1816 100644 --- a/src/js/logic/cbz-to-pdf-page.ts +++ b/src/js/logic/cbz-to-pdf-page.ts @@ -2,9 +2,8 @@ import { showLoader, hideLoader, showAlert } from '../ui.js'; import { downloadFile, formatBytes } from '../utils/helpers.js'; import { state } from '../state.js'; import { createIcons, icons } from 'lucide'; -import { isWasmAvailable, getWasmBaseUrl } from '../config/wasm-cdn-config.js'; -import { showWasmRequiredDialog } from '../utils/wasm-provider.js'; -import { loadPyMuPDF, isPyMuPDFAvailable } from '../utils/pymupdf-loader.js'; +import { loadPyMuPDF } from '../utils/pymupdf-loader.js'; +import type { PyMuPDFInstance } from '@/types'; import JSZip from 'jszip'; import { PDFDocument } from 'pdf-lib'; @@ -176,8 +175,8 @@ async function convertCbzToPdf(file: File): Promise { } async function convertCbrToPdf(file: File): Promise { - const pymupdf = await loadPyMuPDF(); - return await (pymupdf as any).convertToPdf(file, { filetype: 'cbz' }); + const pymupdf = (await loadPyMuPDF()) as PyMuPDFInstance; + return await pymupdf.convertToPdf(file, { filetype: 'cbz' }); } document.addEventListener('DOMContentLoaded', () => { @@ -314,12 +313,12 @@ document.addEventListener('DOMContentLoaded', () => { () => resetState() ); } - } catch (e: any) { + } catch (e: unknown) { console.error(`[${TOOL_NAME}2PDF] ERROR:`, e); hideLoader(); showAlert( 'Error', - `An error occurred during conversion. Error: ${e.message}` + `An error occurred during conversion. Error: ${e instanceof Error ? e.message : String(e)}` ); } }; diff --git a/src/js/logic/change-permissions-page.ts b/src/js/logic/change-permissions-page.ts index 40f7a3b..784358f 100644 --- a/src/js/logic/change-permissions-page.ts +++ b/src/js/logic/change-permissions-page.ts @@ -6,7 +6,7 @@ import { readFileAsArrayBuffer, } from '../utils/helpers.js'; import { icons, createIcons } from 'lucide'; -import { ChangePermissionsState } from '@/types'; +import { ChangePermissionsState, QpdfInstanceExtended } from '@/types'; const pageState: ChangePermissionsState = { file: null, @@ -114,7 +114,7 @@ async function changePermissions() { const inputPath = '/input.pdf'; const outputPath = '/output.pdf'; - let qpdf: any; + let qpdf: QpdfInstanceExtended; const loaderModal = document.getElementById('loader-modal'); const loaderText = document.getElementById('loader-text'); @@ -187,10 +187,10 @@ async function changePermissions() { args.push('--', outputPath); try { qpdf.callMain(args); - } catch (qpdfError: any) { + } catch (qpdfError: unknown) { console.error('qpdf execution error:', qpdfError); - const errorMsg = qpdfError.message || ''; + const errorMsg = qpdfError instanceof Error ? qpdfError.message : ''; if ( errorMsg.includes('invalid password') || @@ -219,7 +219,9 @@ async function changePermissions() { throw new Error('Processing resulted in an empty file.'); } - const blob = new Blob([outputFile], { type: 'application/pdf' }); + const blob = new Blob([new Uint8Array(outputFile)], { + type: 'application/pdf', + }); downloadFile(blob, `permissions-changed-${pageState.file.name}`); if (loaderModal) loaderModal.classList.add('hidden'); @@ -233,16 +235,17 @@ async function changePermissions() { showAlert('Success', successMessage, 'success', () => { resetState(); }); - } catch (error: any) { + } catch (error: unknown) { console.error('Error during PDF permission change:', error); if (loaderModal) loaderModal.classList.add('hidden'); - if (error.message === 'INVALID_PASSWORD') { + const errorMessage = error instanceof Error ? error.message : ''; + if (errorMessage === 'INVALID_PASSWORD') { showAlert( 'Incorrect Password', 'The current password you entered is incorrect. Please try again.' ); - } else if (error.message === 'PASSWORD_REQUIRED') { + } else if (errorMessage === 'PASSWORD_REQUIRED') { showAlert( 'Password Required', 'This PDF is password-protected. Please enter the current password to proceed.' @@ -250,7 +253,7 @@ async function changePermissions() { } else { showAlert( 'Processing Failed', - `An error occurred: ${error.message || 'The PDF might be corrupted or password protected.'}` + `An error occurred: ${errorMessage || 'The PDF might be corrupted or password protected.'}` ); } } finally { diff --git a/src/js/logic/compress-pdf-page.ts b/src/js/logic/compress-pdf-page.ts index 1fabe77..20aa130 100644 --- a/src/js/logic/compress-pdf-page.ts +++ b/src/js/logic/compress-pdf-page.ts @@ -85,7 +85,10 @@ async function performCondenseCompression( scrub: { metadata: customSettings?.removeMetadata ?? preset.scrub.metadata, thumbnails: customSettings?.removeThumbnails ?? preset.scrub.thumbnails, - xmlMetadata: (preset.scrub as any).xmlMetadata ?? false, + xmlMetadata: + 'xmlMetadata' in preset.scrub + ? (preset.scrub as { xmlMetadata: boolean }).xmlMetadata + : false, }, subsetFonts: customSettings?.subsetFonts ?? preset.subsetFonts, save: { @@ -99,8 +102,8 @@ async function performCondenseCompression( try { const result = await pymupdf.compressPdf(fileBlob, options); return result; - } catch (error: any) { - const errorMessage = error?.message || String(error); + } catch (error: unknown) { + const errorMessage = error instanceof Error ? error.message : String(error); if ( errorMessage.includes('PatternType') || errorMessage.includes('pattern') @@ -432,7 +435,7 @@ document.addEventListener('DOMContentLoaded', () => { usedMethod = 'Condense'; // Check if fallback was used - if ((result as any).usedFallback) { + if ((result as { usedFallback?: boolean }).usedFallback) { usedMethod += ' (without image optimization due to unsupported patterns)'; } @@ -551,12 +554,12 @@ document.addEventListener('DOMContentLoaded', () => { ); } } - } catch (e: any) { + } catch (e: unknown) { hideLoader(); console.error('[CompressPDF] Error:', e); showAlert( 'Error', - `An error occurred during compression. Error: ${e.message}` + `An error occurred during compression. Error: ${e instanceof Error ? e.message : String(e)}` ); } }; diff --git a/src/js/logic/crop-pdf-page.ts b/src/js/logic/crop-pdf-page.ts index 71595b7..aabd964 100644 --- a/src/js/logic/crop-pdf-page.ts +++ b/src/js/logic/crop-pdf-page.ts @@ -5,7 +5,7 @@ import { loadPdfWithPasswordPrompt } from '../utils/password-prompt.js'; import Cropper from 'cropperjs'; import * as pdfjsLib from 'pdfjs-dist'; import { PDFDocument as PDFLibDocument } from 'pdf-lib'; -import { CropperState } from '@/types'; +import { CropperState, CropPercentages } from '@/types'; import { loadPdfDocument } from '../utils/load-pdf-document.js'; pdfjsLib.GlobalWorkerOptions.workerSrc = new URL( @@ -167,7 +167,11 @@ async function displayPageAsImage(num: number) { const tempCtx = tempCanvas.getContext('2d'); tempCanvas.width = viewport.width; tempCanvas.height = viewport.height; - await page.render({ canvasContext: tempCtx, viewport: viewport }).promise; + await page.render({ + canvas: null, + canvasContext: tempCtx, + viewport: viewport, + }).promise; if (cropperState.cropper) cropperState.cropper.destroy(); @@ -251,7 +255,7 @@ async function performCrop() { document.getElementById('apply-to-all-toggle') as HTMLInputElement )?.checked; - let finalCropData: Record = {}; + let finalCropData: Record = {}; if (isApplyToAll) { const currentCrop = cropperState.pageCrops[cropperState.currentPageNum]; @@ -286,7 +290,7 @@ async function performCrop() { const fileName = isDestructive ? 'flattened_crop.pdf' : 'standard_crop.pdf'; downloadFile( - new Blob([finalPdfBytes], { type: 'application/pdf' }), + new Blob([new Uint8Array(finalPdfBytes)], { type: 'application/pdf' }), fileName ); showAlert( @@ -304,7 +308,7 @@ async function performCrop() { } async function performMetadataCrop( - cropData: Record + cropData: Record ): Promise { const pdfToModify = await loadPdfDocument(cropperState.originalPdfBytes!); @@ -344,7 +348,7 @@ async function performMetadataCrop( } async function performFlatteningCrop( - cropData: Record + cropData: Record ): Promise { const newPdfDoc = await PDFLibDocument.create(); const sourcePdfDocForCopying = await loadPdfDocument( @@ -364,7 +368,11 @@ async function performFlatteningCrop( const tempCtx = tempCanvas.getContext('2d'); tempCanvas.width = viewport.width; tempCanvas.height = viewport.height; - await page.render({ canvasContext: tempCtx, viewport: viewport }).promise; + await page.render({ + canvas: null, + canvasContext: tempCtx, + viewport: viewport, + }).promise; const finalCanvas = document.createElement('canvas'); const finalCtx = finalCanvas.getContext('2d'); diff --git a/src/js/logic/cropper.ts b/src/js/logic/cropper.ts index 56cb7a5..2c58afb 100644 --- a/src/js/logic/cropper.ts +++ b/src/js/logic/cropper.ts @@ -16,7 +16,17 @@ pdfjsLib.GlobalWorkerOptions.workerSrc = new URL( ).toString(); // --- Global State for the Cropper Tool --- -const cropperState = { +import type { CropPercentages } from '@/types'; +import type { PDFDocumentProxy } from 'pdfjs-dist'; + +const cropperState: { + pdfDoc: PDFDocumentProxy | null; + currentPageNum: number; + cropper: Cropper | null; + originalPdfBytes: Uint8Array | null; + cropperImageElement: HTMLImageElement | null; + pageCrops: Record; +} = { pdfDoc: null, currentPageNum: 1, cropper: null, @@ -46,7 +56,8 @@ function saveCurrentCrop() { * Renders a PDF page to the Cropper UI as an image. * @param {number} num The page number to render. */ -async function displayPageAsImage(num: any) { + +async function displayPageAsImage(num: number) { showLoader(`Rendering Page ${num}...`); try { @@ -57,7 +68,11 @@ async function displayPageAsImage(num: any) { const tempCtx = tempCanvas.getContext('2d'); tempCanvas.width = viewport.width; tempCanvas.height = viewport.height; - await page.render({ canvasContext: tempCtx, viewport: viewport }).promise; + await page.render({ + canvas: null, + canvasContext: tempCtx, + viewport: viewport, + }).promise; if (cropperState.cropper) { cropperState.cropper.destroy(); @@ -107,7 +122,7 @@ async function displayPageAsImage(num: any) { * Handles page navigation. * @param {number} offset -1 for previous, 1 for next. */ -async function changePage(offset: any) { +async function changePage(offset: number) { // Save the current page's crop before changing saveCurrentCrop(); @@ -137,7 +152,13 @@ function enableControls() { /** * Performs a non-destructive crop by updating the page's crop box. */ -async function performMetadataCrop(pdfToModify: any, cropData: any) { +async function performMetadataCrop( + pdfToModify: PDFLibDocument, + cropData: Record< + string, + { x: number; y: number; width: number; height: number } + > +) { for (const pageNum in cropData) { const pdfJsPage = await cropperState.pdfDoc.getPage(Number(pageNum)); const viewport = pdfJsPage.getViewport({ scale: 1 }); @@ -183,7 +204,12 @@ async function performMetadataCrop(pdfToModify: any, cropData: any) { /** * Performs a destructive crop by flattening the selected area to an image. */ -async function performFlatteningCrop(cropData: any) { +async function performFlatteningCrop( + cropData: Record< + string, + { x: number; y: number; width: number; height: number } + > +) { const newPdfDoc = await PDFLibDocument.create(); // Load the original PDF with pdf-lib to copy un-cropped pages from @@ -204,7 +230,11 @@ async function performFlatteningCrop(cropData: any) { const tempCtx = tempCanvas.getContext('2d'); tempCanvas.width = viewport.width; tempCanvas.height = viewport.height; - await page.render({ canvasContext: tempCtx, viewport: viewport }).promise; + await page.render({ + canvas: null, + canvasContext: tempCtx, + viewport: viewport, + }).promise; const finalCanvas = document.createElement('canvas'); const finalCtx = finalCanvas.getContext('2d'); @@ -264,7 +294,7 @@ export async function setupCropperTool() { cropperState.pageCrops = {}; const arrayBuffer = await readFileAsArrayBuffer(state.files[0]); - cropperState.originalPdfBytes = arrayBuffer; + cropperState.originalPdfBytes = new Uint8Array(arrayBuffer as ArrayBuffer); const arrayBufferForPdfJs = (arrayBuffer as ArrayBuffer).slice(0); const loadingTask = getPDFDocument({ data: arrayBufferForPdfJs }); @@ -295,7 +325,7 @@ export async function setupCropperTool() { document.getElementById('apply-to-all-toggle') as HTMLInputElement ).checked; - let finalCropData = {}; + let finalCropData: Record = {}; if (isApplyToAll) { const currentCrop = cropperState.pageCrops[cropperState.currentPageNum]; if (!currentCrop) { @@ -308,10 +338,13 @@ export async function setupCropperTool() { } } else { // If not applying to all, only process pages with saved crops - finalCropData = Object.keys(cropperState.pageCrops).reduce((obj, key) => { - obj[key] = cropperState.pageCrops[key]; - return obj; - }); + finalCropData = Object.keys(cropperState.pageCrops).reduce( + (obj, key) => { + obj[Number(key)] = cropperState.pageCrops[Number(key)]; + return obj; + }, + {} as Record + ); } if (Object.keys(finalCropData).length === 0) { @@ -341,7 +374,7 @@ export async function setupCropperTool() { ? 'flattened_crop.pdf' : 'standard_crop.pdf'; downloadFile( - new Blob([finalPdfBytes], { type: 'application/pdf' }), + new Blob([new Uint8Array(finalPdfBytes)], { type: 'application/pdf' }), fileName ); showAlert('Success', 'Crop complete! Your download has started.'); diff --git a/src/js/logic/csv-to-pdf-page.ts b/src/js/logic/csv-to-pdf-page.ts index e0ec556..fda37de 100644 --- a/src/js/logic/csv-to-pdf-page.ts +++ b/src/js/logic/csv-to-pdf-page.ts @@ -1,231 +1,255 @@ import { showLoader, hideLoader, showAlert } from '../ui.js'; -import { - downloadFile, - readFileAsArrayBuffer, - formatBytes, -} from '../utils/helpers.js'; +import { downloadFile, formatBytes } from '../utils/helpers.js'; import { state } from '../state.js'; import { createIcons, icons } from 'lucide'; document.addEventListener('DOMContentLoaded', () => { - state.files = []; + state.files = []; - const fileInput = document.getElementById('file-input') as HTMLInputElement; - const dropZone = document.getElementById('drop-zone'); - const convertOptions = document.getElementById('convert-options'); - const fileDisplayArea = document.getElementById('file-display-area'); - const fileControls = document.getElementById('file-controls'); - const addMoreBtn = document.getElementById('add-more-btn'); - const clearFilesBtn = document.getElementById('clear-files-btn'); - const backBtn = document.getElementById('back-to-tools'); - const processBtn = document.getElementById('process-btn'); + const fileInput = document.getElementById('file-input') as HTMLInputElement; + const dropZone = document.getElementById('drop-zone'); + const convertOptions = document.getElementById('convert-options'); + const fileDisplayArea = document.getElementById('file-display-area'); + const fileControls = document.getElementById('file-controls'); + const addMoreBtn = document.getElementById('add-more-btn'); + const clearFilesBtn = document.getElementById('clear-files-btn'); + const backBtn = document.getElementById('back-to-tools'); + const processBtn = document.getElementById('process-btn'); - if (backBtn) { - backBtn.addEventListener('click', () => { - window.location.href = import.meta.env.BASE_URL; - }); - } + if (backBtn) { + backBtn.addEventListener('click', () => { + window.location.href = import.meta.env.BASE_URL; + }); + } - const updateUI = async () => { - if (!convertOptions) return; + const updateUI = async () => { + if (!convertOptions) return; - if (state.files.length > 0) { - if (fileDisplayArea) { - fileDisplayArea.innerHTML = ''; + if (state.files.length > 0) { + if (fileDisplayArea) { + fileDisplayArea.innerHTML = ''; - for (let index = 0; index < state.files.length; index++) { - const file = state.files[index]; - const fileDiv = document.createElement('div'); - fileDiv.className = 'flex items-center justify-between bg-gray-700 p-3 rounded-lg text-sm'; + for (let index = 0; index < state.files.length; index++) { + const file = state.files[index]; + const fileDiv = document.createElement('div'); + fileDiv.className = + 'flex items-center justify-between bg-gray-700 p-3 rounded-lg text-sm'; - const infoContainer = document.createElement('div'); - infoContainer.className = 'flex flex-col overflow-hidden'; + const infoContainer = document.createElement('div'); + infoContainer.className = 'flex flex-col overflow-hidden'; - const nameSpan = document.createElement('div'); - nameSpan.className = 'truncate font-medium text-gray-200 text-sm mb-1'; - nameSpan.textContent = file.name; + const nameSpan = document.createElement('div'); + nameSpan.className = + 'truncate font-medium text-gray-200 text-sm mb-1'; + nameSpan.textContent = file.name; - const metaSpan = document.createElement('div'); - metaSpan.className = 'text-xs text-gray-400'; - metaSpan.textContent = formatBytes(file.size); + const metaSpan = document.createElement('div'); + metaSpan.className = 'text-xs text-gray-400'; + metaSpan.textContent = formatBytes(file.size); - infoContainer.append(nameSpan, metaSpan); + infoContainer.append(nameSpan, metaSpan); - const removeBtn = document.createElement('button'); - removeBtn.className = 'ml-4 text-red-400 hover:text-red-300 flex-shrink-0'; - removeBtn.innerHTML = ''; - removeBtn.onclick = () => { - state.files = state.files.filter((_, i) => i !== index); - updateUI(); - }; - - fileDiv.append(infoContainer, removeBtn); - fileDisplayArea.appendChild(fileDiv); - } - - createIcons({ icons }); - } - if (fileControls) fileControls.classList.remove('hidden'); - convertOptions.classList.remove('hidden'); - } else { - if (fileDisplayArea) fileDisplayArea.innerHTML = ''; - if (fileControls) fileControls.classList.add('hidden'); - convertOptions.classList.add('hidden'); - } - }; - - const resetState = () => { - state.files = []; - state.pdfDoc = null; - updateUI(); - }; - - const convertToPdf = async () => { - try { - console.log('[CSV2PDF] Starting conversion...'); - console.log('[CSV2PDF] Number of files:', state.files.length); - - if (state.files.length === 0) { - showAlert('No Files', 'Please select at least one CSV file.'); - hideLoader(); - return; - } - - const { convertCsvToPdf } = await import('../utils/csv-to-pdf.js'); - - if (state.files.length === 1) { - const originalFile = state.files[0]; - console.log('[CSV2PDF] Converting single file:', originalFile.name, 'Size:', originalFile.size, 'bytes'); - - const pdfBlob = await convertCsvToPdf(originalFile, { - onProgress: (percent, message) => { - console.log(`[CSV2PDF] Progress: ${percent}% - ${message}`); - showLoader(message, percent); - } - }); - - console.log('[CSV2PDF] Conversion complete! PDF size:', pdfBlob.size, 'bytes'); - - const fileName = originalFile.name.replace(/\.csv$/i, '') + '.pdf'; - downloadFile(pdfBlob, fileName); - console.log('[CSV2PDF] File downloaded:', fileName); - - hideLoader(); - - showAlert( - 'Conversion Complete', - `Successfully converted ${originalFile.name} to PDF.`, - 'success', - () => resetState() - ); - } else { - console.log('[CSV2PDF] Converting multiple files:', state.files.length); - showLoader('Preparing conversion...'); - const JSZip = (await import('jszip')).default; - const zip = new JSZip(); - - for (let i = 0; i < state.files.length; i++) { - const file = state.files[i]; - console.log(`[CSV2PDF] Converting file ${i + 1}/${state.files.length}:`, file.name); - - const pdfBlob = await convertCsvToPdf(file, { - onProgress: (percent, message) => { - const overallPercent = ((i / state.files.length) * 100) + (percent / state.files.length); - showLoader(`Converting ${i + 1}/${state.files.length}: ${file.name}...`, overallPercent); - } - }); - - console.log(`[CSV2PDF] Converted ${file.name}, PDF size:`, pdfBlob.size); - - const baseName = file.name.replace(/\.csv$/i, ''); - const pdfBuffer = await pdfBlob.arrayBuffer(); - zip.file(`${baseName}.pdf`, pdfBuffer); - } - - console.log('[CSV2PDF] Generating ZIP file...'); - showLoader('Creating ZIP archive...'); - const zipBlob = await zip.generateAsync({ type: 'blob' }); - console.log('[CSV2PDF] ZIP size:', zipBlob.size); - - downloadFile(zipBlob, 'csv-converted.zip'); - - hideLoader(); - - showAlert( - 'Conversion Complete', - `Successfully converted ${state.files.length} CSV file(s) to PDF.`, - 'success', - () => resetState() - ); - } - } catch (e: any) { - console.error('[CSV2PDF] ERROR:', e); - console.error('[CSV2PDF] Error stack:', e.stack); - hideLoader(); - showAlert( - 'Error', - `An error occurred during conversion. Error: ${e.message}` - ); - } - }; - - const handleFileSelect = (files: FileList | null) => { - if (files && files.length > 0) { - state.files = [...state.files, ...Array.from(files)]; + const removeBtn = document.createElement('button'); + removeBtn.className = + 'ml-4 text-red-400 hover:text-red-300 flex-shrink-0'; + removeBtn.innerHTML = ''; + removeBtn.onclick = () => { + state.files = state.files.filter((_, i) => i !== index); updateUI(); + }; + + fileDiv.append(infoContainer, removeBtn); + fileDisplayArea.appendChild(fileDiv); } - }; - if (fileInput && dropZone) { - fileInput.addEventListener('change', (e) => { - handleFileSelect((e.target as HTMLInputElement).files); - }); - - dropZone.addEventListener('dragover', (e) => { - e.preventDefault(); - dropZone.classList.add('bg-gray-700'); - }); - - dropZone.addEventListener('dragleave', (e) => { - e.preventDefault(); - dropZone.classList.remove('bg-gray-700'); - }); - - dropZone.addEventListener('drop', (e) => { - e.preventDefault(); - dropZone.classList.remove('bg-gray-700'); - const files = e.dataTransfer?.files; - if (files && files.length > 0) { - const csvFiles = Array.from(files).filter(f => f.name.toLowerCase().endsWith('.csv') || f.type === 'text/csv'); - if (csvFiles.length > 0) { - const dataTransfer = new DataTransfer(); - csvFiles.forEach(f => dataTransfer.items.add(f)); - handleFileSelect(dataTransfer.files); - } - } - }); - - // Clear value on click to allow re-selecting the same file - fileInput.addEventListener('click', () => { - fileInput.value = ''; - }); - } - - if (addMoreBtn) { - addMoreBtn.addEventListener('click', () => { - fileInput.click(); - }); - } - - if (clearFilesBtn) { - clearFilesBtn.addEventListener('click', () => { - resetState(); - }); - } - - if (processBtn) { - processBtn.addEventListener('click', convertToPdf); + createIcons({ icons }); + } + if (fileControls) fileControls.classList.remove('hidden'); + convertOptions.classList.remove('hidden'); + } else { + if (fileDisplayArea) fileDisplayArea.innerHTML = ''; + if (fileControls) fileControls.classList.add('hidden'); + convertOptions.classList.add('hidden'); } + }; + const resetState = () => { + state.files = []; + state.pdfDoc = null; updateUI(); + }; + + const convertToPdf = async () => { + try { + console.log('[CSV2PDF] Starting conversion...'); + console.log('[CSV2PDF] Number of files:', state.files.length); + + if (state.files.length === 0) { + showAlert('No Files', 'Please select at least one CSV file.'); + hideLoader(); + return; + } + + const { convertCsvToPdf } = await import('../utils/csv-to-pdf.js'); + + if (state.files.length === 1) { + const originalFile = state.files[0]; + console.log( + '[CSV2PDF] Converting single file:', + originalFile.name, + 'Size:', + originalFile.size, + 'bytes' + ); + + const pdfBlob = await convertCsvToPdf(originalFile, { + onProgress: (percent, message) => { + console.log(`[CSV2PDF] Progress: ${percent}% - ${message}`); + showLoader(message, percent); + }, + }); + + console.log( + '[CSV2PDF] Conversion complete! PDF size:', + pdfBlob.size, + 'bytes' + ); + + const fileName = originalFile.name.replace(/\.csv$/i, '') + '.pdf'; + downloadFile(pdfBlob, fileName); + console.log('[CSV2PDF] File downloaded:', fileName); + + hideLoader(); + + showAlert( + 'Conversion Complete', + `Successfully converted ${originalFile.name} to PDF.`, + 'success', + () => resetState() + ); + } else { + console.log('[CSV2PDF] Converting multiple files:', state.files.length); + showLoader('Preparing conversion...'); + const JSZip = (await import('jszip')).default; + const zip = new JSZip(); + + for (let i = 0; i < state.files.length; i++) { + const file = state.files[i]; + console.log( + `[CSV2PDF] Converting file ${i + 1}/${state.files.length}:`, + file.name + ); + + const pdfBlob = await convertCsvToPdf(file, { + onProgress: (percent) => { + const overallPercent = + (i / state.files.length) * 100 + percent / state.files.length; + showLoader( + `Converting ${i + 1}/${state.files.length}: ${file.name}...`, + overallPercent + ); + }, + }); + + console.log( + `[CSV2PDF] Converted ${file.name}, PDF size:`, + pdfBlob.size + ); + + const baseName = file.name.replace(/\.csv$/i, ''); + const pdfBuffer = await pdfBlob.arrayBuffer(); + zip.file(`${baseName}.pdf`, pdfBuffer); + } + + console.log('[CSV2PDF] Generating ZIP file...'); + showLoader('Creating ZIP archive...'); + const zipBlob = await zip.generateAsync({ type: 'blob' }); + console.log('[CSV2PDF] ZIP size:', zipBlob.size); + + downloadFile(zipBlob, 'csv-converted.zip'); + + hideLoader(); + + showAlert( + 'Conversion Complete', + `Successfully converted ${state.files.length} CSV file(s) to PDF.`, + 'success', + () => resetState() + ); + } + } catch (e: unknown) { + console.error('[CSV2PDF] ERROR:', e); + console.error( + '[CSV2PDF] Error stack:', + e instanceof Error ? e.stack : '' + ); + hideLoader(); + showAlert( + 'Error', + `An error occurred during conversion. Error: ${e instanceof Error ? e.message : String(e)}` + ); + } + }; + + const handleFileSelect = (files: FileList | null) => { + if (files && files.length > 0) { + state.files = [...state.files, ...Array.from(files)]; + updateUI(); + } + }; + + if (fileInput && dropZone) { + fileInput.addEventListener('change', (e) => { + handleFileSelect((e.target as HTMLInputElement).files); + }); + + dropZone.addEventListener('dragover', (e) => { + e.preventDefault(); + dropZone.classList.add('bg-gray-700'); + }); + + dropZone.addEventListener('dragleave', (e) => { + e.preventDefault(); + dropZone.classList.remove('bg-gray-700'); + }); + + dropZone.addEventListener('drop', (e) => { + e.preventDefault(); + dropZone.classList.remove('bg-gray-700'); + const files = e.dataTransfer?.files; + if (files && files.length > 0) { + const csvFiles = Array.from(files).filter( + (f) => f.name.toLowerCase().endsWith('.csv') || f.type === 'text/csv' + ); + if (csvFiles.length > 0) { + const dataTransfer = new DataTransfer(); + csvFiles.forEach((f) => dataTransfer.items.add(f)); + handleFileSelect(dataTransfer.files); + } + } + }); + + // Clear value on click to allow re-selecting the same file + fileInput.addEventListener('click', () => { + fileInput.value = ''; + }); + } + + if (addMoreBtn) { + addMoreBtn.addEventListener('click', () => { + fileInput.click(); + }); + } + + if (clearFilesBtn) { + clearFilesBtn.addEventListener('click', () => { + resetState(); + }); + } + + if (processBtn) { + processBtn.addEventListener('click', convertToPdf); + } + + updateUI(); }); diff --git a/src/js/logic/decrypt-pdf-page.ts b/src/js/logic/decrypt-pdf-page.ts index 58f274b..f289ca1 100644 --- a/src/js/logic/decrypt-pdf-page.ts +++ b/src/js/logic/decrypt-pdf-page.ts @@ -170,7 +170,7 @@ async function decryptPdf() { zip.file(`unlocked-${file.name}`, decryptedBytes, { binary: true }); successCount++; - } catch (fileError: any) { + } catch (fileError: unknown) { errorCount++; console.error(`Failed to decrypt ${file.name}:`, fileError); } @@ -194,15 +194,16 @@ async function decryptPdf() { resetState(); }); } - } catch (error: any) { + } catch (error: unknown) { console.error('Error during PDF decryption:', error); - if (error.message === 'INVALID_PASSWORD') { + const errorMessage = error instanceof Error ? error.message : ''; + if (errorMessage === 'INVALID_PASSWORD') { showAlert( 'Incorrect Password', 'The password you entered is incorrect. Please try again.' ); - } else if (error.message?.includes('password')) { + } else if (errorMessage.includes('password')) { showAlert( 'Password Error', 'Unable to decrypt the PDF with the provided password.' @@ -210,7 +211,7 @@ async function decryptPdf() { } else { showAlert( 'Decryption Failed', - `An error occurred: ${error.message || 'The password you entered is wrong or the file is corrupted.'}` + `An error occurred: ${errorMessage || 'The password you entered is wrong or the file is corrupted.'}` ); } } finally { diff --git a/src/js/logic/delete-pages-page.ts b/src/js/logic/delete-pages-page.ts index b61ffa5..2f8102f 100644 --- a/src/js/logic/delete-pages-page.ts +++ b/src/js/logic/delete-pages-page.ts @@ -164,7 +164,7 @@ async function renderThumbnails() { canvas.width = viewport.width; canvas.height = viewport.height; const ctx = canvas.getContext('2d'); - await page.render({ canvasContext: ctx, viewport }).promise; + await page.render({ canvas: null, canvasContext: ctx, viewport }).promise; const wrapper = document.createElement('div'); wrapper.className = 'relative cursor-pointer group'; diff --git a/src/js/logic/deskew-pdf-page.ts b/src/js/logic/deskew-pdf-page.ts index e590d12..ed7bafb 100644 --- a/src/js/logic/deskew-pdf-page.ts +++ b/src/js/logic/deskew-pdf-page.ts @@ -1,9 +1,10 @@ -import { isWasmAvailable, getWasmBaseUrl } from '../config/wasm-cdn-config.js'; -import { showWasmRequiredDialog } from '../utils/wasm-provider.js'; -import { loadPyMuPDF, isPyMuPDFAvailable } from '../utils/pymupdf-loader.js'; +import { loadPyMuPDF } from '../utils/pymupdf-loader.js'; +import type { PyMuPDFInstance } from '@/types'; import { batchDecryptIfNeeded } from '../utils/password-prompt.js'; import { createIcons, icons } from 'lucide'; import { downloadFile } from '../utils/helpers'; +import { isWasmAvailable } from '../config/wasm-cdn-config.js'; +import { showWasmRequiredDialog } from '../utils/wasm-provider.js'; interface DeskewResult { totalPages: number; @@ -13,11 +14,11 @@ interface DeskewResult { } let selectedFiles: File[] = []; -let pymupdf: any = null; +let pymupdf: PyMuPDFInstance | null = null; -async function initPyMuPDF(): Promise { +async function initPyMuPDF(): Promise { if (!pymupdf) { - pymupdf = await loadPyMuPDF(); + pymupdf = (await loadPyMuPDF()) as PyMuPDFInstance; } return pymupdf; } diff --git a/src/js/logic/digital-sign-pdf-page.ts b/src/js/logic/digital-sign-pdf-page.ts index c597e9d..ba1c82d 100644 --- a/src/js/logic/digital-sign-pdf-page.ts +++ b/src/js/logic/digital-sign-pdf-page.ts @@ -400,7 +400,7 @@ async function handleCertFile(file: File): Promise { certStatus.className = 'text-xs text-green-400'; } } - } catch (error) { + } catch { const certStatus = getElement('cert-status'); if (certStatus) { certStatus.textContent = 'Failed to parse PEM file'; diff --git a/src/js/logic/duplicate-organize.ts b/src/js/logic/duplicate-organize.ts index 533a820..e2a11c8 100644 --- a/src/js/logic/duplicate-organize.ts +++ b/src/js/logic/duplicate-organize.ts @@ -16,7 +16,9 @@ pdfjsLib.GlobalWorkerOptions.workerSrc = new URL( import.meta.url ).toString(); -const duplicateOrganizeState = { +const duplicateOrganizeState: { + sortableInstances: { pageGrid?: Sortable }; +} = { sortableInstances: {}, }; @@ -24,13 +26,10 @@ function initializePageGridSortable() { const grid = document.getElementById('page-grid'); if (!grid) return; - // @ts-expect-error TS(2339) FIXME: Property 'pageGrid' does not exist on type '{}'. if (duplicateOrganizeState.sortableInstances.pageGrid) { - // @ts-expect-error TS(2339) FIXME: Property 'pageGrid' does not exist on type '{}'. duplicateOrganizeState.sortableInstances.pageGrid.destroy(); } - // @ts-expect-error TS(2339) FIXME: Property 'pageGrid' does not exist on type '{}'. duplicateOrganizeState.sortableInstances.pageGrid = Sortable.create(grid, { animation: 150, ghostClass: 'sortable-ghost', @@ -38,10 +37,10 @@ function initializePageGridSortable() { dragClass: 'sortable-drag', filter: '.duplicate-btn, .delete-btn', preventOnFilter: true, - onStart: function (evt: any) { + onStart: function (evt: Sortable.SortableEvent) { evt.item.style.opacity = '0.5'; }, - onEnd: function (evt: any) { + onEnd: function (evt: Sortable.SortableEvent) { evt.item.style.opacity = '1'; }, }); @@ -51,7 +50,7 @@ function initializePageGridSortable() { * Attaches event listeners for duplicate and delete to a page thumbnail element. * @param {HTMLElement} element The thumbnail element to attach listeners to. */ -function attachEventListeners(element: any) { +function attachEventListeners(element: HTMLElement) { // Re-number all visible page labels const renumberPages = () => { const grid = document.getElementById('page-grid'); @@ -65,16 +64,16 @@ function attachEventListeners(element: any) { // Duplicate button listener element .querySelector('.duplicate-btn') - .addEventListener('click', (e: any) => { + .addEventListener('click', (e: Event) => { e.stopPropagation(); - const clone = element.cloneNode(true); + const clone = element.cloneNode(true) as HTMLElement; element.after(clone); attachEventListeners(clone); renumberPages(); initializePageGridSortable(); }); - element.querySelector('.delete-btn').addEventListener('click', (e: any) => { + element.querySelector('.delete-btn').addEventListener('click', (e: Event) => { e.stopPropagation(); if (document.getElementById('page-grid').children.length > 1) { element.remove(); @@ -218,7 +217,7 @@ export async function processAndSave() { } const copiedPages = await newPdfDoc.copyPages(state.pdfDoc, finalIndices); - copiedPages.forEach((page: any) => newPdfDoc.addPage(page)); + copiedPages.forEach((page) => newPdfDoc.addPage(page)); const newPdfBytes = await newPdfDoc.save(); downloadFile( diff --git a/src/js/logic/edit-attachments-page.ts b/src/js/logic/edit-attachments-page.ts index a19f125..c0f7c44 100644 --- a/src/js/logic/edit-attachments-page.ts +++ b/src/js/logic/edit-attachments-page.ts @@ -44,7 +44,11 @@ worker.onmessage = function (e) { const data = e.data; if (data.status === 'success' && data.attachments !== undefined) { - pageState.allAttachments = data.attachments.map(function (att: any) { + pageState.allAttachments = data.attachments.map(function (att: { + data: ArrayBuffer; + filename: string; + description: string; + }) { return { ...att, data: new Uint8Array(att.data), diff --git a/src/js/logic/edit-pdf-page.ts b/src/js/logic/edit-pdf-page.ts index 3828392..1996bdc 100644 --- a/src/js/logic/edit-pdf-page.ts +++ b/src/js/logic/edit-pdf-page.ts @@ -11,8 +11,11 @@ const embedPdfWasmUrl = new URL( import.meta.url ).href; -let viewerInstance: any = null; -let docManagerPlugin: any = null; +import type { EmbedPdfContainer } from 'embedpdf-snippet'; +import type { DocManagerPlugin } from '@/types'; + +let viewerInstance: EmbedPdfContainer | null = null; +let docManagerPlugin: DocManagerPlugin | null = null; let isViewerInitialized = false; const fileEntryMap = new Map(); @@ -148,33 +151,37 @@ async function handleFiles(files: FileList) { }); const registry = await viewerInstance.registry; - docManagerPlugin = registry.getPlugin('document-manager').provides(); + docManagerPlugin = registry + .getPlugin('document-manager') + .provides() as unknown as DocManagerPlugin; - docManagerPlugin.onDocumentClosed((data: any) => { - const docId = data?.id || data; + docManagerPlugin.onDocumentClosed((data: { id?: string }) => { + const docId = data?.id || ''; removeFileEntry(docId); }); - docManagerPlugin.onDocumentOpened((data: any) => { - const docId = data?.id; - const docKey = data?.name; - if (!docId) return; - const pendingEntry = fileDisplayArea.querySelector( - `[data-pending-name="${CSS.escape(docKey)}"]` - ) as HTMLElement; - if (pendingEntry) { - pendingEntry.removeAttribute('data-pending-name'); - fileEntryMap.set(docId, pendingEntry); - const removeBtn = pendingEntry.querySelector( - '[data-remove-btn]' + docManagerPlugin.onDocumentOpened( + (data: { id?: string; name?: string }) => { + const docId = data?.id; + const docKey = data?.name; + if (!docId) return; + const pendingEntry = fileDisplayArea.querySelector( + `[data-pending-name="${CSS.escape(docKey)}"]` ) as HTMLElement; - if (removeBtn) { - removeBtn.onclick = () => { - docManagerPlugin.closeDocument(docId); - }; + if (pendingEntry) { + pendingEntry.removeAttribute('data-pending-name'); + fileEntryMap.set(docId, pendingEntry); + const removeBtn = pendingEntry.querySelector( + '[data-remove-btn]' + ) as HTMLElement; + if (removeBtn) { + removeBtn.onclick = () => { + docManagerPlugin.closeDocument(docId); + }; + } } } - }); + ); addFileEntries(fileDisplayArea, decryptedFiles); diff --git a/src/js/logic/email-to-pdf-page.ts b/src/js/logic/email-to-pdf-page.ts index 9c78ada..4992c3b 100644 --- a/src/js/logic/email-to-pdf-page.ts +++ b/src/js/logic/email-to-pdf-page.ts @@ -3,9 +3,7 @@ import { downloadFile, formatBytes } from '../utils/helpers.js'; import { state } from '../state.js'; import { createIcons, icons } from 'lucide'; import { parseEmailFile, renderEmailToHtml } from './email-to-pdf.js'; -import { isWasmAvailable, getWasmBaseUrl } from '../config/wasm-cdn-config.js'; -import { showWasmRequiredDialog } from '../utils/wasm-provider.js'; -import { loadPyMuPDF, isPyMuPDFAvailable } from '../utils/pymupdf-loader.js'; +import { loadPyMuPDF } from '../utils/pymupdf-loader.js'; const EXTENSIONS = ['.eml', '.msg']; const TOOL_NAME = 'Email'; @@ -125,7 +123,11 @@ document.addEventListener('DOMContentLoaded', () => { pageSize, }); - const pdfBlob = await (pymupdf as any).htmlToPdf(htmlContent, { + const pdfBlob = await ( + pymupdf as unknown as { + htmlToPdf: (html: string, options: unknown) => Promise; + } + ).htmlToPdf(htmlContent, { pageSize, margins: { top: 50, right: 50, bottom: 50, left: 50 }, attachments: email.attachments @@ -166,7 +168,11 @@ document.addEventListener('DOMContentLoaded', () => { pageSize, }); - const pdfBlob = await (pymupdf as any).htmlToPdf(htmlContent, { + const pdfBlob = await ( + pymupdf as unknown as { + htmlToPdf: (html: string, options: unknown) => Promise; + } + ).htmlToPdf(htmlContent, { pageSize, margins: { top: 50, right: 50, bottom: 50, left: 50 }, attachments: email.attachments @@ -179,7 +185,7 @@ document.addEventListener('DOMContentLoaded', () => { const baseName = file.name.replace(/\.[^.]+$/, ''); const pdfBuffer = await pdfBlob.arrayBuffer(); zip.file(`${baseName}.pdf`, pdfBuffer); - } catch (e: any) { + } catch (e: unknown) { console.error(`Failed to convert ${file.name}:`, e); } } @@ -196,12 +202,12 @@ document.addEventListener('DOMContentLoaded', () => { () => resetState() ); } - } catch (e: any) { + } catch (e: unknown) { console.error(`[${TOOL_NAME}2PDF] ERROR:`, e); hideLoader(); showAlert( 'Error', - `An error occurred during conversion. Error: ${e.message}` + `An error occurred during conversion. Error: ${e instanceof Error ? e.message : String(e)}` ); } }; diff --git a/src/js/logic/email-to-pdf.ts b/src/js/logic/email-to-pdf.ts index e4c7b63..4515161 100644 --- a/src/js/logic/email-to-pdf.ts +++ b/src/js/logic/email-to-pdf.ts @@ -42,7 +42,14 @@ export async function parseEmlFile(file: File): Promise { .filter(Boolean); // Helper to map parsing result to EmailAttachment - const mapAttachment = (att: any): EmailAttachment => { + interface RawAttachment { + content?: string | ArrayBuffer | Uint8Array; + filename?: string; + mimeType?: string; + contentId?: string; + } + + const mapAttachment = (att: RawAttachment): EmailAttachment => { let content: Uint8Array | undefined; let size = 0; if (att.content) { @@ -67,7 +74,9 @@ export async function parseEmlFile(file: File): Promise { const attachments: EmailAttachment[] = [ ...(email.attachments || []).map(mapAttachment), - ...((email as any).inline || []).map(mapAttachment), + ...((email as { inline?: RawAttachment[] }).inline || []).map( + mapAttachment + ), ]; // Preserve original date string from headers @@ -138,8 +147,16 @@ export async function parseMsgFile(file: File): Promise { } } + interface MsgAttachment { + fileName?: string; + name?: string; + content?: ArrayLike; + mimeType?: string; + pidContentId?: string; + } + const attachments: EmailAttachment[] = (msgData.attachments || []).map( - (att: any) => ({ + (att: MsgAttachment) => ({ filename: att.fileName || att.name || 'unnamed', size: att.content?.length || 0, contentType: att.mimeType || 'application/octet-stream', diff --git a/src/js/logic/encrypt-pdf-page.ts b/src/js/logic/encrypt-pdf-page.ts index 599ea3b..dae312f 100644 --- a/src/js/logic/encrypt-pdf-page.ts +++ b/src/js/logic/encrypt-pdf-page.ts @@ -6,7 +6,7 @@ import { readFileAsArrayBuffer, } from '../utils/helpers.js'; import { icons, createIcons } from 'lucide'; -import { EncryptPdfState } from '@/types'; +import { EncryptPdfState, QpdfInstanceExtended } from '@/types'; const pageState: EncryptPdfState = { file: null, @@ -114,7 +114,7 @@ async function encryptPdf() { const inputPath = '/input.pdf'; const outputPath = '/output.pdf'; - let qpdf: any; + let qpdf: QpdfInstanceExtended; const loaderModal = document.getElementById('loader-modal'); const loaderText = document.getElementById('loader-text'); @@ -154,10 +154,11 @@ async function encryptPdf() { try { qpdf.callMain(args); - } catch (qpdfError: any) { + } catch (qpdfError: unknown) { console.error('qpdf execution error:', qpdfError); throw new Error( - 'Encryption failed: ' + (qpdfError.message || 'Unknown error'), + 'Encryption failed: ' + + (qpdfError instanceof Error ? qpdfError.message : 'Unknown error'), { cause: qpdfError } ); } @@ -169,7 +170,9 @@ async function encryptPdf() { throw new Error('Encryption resulted in an empty file.'); } - const blob = new Blob([outputFile], { type: 'application/pdf' }); + const blob = new Blob([new Uint8Array(outputFile)], { + type: 'application/pdf', + }); downloadFile(blob, `encrypted-${pageState.file.name}`); if (loaderModal) loaderModal.classList.add('hidden'); @@ -183,12 +186,12 @@ async function encryptPdf() { showAlert('Success', successMessage, 'success', () => { resetState(); }); - } catch (error: any) { + } catch (error: unknown) { console.error('Error during PDF encryption:', error); if (loaderModal) loaderModal.classList.add('hidden'); showAlert( 'Encryption Failed', - `An error occurred: ${error.message || 'The PDF might be corrupted.'}` + `An error occurred: ${error instanceof Error ? error.message : 'The PDF might be corrupted.'}` ); } finally { try { diff --git a/src/js/logic/epub-to-pdf-page.ts b/src/js/logic/epub-to-pdf-page.ts index c629dab..8a18076 100644 --- a/src/js/logic/epub-to-pdf-page.ts +++ b/src/js/logic/epub-to-pdf-page.ts @@ -2,9 +2,7 @@ import { showLoader, hideLoader, showAlert } from '../ui.js'; import { downloadFile, formatBytes } from '../utils/helpers.js'; import { state } from '../state.js'; import { createIcons, icons } from 'lucide'; -import { isWasmAvailable, getWasmBaseUrl } from '../config/wasm-cdn-config.js'; -import { showWasmRequiredDialog } from '../utils/wasm-provider.js'; -import { loadPyMuPDF, isPyMuPDFAvailable } from '../utils/pymupdf-loader.js'; +import { loadPyMuPDF } from '../utils/pymupdf-loader.js'; const FILETYPE = 'epub'; const EXTENSIONS = ['.epub']; @@ -145,12 +143,12 @@ document.addEventListener('DOMContentLoaded', () => { () => resetState() ); } - } catch (e: any) { + } catch (e: unknown) { console.error(`[${TOOL_NAME}2PDF] ERROR:`, e); hideLoader(); showAlert( 'Error', - `An error occurred during conversion. Error: ${e.message}` + `An error occurred during conversion. Error: ${e instanceof Error ? e.message : String(e)}` ); } }; diff --git a/src/js/logic/excel-to-pdf-page.ts b/src/js/logic/excel-to-pdf-page.ts index 8d4dbb7..3bb3c27 100644 --- a/src/js/logic/excel-to-pdf-page.ts +++ b/src/js/logic/excel-to-pdf-page.ts @@ -1,216 +1,226 @@ import { showLoader, hideLoader, showAlert } from '../ui.js'; -import { - downloadFile, - readFileAsArrayBuffer, - formatBytes, -} from '../utils/helpers.js'; +import { downloadFile, formatBytes } from '../utils/helpers.js'; import { state } from '../state.js'; import { createIcons, icons } from 'lucide'; -import { getLibreOfficeConverter, type LoadProgress } from '../utils/libreoffice-loader.js'; +import { + getLibreOfficeConverter, + type LoadProgress, +} from '../utils/libreoffice-loader.js'; document.addEventListener('DOMContentLoaded', () => { - state.files = []; + state.files = []; - const fileInput = document.getElementById('file-input') as HTMLInputElement; - const dropZone = document.getElementById('drop-zone'); - const convertOptions = document.getElementById('convert-options'); - const fileDisplayArea = document.getElementById('file-display-area'); - const fileControls = document.getElementById('file-controls'); - const addMoreBtn = document.getElementById('add-more-btn'); - const clearFilesBtn = document.getElementById('clear-files-btn'); - const backBtn = document.getElementById('back-to-tools'); - const processBtn = document.getElementById('process-btn'); + const fileInput = document.getElementById('file-input') as HTMLInputElement; + const dropZone = document.getElementById('drop-zone'); + const convertOptions = document.getElementById('convert-options'); + const fileDisplayArea = document.getElementById('file-display-area'); + const fileControls = document.getElementById('file-controls'); + const addMoreBtn = document.getElementById('add-more-btn'); + const clearFilesBtn = document.getElementById('clear-files-btn'); + const backBtn = document.getElementById('back-to-tools'); + const processBtn = document.getElementById('process-btn'); - if (backBtn) { - backBtn.addEventListener('click', () => { - window.location.href = import.meta.env.BASE_URL; - }); - } + if (backBtn) { + backBtn.addEventListener('click', () => { + window.location.href = import.meta.env.BASE_URL; + }); + } - const updateUI = async () => { - if (!convertOptions) return; + const updateUI = async () => { + if (!convertOptions) return; - if (state.files.length > 0) { - if (fileDisplayArea) { - fileDisplayArea.innerHTML = ''; + if (state.files.length > 0) { + if (fileDisplayArea) { + fileDisplayArea.innerHTML = ''; - for (let index = 0; index < state.files.length; index++) { - const file = state.files[index]; - const fileDiv = document.createElement('div'); - fileDiv.className = 'flex items-center justify-between bg-gray-700 p-3 rounded-lg text-sm'; + for (let index = 0; index < state.files.length; index++) { + const file = state.files[index]; + const fileDiv = document.createElement('div'); + fileDiv.className = + 'flex items-center justify-between bg-gray-700 p-3 rounded-lg text-sm'; - const infoContainer = document.createElement('div'); - infoContainer.className = 'flex flex-col overflow-hidden'; + const infoContainer = document.createElement('div'); + infoContainer.className = 'flex flex-col overflow-hidden'; - const nameSpan = document.createElement('div'); - nameSpan.className = 'truncate font-medium text-gray-200 text-sm mb-1'; - nameSpan.textContent = file.name; + const nameSpan = document.createElement('div'); + nameSpan.className = + 'truncate font-medium text-gray-200 text-sm mb-1'; + nameSpan.textContent = file.name; - const metaSpan = document.createElement('div'); - metaSpan.className = 'text-xs text-gray-400'; - metaSpan.textContent = formatBytes(file.size); + const metaSpan = document.createElement('div'); + metaSpan.className = 'text-xs text-gray-400'; + metaSpan.textContent = formatBytes(file.size); - infoContainer.append(nameSpan, metaSpan); + infoContainer.append(nameSpan, metaSpan); - const removeBtn = document.createElement('button'); - removeBtn.className = 'ml-4 text-red-400 hover:text-red-300 flex-shrink-0'; - removeBtn.innerHTML = ''; - removeBtn.onclick = () => { - state.files = state.files.filter((_, i) => i !== index); - updateUI(); - }; - - fileDiv.append(infoContainer, removeBtn); - fileDisplayArea.appendChild(fileDiv); - } - - createIcons({ icons }); - } - if (fileControls) fileControls.classList.remove('hidden'); - convertOptions.classList.remove('hidden'); - } else { - if (fileDisplayArea) fileDisplayArea.innerHTML = ''; - if (fileControls) fileControls.classList.add('hidden'); - convertOptions.classList.add('hidden'); - } - }; - - const resetState = () => { - state.files = []; - state.pdfDoc = null; - updateUI(); - }; - - const convertToPdf = async () => { - try { - if (state.files.length === 0) { - showAlert('No Files', 'Please select at least one Excel file.'); - hideLoader(); - return; - } - - const converter = getLibreOfficeConverter(); - - // Initialize LibreOffice if not already done - await converter.initialize((progress: LoadProgress) => { - showLoader(progress.message, progress.percent); - }); - - if (state.files.length === 1) { - const originalFile = state.files[0]; - - showLoader('Processing...'); - - const pdfBlob = await converter.convertToPdf(originalFile); - - const fileName = originalFile.name.replace(/\.(xls|xlsx|ods|csv)$/i, '') + '.pdf'; - - downloadFile(pdfBlob, fileName); - - hideLoader(); - - showAlert( - 'Conversion Complete', - `Successfully converted ${originalFile.name} to PDF.`, - 'success', - () => resetState() - ); - } else { - showLoader('Processing...'); - const JSZip = (await import('jszip')).default; - const zip = new JSZip(); - - for (let i = 0; i < state.files.length; i++) { - const file = state.files[i]; - showLoader(`Converting ${i + 1}/${state.files.length}: ${file.name}...`); - - const pdfBlob = await converter.convertToPdf(file); - - const baseName = file.name.replace(/\.(xls|xlsx|ods|csv)$/i, ''); - const pdfBuffer = await pdfBlob.arrayBuffer(); - zip.file(`${baseName}.pdf`, pdfBuffer); - } - - const zipBlob = await zip.generateAsync({ type: 'blob' }); - - downloadFile(zipBlob, 'excel-converted.zip'); - - hideLoader(); - - showAlert( - 'Conversion Complete', - `Successfully converted ${state.files.length} Excel file(s) to PDF.`, - 'success', - () => resetState() - ); - } - } catch (e: any) { - hideLoader(); - showAlert( - 'Error', - `An error occurred during conversion. Error: ${e.message}` - ); - } - }; - - const handleFileSelect = (files: FileList | null) => { - if (files && files.length > 0) { - state.files = [...state.files, ...Array.from(files)]; + const removeBtn = document.createElement('button'); + removeBtn.className = + 'ml-4 text-red-400 hover:text-red-300 flex-shrink-0'; + removeBtn.innerHTML = ''; + removeBtn.onclick = () => { + state.files = state.files.filter((_, i) => i !== index); updateUI(); + }; + + fileDiv.append(infoContainer, removeBtn); + fileDisplayArea.appendChild(fileDiv); } - }; - if (fileInput && dropZone) { - fileInput.addEventListener('change', (e) => { - handleFileSelect((e.target as HTMLInputElement).files); - }); - - dropZone.addEventListener('dragover', (e) => { - e.preventDefault(); - dropZone.classList.add('bg-gray-700'); - }); - - dropZone.addEventListener('dragleave', (e) => { - e.preventDefault(); - dropZone.classList.remove('bg-gray-700'); - }); - - dropZone.addEventListener('drop', (e) => { - e.preventDefault(); - dropZone.classList.remove('bg-gray-700'); - const files = e.dataTransfer?.files; - if (files && files.length > 0) { - const excelFiles = Array.from(files).filter(f => { - const name = f.name.toLowerCase(); - return name.endsWith('.xls') || name.endsWith('.xlsx') || name.endsWith('.ods') || name.endsWith('.csv'); - }); - if (excelFiles.length > 0) { - const dataTransfer = new DataTransfer(); - excelFiles.forEach(f => dataTransfer.items.add(f)); - handleFileSelect(dataTransfer.files); - } - } - }); - - // Clear value on click to allow re-selecting the same file - fileInput.addEventListener('click', () => { - fileInput.value = ''; - }); + createIcons({ icons }); + } + if (fileControls) fileControls.classList.remove('hidden'); + convertOptions.classList.remove('hidden'); + } else { + if (fileDisplayArea) fileDisplayArea.innerHTML = ''; + if (fileControls) fileControls.classList.add('hidden'); + convertOptions.classList.add('hidden'); } + }; - if (addMoreBtn) { - addMoreBtn.addEventListener('click', () => { - fileInput.click(); + const resetState = () => { + state.files = []; + state.pdfDoc = null; + updateUI(); + }; + + const convertToPdf = async () => { + try { + if (state.files.length === 0) { + showAlert('No Files', 'Please select at least one Excel file.'); + hideLoader(); + return; + } + + const converter = getLibreOfficeConverter(); + + // Initialize LibreOffice if not already done + await converter.initialize((progress: LoadProgress) => { + showLoader(progress.message, progress.percent); + }); + + if (state.files.length === 1) { + const originalFile = state.files[0]; + + showLoader('Processing...'); + + const pdfBlob = await converter.convertToPdf(originalFile); + + const fileName = + originalFile.name.replace(/\.(xls|xlsx|ods|csv)$/i, '') + '.pdf'; + + downloadFile(pdfBlob, fileName); + + hideLoader(); + + showAlert( + 'Conversion Complete', + `Successfully converted ${originalFile.name} to PDF.`, + 'success', + () => resetState() + ); + } else { + showLoader('Processing...'); + const JSZip = (await import('jszip')).default; + const zip = new JSZip(); + + for (let i = 0; i < state.files.length; i++) { + const file = state.files[i]; + showLoader( + `Converting ${i + 1}/${state.files.length}: ${file.name}...` + ); + + const pdfBlob = await converter.convertToPdf(file); + + const baseName = file.name.replace(/\.(xls|xlsx|ods|csv)$/i, ''); + const pdfBuffer = await pdfBlob.arrayBuffer(); + zip.file(`${baseName}.pdf`, pdfBuffer); + } + + const zipBlob = await zip.generateAsync({ type: 'blob' }); + + downloadFile(zipBlob, 'excel-converted.zip'); + + hideLoader(); + + showAlert( + 'Conversion Complete', + `Successfully converted ${state.files.length} Excel file(s) to PDF.`, + 'success', + () => resetState() + ); + } + } catch (e: unknown) { + hideLoader(); + showAlert( + 'Error', + `An error occurred during conversion. Error: ${e instanceof Error ? e.message : String(e)}` + ); + } + }; + + const handleFileSelect = (files: FileList | null) => { + if (files && files.length > 0) { + state.files = [...state.files, ...Array.from(files)]; + updateUI(); + } + }; + + if (fileInput && dropZone) { + fileInput.addEventListener('change', (e) => { + handleFileSelect((e.target as HTMLInputElement).files); + }); + + dropZone.addEventListener('dragover', (e) => { + e.preventDefault(); + dropZone.classList.add('bg-gray-700'); + }); + + dropZone.addEventListener('dragleave', (e) => { + e.preventDefault(); + dropZone.classList.remove('bg-gray-700'); + }); + + dropZone.addEventListener('drop', (e) => { + e.preventDefault(); + dropZone.classList.remove('bg-gray-700'); + const files = e.dataTransfer?.files; + if (files && files.length > 0) { + const excelFiles = Array.from(files).filter((f) => { + const name = f.name.toLowerCase(); + return ( + name.endsWith('.xls') || + name.endsWith('.xlsx') || + name.endsWith('.ods') || + name.endsWith('.csv') + ); }); - } + if (excelFiles.length > 0) { + const dataTransfer = new DataTransfer(); + excelFiles.forEach((f) => dataTransfer.items.add(f)); + handleFileSelect(dataTransfer.files); + } + } + }); - if (clearFilesBtn) { - clearFilesBtn.addEventListener('click', () => { - resetState(); - }); - } + // Clear value on click to allow re-selecting the same file + fileInput.addEventListener('click', () => { + fileInput.value = ''; + }); + } - if (processBtn) { - processBtn.addEventListener('click', convertToPdf); - } + if (addMoreBtn) { + addMoreBtn.addEventListener('click', () => { + fileInput.click(); + }); + } + + if (clearFilesBtn) { + clearFilesBtn.addEventListener('click', () => { + resetState(); + }); + } + + if (processBtn) { + processBtn.addEventListener('click', convertToPdf); + } }); diff --git a/src/js/logic/extract-images-page.ts b/src/js/logic/extract-images-page.ts index f9dd28c..e5bbf51 100644 --- a/src/js/logic/extract-images-page.ts +++ b/src/js/logic/extract-images-page.ts @@ -8,9 +8,7 @@ import { } from '../utils/helpers.js'; import { state } from '../state.js'; import { createIcons, icons } from 'lucide'; -import { isWasmAvailable, getWasmBaseUrl } from '../config/wasm-cdn-config.js'; -import { showWasmRequiredDialog } from '../utils/wasm-provider.js'; -import { loadPyMuPDF, isPyMuPDFAvailable } from '../utils/pymupdf-loader.js'; +import { loadPyMuPDF } from '../utils/pymupdf-loader.js'; import { batchDecryptIfNeeded } from '../utils/password-prompt.js'; interface ExtractedImage { @@ -88,7 +86,7 @@ document.addEventListener('DOMContentLoaded', () => { const arrayBuffer = await readFileAsArrayBuffer(file); const pdfDoc = await getPDFDocument({ data: arrayBuffer }).promise; metaSpan.textContent = `${formatBytes(file.size)} • ${pdfDoc.numPages} pages`; - } catch (error) { + } catch { metaSpan.textContent = `${formatBytes(file.size)} • Could not load page count`; } } @@ -118,7 +116,7 @@ document.addEventListener('DOMContentLoaded', () => { if (!imagesGrid || !imagesContainer) return; imagesGrid.innerHTML = ''; - extractedImages.forEach((img, index) => { + extractedImages.forEach((img) => { const blob = new Blob([new Uint8Array(img.data)]); const url = URL.createObjectURL(blob); @@ -212,11 +210,11 @@ document.addEventListener('DOMContentLoaded', () => { 'success' ); } - } catch (e: any) { + } catch (e: unknown) { hideLoader(); showAlert( 'Error', - `An error occurred during extraction. Error: ${e.message}` + `An error occurred during extraction. Error: ${e instanceof Error ? e.message : String(e)}` ); } }; diff --git a/src/js/logic/extract-pages-page.ts b/src/js/logic/extract-pages-page.ts index 174ba7f..8e175db 100644 --- a/src/js/logic/extract-pages-page.ts +++ b/src/js/logic/extract-pages-page.ts @@ -12,7 +12,7 @@ import { loadPdfDocument } from '../utils/load-pdf-document.js'; interface ExtractState { file: File | null; - pdfDoc: any; + pdfDoc: PDFDocument | null; totalPages: number; } diff --git a/src/js/logic/extract-tables-page.ts b/src/js/logic/extract-tables-page.ts index c7bbb8b..e377221 100644 --- a/src/js/logic/extract-tables-page.ts +++ b/src/js/logic/extract-tables-page.ts @@ -2,9 +2,7 @@ import { showLoader, hideLoader, showAlert } from '../ui.js'; import { downloadFile, formatBytes } from '../utils/helpers.js'; import { createIcons, icons } from 'lucide'; import JSZip from 'jszip'; -import { isWasmAvailable, getWasmBaseUrl } from '../config/wasm-cdn-config.js'; -import { showWasmRequiredDialog } from '../utils/wasm-provider.js'; -import { loadPyMuPDF, isPyMuPDFAvailable } from '../utils/pymupdf-loader.js'; +import { loadPyMuPDF } from '../utils/pymupdf-loader.js'; import { loadPdfWithPasswordPrompt } from '../utils/password-prompt.js'; let file: File | null = null; diff --git a/src/js/logic/fb2-to-pdf-page.ts b/src/js/logic/fb2-to-pdf-page.ts index 8058460..7195a5b 100644 --- a/src/js/logic/fb2-to-pdf-page.ts +++ b/src/js/logic/fb2-to-pdf-page.ts @@ -2,9 +2,7 @@ import { showLoader, hideLoader, showAlert } from '../ui.js'; import { downloadFile, formatBytes } from '../utils/helpers.js'; import { state } from '../state.js'; import { createIcons, icons } from 'lucide'; -import { isWasmAvailable, getWasmBaseUrl } from '../config/wasm-cdn-config.js'; -import { showWasmRequiredDialog } from '../utils/wasm-provider.js'; -import { loadPyMuPDF, isPyMuPDFAvailable } from '../utils/pymupdf-loader.js'; +import { loadPyMuPDF } from '../utils/pymupdf-loader.js'; const FILETYPE = 'fb2'; const EXTENSIONS = ['.fb2']; @@ -141,12 +139,12 @@ document.addEventListener('DOMContentLoaded', () => { () => resetState() ); } - } catch (e: any) { + } catch (e: unknown) { console.error(`[${TOOL_NAME}2PDF] ERROR:`, e); hideLoader(); showAlert( 'Error', - `An error occurred during conversion. Error: ${e.message}` + `An error occurred during conversion. Error: ${e instanceof Error ? e.message : String(e)}` ); } }; diff --git a/src/js/logic/form-creator.ts b/src/js/logic/form-creator.ts index 7ee262a..9689e98 100644 --- a/src/js/logic/form-creator.ts +++ b/src/js/logic/form-creator.ts @@ -28,7 +28,7 @@ type PdfViewerWindow = Window & { }; import { initializeGlobalShortcuts } from '../utils/shortcuts-init.js'; -import { downloadFile, hexToRgb, getPDFDocument } from '../utils/helpers.js'; +import { downloadFile, hexToRgb } from '../utils/helpers.js'; import { loadPdfWithPasswordPrompt } from '../utils/password-prompt.js'; import { createIcons, icons } from 'lucide'; import * as pdfjsLib from 'pdfjs-dist'; @@ -319,15 +319,15 @@ toolItems.forEach((item) => { let touchStartY = 0; let isTouchDragging = false; - item.addEventListener('touchstart', (e) => { + item.addEventListener('touchstart', (e: TouchEvent) => { const touch = e.touches[0]; touchStartX = touch.clientX; touchStartY = touch.clientY; isTouchDragging = false; }); - item.addEventListener('touchmove', (e) => { - e.preventDefault(); // Prevent scrolling while dragging + item.addEventListener('touchmove', (e: TouchEvent) => { + e.preventDefault(); const touch = e.touches[0]; const moveX = Math.abs(touch.clientX - touchStartX); const moveY = Math.abs(touch.clientY - touchStartY); @@ -338,7 +338,7 @@ toolItems.forEach((item) => { } }); - item.addEventListener('touchend', (e) => { + item.addEventListener('touchend', (e: TouchEvent) => { e.preventDefault(); if (!isTouchDragging) { // It was a tap, treat as click @@ -2131,8 +2131,7 @@ downloadBtn.addEventListener('click', async () => { nameCount.set(field.name, count + 1); if (existingFieldNames.has(field.name)) { - if (field.type === 'radio' && existingRadioGroups.has(field.name)) { - } else { + if (!(field.type === 'radio' && existingRadioGroups.has(field.name))) { conflictsWithPdf.push(field.name); } } @@ -2337,7 +2336,7 @@ downloadBtn.addEventListener('click', async () => { const existingField = form.getFieldMaybe(groupName); if (existingField) { - radioGroup = existingField; + radioGroup = existingField as PDFRadioGroup; radioGroups.set(groupName, radioGroup); console.log(`Using existing radio group from PDF: ${groupName}`); } else { @@ -2788,11 +2787,11 @@ function getPageDimensions(size: string): { width: number; height: number } { case 'a3': dimensions = PageSizes.A3; break; - case 'custom': - // Get custom dimensions from inputs + case 'custom': { const width = parseInt(customWidth.value) || 612; const height = parseInt(customHeight.value) || 792; return { width, height }; + } default: dimensions = PageSizes.Letter; } diff --git a/src/js/logic/image-to-pdf-page.ts b/src/js/logic/image-to-pdf-page.ts index 6101cb6..f566a3e 100644 --- a/src/js/logic/image-to-pdf-page.ts +++ b/src/js/logic/image-to-pdf-page.ts @@ -1,9 +1,8 @@ import { createIcons, icons } from 'lucide'; import { showAlert, showLoader, hideLoader } from '../ui.js'; import { downloadFile, formatBytes } from '../utils/helpers.js'; -import { isWasmAvailable, getWasmBaseUrl } from '../config/wasm-cdn-config.js'; -import { showWasmRequiredDialog } from '../utils/wasm-provider.js'; -import { loadPyMuPDF, isPyMuPDFAvailable } from '../utils/pymupdf-loader.js'; +import { loadPyMuPDF } from '../utils/pymupdf-loader.js'; +import type { PyMuPDFInstance } from '@/types'; import heic2any from 'heic2any'; import { getSelectedQuality, @@ -16,7 +15,7 @@ const SUPPORTED_FORMATS_DISPLAY = 'JPG, PNG, BMP, GIF, TIFF, PNM, PGM, PBM, PPM, PAM, JXR, JPX, JP2, PSD, SVG, HEIC, WebP'; let files: File[] = []; -let pymupdf: any = null; +let pymupdf: PyMuPDFInstance | null = null; if (document.readyState === 'loading') { document.addEventListener('DOMContentLoaded', initializePage); @@ -177,9 +176,9 @@ function updateUI() { } } -async function ensurePyMuPDF(): Promise { +async function ensurePyMuPDF(): Promise { if (!pymupdf) { - pymupdf = await loadPyMuPDF(); + pymupdf = (await loadPyMuPDF()) as PyMuPDFInstance; } return pymupdf; } @@ -274,7 +273,7 @@ async function convertToPdf() { const processed = await preprocessFile(file); const compressed = await compressImageFile(processed, quality); processedFiles.push(compressed); - } catch (error: any) { + } catch (error: unknown) { console.warn(error); throw error; } @@ -291,11 +290,11 @@ async function convertToPdf() { showAlert('Success', 'PDF created successfully!', 'success', () => { resetState(); }); - } catch (e: any) { + } catch (e: unknown) { console.error('[ImageToPDF]', e); showAlert( 'Conversion Error', - e.message || 'Failed to convert images to PDF.' + e instanceof Error ? e.message : 'Failed to convert images to PDF.' ); } finally { hideLoader(); diff --git a/src/js/logic/index.ts b/src/js/logic/index.ts index d7491f9..736d9e8 100644 --- a/src/js/logic/index.ts +++ b/src/js/logic/index.ts @@ -1,81 +1,18 @@ - - - - - - - - - - - - - - - - - -import { pdfToMarkdown } from './pdf-to-markdown.js'; -import { repairPdf } from './repair-pdf.js'; - - // import { mdToPdf } from './md-to-pdf.js'; - - - - - - import { processAndSave } from './duplicate-organize.js'; - - - - - -import { wordToPdf } from './word-to-pdf.js'; - import { setupCropperTool } from './cropper.js'; - - - - - - - - -export const toolLogic = { - - - - - - - - - - - - - - - - - - - +export const toolLogic: Record< + string, + | { + process?: (...args: unknown[]) => Promise; + setup?: (...args: unknown[]) => Promise; + } + | ((...args: unknown[]) => unknown) +> = { 'duplicate-organize': { process: processAndSave }, - - - - - - cropper: { setup: setupCropperTool }, - - }; - diff --git a/src/js/logic/jpg-to-pdf-page.ts b/src/js/logic/jpg-to-pdf-page.ts index e4e37ba..009aee2 100644 --- a/src/js/logic/jpg-to-pdf-page.ts +++ b/src/js/logic/jpg-to-pdf-page.ts @@ -2,6 +2,7 @@ import { createIcons, icons } from 'lucide'; import { showAlert, showLoader, hideLoader } from '../ui.js'; import { downloadFile, formatBytes } from '../utils/helpers.js'; import { loadPyMuPDF } from '../utils/pymupdf-loader.js'; +import type { PyMuPDFInstance } from '@/types'; import { getSelectedQuality, compressImageFile, @@ -11,7 +12,7 @@ const SUPPORTED_FORMATS = '.jpg,.jpeg,.jp2,.jpx'; const SUPPORTED_MIME_TYPES = ['image/jpeg', 'image/jpg', 'image/jp2']; let files: File[] = []; -let pymupdf: any = null; +let pymupdf: PyMuPDFInstance | null = null; if (document.readyState === 'loading') { document.addEventListener('DOMContentLoaded', initializePage); @@ -168,9 +169,9 @@ function updateUI() { } } -async function ensurePyMuPDF(): Promise { +async function ensurePyMuPDF(): Promise { if (!pymupdf) { - pymupdf = await loadPyMuPDF(); + pymupdf = (await loadPyMuPDF()) as PyMuPDFInstance; } return pymupdf; } @@ -200,11 +201,11 @@ async function convertToPdf() { showAlert('Success', 'PDF created successfully!', 'success', () => { resetState(); }); - } catch (e: any) { + } catch (e: unknown) { console.error('[JpgToPdf]', e); showAlert( 'Conversion Error', - e.message || 'Failed to convert images to PDF.' + e instanceof Error ? e.message : 'Failed to convert images to PDF.' ); } finally { hideLoader(); diff --git a/src/js/logic/linearize-pdf-page.ts b/src/js/logic/linearize-pdf-page.ts index 9c227dd..3ce422f 100644 --- a/src/js/logic/linearize-pdf-page.ts +++ b/src/js/logic/linearize-pdf-page.ts @@ -8,7 +8,7 @@ import { import { icons, createIcons } from 'lucide'; import JSZip from 'jszip'; import { deduplicateFileName } from '../utils/deduplicate-filename.js'; -import { LinearizePdfState } from '@/types'; +import { LinearizePdfState, QpdfInstanceExtended } from '@/types'; const pageState: LinearizePdfState = { files: [], @@ -111,7 +111,7 @@ async function linearizePdf() { const zip = new JSZip(); const usedNames = new Set(); - let qpdf: any; + let qpdf: QpdfInstanceExtended; let successCount = 0; let errorCount = 0; @@ -150,7 +150,7 @@ async function linearizePdf() { ); zip.file(zipEntryName, outputFile, { binary: true }); successCount++; - } catch (fileError: any) { + } catch (fileError: unknown) { errorCount++; console.error(`Failed to linearize ${file.name}:`, fileError); } finally { @@ -187,11 +187,11 @@ async function linearizePdf() { showAlert('Processing Complete', alertMessage, 'success', () => { resetState(); }); - } catch (error: any) { + } catch (error: unknown) { console.error('Linearization process error:', error); showAlert( 'Linearization Failed', - `An error occurred: ${error.message || 'Unknown error'}.` + `An error occurred: ${error instanceof Error ? error.message : 'Unknown error'}.` ); } finally { if (loaderModal) loaderModal.classList.add('hidden'); diff --git a/src/js/logic/merge-pdf-page.ts b/src/js/logic/merge-pdf-page.ts index 8d70c6b..5a0ee9b 100644 --- a/src/js/logic/merge-pdf-page.ts +++ b/src/js/logic/merge-pdf-page.ts @@ -16,15 +16,15 @@ import { import { createIcons, icons } from 'lucide'; import * as pdfjsLib from 'pdfjs-dist'; import Sortable from 'sortablejs'; +import type { MergeJob, MergeFile, MergeMessage, MergeResponse } from '@/types'; -// @ts-ignore pdfjsLib.GlobalWorkerOptions.workerSrc = new URL( 'pdfjs-dist/build/pdf.worker.min.mjs', import.meta.url ).toString(); interface MergeState { - pdfDocs: Record; + pdfDocs: Record; pdfBytes: Record; activeMode: 'file' | 'page'; sortableInstances: { @@ -66,10 +66,10 @@ function initializeFileListSortable() { ghostClass: 'sortable-ghost', chosenClass: 'sortable-chosen', dragClass: 'sortable-drag', - onStart: function (evt: any) { + onStart: function (evt: Sortable.SortableEvent) { evt.item.style.opacity = '0.5'; }, - onEnd: function (evt: any) { + onEnd: function (evt: Sortable.SortableEvent) { evt.item.style.opacity = '1'; }, }); @@ -88,10 +88,10 @@ function initializePageThumbnailsSortable() { ghostClass: 'sortable-ghost', chosenClass: 'sortable-chosen', dragClass: 'sortable-drag', - onStart: function (evt: any) { + onStart: function (evt: Sortable.SortableEvent) { evt.item.style.opacity = '0.5'; }, - onEnd: function (evt: any) { + onEnd: function (evt: Sortable.SortableEvent) { evt.item.style.opacity = '1'; }, }); @@ -201,7 +201,7 @@ async function renderPageMergeThumbnails() { batchSize: 8, useLazyLoading: true, lazyLoadMargin: '300px', - onProgress: (current, total) => { + onProgress: () => { currentPageNumber++; showLoader(`Rendering page previews...`); }, @@ -288,9 +288,7 @@ export async function merge() { showLoader('Merging PDFs...'); try { - // @ts-ignore const jobs: MergeJob[] = []; - // @ts-ignore const filesToMerge: MergeFile[] = []; const uniqueFileNames = new Set(); @@ -387,7 +385,6 @@ export async function merge() { } } - // @ts-ignore const message: MergeMessage = { command: 'merge', files: filesToMerge, @@ -400,7 +397,6 @@ export async function merge() { filesToMerge.map((f) => f.data) ); - // @ts-ignore mergeWorker.onmessage = (e: MessageEvent) => { hideLoader(); if (e.data.status === 'success') { diff --git a/src/js/logic/mobi-to-pdf-page.ts b/src/js/logic/mobi-to-pdf-page.ts index 7ddd5c1..d70c223 100644 --- a/src/js/logic/mobi-to-pdf-page.ts +++ b/src/js/logic/mobi-to-pdf-page.ts @@ -2,9 +2,7 @@ import { showLoader, hideLoader, showAlert } from '../ui.js'; import { downloadFile, formatBytes } from '../utils/helpers.js'; import { state } from '../state.js'; import { createIcons, icons } from 'lucide'; -import { isWasmAvailable, getWasmBaseUrl } from '../config/wasm-cdn-config.js'; -import { showWasmRequiredDialog } from '../utils/wasm-provider.js'; -import { loadPyMuPDF, isPyMuPDFAvailable } from '../utils/pymupdf-loader.js'; +import { loadPyMuPDF } from '../utils/pymupdf-loader.js'; const FILETYPE = 'mobi'; const EXTENSIONS = ['.mobi']; @@ -141,12 +139,12 @@ document.addEventListener('DOMContentLoaded', () => { () => resetState() ); } - } catch (e: any) { + } catch (e: unknown) { console.error(`[${TOOL_NAME}2PDF] ERROR:`, e); hideLoader(); showAlert( 'Error', - `An error occurred during conversion. Error: ${e.message}` + `An error occurred during conversion. Error: ${e instanceof Error ? e.message : String(e)}` ); } }; diff --git a/src/js/logic/odt-to-pdf-page.ts b/src/js/logic/odt-to-pdf-page.ts index 43ff895..1cb1d0a 100644 --- a/src/js/logic/odt-to-pdf-page.ts +++ b/src/js/logic/odt-to-pdf-page.ts @@ -1,215 +1,223 @@ import { showLoader, hideLoader, showAlert } from '../ui.js'; -import { - downloadFile, - readFileAsArrayBuffer, - formatBytes, -} from '../utils/helpers.js'; +import { downloadFile, formatBytes } from '../utils/helpers.js'; import { state } from '../state.js'; import { createIcons, icons } from 'lucide'; -import { getLibreOfficeConverter, type LoadProgress } from '../utils/libreoffice-loader.js'; +import { + getLibreOfficeConverter, + type LoadProgress, +} from '../utils/libreoffice-loader.js'; document.addEventListener('DOMContentLoaded', () => { - state.files = []; + state.files = []; - const fileInput = document.getElementById('file-input') as HTMLInputElement; - const dropZone = document.getElementById('drop-zone'); - const convertOptions = document.getElementById('convert-options'); - const fileDisplayArea = document.getElementById('file-display-area'); - const fileControls = document.getElementById('file-controls'); - const addMoreBtn = document.getElementById('add-more-btn'); - const clearFilesBtn = document.getElementById('clear-files-btn'); - const backBtn = document.getElementById('back-to-tools'); - const processBtn = document.getElementById('process-btn'); + const fileInput = document.getElementById('file-input') as HTMLInputElement; + const dropZone = document.getElementById('drop-zone'); + const convertOptions = document.getElementById('convert-options'); + const fileDisplayArea = document.getElementById('file-display-area'); + const fileControls = document.getElementById('file-controls'); + const addMoreBtn = document.getElementById('add-more-btn'); + const clearFilesBtn = document.getElementById('clear-files-btn'); + const backBtn = document.getElementById('back-to-tools'); + const processBtn = document.getElementById('process-btn'); - if (backBtn) { - backBtn.addEventListener('click', () => { - window.location.href = import.meta.env.BASE_URL; - }); - } + if (backBtn) { + backBtn.addEventListener('click', () => { + window.location.href = import.meta.env.BASE_URL; + }); + } - const updateUI = async () => { - if (!convertOptions) return; + const updateUI = async () => { + if (!convertOptions) return; - if (state.files.length > 0) { - if (fileDisplayArea) { - fileDisplayArea.innerHTML = ''; + if (state.files.length > 0) { + if (fileDisplayArea) { + fileDisplayArea.innerHTML = ''; - for (let index = 0; index < state.files.length; index++) { - const file = state.files[index]; - const fileDiv = document.createElement('div'); - fileDiv.className = 'flex items-center justify-between bg-gray-700 p-3 rounded-lg text-sm'; + for (let index = 0; index < state.files.length; index++) { + const file = state.files[index]; + const fileDiv = document.createElement('div'); + fileDiv.className = + 'flex items-center justify-between bg-gray-700 p-3 rounded-lg text-sm'; - const infoContainer = document.createElement('div'); - infoContainer.className = 'flex flex-col overflow-hidden'; + const infoContainer = document.createElement('div'); + infoContainer.className = 'flex flex-col overflow-hidden'; - const nameSpan = document.createElement('div'); - nameSpan.className = 'truncate font-medium text-gray-200 text-sm mb-1'; - nameSpan.textContent = file.name; + const nameSpan = document.createElement('div'); + nameSpan.className = + 'truncate font-medium text-gray-200 text-sm mb-1'; + nameSpan.textContent = file.name; - const metaSpan = document.createElement('div'); - metaSpan.className = 'text-xs text-gray-400'; - metaSpan.textContent = formatBytes(file.size); + const metaSpan = document.createElement('div'); + metaSpan.className = 'text-xs text-gray-400'; + metaSpan.textContent = formatBytes(file.size); - infoContainer.append(nameSpan, metaSpan); + infoContainer.append(nameSpan, metaSpan); - const removeBtn = document.createElement('button'); - removeBtn.className = 'ml-4 text-red-400 hover:text-red-300 flex-shrink-0'; - removeBtn.innerHTML = ''; - removeBtn.onclick = () => { - state.files = state.files.filter((_, i) => i !== index); - updateUI(); - }; - - fileDiv.append(infoContainer, removeBtn); - fileDisplayArea.appendChild(fileDiv); - } - - createIcons({ icons }); - } - if (fileControls) fileControls.classList.remove('hidden'); - convertOptions.classList.remove('hidden'); - } else { - if (fileDisplayArea) fileDisplayArea.innerHTML = ''; - if (fileControls) fileControls.classList.add('hidden'); - convertOptions.classList.add('hidden'); - } - }; - - const resetState = () => { - state.files = []; - state.pdfDoc = null; - updateUI(); - }; - - const convertToPdf = async () => { - try { - if (state.files.length === 0) { - showAlert('No Files', 'Please select at least one ODT file.'); - hideLoader(); - return; - } - - const converter = getLibreOfficeConverter(); - - // Initialize LibreOffice if not already done - await converter.initialize((progress: LoadProgress) => { - showLoader(progress.message, progress.percent); - }); - - if (state.files.length === 1) { - const originalFile = state.files[0]; - - showLoader('Processing...'); - - const pdfBlob = await converter.convertToPdf(originalFile); - - const fileName = originalFile.name.replace(/\.odt$/i, '') + '.pdf'; - - downloadFile(pdfBlob, fileName); - - hideLoader(); - - showAlert( - 'Conversion Complete', - `Successfully converted ${originalFile.name} to PDF.`, - 'success', - () => resetState() - ); - } else { - showLoader('Processing...'); - const JSZip = (await import('jszip')).default; - const zip = new JSZip(); - - for (let i = 0; i < state.files.length; i++) { - const file = state.files[i]; - showLoader(`Converting ${i + 1}/${state.files.length}: ${file.name}...`); - - const pdfBlob = await converter.convertToPdf(file); - - const baseName = file.name.replace(/\.odt$/i, ''); - const pdfBuffer = await pdfBlob.arrayBuffer(); - zip.file(`${baseName}.pdf`, pdfBuffer); - } - - const zipBlob = await zip.generateAsync({ type: 'blob' }); - - downloadFile(zipBlob, 'odt-converted.zip'); - - hideLoader(); - - showAlert( - 'Conversion Complete', - `Successfully converted ${state.files.length} ODT file(s) to PDF.`, - 'success', - () => resetState() - ); - } - } catch (e: any) { - hideLoader(); - showAlert( - 'Error', - `An error occurred during conversion. Error: ${e.message}` - ); - } - }; - - const handleFileSelect = (files: FileList | null) => { - if (files && files.length > 0) { - state.files = [...state.files, ...Array.from(files)]; + const removeBtn = document.createElement('button'); + removeBtn.className = + 'ml-4 text-red-400 hover:text-red-300 flex-shrink-0'; + removeBtn.innerHTML = ''; + removeBtn.onclick = () => { + state.files = state.files.filter((_, i) => i !== index); updateUI(); + }; + + fileDiv.append(infoContainer, removeBtn); + fileDisplayArea.appendChild(fileDiv); } - }; - if (fileInput && dropZone) { - fileInput.addEventListener('change', (e) => { - handleFileSelect((e.target as HTMLInputElement).files); - }); - - dropZone.addEventListener('dragover', (e) => { - e.preventDefault(); - dropZone.classList.add('bg-gray-700'); - }); - - dropZone.addEventListener('dragleave', (e) => { - e.preventDefault(); - dropZone.classList.remove('bg-gray-700'); - }); - - dropZone.addEventListener('drop', (e) => { - e.preventDefault(); - dropZone.classList.remove('bg-gray-700'); - const files = e.dataTransfer?.files; - if (files && files.length > 0) { - const odtFiles = Array.from(files).filter(f => f.name.toLowerCase().endsWith('.odt') || f.type === 'application/vnd.oasis.opendocument.text'); - if (odtFiles.length > 0) { - const dataTransfer = new DataTransfer(); - odtFiles.forEach(f => dataTransfer.items.add(f)); - handleFileSelect(dataTransfer.files); - } - } - }); - - // Clear value on click to allow re-selecting the same file - fileInput.addEventListener('click', () => { - fileInput.value = ''; - }); - } - - if (addMoreBtn) { - addMoreBtn.addEventListener('click', () => { - fileInput.click(); - }); - } - - if (clearFilesBtn) { - clearFilesBtn.addEventListener('click', () => { - resetState(); - }); - } - - if (processBtn) { - processBtn.addEventListener('click', convertToPdf); + createIcons({ icons }); + } + if (fileControls) fileControls.classList.remove('hidden'); + convertOptions.classList.remove('hidden'); + } else { + if (fileDisplayArea) fileDisplayArea.innerHTML = ''; + if (fileControls) fileControls.classList.add('hidden'); + convertOptions.classList.add('hidden'); } + }; + const resetState = () => { + state.files = []; + state.pdfDoc = null; updateUI(); + }; + + const convertToPdf = async () => { + try { + if (state.files.length === 0) { + showAlert('No Files', 'Please select at least one ODT file.'); + hideLoader(); + return; + } + + const converter = getLibreOfficeConverter(); + + // Initialize LibreOffice if not already done + await converter.initialize((progress: LoadProgress) => { + showLoader(progress.message, progress.percent); + }); + + if (state.files.length === 1) { + const originalFile = state.files[0]; + + showLoader('Processing...'); + + const pdfBlob = await converter.convertToPdf(originalFile); + + const fileName = originalFile.name.replace(/\.odt$/i, '') + '.pdf'; + + downloadFile(pdfBlob, fileName); + + hideLoader(); + + showAlert( + 'Conversion Complete', + `Successfully converted ${originalFile.name} to PDF.`, + 'success', + () => resetState() + ); + } else { + showLoader('Processing...'); + const JSZip = (await import('jszip')).default; + const zip = new JSZip(); + + for (let i = 0; i < state.files.length; i++) { + const file = state.files[i]; + showLoader( + `Converting ${i + 1}/${state.files.length}: ${file.name}...` + ); + + const pdfBlob = await converter.convertToPdf(file); + + const baseName = file.name.replace(/\.odt$/i, ''); + const pdfBuffer = await pdfBlob.arrayBuffer(); + zip.file(`${baseName}.pdf`, pdfBuffer); + } + + const zipBlob = await zip.generateAsync({ type: 'blob' }); + + downloadFile(zipBlob, 'odt-converted.zip'); + + hideLoader(); + + showAlert( + 'Conversion Complete', + `Successfully converted ${state.files.length} ODT file(s) to PDF.`, + 'success', + () => resetState() + ); + } + } catch (e: unknown) { + hideLoader(); + showAlert( + 'Error', + `An error occurred during conversion. Error: ${e instanceof Error ? e.message : String(e)}` + ); + } + }; + + const handleFileSelect = (files: FileList | null) => { + if (files && files.length > 0) { + state.files = [...state.files, ...Array.from(files)]; + updateUI(); + } + }; + + if (fileInput && dropZone) { + fileInput.addEventListener('change', (e) => { + handleFileSelect((e.target as HTMLInputElement).files); + }); + + dropZone.addEventListener('dragover', (e) => { + e.preventDefault(); + dropZone.classList.add('bg-gray-700'); + }); + + dropZone.addEventListener('dragleave', (e) => { + e.preventDefault(); + dropZone.classList.remove('bg-gray-700'); + }); + + dropZone.addEventListener('drop', (e) => { + e.preventDefault(); + dropZone.classList.remove('bg-gray-700'); + const files = e.dataTransfer?.files; + if (files && files.length > 0) { + const odtFiles = Array.from(files).filter( + (f) => + f.name.toLowerCase().endsWith('.odt') || + f.type === 'application/vnd.oasis.opendocument.text' + ); + if (odtFiles.length > 0) { + const dataTransfer = new DataTransfer(); + odtFiles.forEach((f) => dataTransfer.items.add(f)); + handleFileSelect(dataTransfer.files); + } + } + }); + + // Clear value on click to allow re-selecting the same file + fileInput.addEventListener('click', () => { + fileInput.value = ''; + }); + } + + if (addMoreBtn) { + addMoreBtn.addEventListener('click', () => { + fileInput.click(); + }); + } + + if (clearFilesBtn) { + clearFilesBtn.addEventListener('click', () => { + resetState(); + }); + } + + if (processBtn) { + processBtn.addEventListener('click', convertToPdf); + } + + updateUI(); }); diff --git a/src/js/logic/organize-pdf-page.ts b/src/js/logic/organize-pdf-page.ts index bb485dd..ce5bbfa 100644 --- a/src/js/logic/organize-pdf-page.ts +++ b/src/js/logic/organize-pdf-page.ts @@ -15,10 +15,10 @@ pdfjsLib.GlobalWorkerOptions.workerSrc = new URL( interface OrganizeState { file: File | null; - pdfDoc: any; - pdfJsDoc: any; + pdfDoc: PDFDocument | null; + pdfJsDoc: pdfjsLib.PDFDocumentProxy | null; totalPages: number; - sortableInstance: any; + sortableInstance: Sortable | null; } const organizeState: OrganizeState = { @@ -282,7 +282,7 @@ async function renderThumbnails() { canvas.width = viewport.width; canvas.height = viewport.height; const ctx = canvas.getContext('2d'); - await page.render({ canvasContext: ctx, viewport }).promise; + await page.render({ canvas: null, canvasContext: ctx, viewport }).promise; const wrapper = document.createElement('div'); wrapper.className = diff --git a/src/js/logic/pages-to-pdf-page.ts b/src/js/logic/pages-to-pdf-page.ts index b0d298b..9831c56 100644 --- a/src/js/logic/pages-to-pdf-page.ts +++ b/src/js/logic/pages-to-pdf-page.ts @@ -150,12 +150,12 @@ document.addEventListener('DOMContentLoaded', () => { () => resetState() ); } - } catch (e: any) { + } catch (e: unknown) { hideLoader(); console.error(`[${FILETYPE_NAME}ToPDF] Error:`, e); showAlert( 'Error', - `An error occurred during conversion. Error: ${e.message}` + `An error occurred during conversion. Error: ${e instanceof Error ? e.message : String(e)}` ); } }; diff --git a/src/js/logic/pdf-booklet-page.ts b/src/js/logic/pdf-booklet-page.ts index e43cd3a..ffadfe3 100644 --- a/src/js/logic/pdf-booklet-page.ts +++ b/src/js/logic/pdf-booklet-page.ts @@ -226,9 +226,9 @@ async function generatePreview() { const ctx = offscreen.getContext('2d')!; await page.render({ - canvasContext: ctx as any, + canvasContext: ctx as unknown as CanvasRenderingContext2D, viewport: viewport, - canvas: offscreen as any, + canvas: offscreen as unknown as HTMLCanvasElement, }).promise; const bitmap = await createImageBitmap(offscreen); diff --git a/src/js/logic/pdf-layers-page.ts b/src/js/logic/pdf-layers-page.ts index cd01cc2..9bd8335 100644 --- a/src/js/logic/pdf-layers-page.ts +++ b/src/js/logic/pdf-layers-page.ts @@ -7,9 +7,7 @@ import { getPDFDocument, } from '../utils/helpers.js'; import { createIcons, icons } from 'lucide'; -import { isWasmAvailable, getWasmBaseUrl } from '../config/wasm-cdn-config.js'; -import { showWasmRequiredDialog } from '../utils/wasm-provider.js'; -import { loadPyMuPDF, isPyMuPDFAvailable } from '../utils/pymupdf-loader.js'; +import { loadPyMuPDF } from '../utils/pymupdf-loader.js'; import { loadPdfWithPasswordPrompt } from '../utils/password-prompt.js'; interface LayerData { @@ -24,7 +22,21 @@ interface LayerData { } let currentFile: File | null = null; -let currentDoc: any = null; +interface PyMuPDFDocument { + getLayerConfig: () => LayerData[]; + addLayer: (name: string) => { number: number; xref: number }; + setLayerConfig: (layers: LayerData[]) => void; + setLayerVisibility: (xref: number, visible: boolean) => void; + deleteOCG: (xref: number) => void; + addOCGWithParent: ( + name: string, + parentXref: number + ) => { number: number; xref: number }; + addOCG: (name: string) => { number: number; xref: number }; + save: () => Uint8Array; +} + +let currentDoc: PyMuPDFDocument | null = null; const layersMap = new Map(); let nextDisplayOrder = 0; @@ -274,7 +286,7 @@ document.addEventListener('DOMContentLoaded', () => { if (!childName || !childName.trim()) return; try { - const childXref = currentDoc.addOCGWithParent( + const childResult = currentDoc.addOCGWithParent( childName.trim(), parentXref ); @@ -285,9 +297,9 @@ document.addEventListener('DOMContentLoaded', () => { } }); - layersMap.set(childXref, { - number: childXref, - xref: childXref, + layersMap.set(childResult.xref, { + number: childResult.number, + xref: childResult.xref, text: childName.trim(), on: true, locked: false, @@ -316,16 +328,17 @@ document.addEventListener('DOMContentLoaded', () => { const pymupdf = await loadPyMuPDF(); showLoader(`Loading layers from ${currentFile.name}...`); - currentDoc = await pymupdf.open(currentFile); + currentDoc = await ( + pymupdf as { open: (file: File) => Promise } + ).open(currentFile); showLoader('Reading layer configuration...'); const existingLayers = currentDoc.getLayerConfig(); - // Reset and populate layers map layersMap.clear(); nextDisplayOrder = 0; - existingLayers.forEach((layer: any) => { + existingLayers.forEach((layer: LayerData) => { layersMap.set(layer.number, { number: layer.number, xref: layer.xref ?? layer.number, @@ -350,9 +363,12 @@ document.addEventListener('DOMContentLoaded', () => { renderLayers(); setupLayerHandlers(); - } catch (error: any) { + } catch (error: unknown) { hideLoader(); - showAlert('Error', error.message || 'Failed to load PDF layers'); + showAlert( + 'Error', + error instanceof Error ? error.message : 'Failed to load PDF layers' + ); console.error('Layers error:', error); } }; @@ -373,13 +389,13 @@ document.addEventListener('DOMContentLoaded', () => { } try { - const xref = currentDoc.addOCG(name); + const layerResult = currentDoc.addOCG(name); newLayerInput.value = ''; const newDisplayOrder = nextDisplayOrder++; - layersMap.set(xref, { - number: xref, - xref: xref, + layersMap.set(layerResult.xref, { + number: layerResult.number, + xref: layerResult.xref, text: name, on: true, locked: false, @@ -389,8 +405,12 @@ document.addEventListener('DOMContentLoaded', () => { }); renderLayers(); - } catch (err: any) { - showAlert('Error', 'Failed to add layer: ' + err.message); + } catch (err: unknown) { + showAlert( + 'Error', + 'Failed to add layer: ' + + (err instanceof Error ? err.message : String(err)) + ); } }; } @@ -409,9 +429,13 @@ document.addEventListener('DOMContentLoaded', () => { hideLoader(); resetState(); showAlert('Success', 'PDF with layer changes saved!', 'success'); - } catch (err: any) { + } catch (err: unknown) { hideLoader(); - showAlert('Error', 'Failed to save PDF: ' + err.message); + showAlert( + 'Error', + 'Failed to save PDF: ' + + (err instanceof Error ? err.message : String(err)) + ); } }; } diff --git a/src/js/logic/pdf-multi-tool.ts b/src/js/logic/pdf-multi-tool.ts index 2ad5437..0e442da 100644 --- a/src/js/logic/pdf-multi-tool.ts +++ b/src/js/logic/pdf-multi-tool.ts @@ -9,7 +9,6 @@ import { renderPagesProgressively, cleanupLazyRendering, renderPageToCanvas, - createPlaceholder, } from '../utils/render-utils'; import { initializeGlobalShortcuts } from '../utils/shortcuts-init.js'; import { repairPdfFile } from './repair-pdf.js'; @@ -841,7 +840,7 @@ function deletePage(index: number) { async function insertPdfAfter(index: number) { document.getElementById('insert-pdf-input')?.click(); - (window as any).__insertAfterIndex = index; + (window as unknown as Record).__insertAfterIndex = index; } async function handleInsertPdf(e: Event) { @@ -849,7 +848,8 @@ async function handleInsertPdf(e: Event) { const file = input.files?.[0]; if (!file) return; - const insertAfterIndex = (window as any).__insertAfterIndex; + const insertAfterIndex = (window as unknown as Record) + .__insertAfterIndex as number | undefined; if (insertAfterIndex === undefined) return; try { @@ -974,7 +974,7 @@ function addBlankPage() { rotation: 0, visualRotation: 0, canvas, - pdfDoc: null as any, + pdfDoc: null!, originalPageIndex: -1, fileName: '', // Blank page has no file }; diff --git a/src/js/logic/pdf-to-csv-page.ts b/src/js/logic/pdf-to-csv-page.ts index 0c7450d..9704953 100644 --- a/src/js/logic/pdf-to-csv-page.ts +++ b/src/js/logic/pdf-to-csv-page.ts @@ -1,10 +1,7 @@ import { showLoader, hideLoader, showAlert } from '../ui.js'; import { downloadFile, formatBytes } from '../utils/helpers.js'; import { createIcons, icons } from 'lucide'; -import JSZip from 'jszip'; -import { isWasmAvailable, getWasmBaseUrl } from '../config/wasm-cdn-config.js'; -import { showWasmRequiredDialog } from '../utils/wasm-provider.js'; -import { loadPyMuPDF, isPyMuPDFAvailable } from '../utils/pymupdf-loader.js'; +import { loadPyMuPDF } from '../utils/pymupdf-loader.js'; import { loadPdfWithPasswordPrompt } from '../utils/password-prompt.js'; let file: File | null = null; diff --git a/src/js/logic/pdf-to-docx-page.ts b/src/js/logic/pdf-to-docx-page.ts index cba6a73..49b9a8f 100644 --- a/src/js/logic/pdf-to-docx-page.ts +++ b/src/js/logic/pdf-to-docx-page.ts @@ -8,9 +8,7 @@ import { } from '../utils/helpers.js'; import { state } from '../state.js'; import { createIcons, icons } from 'lucide'; -import { isWasmAvailable, getWasmBaseUrl } from '../config/wasm-cdn-config.js'; -import { showWasmRequiredDialog } from '../utils/wasm-provider.js'; -import { loadPyMuPDF, isPyMuPDFAvailable } from '../utils/pymupdf-loader.js'; +import { loadPyMuPDF } from '../utils/pymupdf-loader.js'; import { batchDecryptIfNeeded } from '../utils/password-prompt.js'; import { deduplicateFileName } from '../utils/deduplicate-filename.js'; @@ -73,7 +71,7 @@ document.addEventListener('DOMContentLoaded', () => { const arrayBuffer = await readFileAsArrayBuffer(file); const pdfDoc = await getPDFDocument({ data: arrayBuffer }).promise; metaSpan.textContent = `${formatBytes(file.size)} • ${pdfDoc.numPages} pages`; - } catch (error) { + } catch { metaSpan.textContent = `${formatBytes(file.size)} • Could not load page count`; } } @@ -160,11 +158,11 @@ document.addEventListener('DOMContentLoaded', () => { () => resetState() ); } - } catch (e: any) { + } catch (e: unknown) { hideLoader(); showAlert( 'Error', - `An error occurred during conversion. Error: ${e.message}` + `An error occurred during conversion. Error: ${e instanceof Error ? e.message : String(e)}` ); } }; diff --git a/src/js/logic/pdf-to-excel-page.ts b/src/js/logic/pdf-to-excel-page.ts index 75e261c..1fb7542 100644 --- a/src/js/logic/pdf-to-excel-page.ts +++ b/src/js/logic/pdf-to-excel-page.ts @@ -1,9 +1,7 @@ import { showLoader, hideLoader, showAlert } from '../ui.js'; import { downloadFile, formatBytes } from '../utils/helpers.js'; import { createIcons, icons } from 'lucide'; -import { isWasmAvailable, getWasmBaseUrl } from '../config/wasm-cdn-config.js'; -import { showWasmRequiredDialog } from '../utils/wasm-provider.js'; -import { loadPyMuPDF, isPyMuPDFAvailable } from '../utils/pymupdf-loader.js'; +import { loadPyMuPDF } from '../utils/pymupdf-loader.js'; import { loadPdfWithPasswordPrompt } from '../utils/password-prompt.js'; import * as XLSX from 'xlsx'; let file: File | null = null; diff --git a/src/js/logic/pdf-to-markdown-page.ts b/src/js/logic/pdf-to-markdown-page.ts index 7c7451d..8d166b5 100644 --- a/src/js/logic/pdf-to-markdown-page.ts +++ b/src/js/logic/pdf-to-markdown-page.ts @@ -8,9 +8,7 @@ import { } from '../utils/helpers.js'; import { state } from '../state.js'; import { createIcons, icons } from 'lucide'; -import { isWasmAvailable, getWasmBaseUrl } from '../config/wasm-cdn-config.js'; -import { showWasmRequiredDialog } from '../utils/wasm-provider.js'; -import { loadPyMuPDF, isPyMuPDFAvailable } from '../utils/pymupdf-loader.js'; +import { loadPyMuPDF } from '../utils/pymupdf-loader.js'; import { batchDecryptIfNeeded } from '../utils/password-prompt.js'; import { deduplicateFileName } from '../utils/deduplicate-filename.js'; @@ -76,7 +74,7 @@ document.addEventListener('DOMContentLoaded', () => { const arrayBuffer = await readFileAsArrayBuffer(file); const pdfDoc = await getPDFDocument({ data: arrayBuffer }).promise; metaSpan.textContent = `${formatBytes(file.size)} • ${pdfDoc.numPages} pages`; - } catch (error) { + } catch { metaSpan.textContent = `${formatBytes(file.size)} • Could not load page count`; } } @@ -162,11 +160,11 @@ document.addEventListener('DOMContentLoaded', () => { () => resetState() ); } - } catch (e: any) { + } catch (e: unknown) { hideLoader(); showAlert( 'Error', - `An error occurred during conversion. Error: ${e.message}` + `An error occurred during conversion. Error: ${e instanceof Error ? e.message : String(e)}` ); } }; diff --git a/src/js/logic/pdf-to-markdown.ts b/src/js/logic/pdf-to-markdown.ts index 4ea06dc..f6bab00 100644 --- a/src/js/logic/pdf-to-markdown.ts +++ b/src/js/logic/pdf-to-markdown.ts @@ -1,5 +1,9 @@ import { showLoader, hideLoader, showAlert } from '../ui.js'; -import { downloadFile, readFileAsArrayBuffer, getPDFDocument } from '../utils/helpers.js'; +import { + downloadFile, + readFileAsArrayBuffer, + getPDFDocument, +} from '../utils/helpers.js'; import { state } from '../state.js'; export async function pdfToMarkdown() { @@ -14,7 +18,9 @@ export async function pdfToMarkdown() { const page = await pdf.getPage(i); const content = await page.getTextContent(); // This is a simple text extraction. For more advanced formatting, more complex logic is needed. - const text = content.items.map((item: any) => item.str).join(' '); + const text = content.items + .map((item) => ('str' in item ? item.str : '')) + .join(' '); markdown += text + '\n\n'; // Add double newline for paragraph breaks between pages } diff --git a/src/js/logic/pdf-to-pdfa-page.ts b/src/js/logic/pdf-to-pdfa-page.ts index 53e4169..25a1762 100644 --- a/src/js/logic/pdf-to-pdfa-page.ts +++ b/src/js/logic/pdf-to-pdfa-page.ts @@ -10,6 +10,7 @@ import { state } from '../state.js'; import { createIcons, icons } from 'lucide'; import { convertFileToPdfA, type PdfALevel } from '../utils/ghostscript-loader'; import { loadPyMuPDF, isPyMuPDFAvailable } from '../utils/pymupdf-loader.js'; +import type { PyMuPDFInstance } from '@/types'; import { showWasmRequiredDialog } from '../utils/wasm-provider.js'; import { batchDecryptIfNeeded } from '../utils/password-prompt.js'; import { deduplicateFileName } from '../utils/deduplicate-filename.js'; @@ -134,7 +135,7 @@ document.addEventListener('DOMContentLoaded', () => { const pymupdf = await loadPyMuPDF(); // Rasterize PDF to images and back to PDF (300 DPI for quality) - const flattenedBlob = await (pymupdf as any).rasterizePdf( + const flattenedBlob = await (pymupdf as PyMuPDFInstance).rasterizePdf( originalFile, { dpi: 300, @@ -205,11 +206,11 @@ document.addEventListener('DOMContentLoaded', () => { () => resetState() ); } - } catch (e: any) { + } catch (e: unknown) { hideLoader(); showAlert( 'Error', - `An error occurred during conversion. Error: ${e.message}` + `An error occurred during conversion. Error: ${e instanceof Error ? e.message : String(e)}` ); } }; diff --git a/src/js/logic/pdf-to-svg-page.ts b/src/js/logic/pdf-to-svg-page.ts index e802029..6132a1b 100644 --- a/src/js/logic/pdf-to-svg-page.ts +++ b/src/js/logic/pdf-to-svg-page.ts @@ -2,12 +2,12 @@ import { showLoader, hideLoader, showAlert } from '../ui.js'; import { downloadFile, formatBytes } from '../utils/helpers.js'; import { createIcons, icons } from 'lucide'; import JSZip from 'jszip'; -import { isWasmAvailable, getWasmBaseUrl } from '../config/wasm-cdn-config.js'; -import { showWasmRequiredDialog } from '../utils/wasm-provider.js'; import { loadPyMuPDF, isPyMuPDFAvailable } from '../utils/pymupdf-loader.js'; +import type { PyMuPDFInstance } from '@/types'; import { batchDecryptIfNeeded } from '../utils/password-prompt.js'; +import { showWasmRequiredDialog } from '../utils/wasm-provider.js'; -let pymupdf: any = null; +let pymupdf: PyMuPDFInstance | null = null; let files: File[] = []; const updateUI = () => { diff --git a/src/js/logic/pdf-to-text-page.ts b/src/js/logic/pdf-to-text-page.ts index 7f41d8d..b796e2b 100644 --- a/src/js/logic/pdf-to-text-page.ts +++ b/src/js/logic/pdf-to-text-page.ts @@ -1,14 +1,13 @@ import { createIcons, icons } from 'lucide'; import { showAlert, showLoader, hideLoader } from '../ui.js'; import { downloadFile, formatBytes } from '../utils/helpers.js'; -import { isWasmAvailable, getWasmBaseUrl } from '../config/wasm-cdn-config.js'; -import { showWasmRequiredDialog } from '../utils/wasm-provider.js'; -import { loadPyMuPDF, isPyMuPDFAvailable } from '../utils/pymupdf-loader.js'; +import { loadPyMuPDF } from '../utils/pymupdf-loader.js'; +import type { PyMuPDFInstance } from '@/types'; import { batchDecryptIfNeeded } from '../utils/password-prompt.js'; import { deduplicateFileName } from '../utils/deduplicate-filename.js'; let files: File[] = []; -let pymupdf: any = null; +let pymupdf: PyMuPDFInstance | null = null; if (document.readyState === 'loading') { document.addEventListener('DOMContentLoaded', initializePage); @@ -159,9 +158,9 @@ function updateUI() { } } -async function ensurePyMuPDF(): Promise { +async function ensurePyMuPDF(): Promise { if (!pymupdf) { - pymupdf = await loadPyMuPDF(); + pymupdf = (await loadPyMuPDF()) as PyMuPDFInstance; } return pymupdf; } @@ -230,12 +229,12 @@ async function extractText() { } ); } - } catch (e: any) { + } catch (e: unknown) { console.error('[PDFToText]', e); hideLoader(); showAlert( 'Extraction Error', - e.message || 'Failed to extract text from PDF.' + e instanceof Error ? e.message : 'Failed to extract text from PDF.' ); } } diff --git a/src/js/logic/pdf-workflow-page.ts b/src/js/logic/pdf-workflow-page.ts index 1a6e59c..9de0984 100644 --- a/src/js/logic/pdf-workflow-page.ts +++ b/src/js/logic/pdf-workflow-page.ts @@ -913,8 +913,15 @@ function showNodeSettings(node: BaseWorkflowNode) { content.appendChild(divider); } + interface FileInputNode extends BaseWorkflowNode { + hasFile(): boolean; + getFilenames(): string[]; + removeFile(index: number): void; + addFiles(files: File[]): Promise; + } + const fileInputConfigs: { - cls: any; + cls: new (...args: unknown[]) => FileInputNode; label: string; accept: string; btnLabel: string; @@ -1534,7 +1541,7 @@ function showNodeSettings(node: BaseWorkflowNode) { } } - for (const [dropdownKey, mapping] of Object.entries(conditionalVisibility)) { + for (const [dropdownKey] of Object.entries(conditionalVisibility)) { const ctrl = controlEntries.find(([k]) => k === dropdownKey)?.[1] as | { value?: unknown } | undefined; diff --git a/src/js/logic/png-to-pdf-page.ts b/src/js/logic/png-to-pdf-page.ts index a286e67..134245e 100644 --- a/src/js/logic/png-to-pdf-page.ts +++ b/src/js/logic/png-to-pdf-page.ts @@ -160,9 +160,13 @@ function updateUI() { } } -function sanitizeImageAsJpeg(imageBytes: any) { +function sanitizeImageAsJpeg(imageBytes: Uint8Array | ArrayBuffer) { return new Promise((resolve, reject) => { - const blob = new Blob([imageBytes]); + const blob = new Blob([ + imageBytes instanceof Uint8Array + ? new Uint8Array(imageBytes) + : imageBytes, + ]); const imageUrl = URL.createObjectURL(blob); const img = new Image(); @@ -248,9 +252,9 @@ async function convertToPdf() { showAlert('Success', 'PDF created successfully!', 'success', () => { resetState(); }); - } catch (e: any) { + } catch (e: unknown) { console.error(e); - showAlert('Conversion Error', e.message); + showAlert('Conversion Error', e instanceof Error ? e.message : String(e)); } finally { hideLoader(); } diff --git a/src/js/logic/powerpoint-to-pdf-page.ts b/src/js/logic/powerpoint-to-pdf-page.ts index e45baea..49be923 100644 --- a/src/js/logic/powerpoint-to-pdf-page.ts +++ b/src/js/logic/powerpoint-to-pdf-page.ts @@ -1,9 +1,5 @@ import { showLoader, hideLoader, showAlert } from '../ui.js'; -import { - downloadFile, - readFileAsArrayBuffer, - formatBytes, -} from '../utils/helpers.js'; +import { downloadFile, formatBytes } from '../utils/helpers.js'; import { state } from '../state.js'; import { createIcons, icons } from 'lucide'; import { @@ -159,11 +155,11 @@ document.addEventListener('DOMContentLoaded', () => { () => resetState() ); } - } catch (e: any) { + } catch (e: unknown) { hideLoader(); showAlert( 'Error', - `An error occurred during conversion. Error: ${e.message}` + `An error occurred during conversion. Error: ${e instanceof Error ? e.message : String(e)}` ); } }; diff --git a/src/js/logic/prepare-pdf-for-ai-page.ts b/src/js/logic/prepare-pdf-for-ai-page.ts index b4ff23b..07d28bd 100644 --- a/src/js/logic/prepare-pdf-for-ai-page.ts +++ b/src/js/logic/prepare-pdf-for-ai-page.ts @@ -9,6 +9,7 @@ import { import { state } from '../state.js'; import { createIcons, icons } from 'lucide'; import { loadPyMuPDF } from '../utils/pymupdf-loader.js'; +import type { PyMuPDFInstance } from '@/types'; import { batchDecryptIfNeeded } from '../utils/password-prompt.js'; import { deduplicateFileName } from '../utils/deduplicate-filename.js'; @@ -117,7 +118,9 @@ document.addEventListener('DOMContentLoaded', () => { const file = state.files[0]; showLoader(`Extracting ${file.name} for AI...`); - const llamaDocs = await (pymupdf as any).pdfToLlamaIndex(file); + const llamaDocs = await (pymupdf as PyMuPDFInstance).pdfToLlamaIndex( + file + ); const outName = file.name.replace(/\.pdf$/i, '') + '_llm.json'; const jsonContent = JSON.stringify(llamaDocs, null, 2); downloadFile( @@ -144,7 +147,9 @@ document.addEventListener('DOMContentLoaded', () => { `Extracting ${file.name} for AI (${completed + 1}/${total})...` ); - const llamaDocs = await (pymupdf as any).pdfToLlamaIndex(file); + const llamaDocs = await ( + pymupdf as PyMuPDFInstance + ).pdfToLlamaIndex(file); const outName = file.name.replace(/\.pdf$/i, '') + '_llm.json'; const jsonContent = JSON.stringify(llamaDocs, null, 2); const zipEntryName = deduplicateFileName(outName, usedNames); @@ -180,11 +185,11 @@ document.addEventListener('DOMContentLoaded', () => { ); } } - } catch (e: any) { + } catch (e: unknown) { hideLoader(); showAlert( 'Error', - `An error occurred during extraction. Error: ${e.message}` + `An error occurred during extraction. Error: ${e instanceof Error ? e.message : String(e)}` ); } }; diff --git a/src/js/logic/psd-to-pdf-page.ts b/src/js/logic/psd-to-pdf-page.ts index 6a17d23..5227883 100644 --- a/src/js/logic/psd-to-pdf-page.ts +++ b/src/js/logic/psd-to-pdf-page.ts @@ -2,18 +2,17 @@ import { showLoader, hideLoader, showAlert } from '../ui.js'; import { downloadFile, formatBytes } from '../utils/helpers.js'; import { state } from '../state.js'; import { createIcons, icons } from 'lucide'; -import { isWasmAvailable, getWasmBaseUrl } from '../config/wasm-cdn-config.js'; -import { showWasmRequiredDialog } from '../utils/wasm-provider.js'; -import { loadPyMuPDF, isPyMuPDFAvailable } from '../utils/pymupdf-loader.js'; +import { loadPyMuPDF } from '../utils/pymupdf-loader.js'; +import type { PyMuPDFInstance } from '@/types'; const ACCEPTED_EXTENSIONS = ['.psd']; const FILETYPE_NAME = 'PSD'; -let pymupdf: any = null; +let pymupdf: PyMuPDFInstance | null = null; -async function ensurePyMuPDF(): Promise { +async function ensurePyMuPDF(): Promise { if (!pymupdf) { - pymupdf = await loadPyMuPDF(); + pymupdf = (await loadPyMuPDF()) as PyMuPDFInstance; } return pymupdf; } diff --git a/src/js/logic/rasterize-pdf-page.ts b/src/js/logic/rasterize-pdf-page.ts index ea4dd6a..d9efdd9 100644 --- a/src/js/logic/rasterize-pdf-page.ts +++ b/src/js/logic/rasterize-pdf-page.ts @@ -8,11 +8,11 @@ import { } from '../utils/helpers.js'; import { state } from '../state.js'; import { createIcons, icons } from 'lucide'; -import { isWasmAvailable, getWasmBaseUrl } from '../config/wasm-cdn-config.js'; -import { showWasmRequiredDialog } from '../utils/wasm-provider.js'; import { loadPyMuPDF, isPyMuPDFAvailable } from '../utils/pymupdf-loader.js'; +import type { PyMuPDFInstance } from '@/types'; import { batchDecryptIfNeeded } from '../utils/password-prompt.js'; import { deduplicateFileName } from '../utils/deduplicate-filename.js'; +import { showWasmRequiredDialog } from '../utils/wasm-provider.js'; document.addEventListener('DOMContentLoaded', () => { const fileInput = document.getElementById('file-input') as HTMLInputElement; @@ -136,12 +136,15 @@ document.addEventListener('DOMContentLoaded', () => { const file = state.files[0]; showLoader(`Rasterizing ${file.name}...`); - const rasterizedBlob = await (pymupdf as any).rasterizePdf(file, { - dpi, - format, - grayscale, - quality: 95, - }); + const rasterizedBlob = await (pymupdf as PyMuPDFInstance).rasterizePdf( + file, + { + dpi, + format, + grayscale, + quality: 95, + } + ); const outName = file.name.replace(/\.pdf$/i, '') + '_rasterized.pdf'; downloadFile(rasterizedBlob, outName); @@ -165,7 +168,9 @@ document.addEventListener('DOMContentLoaded', () => { `Rasterizing ${file.name} (${completed + 1}/${total})...` ); - const rasterizedBlob = await (pymupdf as any).rasterizePdf(file, { + const rasterizedBlob = await ( + pymupdf as PyMuPDFInstance + ).rasterizePdf(file, { dpi, format, grayscale, @@ -210,11 +215,11 @@ document.addEventListener('DOMContentLoaded', () => { ); } } - } catch (e: any) { + } catch (e: unknown) { hideLoader(); showAlert( 'Error', - `An error occurred during rasterization. Error: ${e.message}` + `An error occurred during rasterization. Error: ${e instanceof Error ? e.message : String(e)}` ); } }; diff --git a/src/js/logic/redact.ts b/src/js/logic/redact.ts index 6f52891..7446713 100644 --- a/src/js/logic/redact.ts +++ b/src/js/logic/redact.ts @@ -2,16 +2,18 @@ import { showLoader, hideLoader, showAlert } from '../ui.js'; import { downloadFile } from '../utils/helpers.js'; import { state } from '../state.js'; +import type { RedactionRect } from '@/types'; + // @ts-expect-error TS(2339) FIXME: Property 'PDFLib' does not exist on type 'Window &... Remove this comment to see the full error message const { rgb } = window.PDFLib; -export async function redact(redactions: any, canvasScale: any) { +export async function redact(redactions: RedactionRect[], canvasScale: number) { showLoader('Applying redactions...'); try { const pdfPages = state.pdfDoc.getPages(); const conversionScale = 1 / canvasScale; - redactions.forEach((r: any) => { + redactions.forEach((r: RedactionRect) => { const page = pdfPages[r.pageIndex]; const { height: pageHeight } = page.getSize(); @@ -32,7 +34,7 @@ export async function redact(redactions: any, canvasScale: any) { const redactedBytes = await state.pdfDoc.save(); downloadFile( - new Blob([redactedBytes], { type: 'application/pdf' }), + new Blob([new Uint8Array(redactedBytes)], { type: 'application/pdf' }), 'redacted.pdf' ); } catch (e) { diff --git a/src/js/logic/remove-blank-pages-page.ts b/src/js/logic/remove-blank-pages-page.ts index 5ece8d9..5af9fc6 100644 --- a/src/js/logic/remove-blank-pages-page.ts +++ b/src/js/logic/remove-blank-pages-page.ts @@ -36,7 +36,7 @@ function hideLoader() { function showAlert( title: string, msg: string, - type = 'error', + _type = 'error', cb?: () => void ) { const modal = document.getElementById('alert-modal'); @@ -138,7 +138,7 @@ async function handleFileUpload(file: File) { } async function isPageBlank( - page: any, + page: pdfjsLib.PDFPageProxy, maxNonWhitePercent = 0.5 ): Promise { const viewport = page.getViewport({ scale: 0.5 }); @@ -149,7 +149,7 @@ async function isPageBlank( canvas.width = viewport.width; canvas.height = viewport.height; - await page.render({ canvasContext: ctx, viewport }).promise; + await page.render({ canvas: null, canvasContext: ctx, viewport }).promise; const imageData = ctx.getImageData(0, 0, canvas.width, canvas.height); const data = imageData.data; @@ -165,7 +165,7 @@ async function isPageBlank( return nonWhitePercent <= maxNonWhitePercent; } -async function generateThumbnail(page: any): Promise { +async function generateThumbnail(page: pdfjsLib.PDFPageProxy): Promise { const viewport = page.getViewport({ scale: 1 }); const canvas = document.createElement('canvas'); const ctx = canvas.getContext('2d'); @@ -174,7 +174,7 @@ async function generateThumbnail(page: any): Promise { canvas.width = viewport.width; canvas.height = viewport.height; - await page.render({ canvasContext: ctx, viewport }).promise; + await page.render({ canvas: null, canvasContext: ctx, viewport }).promise; return canvas.toDataURL('image/jpeg', 0.7); } diff --git a/src/js/logic/remove-restrictions-page.ts b/src/js/logic/remove-restrictions-page.ts index b8c97ff..1f10924 100644 --- a/src/js/logic/remove-restrictions-page.ts +++ b/src/js/logic/remove-restrictions-page.ts @@ -6,7 +6,7 @@ import { readFileAsArrayBuffer, } from '../utils/helpers.js'; import { icons, createIcons } from 'lucide'; -import { RemoveRestrictionsState } from '@/types'; +import { RemoveRestrictionsState, QpdfInstanceExtended } from '@/types'; const pageState: RemoveRestrictionsState = { file: null, @@ -98,7 +98,7 @@ async function removeRestrictions() { const inputPath = '/input.pdf'; const outputPath = '/output.pdf'; - let qpdf: any; + let qpdf: QpdfInstanceExtended; const loaderModal = document.getElementById('loader-modal'); const loaderText = document.getElementById('loader-text'); @@ -127,12 +127,10 @@ async function removeRestrictions() { try { qpdf.callMain(args); - } catch (qpdfError: any) { + } catch (qpdfError: unknown) { console.error('qpdf execution error:', qpdfError); - if ( - qpdfError.message?.includes('password') || - qpdfError.message?.includes('encrypt') - ) { + const qpdfMsg = qpdfError instanceof Error ? qpdfError.message : ''; + if (qpdfMsg.includes('password') || qpdfMsg.includes('encrypt')) { throw new Error( 'Failed to remove restrictions. The PDF may require the correct owner password.', { cause: qpdfError } @@ -140,8 +138,7 @@ async function removeRestrictions() { } throw new Error( - 'Failed to remove restrictions: ' + - (qpdfError.message || 'Unknown error'), + 'Failed to remove restrictions: ' + (qpdfMsg || 'Unknown error'), { cause: qpdfError } ); } @@ -153,7 +150,9 @@ async function removeRestrictions() { throw new Error('Operation resulted in an empty file.'); } - const blob = new Blob([outputFile], { type: 'application/pdf' }); + const blob = new Blob([new Uint8Array(outputFile)], { + type: 'application/pdf', + }); downloadFile(blob, `unrestricted-${pageState.file.name}`); if (loaderModal) loaderModal.classList.add('hidden'); @@ -166,12 +165,12 @@ async function removeRestrictions() { resetState(); } ); - } catch (error: any) { + } catch (error: unknown) { console.error('Error during restriction removal:', error); if (loaderModal) loaderModal.classList.add('hidden'); showAlert( 'Operation Failed', - `An error occurred: ${error.message || 'The PDF might be corrupted or password-protected.'}` + `An error occurred: ${error instanceof Error ? error.message : 'The PDF might be corrupted or password-protected.'}` ); } finally { try { diff --git a/src/js/logic/repair-pdf.ts b/src/js/logic/repair-pdf.ts index e323af1..547f96e 100644 --- a/src/js/logic/repair-pdf.ts +++ b/src/js/logic/repair-pdf.ts @@ -8,11 +8,12 @@ import { state } from '../state.js'; import JSZip from 'jszip'; import { deduplicateFileName } from '../utils/deduplicate-filename.js'; import { batchDecryptIfNeeded } from '../utils/password-prompt.js'; +import type { QpdfInstanceExtended } from '@/types'; export async function repairPdfFile(file: File): Promise { const inputPath = '/input.pdf'; const outputPath = '/repaired_form.pdf'; - let qpdf: any; + let qpdf: QpdfInstanceExtended; try { qpdf = await initializeQpdf(); diff --git a/src/js/logic/rtf-to-pdf-page.ts b/src/js/logic/rtf-to-pdf-page.ts index 213b5c9..5bffe79 100644 --- a/src/js/logic/rtf-to-pdf-page.ts +++ b/src/js/logic/rtf-to-pdf-page.ts @@ -1,9 +1,5 @@ import { showLoader, hideLoader, showAlert } from '../ui.js'; -import { - downloadFile, - readFileAsArrayBuffer, - formatBytes, -} from '../utils/helpers.js'; +import { downloadFile, formatBytes } from '../utils/helpers.js'; import { state } from '../state.js'; import { createIcons, icons } from 'lucide'; import { @@ -158,11 +154,11 @@ document.addEventListener('DOMContentLoaded', () => { () => resetState() ); } - } catch (e: any) { + } catch (e: unknown) { hideLoader(); showAlert( 'Error', - `An error occurred during conversion. Error: ${e.message}` + `An error occurred during conversion. Error: ${e instanceof Error ? e.message : String(e)}` ); } }; diff --git a/src/js/logic/shortcuts.ts b/src/js/logic/shortcuts.ts index 1161c4a..49ff543 100644 --- a/src/js/logic/shortcuts.ts +++ b/src/js/logic/shortcuts.ts @@ -1,192 +1,202 @@ import { categories } from '../config/tools.js'; +import type { ToolEntry } from '@/types'; export class ShortcutsManager { - private static STORAGE_KEY = 'bentopdf_shortcuts'; - private static shortcuts: Map = new Map(); - private static defaultShortcuts: Map = new Map(); + private static STORAGE_KEY = 'bentopdf_shortcuts'; + private static shortcuts: Map = new Map(); + private static defaultShortcuts: Map = new Map(); - private static getToolId(tool: any): string { - if (tool.id) return tool.id; - if (tool.href) { - // Extract filename without extension from href - const match = tool.href.match(/\/([^/]+)\.html$/); - return match ? match[1] : tool.href; - } - return 'unknown'; + private static getToolId(tool: ToolEntry): string { + if (tool.id) return tool.id; + if (tool.href) { + // Extract filename without extension from href + const match = tool.href.match(/\/([^/]+)\.html$/); + return match ? match[1] : tool.href; } + return 'unknown'; + } - static init() { - this.loadDefaults(); - this.loadFromStorage(); - this.setupGlobalListener(); - } + static init() { + this.loadDefaults(); + this.loadFromStorage(); + this.setupGlobalListener(); + } - private static loadDefaults() { - this.defaultShortcuts.set('merge', 'mod+shift+m'); - this.defaultShortcuts.set('split', 'mod+shift+s'); - this.defaultShortcuts.set('compress', 'mod+shift+c'); - } + private static loadDefaults() { + this.defaultShortcuts.set('merge', 'mod+shift+m'); + this.defaultShortcuts.set('split', 'mod+shift+s'); + this.defaultShortcuts.set('compress', 'mod+shift+c'); + } - private static loadFromStorage() { - const stored = localStorage.getItem(this.STORAGE_KEY); - if (stored) { - try { - const parsed = JSON.parse(stored); - this.shortcuts = new Map(Object.entries(parsed)); + private static loadFromStorage() { + const stored = localStorage.getItem(this.STORAGE_KEY); + if (stored) { + try { + const parsed = JSON.parse(stored); + this.shortcuts = new Map(Object.entries(parsed)); - const allTools = categories.flatMap(c => c.tools); - const validToolIds = allTools.map(t => this.getToolId(t)); - for (const [toolId, _] of this.shortcuts.entries()) { - if (!validToolIds.includes(toolId)) { - this.shortcuts.delete(toolId); - } - } - - if (this.shortcuts.size !== Object.keys(parsed).length) { - this.save(); - } - } catch (e) { - console.error('Failed to parse shortcuts from local storage', e); - this.shortcuts = new Map(this.defaultShortcuts); - } - } else { - this.shortcuts = new Map(this.defaultShortcuts); - } - } - - static save() { - const obj = Object.fromEntries(this.shortcuts); - localStorage.setItem(this.STORAGE_KEY, JSON.stringify(obj)); - } - - static reset() { - this.shortcuts = new Map(this.defaultShortcuts); - this.save(); - // Dispatch event to update UI if needed - window.dispatchEvent(new CustomEvent('shortcuts-updated')); - } - - static getShortcut(toolId: string): string | undefined { - return this.shortcuts.get(toolId); - } - - static findToolByShortcut(key: string): string | undefined { - for (const [id, k] of this.shortcuts.entries()) { - if (k === key) { - return id; - } - } - return undefined; - } - - static setShortcut(toolId: string, key: string) { - if (key) { - this.shortcuts.set(toolId, key); - } else { + const allTools = categories.flatMap((c) => c.tools); + const validToolIds = allTools.map((t) => this.getToolId(t)); + for (const [toolId] of this.shortcuts.entries()) { + if (!validToolIds.includes(toolId)) { this.shortcuts.delete(toolId); + } } - this.save(); - window.dispatchEvent(new CustomEvent('shortcuts-updated')); - } - static getAllShortcuts(): Map { - return this.shortcuts; - } - - static exportSettings() { - // Create a map with all tools, defaulting to empty string if not set - const exportObj: Record = {}; - - const allTools = categories.flatMap(c => c.tools); - - allTools.forEach(tool => { - const toolId = this.getToolId(tool); - exportObj[toolId] = this.shortcuts.get(toolId) || ''; - }); - - const dataStr = "data:text/json;charset=utf-8," + encodeURIComponent(JSON.stringify(exportObj, null, 2)); - const downloadAnchorNode = document.createElement('a'); - downloadAnchorNode.setAttribute("href", dataStr); - downloadAnchorNode.setAttribute("download", "bentopdf_shortcuts.json"); - document.body.appendChild(downloadAnchorNode); - downloadAnchorNode.click(); - downloadAnchorNode.remove(); - } - - static importSettings(jsonString: string): boolean { - try { - const parsed = JSON.parse(jsonString); - for (const key in parsed) { - if (typeof parsed[key] !== 'string') { - throw new Error('Invalid shortcut format'); - } - } - this.shortcuts = new Map(Object.entries(parsed)); - this.save(); - window.dispatchEvent(new CustomEvent('shortcuts-updated')); - return true; - } catch (e) { - console.error('Import failed', e); - return false; + if (this.shortcuts.size !== Object.keys(parsed).length) { + this.save(); } + } catch (e) { + console.error('Failed to parse shortcuts from local storage', e); + this.shortcuts = new Map(this.defaultShortcuts); + } + } else { + this.shortcuts = new Map(this.defaultShortcuts); } + } - private static setupGlobalListener() { - window.addEventListener('keydown', (e) => { - // Ignore if typing in an input or textarea - const target = e.target as HTMLElement; - if (target.tagName === 'INPUT' || target.tagName === 'TEXTAREA' || target.isContentEditable) { - return; - } + static save() { + const obj = Object.fromEntries(this.shortcuts); + localStorage.setItem(this.STORAGE_KEY, JSON.stringify(obj)); + } - // Build key string - const keys: string[] = []; - const isMac = navigator.userAgent.toUpperCase().includes('MAC'); + static reset() { + this.shortcuts = new Map(this.defaultShortcuts); + this.save(); + // Dispatch event to update UI if needed + window.dispatchEvent(new CustomEvent('shortcuts-updated')); + } - // On Mac: metaKey = Command, ctrlKey = Control - // On Windows/Linux: metaKey is rare, ctrlKey = Ctrl - if (isMac) { - if (e.metaKey) keys.push('mod'); // Command on Mac - if (e.ctrlKey) keys.push('ctrl'); // Control on Mac (separate from Command) - } else { - if (e.ctrlKey || e.metaKey) keys.push('mod'); // Ctrl on Windows/Linux - } - if (e.altKey) keys.push('alt'); - if (e.shiftKey) keys.push('shift'); + static getShortcut(toolId: string): string | undefined { + return this.shortcuts.get(toolId); + } - let key = e.key.toLowerCase(); - - if (e.altKey && e.code) { - if (e.code.startsWith('Key')) { - key = e.code.slice(3).toLowerCase(); - } else if (e.code.startsWith('Digit')) { - key = e.code.slice(5); - } - } - - if (!['control', 'shift', 'alt', 'meta'].includes(key)) { - keys.push(key); - } - - const combo = keys.join('+'); - - for (const [toolId, shortcut] of this.shortcuts.entries()) { - if (shortcut === combo) { - e.preventDefault(); - e.stopPropagation(); - - // Find the tool in categories - const allTools = categories.flatMap(c => c.tools); - const tool = allTools.find(t => this.getToolId(t) === toolId); - - if (tool && (tool as any).href) { - // All tools now use href - navigate to the page - const href = (tool as any).href; - window.location.href = href.startsWith('/') ? href : `/${href}`; - } - return; - } - } - }, { capture: true }); + static findToolByShortcut(key: string): string | undefined { + for (const [id, k] of this.shortcuts.entries()) { + if (k === key) { + return id; + } } + return undefined; + } + + static setShortcut(toolId: string, key: string) { + if (key) { + this.shortcuts.set(toolId, key); + } else { + this.shortcuts.delete(toolId); + } + this.save(); + window.dispatchEvent(new CustomEvent('shortcuts-updated')); + } + + static getAllShortcuts(): Map { + return this.shortcuts; + } + + static exportSettings() { + // Create a map with all tools, defaulting to empty string if not set + const exportObj: Record = {}; + + const allTools = categories.flatMap((c) => c.tools); + + allTools.forEach((tool) => { + const toolId = this.getToolId(tool); + exportObj[toolId] = this.shortcuts.get(toolId) || ''; + }); + + const dataStr = + 'data:text/json;charset=utf-8,' + + encodeURIComponent(JSON.stringify(exportObj, null, 2)); + const downloadAnchorNode = document.createElement('a'); + downloadAnchorNode.setAttribute('href', dataStr); + downloadAnchorNode.setAttribute('download', 'bentopdf_shortcuts.json'); + document.body.appendChild(downloadAnchorNode); + downloadAnchorNode.click(); + downloadAnchorNode.remove(); + } + + static importSettings(jsonString: string): boolean { + try { + const parsed = JSON.parse(jsonString); + for (const key in parsed) { + if (typeof parsed[key] !== 'string') { + throw new Error('Invalid shortcut format'); + } + } + this.shortcuts = new Map(Object.entries(parsed)); + this.save(); + window.dispatchEvent(new CustomEvent('shortcuts-updated')); + return true; + } catch (e) { + console.error('Import failed', e); + return false; + } + } + + private static setupGlobalListener() { + window.addEventListener( + 'keydown', + (e) => { + // Ignore if typing in an input or textarea + const target = e.target as HTMLElement; + if ( + target.tagName === 'INPUT' || + target.tagName === 'TEXTAREA' || + target.isContentEditable + ) { + return; + } + + // Build key string + const keys: string[] = []; + const isMac = navigator.userAgent.toUpperCase().includes('MAC'); + + // On Mac: metaKey = Command, ctrlKey = Control + // On Windows/Linux: metaKey is rare, ctrlKey = Ctrl + if (isMac) { + if (e.metaKey) keys.push('mod'); // Command on Mac + if (e.ctrlKey) keys.push('ctrl'); // Control on Mac (separate from Command) + } else { + if (e.ctrlKey || e.metaKey) keys.push('mod'); // Ctrl on Windows/Linux + } + if (e.altKey) keys.push('alt'); + if (e.shiftKey) keys.push('shift'); + + let key = e.key.toLowerCase(); + + if (e.altKey && e.code) { + if (e.code.startsWith('Key')) { + key = e.code.slice(3).toLowerCase(); + } else if (e.code.startsWith('Digit')) { + key = e.code.slice(5); + } + } + + if (!['control', 'shift', 'alt', 'meta'].includes(key)) { + keys.push(key); + } + + const combo = keys.join('+'); + + for (const [toolId, shortcut] of this.shortcuts.entries()) { + if (shortcut === combo) { + e.preventDefault(); + e.stopPropagation(); + + // Find the tool in categories + const allTools = categories.flatMap((c) => c.tools); + const tool = allTools.find((t) => this.getToolId(t) === toolId); + + if (tool && tool.href) { + const href = tool.href; + window.location.href = href.startsWith('/') ? href : `/${href}`; + } + return; + } + } + }, + { capture: true } + ); + } } diff --git a/src/js/logic/split-pdf-page.ts b/src/js/logic/split-pdf-page.ts index 045b24a..e083ce5 100644 --- a/src/js/logic/split-pdf-page.ts +++ b/src/js/logic/split-pdf-page.ts @@ -16,7 +16,6 @@ import JSZip from 'jszip'; import { PDFDocument as PDFLibDocument } from 'pdf-lib'; import { loadPdfDocument } from '../utils/load-pdf-document.js'; -// @ts-ignore pdfjsLib.GlobalWorkerOptions.workerSrc = new URL( 'pdfjs-dist/build/pdf.worker.min.mjs', import.meta.url @@ -178,7 +177,7 @@ document.addEventListener('DOMContentLoaded', () => { imgContainer.append(img, pageNumDiv); wrapper.appendChild(imgContainer); - const handleSelection = (e: any) => { + const handleSelection = (e: Event) => { e.preventDefault(); e.stopPropagation(); @@ -286,7 +285,7 @@ document.addEventListener('DOMContentLoaded', () => { let indicesToExtract: number[] = []; switch (splitMode) { - case 'range': + case 'range': { const pageRangeInput = ( document.getElementById('page-range') as HTMLInputElement ).value; @@ -331,7 +330,7 @@ document.addEventListener('DOMContentLoaded', () => { const group = rangeGroups[i]; const newPdf = await PDFLibDocument.create(); const copiedPages = await newPdf.copyPages(state.pdfDoc, group); - copiedPages.forEach((page: any) => newPdf.addPage(page)); + copiedPages.forEach((page) => newPdf.addPage(page)); const pdfBytes = await newPdf.save(); const minPage = Math.min(...group) + 1; @@ -357,8 +356,9 @@ document.addEventListener('DOMContentLoaded', () => { return; } break; + } - case 'even-odd': + case 'even-odd': { const choiceElement = document.querySelector( 'input[name="even-odd-choice"]:checked' ) as HTMLInputElement; @@ -371,6 +371,7 @@ document.addEventListener('DOMContentLoaded', () => { if (choice === 'odd' && (i + 1) % 2 !== 0) indicesToExtract.push(i); } break; + } case 'all': indicesToExtract = Array.from({ length: totalPages }, (_, i) => i); break; @@ -379,8 +380,7 @@ document.addEventListener('DOMContentLoaded', () => { document.querySelectorAll('.page-thumbnail-wrapper.selected') ).map((el) => parseInt((el as HTMLElement).dataset.pageIndex || '0')); break; - case 'bookmarks': - // Check if CPDF is configured + case 'bookmarks': { if (!isCpdfAvailable()) { showWasmRequiredDialog('cpdf'); hideLoader(); @@ -404,7 +404,7 @@ document.addEventListener('DOMContentLoaded', () => { if (bookmarkLevel === 'all' || level === parseInt(bookmarkLevel)) { if (page > 1 && !splitPages.includes(page - 1)) { - splitPages.push(page - 1); // Convert to 0-based index + splitPages.push(page - 1); } } } @@ -434,7 +434,7 @@ document.addEventListener('DOMContentLoaded', () => { state.pdfDoc, pageIndices ); - copiedPages.forEach((page: any) => newPdf.addPage(page)); + copiedPages.forEach((page) => newPdf.addPage(page)); const pdfBytes2 = await newPdf.save(); zip.file(`split-${i + 1}.pdf`, pdfBytes2); } @@ -446,8 +446,9 @@ document.addEventListener('DOMContentLoaded', () => { resetState(); }); return; + } - case 'n-times': + case 'n-times': { const nValue = parseInt( (document.getElementById('split-n-value') as HTMLInputElement) ?.value || '5' @@ -470,7 +471,7 @@ document.addEventListener('DOMContentLoaded', () => { state.pdfDoc, pageIndices ); - copiedPages.forEach((page: any) => newPdf.addPage(page)); + copiedPages.forEach((page) => newPdf.addPage(page)); const pdfBytes3 = await newPdf.save(); zip2.file(`split-${i + 1}.pdf`, pdfBytes3); } @@ -482,6 +483,7 @@ document.addEventListener('DOMContentLoaded', () => { resetState(); }); return; + } } const uniqueIndices = [...new Set(indicesToExtract)]; @@ -506,8 +508,7 @@ document.addEventListener('DOMContentLoaded', () => { ]); newPdf.addPage(copiedPage); const pdfBytes = await newPdf.save(); - // @ts-ignore - zip.file(`page-${index + 1}.pdf`, pdfBytes); + zip.file(`page-${index + 1}.pdf`, new Uint8Array(pdfBytes)); } const zipBlob = await zip.generateAsync({ type: 'blob' }); downloadFile(zipBlob, 'split-pages.zip'); @@ -517,7 +518,7 @@ document.addEventListener('DOMContentLoaded', () => { state.pdfDoc, uniqueIndices as number[] ); - copiedPages.forEach((page: any) => newPdf.addPage(page)); + copiedPages.forEach((page) => newPdf.addPage(page)); const pdfBytes = await newPdf.save(); downloadFile( new Blob([new Uint8Array(pdfBytes)], { type: 'application/pdf' }), @@ -532,11 +533,13 @@ document.addEventListener('DOMContentLoaded', () => { showAlert('Success', 'PDF split successfully!', 'success', () => { resetState(); }); - } catch (e: any) { + } catch (e: unknown) { console.error(e); showAlert( 'Error', - e.message || 'Failed to split PDF. Please check your selection.' + e instanceof Error + ? e.message + : 'Failed to split PDF. Please check your selection.' ); } finally { hideLoader(); diff --git a/src/js/logic/svg-to-pdf-page.ts b/src/js/logic/svg-to-pdf-page.ts index 9073619..4a6cfd2 100644 --- a/src/js/logic/svg-to-pdf-page.ts +++ b/src/js/logic/svg-to-pdf-page.ts @@ -1,10 +1,6 @@ import { createIcons, icons } from 'lucide'; import { showAlert, showLoader, hideLoader } from '../ui.js'; -import { - downloadFile, - readFileAsArrayBuffer, - formatBytes, -} from '../utils/helpers.js'; +import { downloadFile, formatBytes } from '../utils/helpers.js'; import { PDFDocument as PDFLibDocument } from 'pdf-lib'; import { getSelectedQuality, @@ -261,9 +257,9 @@ async function convertToPdf() { showAlert('Success', 'PDF created successfully!', 'success', () => { resetState(); }); - } catch (e: any) { + } catch (e: unknown) { console.error(e); - showAlert('Conversion Error', e.message); + showAlert('Conversion Error', e instanceof Error ? e.message : String(e)); } finally { hideLoader(); } diff --git a/src/js/logic/table-of-contents.ts b/src/js/logic/table-of-contents.ts index eb4b4b5..b079e19 100644 --- a/src/js/logic/table-of-contents.ts +++ b/src/js/logic/table-of-contents.ts @@ -36,15 +36,6 @@ const backToToolsBtn = document.getElementById( 'back-to-tools' ) as HTMLButtonElement; -interface GenerateTOCMessage { - command: 'generate-toc'; - pdfData: ArrayBuffer; - title: string; - fontSize: number; - fontFamily: number; - addBookmark: boolean; -} - interface TOCSuccessResponse { status: 'success'; pdfBytes: ArrayBuffer; diff --git a/src/js/logic/txt-to-pdf-page.ts b/src/js/logic/txt-to-pdf-page.ts index be41b94..0dcc2ff 100644 --- a/src/js/logic/txt-to-pdf-page.ts +++ b/src/js/logic/txt-to-pdf-page.ts @@ -1,9 +1,7 @@ import { showLoader, hideLoader, showAlert } from '../ui.js'; import { downloadFile, formatBytes } from '../utils/helpers.js'; import { createIcons, icons } from 'lucide'; -import { isWasmAvailable, getWasmBaseUrl } from '../config/wasm-cdn-config.js'; -import { showWasmRequiredDialog } from '../utils/wasm-provider.js'; -import { loadPyMuPDF, isPyMuPDFAvailable } from '../utils/pymupdf-loader.js'; +import { loadPyMuPDF } from '../utils/pymupdf-loader.js'; let files: File[] = []; let currentMode: 'upload' | 'text' = 'upload'; @@ -140,9 +138,12 @@ async function convert() { resetState(); } ); - } catch (e: any) { + } catch (e: unknown) { console.error('[TxtToPDF] Error:', e); - showAlert('Error', `Failed to convert text to PDF. ${e.message || ''}`); + showAlert( + 'Error', + `Failed to convert text to PDF. ${e instanceof Error ? e.message : ''}` + ); } finally { hideLoader(); } diff --git a/src/js/logic/validate-signature-pdf.ts b/src/js/logic/validate-signature-pdf.ts index ea16da5..697b78d 100644 --- a/src/js/logic/validate-signature-pdf.ts +++ b/src/js/logic/validate-signature-pdf.ts @@ -100,7 +100,13 @@ export function validateSignature( Array.from(signature.contents) ); const asn1 = forge.asn1.fromDer(binaryString); - const p7 = forge.pkcs7.messageFromAsn1(asn1) as any; + const p7 = forge.pkcs7.messageFromAsn1( + asn1 + ) as forge.pkcs7.PkcsSignedData & { + rawCapture?: { + authenticatedAttributes?: Array<{ type: string; value: Date }>; + }; + }; if (!p7.certificates || p7.certificates.length === 0) { result.errorMessage = 'No certificates found in signature'; @@ -166,10 +172,8 @@ export function validateSignature( } else { // Try to extract from authenticated attributes try { - const signedData = p7 as any; - if (signedData.rawCapture?.authenticatedAttributes) { - // Look for signing time attribute - for (const attr of signedData.rawCapture.authenticatedAttributes) { + if (p7.rawCapture?.authenticatedAttributes) { + for (const attr of p7.rawCapture.authenticatedAttributes) { if (attr.type === forge.pki.oids.signingTime) { result.signatureDate = attr.value; break; @@ -185,7 +189,7 @@ export function validateSignature( } if (signature.byteRange && signature.byteRange.length === 4) { - const [start1, len1, start2, len2] = signature.byteRange; + const [, len1, start2, len2] = signature.byteRange; const totalCovered = len1 + len2; const expectedEnd = start2 + len2; diff --git a/src/js/logic/webp-to-pdf-page.ts b/src/js/logic/webp-to-pdf-page.ts index bcf38ae..2b2e804 100644 --- a/src/js/logic/webp-to-pdf-page.ts +++ b/src/js/logic/webp-to-pdf-page.ts @@ -160,9 +160,13 @@ function updateUI() { } } -function sanitizeImageAsJpeg(imageBytes: any) { +function sanitizeImageAsJpeg(imageBytes: Uint8Array | ArrayBuffer) { return new Promise((resolve, reject) => { - const blob = new Blob([imageBytes]); + const blob = new Blob([ + imageBytes instanceof Uint8Array + ? new Uint8Array(imageBytes) + : imageBytes, + ]); const imageUrl = URL.createObjectURL(blob); const img = new Image(); @@ -248,9 +252,9 @@ async function convertToPdf() { showAlert('Success', 'PDF created successfully!', 'success', () => { resetState(); }); - } catch (e: any) { + } catch (e: unknown) { console.error(e); - showAlert('Conversion Error', e.message); + showAlert('Conversion Error', e instanceof Error ? e.message : String(e)); } finally { hideLoader(); } diff --git a/src/js/logic/word-to-pdf-page.ts b/src/js/logic/word-to-pdf-page.ts index 933075a..76202f9 100644 --- a/src/js/logic/word-to-pdf-page.ts +++ b/src/js/logic/word-to-pdf-page.ts @@ -1,9 +1,5 @@ import { showLoader, hideLoader, showAlert } from '../ui.js'; -import { - downloadFile, - readFileAsArrayBuffer, - formatBytes, -} from '../utils/helpers.js'; +import { downloadFile, formatBytes } from '../utils/helpers.js'; import { state } from '../state.js'; import { createIcons, icons } from 'lucide'; import { @@ -187,13 +183,16 @@ document.addEventListener('DOMContentLoaded', () => { () => resetState() ); } - } catch (e: any) { + } catch (e: unknown) { console.error('[Word2PDF] ERROR:', e); - console.error('[Word2PDF] Error stack:', e.stack); + console.error( + '[Word2PDF] Error stack:', + e instanceof Error ? e.stack : '' + ); hideLoader(); showAlert( 'Error', - `An error occurred during conversion. Error: ${e.message}` + `An error occurred during conversion. Error: ${e instanceof Error ? e.message : String(e)}` ); } }; diff --git a/src/js/logic/word-to-pdf.ts b/src/js/logic/word-to-pdf.ts index 6439ab2..7040814 100644 --- a/src/js/logic/word-to-pdf.ts +++ b/src/js/logic/word-to-pdf.ts @@ -15,13 +15,18 @@ export async function wordToPdf() { try { const mammothOptions = { // @ts-expect-error TS(2304) FIXME: Cannot find name 'mammoth'. - convertImage: mammoth.images.inline((element: any) => { - return element.read('base64').then((imageBuffer: any) => { - return { - src: `data:${element.contentType};base64,${imageBuffer}`, - }; - }); - }), + convertImage: mammoth.images.inline( + (element: { + read: (encoding: string) => Promise; + contentType: string; + }) => { + return element.read('base64').then((imageBuffer: string) => { + return { + src: `data:${element.contentType};base64,${imageBuffer}`, + }; + }); + } + ), }; const arrayBuffer = await readFileAsArrayBuffer(file); // @ts-expect-error TS(2304) FIXME: Cannot find name 'mammoth'. @@ -77,7 +82,18 @@ export async function wordToPdf() { }); await doc.html(previewContent, { - callback: function (doc: any) { + callback: function (doc: { + internal: { pageSize: { getHeight: () => number } }; + link: ( + x: number, + y: number, + w: number, + h: number, + opts: { url: string } + ) => void; + save: (name: string) => void; + setPage: (page: number) => void; + }) { const links = previewContent.querySelectorAll('a'); const pageHeight = doc.internal.pageSize.getHeight(); const containerRect = previewContent.getBoundingClientRect(); // Get container's position diff --git a/src/js/logic/wpd-to-pdf-page.ts b/src/js/logic/wpd-to-pdf-page.ts index 31200ea..04e75fc 100644 --- a/src/js/logic/wpd-to-pdf-page.ts +++ b/src/js/logic/wpd-to-pdf-page.ts @@ -150,12 +150,12 @@ document.addEventListener('DOMContentLoaded', () => { () => resetState() ); } - } catch (e: any) { + } catch (e: unknown) { hideLoader(); console.error(`[${FILETYPE_NAME}ToPDF] Error:`, e); showAlert( 'Error', - `An error occurred during conversion. Error: ${e.message}` + `An error occurred during conversion. Error: ${e instanceof Error ? e.message : String(e)}` ); } }; diff --git a/src/js/logic/wps-to-pdf-page.ts b/src/js/logic/wps-to-pdf-page.ts index b101dcb..558bb7a 100644 --- a/src/js/logic/wps-to-pdf-page.ts +++ b/src/js/logic/wps-to-pdf-page.ts @@ -150,12 +150,12 @@ document.addEventListener('DOMContentLoaded', () => { () => resetState() ); } - } catch (e: any) { + } catch (e: unknown) { hideLoader(); console.error(`[${FILETYPE_NAME}ToPDF] Error:`, e); showAlert( 'Error', - `An error occurred during conversion. Error: ${e.message}` + `An error occurred during conversion. Error: ${e instanceof Error ? e.message : String(e)}` ); } }; diff --git a/src/js/logic/xps-to-pdf-page.ts b/src/js/logic/xps-to-pdf-page.ts index 0632d4d..e8a10a8 100644 --- a/src/js/logic/xps-to-pdf-page.ts +++ b/src/js/logic/xps-to-pdf-page.ts @@ -2,9 +2,7 @@ import { showLoader, hideLoader, showAlert } from '../ui.js'; import { downloadFile, formatBytes } from '../utils/helpers.js'; import { state } from '../state.js'; import { createIcons, icons } from 'lucide'; -import { isWasmAvailable, getWasmBaseUrl } from '../config/wasm-cdn-config.js'; -import { showWasmRequiredDialog } from '../utils/wasm-provider.js'; -import { loadPyMuPDF, isPyMuPDFAvailable } from '../utils/pymupdf-loader.js'; +import { loadPyMuPDF } from '../utils/pymupdf-loader.js'; import { deduplicateFileName } from '../utils/deduplicate-filename.js'; const FILETYPE = 'xps'; @@ -147,12 +145,12 @@ document.addEventListener('DOMContentLoaded', () => { () => resetState() ); } - } catch (e: any) { + } catch (e: unknown) { console.error(`[${TOOL_NAME}2PDF] ERROR:`, e); hideLoader(); showAlert( 'Error', - `An error occurred during conversion. Error: ${e.message}` + `An error occurred during conversion. Error: ${e instanceof Error ? e.message : String(e)}` ); } }; diff --git a/src/js/main.ts b/src/js/main.ts index 50458f2..5c0befc 100644 --- a/src/js/main.ts +++ b/src/js/main.ts @@ -6,13 +6,11 @@ import '@phosphor-icons/web/regular'; import * as pdfjsLib from 'pdfjs-dist'; import '../css/styles.css'; import { formatShortcutDisplay, formatStars } from './utils/helpers.js'; -import { APP_VERSION } from '../version.js'; import { initI18n, applyTranslations, rewriteLinks, injectLanguageSwitcher, - createLanguageSwitcher, t, } from './i18n/index.js'; import { @@ -501,7 +499,7 @@ const init = async () => { } }); - dom.toolGrid.addEventListener('click', (e) => { + dom.toolGrid.addEventListener('click', () => { // All tools now use href and navigate directly - no modal handling needed }); } @@ -889,7 +887,7 @@ const init = async () => { }); } - function getToolId(tool: any): string { + function getToolId(tool: { id?: string; href?: string }): string { if (tool.id) return tool.id; if (tool.href) { const match = tool.href.match(/\/([^/]+)\.html$/); diff --git a/src/js/state.ts b/src/js/state.ts index 4950b43..26d48ac 100644 --- a/src/js/state.ts +++ b/src/js/state.ts @@ -1,4 +1,12 @@ -export const state = { +import type { PDFDocument } from 'pdf-lib'; + +export const state: { + activeTool: string | null; + files: File[]; + pdfDoc: PDFDocument | null; + pdfPages: unknown[]; + currentPdfUrl: string | null; +} = { activeTool: null, files: [], pdfDoc: null, diff --git a/src/js/types/alternate-merge-page-type.ts b/src/js/types/alternate-merge-page-type.ts index 488ecb7..abee4e9 100644 --- a/src/js/types/alternate-merge-page-type.ts +++ b/src/js/types/alternate-merge-page-type.ts @@ -1,5 +1,7 @@ +import type { PDFDocumentProxy } from 'pdfjs-dist'; + export interface AlternateMergeState { - files: File[]; - pdfBytes: Map; - pdfDocs: Map; -} \ No newline at end of file + files: File[]; + pdfBytes: Map; + pdfDocs: Map; +} diff --git a/src/js/types/canvas-editor-type.ts b/src/js/types/canvas-editor-type.ts new file mode 100644 index 0000000..905482c --- /dev/null +++ b/src/js/types/canvas-editor-type.ts @@ -0,0 +1,7 @@ +export interface CropBox { + x: number; + y: number; + width: number; + height: number; + scale: number | 'fit'; +} diff --git a/src/js/types/crop-pdf-type.ts b/src/js/types/crop-pdf-type.ts index 7b8bcf6..e13e39d 100644 --- a/src/js/types/crop-pdf-type.ts +++ b/src/js/types/crop-pdf-type.ts @@ -1,8 +1,18 @@ -export interface CropperState { - pdfDoc: any; - currentPageNum: number; - cropper: any; - originalPdfBytes: ArrayBuffer | null; - pageCrops: Record; - file: File | null; +import type { PDFDocumentProxy } from 'pdfjs-dist'; +import Cropper from 'cropperjs'; + +export interface CropPercentages { + x: number; + y: number; + width: number; + height: number; +} + +export interface CropperState { + pdfDoc: PDFDocumentProxy | null; + currentPageNum: number; + cropper: Cropper | null; + originalPdfBytes: ArrayBuffer | null; + pageCrops: Record; + file: File | null; } diff --git a/src/js/types/delete-pages-type.ts b/src/js/types/delete-pages-type.ts index 0f42730..1ce512d 100644 --- a/src/js/types/delete-pages-type.ts +++ b/src/js/types/delete-pages-type.ts @@ -1,7 +1,10 @@ +import { PDFDocument as PDFLibDocument } from 'pdf-lib'; +import type { PDFDocumentProxy } from 'pdfjs-dist'; + export interface DeletePagesState { - file: File | null; - pdfDoc: any; - pdfJsDoc: any; - totalPages: number; - pagesToDelete: Set; + file: File | null; + pdfDoc: PDFLibDocument | null; + pdfJsDoc: PDFDocumentProxy | null; + totalPages: number; + pagesToDelete: Set; } diff --git a/src/js/types/edit-pdf-type.ts b/src/js/types/edit-pdf-type.ts new file mode 100644 index 0000000..a6d8067 --- /dev/null +++ b/src/js/types/edit-pdf-type.ts @@ -0,0 +1,15 @@ +export interface DocManagerPlugin { + onDocumentClosed: ( + callback: (data: { id?: string } | string) => void + ) => void; + onDocumentOpened: ( + callback: (data: { id?: string; name?: string }) => void + ) => void; + openDocumentBuffer: (opts: { + buffer: ArrayBuffer; + name?: string; + autoActivate?: boolean; + }) => void; + closeDocument: (id: string) => void; + saveAsCopy: (id: string) => Promise; +} diff --git a/src/js/types/index.ts b/src/js/types/index.ts index acd03cd..e2c3ba0 100644 --- a/src/js/types/index.ts +++ b/src/js/types/index.ts @@ -56,3 +56,12 @@ export * from './pdf-to-tiff-type.ts'; export * from './pdf-to-cbz-type.ts'; export * from './password-prompt-type.ts'; export * from './config-types.ts'; +export * from './utils-types.ts'; +export * from './merge-worker-type.ts'; +export * from './canvas-editor-type.ts'; +export * from './edit-pdf-type.ts'; +export * from './redact-type.ts'; +export * from './shortcuts-type.ts'; +export * from './ui-type.ts'; +export * from './markdown-editor-type.ts'; +export * from './sanitize-type.ts'; diff --git a/src/js/types/markdown-editor-type.ts b/src/js/types/markdown-editor-type.ts new file mode 100644 index 0000000..5050a32 --- /dev/null +++ b/src/js/types/markdown-editor-type.ts @@ -0,0 +1,28 @@ +export interface MarkdownItOptions { + html: boolean; + breaks: boolean; + linkify: boolean; + typographer: boolean; + highlight?: (str: string, lang: string) => string; +} + +export interface MarkdownItToken { + attrSet(name: string, value: string): void; + [key: string]: unknown; +} + +export interface MarkdownItRendererSelf { + renderToken( + tokens: MarkdownItToken[], + idx: number, + options: MarkdownItOptions + ): string; +} + +export type MarkdownItRenderRule = ( + tokens: MarkdownItToken[], + idx: number, + options: MarkdownItOptions, + env: unknown, + self: MarkdownItRendererSelf +) => string; diff --git a/src/js/types/merge-pdf-type.ts b/src/js/types/merge-pdf-type.ts index 2fbfed9..a5cc128 100644 --- a/src/js/types/merge-pdf-type.ts +++ b/src/js/types/merge-pdf-type.ts @@ -1,5 +1,7 @@ +import type { PDFDocumentProxy } from 'pdfjs-dist'; + export interface MergeState { - files: File[]; - pdfBytes: Map; - pdfDocs: Map; + files: File[]; + pdfBytes: Map; + pdfDocs: Map; } diff --git a/src/js/types/merge-worker-type.ts b/src/js/types/merge-worker-type.ts new file mode 100644 index 0000000..e5c65df --- /dev/null +++ b/src/js/types/merge-worker-type.ts @@ -0,0 +1,32 @@ +export interface MergeJob { + fileName: string; + rangeType: 'all' | 'specific' | 'single' | 'range'; + rangeString?: string; + pageIndex?: number; + startPage?: number; + endPage?: number; +} + +export interface MergeFile { + name: string; + data: ArrayBuffer; +} + +export interface MergeMessage { + command: 'merge'; + files: MergeFile[]; + jobs: MergeJob[]; + cpdfUrl?: string; +} + +export interface MergeSuccessResponse { + status: 'success'; + pdfBytes: ArrayBuffer; +} + +export interface MergeErrorResponse { + status: 'error'; + message: string; +} + +export type MergeResponse = MergeSuccessResponse | MergeErrorResponse; diff --git a/src/js/types/redact-type.ts b/src/js/types/redact-type.ts new file mode 100644 index 0000000..63b3bd2 --- /dev/null +++ b/src/js/types/redact-type.ts @@ -0,0 +1,7 @@ +export interface RedactionRect { + pageIndex: number; + canvasX: number; + canvasY: number; + canvasWidth: number; + canvasHeight: number; +} diff --git a/src/js/types/sanitize-type.ts b/src/js/types/sanitize-type.ts new file mode 100644 index 0000000..371aeeb --- /dev/null +++ b/src/js/types/sanitize-type.ts @@ -0,0 +1,8 @@ +import type { PDFDict } from 'pdf-lib'; + +export interface PDFDocumentInternal { + getInfoDict(): PDFDict; + javaScripts?: unknown[]; + embeddedFiles?: unknown[]; + fonts?: unknown[]; +} diff --git a/src/js/types/shortcuts-type.ts b/src/js/types/shortcuts-type.ts new file mode 100644 index 0000000..b9e8931 --- /dev/null +++ b/src/js/types/shortcuts-type.ts @@ -0,0 +1,5 @@ +export interface ToolEntry { + id?: string; + href?: string; + name?: string; +} diff --git a/src/js/types/ui-type.ts b/src/js/types/ui-type.ts new file mode 100644 index 0000000..15911e7 --- /dev/null +++ b/src/js/types/ui-type.ts @@ -0,0 +1,5 @@ +export interface FileInputOptions { + multiple?: boolean; + accept?: string; + showControls?: boolean; +} diff --git a/src/js/types/utils-types.ts b/src/js/types/utils-types.ts new file mode 100644 index 0000000..8fbc2c8 --- /dev/null +++ b/src/js/types/utils-types.ts @@ -0,0 +1,235 @@ +import type { PDFName, PDFObject } from 'pdf-lib'; + +export interface PDFDictLike { + keys(): PDFName[]; + values(): PDFObject[]; + entries(): [PDFName, PDFObject][]; + set(key: PDFName, value: PDFObject): void; + get(key: PDFName, preservePDFNull?: boolean): PDFObject | undefined; + has(key: PDFName): boolean; + delete(key: PDFName): boolean; + lookup(key: PDFName): PDFObject | undefined; + asArray?(): PDFObject[]; +} + +export interface WindowWithCoherentPdf { + coherentpdf?: unknown; +} + +export interface WindowWithLucide { + lucide?: { + createIcons(): void; + }; +} + +export interface WindowWithI18next { + i18next?: { + t(key: string): string; + }; +} + +export interface GlobalScopeWithGhostscript { + loadGS?: (config: { baseUrl: string }) => Promise; + GhostscriptWASM?: new (url: string) => GhostscriptDynamicInstance; +} + +export interface GhostscriptDynamicInstance { + convertToPDFA?(pdfBuffer: ArrayBuffer, profile: string): Promise; + fontToOutline?(pdfBuffer: ArrayBuffer): Promise; + init?(): Promise; +} + +export interface PyMuPDFCompressOptions { + images: { + enabled: boolean; + quality: number; + dpiTarget: number; + dpiThreshold: number; + convertToGray: boolean; + }; + scrub: { + metadata: boolean; + thumbnails: boolean; + xmlMetadata?: boolean; + }; + subsetFonts: boolean; + save: { + garbage: 4; + deflate: boolean; + clean: boolean; + useObjstms: boolean; + }; +} + +export interface PyMuPDFExtractTextOptions { + format?: string; + pages?: number[]; +} + +export interface PyMuPDFRasterizeOptions { + dpi?: number; + format?: string; + pages?: number[]; + grayscale?: boolean; + quality?: number; +} + +export interface PyMuPDFDocument { + pageCount: number; + pages: (() => PyMuPDFPage[]) & PyMuPDFPage[]; + needsPass: boolean; + isEncrypted: boolean; + authenticate(password: string): boolean; + getPage(index: number): PyMuPDFPage; + save(): Uint8Array; + close(): void; + getLayerConfig?(): unknown[]; + addLayer?(name: string): { number: number; xref: number }; + setLayerConfig?(layers: unknown[]): void; + setLayerVisibility?(xref: number, visible: boolean): void; + deleteOCG?(xref: number): void; + addOCGWithParent?( + name: string, + parentXref: number + ): { number: number; xref: number }; + addOCG?(name: string): { number: number; xref: number }; + applyRedactions?(): void; + searchFor?(text: string, pageNum: number): unknown[]; +} + +export interface PyMuPDFPage { + getText(format?: string): string; + getImages(): Array<{ data: Uint8Array; ext: string; xref: number }>; + extractTables?(): unknown[]; + findTables(): Array<{ + rows: (string | null)[][]; + markdown: string; + rowCount: number; + colCount: number; + }>; + extractImage(xref: number): { data: Uint8Array; ext: string } | null; + toSvg?(): string; + toPixmap?(options?: { dpi?: number }): { + toBlob(format: string, quality?: number): Blob; + }; + addRedactAnnot?(rect: unknown): void; + searchFor(text: string): unknown[]; + addRedaction(rect: unknown, text: string, fill: unknown): void; + applyRedactions(): void; +} + +export interface PyMuPDFTextToPdfOptions { + fontSize?: number; + pageSize?: string; + fontName?: string; + textColor?: string; + margins?: number; +} + +export interface PyMuPDFDeskewOptions { + threshold?: number; + dpi?: number; +} + +export interface PyMuPDFInstance { + load(): Promise; + compressPdf( + file: Blob, + options: PyMuPDFCompressOptions + ): Promise<{ blob: Blob; compressedSize: number; usedFallback?: boolean }>; + convertToPdf( + file: Blob | File, + ext: string | { filetype: string } + ): Promise; + extractText(file: Blob, options?: PyMuPDFExtractTextOptions): Promise; + extractImages(file: Blob): Promise>; + extractTables(file: Blob): Promise; + toSvg(file: Blob, pageNum: number): Promise; + renderPageToImage(file: Blob, pageNum: number, scale: number): Promise; + getPageCount(file: Blob): Promise; + rasterizePdf( + file: Blob | File, + options: PyMuPDFRasterizeOptions + ): Promise; + open(file: Blob | File, password?: string): Promise; + textToPdf(text: string, options?: PyMuPDFTextToPdfOptions): Promise; + pdfToDocx(file: Blob | File): Promise; + pdfToMarkdown( + file: Blob | File, + options?: { includeImages?: boolean } + ): Promise; + pdfToText(file: Blob | File): Promise; + deskewPdf( + file: Blob, + options?: PyMuPDFDeskewOptions + ): Promise<{ + pdf: Blob; + result: { + totalPages: number; + correctedPages: number; + angles: number[]; + corrected: boolean[]; + }; + }>; + imageToPdf(file: File, options?: { imageType?: string }): Promise; + imagesToPdf(files: File[]): Promise; + htmlToPdf(html: string, options: unknown): Promise; + pdfToLlamaIndex(file: File): Promise; +} + +export type { QpdfInstance as QpdfWasmInstance } from '@neslinesli93/qpdf-wasm'; + +export interface QpdfInstanceExtended { + callMain: (args: string[]) => number; + FS: EmscriptenFSExtended; +} + +export interface EmscriptenFSExtended { + mkdir: (path: string) => void; + mount: ( + type: unknown, + opts: { blobs: { name: string; data: Blob }[] }, + mountPoint: string + ) => void; + unmount: (mountPoint: string) => void; + writeFile: ( + path: string, + data: Uint8Array | string, + opts?: { encoding?: string } + ) => void; + readFile: (path: string, opts?: { encoding?: string }) => Uint8Array; + unlink: (path: string) => void; + analyzePath: (path: string) => { exists: boolean }; +} + +export interface CpdfInstance { + setSlow: () => void; + fromMemory: (data: Uint8Array, userpw: string) => unknown; + isEncrypted: (pdf: unknown) => boolean; + decryptPdf: (pdf: unknown, password: string) => void; + decryptPdfOwner: (pdf: unknown, password: string) => void; + toMemory: (pdf: unknown, linearize: boolean, makeId: boolean) => Uint8Array; + deletePdf: (pdf: unknown) => void; + startGetBookmarkInfo: (pdf: unknown) => void; + numberBookmarks: () => number; + getBookmarkLevel: (index: number) => number; + getBookmarkPage: (pdf: unknown, index: number) => number; + endGetBookmarkInfo: () => void; + parsePagespec: (pdf: unknown, pagespec: string) => unknown; + removePageLabels: (pdf: unknown) => void; + all: (pdf: unknown) => unknown; + addPageLabels: ( + pdf: unknown, + style: unknown, + prefix: string, + offset: number, + range: unknown, + progress: boolean + ) => void; + decimalArabic: number; + lowercaseRoman: number; + uppercaseRoman: number; + lowercaseLetters: number; + uppercaseLetters: number; + noLabelPrefixOnly?: number; +} diff --git a/src/js/ui.ts b/src/js/ui.ts index 935459d..97b1a95 100644 --- a/src/js/ui.ts +++ b/src/js/ui.ts @@ -1,6 +1,5 @@ import { resetState } from './state.js'; import { formatBytes, getPDFDocument } from './utils/helpers.js'; -import { tesseractLanguages } from './config/tesseract-languages.js'; import { renderPagesProgressively, cleanupLazyRendering, @@ -14,6 +13,7 @@ import { } from './utils/rotation-state.js'; import * as pdfjsLib from 'pdfjs-dist'; import { t } from './i18n/i18n'; +import type { FileInputOptions } from '@/types'; pdfjsLib.GlobalWorkerOptions.workerSrc = new URL( 'pdfjs-dist/build/pdf.worker.min.mjs', @@ -112,8 +112,8 @@ export const hideLoader = () => { }; export const showAlert = ( - title: any, - message: any, + title: string, + message: string, type: string = 'error', callback?: () => void ) => { @@ -137,7 +137,7 @@ export const hideAlert = () => { if (dom.alertModal) dom.alertModal.classList.add('hidden'); }; -export const switchView = (view: any) => { +export const switchView = (view: string) => { if (view === 'grid') { dom.gridView.classList.remove('hidden'); dom.toolInterface.classList.add('hidden'); @@ -170,11 +170,13 @@ export const switchView = (view: any) => { } }; -const thumbnailState = { +const thumbnailState: { + sortableInstances: Record; +} = { sortableInstances: {}, }; -function initializeOrganizeSortable(containerId: any) { +function initializeOrganizeSortable(containerId: string) { const container = document.getElementById(containerId); if (!container) return; @@ -189,10 +191,10 @@ function initializeOrganizeSortable(containerId: any) { dragClass: 'sortable-drag', filter: '.delete-page-btn', preventOnFilter: true, - onStart: function (evt: any) { + onStart: function (evt: Sortable.SortableEvent) { evt.item.style.opacity = '0.5'; }, - onEnd: function (evt: any) { + onEnd: function (evt: Sortable.SortableEvent) { evt.item.style.opacity = '1'; }, }); @@ -203,7 +205,10 @@ function initializeOrganizeSortable(containerId: any) { * @param {string} toolId The ID of the active tool. * @param {object} pdfDoc The loaded pdf-lib document instance. */ -export const renderPageThumbnails = async (toolId: any, pdfDoc: any) => { +export const renderPageThumbnails = async ( + toolId: string, + pdfDoc: { save: () => Promise } +) => { const containerId = toolId === 'organize' ? 'page-organizer' @@ -229,8 +234,7 @@ export const renderPageThumbnails = async (toolId: any, pdfDoc: any) => { // Function to create wrapper element for each page const createWrapper = (canvas: HTMLCanvasElement, pageNumber: number) => { const wrapper = document.createElement('div'); - // @ts-expect-error TS(2322) FIXME: Type 'number' is not assignable to type 'string'. - wrapper.dataset.pageIndex = pageNumber - 1; + wrapper.dataset.pageIndex = String(pageNumber - 1); const imgContainer = document.createElement('div'); imgContainer.className = 'relative'; @@ -452,10 +456,10 @@ export const renderPageThumbnails = async (toolId: any, pdfDoc: any) => { * @param {HTMLElement} container The DOM element to render the list into. * @param {File[]} files The array of file objects. */ -export const renderFileDisplay = (container: any, files: any) => { +export const renderFileDisplay = (container: HTMLElement, files: File[]) => { container.textContent = ''; if (files.length > 0) { - files.forEach((file: any) => { + files.forEach((file: File) => { const fileDiv = document.createElement('div'); fileDiv.className = 'flex items-center justify-between bg-gray-700 p-3 rounded-lg text-sm'; @@ -474,13 +478,10 @@ export const renderFileDisplay = (container: any, files: any) => { } }; -const createFileInputHTML = (options = {}) => { - // @ts-expect-error TS(2339) FIXME: Property 'multiple' does not exist on type '{}'. +const createFileInputHTML = (options: FileInputOptions = {}) => { const multiple = options.multiple ? 'multiple' : ''; - // @ts-expect-error TS(2339) FIXME: Property 'accept' does not exist on type '{}'. const acceptedFiles = options.accept || 'application/pdf'; - // @ts-expect-error TS(2339) FIXME: Property 'showControls' does not exist on type '{}... Remove this comment to see the full error message - const showControls = options.showControls || false; // NEW: Add this parameter + const showControls = options.showControls || false; return `
diff --git a/src/js/utils/compress.ts b/src/js/utils/compress.ts index 47a15bb..d933406 100644 --- a/src/js/utils/compress.ts +++ b/src/js/utils/compress.ts @@ -69,7 +69,10 @@ export async function performCondenseCompression( scrub: { metadata: customSettings?.removeMetadata ?? preset.scrub.metadata, thumbnails: customSettings?.removeThumbnails ?? preset.scrub.thumbnails, - xmlMetadata: (preset.scrub as any).xmlMetadata ?? false, + xmlMetadata: + ('xmlMetadata' in preset.scrub + ? (preset.scrub as { xmlMetadata?: boolean }).xmlMetadata + : undefined) ?? false, }, subsetFonts: customSettings?.subsetFonts ?? preset.subsetFonts, save: { @@ -95,8 +98,11 @@ export async function performCondenseCompression( try { const result = await pymupdf.compressPdf(fileBlob, fallbackOptions); return { ...result, usedFallback: true }; - } catch (fallbackError: any) { - const msg = fallbackError?.message || String(fallbackError); + } catch (fallbackError: unknown) { + const msg = + fallbackError instanceof Error + ? fallbackError.message + : String(fallbackError); throw new Error(`PDF compression failed: ${msg}`, { cause: fallbackError, }); diff --git a/src/js/utils/cpdf-helper.ts b/src/js/utils/cpdf-helper.ts index a1a004b..32a59aa 100644 --- a/src/js/utils/cpdf-helper.ts +++ b/src/js/utils/cpdf-helper.ts @@ -1,4 +1,5 @@ import { WasmProvider } from './wasm-provider'; +import type { WindowWithCoherentPdf, CpdfInstance } from '@/types'; let cpdfLoaded = false; let cpdfLoadPromise: Promise | null = null; @@ -31,7 +32,7 @@ export async function isCpdfLoaded(): Promise { } cpdfLoadPromise = new Promise((resolve, reject) => { - if (typeof (window as any).coherentpdf !== 'undefined') { + if (typeof (window as WindowWithCoherentPdf).coherentpdf !== 'undefined') { cpdfLoaded = true; resolve(); return; @@ -53,7 +54,7 @@ export async function isCpdfLoaded(): Promise { return cpdfLoadPromise; } -export async function getCpdf(): Promise { +export async function getCpdf(): Promise { await isCpdfLoaded(); - return (window as any).coherentpdf; + return (window as WindowWithCoherentPdf).coherentpdf as CpdfInstance; } diff --git a/src/js/utils/disabled-tools.ts b/src/js/utils/disabled-tools.ts index dfbc704..876e635 100644 --- a/src/js/utils/disabled-tools.ts +++ b/src/js/utils/disabled-tools.ts @@ -27,7 +27,9 @@ export async function loadRuntimeConfig(): Promise { (c): c is string => typeof c === 'string' ); } - } catch {} + } catch { + console.error('[LOAD_RUNTIME_CONFIG] Failed to load runtime configuration'); + } } export function isToolDisabled(toolId: string): boolean { diff --git a/src/js/utils/ghostscript-dynamic-loader.ts b/src/js/utils/ghostscript-dynamic-loader.ts index e43e3fa..4a84375 100644 --- a/src/js/utils/ghostscript-dynamic-loader.ts +++ b/src/js/utils/ghostscript-dynamic-loader.ts @@ -1,7 +1,11 @@ import { WasmProvider } from './wasm-provider.js'; +import type { + GlobalScopeWithGhostscript, + GhostscriptDynamicInstance, +} from '@/types'; -let cachedGS: any = null; -let loadPromise: Promise | null = null; +let cachedGS: GhostscriptInterface | null = null; +let loadPromise: Promise | null = null; export interface GhostscriptInterface { convertToPDFA(pdfBuffer: ArrayBuffer, profile: string): Promise; @@ -32,16 +36,20 @@ export async function loadGhostscript(): Promise { await loadScript(wrapperUrl); - const globalScope = - typeof globalThis !== 'undefined' ? globalThis : window; + const globalScope = ( + typeof globalThis !== 'undefined' ? globalThis : window + ) as typeof globalThis & GlobalScopeWithGhostscript; - if (typeof (globalScope as any).loadGS === 'function') { - cachedGS = await (globalScope as any).loadGS({ + if (typeof globalScope.loadGS === 'function') { + const instance = await globalScope.loadGS({ baseUrl: normalizedUrl, }); - } else if (typeof (globalScope as any).GhostscriptWASM === 'function') { - cachedGS = new (globalScope as any).GhostscriptWASM(normalizedUrl); - await cachedGS.init?.(); + cachedGS = instance as unknown as GhostscriptInterface; + } else if (typeof globalScope.GhostscriptWASM === 'function') { + const instance: GhostscriptDynamicInstance = + new globalScope.GhostscriptWASM(normalizedUrl); + await instance.init?.(); + cachedGS = instance as unknown as GhostscriptInterface; } else { throw new Error( 'Ghostscript wrapper did not expose expected interface. Expected loadGS() or GhostscriptWASM class.' @@ -49,10 +57,11 @@ export async function loadGhostscript(): Promise { } return cachedGS; - } catch (error: any) { + } catch (error: unknown) { loadPromise = null; + const msg = error instanceof Error ? error.message : String(error); throw new Error( - `Failed to load Ghostscript from ${normalizedUrl}: ${error.message}`, + `Failed to load Ghostscript from ${normalizedUrl}: ${msg}`, { cause: error } ); } diff --git a/src/js/utils/helpers.ts b/src/js/utils/helpers.ts index 7ec12f2..5056ac7 100644 --- a/src/js/utils/helpers.ts +++ b/src/js/utils/helpers.ts @@ -1,8 +1,10 @@ import createModule from '@neslinesli93/qpdf-wasm'; +import type { QpdfInstanceExtended } from '@/types'; import { showLoader, hideLoader, showAlert } from '../ui.js'; import { createIcons } from 'lucide'; import { state, resetState } from '../state.js'; import * as pdfjsLib from 'pdfjs-dist'; +import type { DocumentInitParameters } from 'pdfjs-dist/types/src/display/api'; const STANDARD_SIZES = { A4: { width: 595.28, height: 841.89 }, @@ -13,7 +15,7 @@ const STANDARD_SIZES = { A5: { width: 419.53, height: 595.28 }, }; -export function getStandardPageName(width: any, height: any) { +export function getStandardPageName(width: number, height: number) { const tolerance = 1; // Allow for minor floating point variations for (const [name, size] of Object.entries(STANDARD_SIZES)) { if ( @@ -28,7 +30,7 @@ export function getStandardPageName(width: any, height: any) { return 'Custom'; } -export function convertPoints(points: any, unit: any) { +export function convertPoints(points: number, unit: string) { let result: number; switch (unit) { case 'in': @@ -59,7 +61,7 @@ export function hexToRgb(hex: string): { r: number; g: number; b: number } { : { r: 0, g: 0, b: 0 }; } -export const formatBytes = (bytes: any, decimals = 1) => { +export const formatBytes = (bytes: number, decimals = 1) => { if (bytes === 0) return '0 Bytes'; const k = 1024; const dm = decimals < 0 ? 0 : decimals; @@ -79,10 +81,12 @@ export const downloadFile = (blob: Blob, filename: string): void => { URL.revokeObjectURL(url); }; -export const readFileAsArrayBuffer = (file: any) => { +export const readFileAsArrayBuffer = ( + file: Blob +): Promise => { return new Promise((resolve, reject) => { const reader = new FileReader(); - reader.onload = () => resolve(reader.result); + reader.onload = () => resolve(reader.result as ArrayBuffer | null); reader.onerror = (error) => reject(error); reader.readAsArrayBuffer(file); }); @@ -139,7 +143,7 @@ export function parsePageRanges( * @param {string} isoDateString - The ISO 8601 date string. * @returns {string} A localized date and time string, or the original string if parsing fails. */ -export function formatIsoDate(isoDateString) { +export function formatIsoDate(isoDateString: string) { if (!isoDateString || typeof isoDateString !== 'string') { return isoDateString; // Return original value if it's not a valid string } @@ -156,20 +160,20 @@ export function formatIsoDate(isoDateString) { } } -let qpdfInstance: any = null; +let qpdfInstance: QpdfInstanceExtended | null = null; /** * Initialize qpdf-wasm singleton. * Subsequent calls return the same instance. */ -export async function initializeQpdf() { +export async function initializeQpdf(): Promise { if (qpdfInstance) return qpdfInstance; showLoader('Initializing PDF engine...'); try { - qpdfInstance = await createModule({ + qpdfInstance = (await createModule({ locateFile: () => import.meta.env.BASE_URL + 'qpdf.wasm', - }); + })) as unknown as QpdfInstanceExtended; } catch (error) { console.error('Failed to initialize qpdf-wasm:', error); showAlert( @@ -267,24 +271,19 @@ export function resetAndReloadTool(preResetCallback?: () => void) { * @param src The source to load (url string, typed array, or parameters object) * @returns The PDF loading task */ -export function getPDFDocument(src: any) { - let params = src; +export function getPDFDocument( + src: string | Uint8Array | ArrayBuffer | DocumentInitParameters +) { + let params: DocumentInitParameters; - // Handle different input types similar to how getDocument handles them, - // but we ensure we have an object to attach wasmUrl to. if (typeof src === 'string') { params = { url: src }; } else if (src instanceof Uint8Array || src instanceof ArrayBuffer) { params = { data: src }; + } else { + params = src; } - // Ensure params is an object - if (typeof params !== 'object' || params === null) { - params = {}; - } - - // Add wasmUrl pointing to our public/wasm directory - // This is required for PDF.js v5+ to load OpenJPEG for certain images return pdfjsLib.getDocument({ ...params, wasmUrl: import.meta.env.BASE_URL + 'pdfjs-viewer/wasm/', @@ -413,7 +412,7 @@ export function formatRawDate(raw: string): string { year, hoursStr, minsStr, - secsStr, + _secsStr, timezone, ] = match; @@ -455,8 +454,8 @@ export function formatRawDate(raw: string): string { return `${fullDay}, ${fullMonth} ${dom}, ${year} at ${hours}:${minsStr} ${ampm} (${formattedTz})`; } - } catch (e) { - // Fallback to raw string if parsing fails + } catch { + console.error('Error parsing date string:', raw); } return raw; } diff --git a/src/js/utils/libreoffice-loader.ts b/src/js/utils/libreoffice-loader.ts index 54b5b73..9936eb6 100644 --- a/src/js/utils/libreoffice-loader.ts +++ b/src/js/utils/libreoffice-loader.ts @@ -1,18 +1,19 @@ /** * LibreOffice WASM Converter Wrapper - * + * * Uses @matbee/libreoffice-converter package for document conversion. * Handles progress tracking and provides simpler API. */ import { WorkerBrowserConverter } from '@matbee/libreoffice-converter/browser'; +import type { InputFormat } from '@matbee/libreoffice-converter/browser'; const LIBREOFFICE_LOCAL_PATH = import.meta.env.BASE_URL + 'libreoffice-wasm/'; export interface LoadProgress { - phase: 'loading' | 'initializing' | 'converting' | 'complete' | 'ready'; - percent: number; - message: string; + phase: 'loading' | 'initializing' | 'converting' | 'complete' | 'ready'; + percent: number; + message: string; } export type ProgressCallback = (progress: LoadProgress) => void; @@ -21,140 +22,161 @@ export type ProgressCallback = (progress: LoadProgress) => void; let converterInstance: LibreOfficeConverter | null = null; export class LibreOfficeConverter { - private converter: WorkerBrowserConverter | null = null; - private initialized = false; - private initializing = false; - private basePath: string; + private converter: WorkerBrowserConverter | null = null; + private initialized = false; + private initializing = false; + private basePath: string; - constructor(basePath?: string) { - this.basePath = basePath || LIBREOFFICE_LOCAL_PATH; + constructor(basePath?: string) { + this.basePath = basePath || LIBREOFFICE_LOCAL_PATH; + } + + async initialize(onProgress?: ProgressCallback): Promise { + if (this.initialized) return; + + if (this.initializing) { + while (this.initializing) { + await new Promise((r) => setTimeout(r, 100)); + } + return; } - async initialize(onProgress?: ProgressCallback): Promise { - if (this.initialized) return; + this.initializing = true; + let progressCallback = onProgress; // Store original callback - if (this.initializing) { - while (this.initializing) { - await new Promise(r => setTimeout(r, 100)); - } - return; - } + try { + progressCallback?.({ + phase: 'loading', + percent: 0, + message: 'Loading conversion engine...', + }); - - this.initializing = true; - let progressCallback = onProgress; // Store original callback - - try { - progressCallback?.({ phase: 'loading', percent: 0, message: 'Loading conversion engine...' }); - - this.converter = new WorkerBrowserConverter({ - sofficeJs: `${this.basePath}soffice.js`, - sofficeWasm: `${this.basePath}soffice.wasm.gz`, - sofficeData: `${this.basePath}soffice.data.gz`, - sofficeWorkerJs: `${this.basePath}soffice.worker.js`, - browserWorkerJs: `${this.basePath}browser.worker.global.js`, - verbose: false, - onProgress: (info: { phase: string; percent: number; message: string }) => { - if (progressCallback && !this.initialized) { - const simplifiedMessage = `Loading conversion engine (${Math.round(info.percent)}%)...`; - progressCallback({ - phase: info.phase as LoadProgress['phase'], - percent: info.percent, - message: simplifiedMessage - }); - } - }, - onReady: () => { - console.log('[LibreOffice] Ready!'); - }, - onError: (error: Error) => { - console.error('[LibreOffice] Error:', error); - }, + this.converter = new WorkerBrowserConverter({ + sofficeJs: `${this.basePath}soffice.js`, + sofficeWasm: `${this.basePath}soffice.wasm.gz`, + sofficeData: `${this.basePath}soffice.data.gz`, + sofficeWorkerJs: `${this.basePath}soffice.worker.js`, + browserWorkerJs: `${this.basePath}browser.worker.global.js`, + verbose: false, + onProgress: (info: { + phase: string; + percent: number; + message: string; + }) => { + if (progressCallback && !this.initialized) { + const simplifiedMessage = `Loading conversion engine (${Math.round(info.percent)}%)...`; + progressCallback({ + phase: info.phase as LoadProgress['phase'], + percent: info.percent, + message: simplifiedMessage, }); + } + }, + onReady: () => { + console.log('[LibreOffice] Ready!'); + }, + onError: (error: Error) => { + console.error('[LibreOffice] Error:', error); + }, + }); - await this.converter.initialize(); - this.initialized = true; + await this.converter.initialize(); + this.initialized = true; - // Call completion message - progressCallback?.({ phase: 'ready', percent: 100, message: 'Conversion engine ready!' }); + // Call completion message + progressCallback?.({ + phase: 'ready', + percent: 100, + message: 'Conversion engine ready!', + }); - // Null out the callback to prevent any late-firing progress updates - progressCallback = undefined; - } finally { - this.initializing = false; - } + // Null out the callback to prevent any late-firing progress updates + progressCallback = undefined; + } finally { + this.initializing = false; + } + } + + isReady(): boolean { + return this.initialized && this.converter !== null; + } + + async convertToPdf(file: File): Promise { + if (!this.converter) { + throw new Error('Converter not initialized'); } - isReady(): boolean { - return this.initialized && this.converter !== null; + console.log(`[LibreOffice] Converting ${file.name} to PDF...`); + console.log( + `[LibreOffice] File type: ${file.type}, Size: ${file.size} bytes` + ); + + try { + console.log(`[LibreOffice] Reading file as ArrayBuffer...`); + const arrayBuffer = await file.arrayBuffer(); + const uint8Array = new Uint8Array(arrayBuffer); + console.log(`[LibreOffice] File loaded, ${uint8Array.length} bytes`); + + console.log(`[LibreOffice] Calling converter.convert() with buffer...`); + const startTime = Date.now(); + + // Detect input format - critical for CSV to apply import filters + const ext = file.name.split('.').pop()?.toLowerCase() || ''; + console.log(`[LibreOffice] Detected format from extension: ${ext}`); + + const result = await this.converter.convert( + uint8Array, + { + outputFormat: 'pdf', + inputFormat: ext as InputFormat, + }, + file.name + ); + + const duration = Date.now() - startTime; + console.log( + `[LibreOffice] Conversion complete! Duration: ${duration}ms, Size: ${result.data.length} bytes` + ); + + // Create a copy to avoid SharedArrayBuffer type issues + const data = new Uint8Array(result.data); + return new Blob([data], { type: result.mimeType }); + } catch (error) { + console.error(`[LibreOffice] Conversion FAILED for ${file.name}:`, error); + console.error(`[LibreOffice] Error details:`, { + message: error instanceof Error ? error.message : String(error), + stack: error instanceof Error ? error.stack : undefined, + }); + throw error; } + } - async convertToPdf(file: File): Promise { - if (!this.converter) { - throw new Error('Converter not initialized'); - } + async wordToPdf(file: File): Promise { + return this.convertToPdf(file); + } - console.log(`[LibreOffice] Converting ${file.name} to PDF...`); - console.log(`[LibreOffice] File type: ${file.type}, Size: ${file.size} bytes`); + async pptToPdf(file: File): Promise { + return this.convertToPdf(file); + } - try { - console.log(`[LibreOffice] Reading file as ArrayBuffer...`); - const arrayBuffer = await file.arrayBuffer(); - const uint8Array = new Uint8Array(arrayBuffer); - console.log(`[LibreOffice] File loaded, ${uint8Array.length} bytes`); + async excelToPdf(file: File): Promise { + return this.convertToPdf(file); + } - console.log(`[LibreOffice] Calling converter.convert() with buffer...`); - const startTime = Date.now(); - - // Detect input format - critical for CSV to apply import filters - const ext = file.name.split('.').pop()?.toLowerCase() || ''; - console.log(`[LibreOffice] Detected format from extension: ${ext}`); - - const result = await this.converter.convert(uint8Array, { - outputFormat: 'pdf', - inputFormat: ext as any, // Explicitly specify format for CSV import filters - }, file.name); - - const duration = Date.now() - startTime; - console.log(`[LibreOffice] Conversion complete! Duration: ${duration}ms, Size: ${result.data.length} bytes`); - - // Create a copy to avoid SharedArrayBuffer type issues - const data = new Uint8Array(result.data); - return new Blob([data], { type: result.mimeType }); - } catch (error) { - console.error(`[LibreOffice] Conversion FAILED for ${file.name}:`, error); - console.error(`[LibreOffice] Error details:`, { - message: error instanceof Error ? error.message : String(error), - stack: error instanceof Error ? error.stack : undefined - }); - throw error; - } - } - - async wordToPdf(file: File): Promise { - return this.convertToPdf(file); - } - - async pptToPdf(file: File): Promise { - return this.convertToPdf(file); - } - - async excelToPdf(file: File): Promise { - return this.convertToPdf(file); - } - - async destroy(): Promise { - if (this.converter) { - await this.converter.destroy(); - } - this.converter = null; - this.initialized = false; + async destroy(): Promise { + if (this.converter) { + await this.converter.destroy(); } + this.converter = null; + this.initialized = false; + } } -export function getLibreOfficeConverter(basePath?: string): LibreOfficeConverter { - if (!converterInstance) { - converterInstance = new LibreOfficeConverter(basePath); - } - return converterInstance; +export function getLibreOfficeConverter( + basePath?: string +): LibreOfficeConverter { + if (!converterInstance) { + converterInstance = new LibreOfficeConverter(basePath); + } + return converterInstance; } diff --git a/src/js/utils/markdown-editor.ts b/src/js/utils/markdown-editor.ts index a1a450f..8fbbb25 100644 --- a/src/js/utils/markdown-editor.ts +++ b/src/js/utils/markdown-editor.ts @@ -29,8 +29,14 @@ import taskLists from 'markdown-it-task-lists'; import anchor from 'markdown-it-anchor'; import tocDoneRight from 'markdown-it-toc-done-right'; import { applyTranslations } from '../i18n/i18n'; - - +import type { + WindowWithLucide, + WindowWithI18next, + MarkdownItOptions, + MarkdownItToken, + MarkdownItRendererSelf, + MarkdownItRenderRule, +} from '@/types'; // Register highlight.js languages hljs.registerLanguage('javascript', javascript); @@ -66,18 +72,7 @@ export interface MarkdownEditorOptions { onBack?: () => void; } -export interface MarkdownItOptions { - /** Enable HTML tags in source */ - html: boolean; - /** Convert '\n' in paragraphs into
*/ - breaks: boolean; - /** Autoconvert URL-like text to links */ - linkify: boolean; - /** Enable some language-neutral replacement + quotes beautification */ - typographer: boolean; - /** Highlight function for fenced code blocks */ - highlight?: (str: string, lang: string) => string; -} +export type { MarkdownItOptions } from '@/types'; const DEFAULT_MARKDOWN = `# Welcome to BentoPDF Markdown Editor @@ -277,7 +272,7 @@ export class MarkdownEditor { html: true, breaks: false, linkify: true, - typographer: true + typographer: true, }; constructor(container: HTMLElement, options: MarkdownEditorOptions) { @@ -303,22 +298,36 @@ export class MarkdownEditor { startOnLoad: false, theme: 'default', securityLevel: 'loose', - fontFamily: '-apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif' + fontFamily: + '-apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif', }); this.mermaidInitialized = true; } } private configureLinkRenderer(): void { - // Override link renderer to add target="_blank" and rel="noopener" - const defaultRender = this.md.renderer.rules.link_open || - ((tokens: any[], idx: number, options: any, _env: any, self: any) => self.renderToken(tokens, idx, options)); + const existingRule = this.md.renderer.rules.link_open; + const defaultRender: MarkdownItRenderRule = existingRule + ? (existingRule as unknown as MarkdownItRenderRule) + : ( + tokens: MarkdownItToken[], + idx: number, + options: MarkdownItOptions, + env: unknown, + self: MarkdownItRendererSelf + ) => self.renderToken(tokens, idx, options); - this.md.renderer.rules.link_open = (tokens: any[], idx: number, options: any, env: any, self: any) => { - const token = tokens[idx]; + this.md.renderer.rules.link_open = (tokens, idx, options, env, self) => { + const token = tokens[idx] as unknown as MarkdownItToken; token.attrSet('target', '_blank'); token.attrSet('rel', 'noopener noreferrer'); - return defaultRender(tokens, idx, options, env, self); + return defaultRender( + tokens as unknown as MarkdownItToken[], + idx, + options as unknown as MarkdownItOptions, + env as unknown, + self as unknown as MarkdownItRendererSelf + ); }; } @@ -417,12 +426,11 @@ export class MarkdownEditor { this.applyI18n(); // Initialize Lucide icons - if (typeof (window as any).lucide !== 'undefined') { - (window as any).lucide.createIcons(); + if (typeof (window as WindowWithLucide).lucide !== 'undefined') { + (window as WindowWithLucide).lucide?.createIcons(); } } - private setupEventListeners(): void { // Editor input this.editor?.addEventListener('input', () => { @@ -441,9 +449,13 @@ export class MarkdownEditor { this.editor?.addEventListener('scroll', () => { if (this.syncScroll && !this.isSyncing && this.editor && this.preview) { this.isSyncing = true; - const scrollPercentage = this.editor.scrollTop / (this.editor.scrollHeight - this.editor.clientHeight); - this.preview.scrollTop = scrollPercentage * (this.preview.scrollHeight - this.preview.clientHeight); - setTimeout(() => this.isSyncing = false, 10); + const scrollPercentage = + this.editor.scrollTop / + (this.editor.scrollHeight - this.editor.clientHeight); + this.preview.scrollTop = + scrollPercentage * + (this.preview.scrollHeight - this.preview.clientHeight); + setTimeout(() => (this.isSyncing = false), 10); } }); @@ -451,9 +463,13 @@ export class MarkdownEditor { this.preview?.addEventListener('scroll', () => { if (this.syncScroll && !this.isSyncing && this.editor && this.preview) { this.isSyncing = true; - const scrollPercentage = this.preview.scrollTop / (this.preview.scrollHeight - this.preview.clientHeight); - this.editor.scrollTop = scrollPercentage * (this.editor.scrollHeight - this.editor.clientHeight); - setTimeout(() => this.isSyncing = false, 10); + const scrollPercentage = + this.preview.scrollTop / + (this.preview.scrollHeight - this.preview.clientHeight); + this.editor.scrollTop = + scrollPercentage * + (this.editor.scrollHeight - this.editor.clientHeight); + setTimeout(() => (this.isSyncing = false), 10); } }); @@ -474,22 +490,30 @@ export class MarkdownEditor { }); // Settings modal close - document.getElementById('mdCloseSettings')?.addEventListener('click', () => { - const modal = document.getElementById('mdSettingsModal'); - if (modal) { - modal.style.display = 'none'; - } - }); - - // Close modal on overlay click - document.getElementById('mdSettingsModal')?.addEventListener('click', (e) => { - if ((e.target as HTMLElement).classList.contains('md-editor-modal-overlay')) { + document + .getElementById('mdCloseSettings') + ?.addEventListener('click', () => { const modal = document.getElementById('mdSettingsModal'); if (modal) { modal.style.display = 'none'; } - } - }); + }); + + // Close modal on overlay click + document + .getElementById('mdSettingsModal') + ?.addEventListener('click', (e) => { + if ( + (e.target as HTMLElement).classList.contains( + 'md-editor-modal-overlay' + ) + ) { + const modal = document.getElementById('mdSettingsModal'); + if (modal) { + modal.style.display = 'none'; + } + } + }); // Settings checkboxes document.getElementById('mdOptHtml')?.addEventListener('change', (e) => { @@ -507,10 +531,12 @@ export class MarkdownEditor { this.updateMarkdownIt(); }); - document.getElementById('mdOptTypographer')?.addEventListener('change', (e) => { - this.mdOptions.typographer = (e.target as HTMLInputElement).checked; - this.updateMarkdownIt(); - }); + document + .getElementById('mdOptTypographer') + ?.addEventListener('change', (e) => { + this.mdOptions.typographer = (e.target as HTMLInputElement).checked; + this.updateMarkdownIt(); + }); // Preset selector document.getElementById('mdPreset')?.addEventListener('change', (e) => { @@ -549,7 +575,8 @@ export class MarkdownEditor { const start = this.editor!.selectionStart; const end = this.editor!.selectionEnd; const value = this.editor!.value; - this.editor!.value = value.substring(0, start) + ' ' + value.substring(end); + this.editor!.value = + value.substring(0, start) + ' ' + value.substring(end); this.editor!.selectionStart = this.editor!.selectionEnd = start + 2; this.updatePreview(); } @@ -563,18 +590,37 @@ export class MarkdownEditor { // Update options based on preset if (preset === 'commonmark') { - this.mdOptions = { html: false, breaks: false, linkify: false, typographer: false }; + this.mdOptions = { + html: false, + breaks: false, + linkify: false, + typographer: false, + }; } else if (preset === 'zero') { - this.mdOptions = { html: false, breaks: false, linkify: false, typographer: false }; + this.mdOptions = { + html: false, + breaks: false, + linkify: false, + typographer: false, + }; } else { - this.mdOptions = { html: true, breaks: false, linkify: true, typographer: true }; + this.mdOptions = { + html: true, + breaks: false, + linkify: true, + typographer: true, + }; } // Update UI checkboxes - (document.getElementById('mdOptHtml') as HTMLInputElement).checked = this.mdOptions.html; - (document.getElementById('mdOptBreaks') as HTMLInputElement).checked = this.mdOptions.breaks; - (document.getElementById('mdOptLinkify') as HTMLInputElement).checked = this.mdOptions.linkify; - (document.getElementById('mdOptTypographer') as HTMLInputElement).checked = this.mdOptions.typographer; + (document.getElementById('mdOptHtml') as HTMLInputElement).checked = + this.mdOptions.html; + (document.getElementById('mdOptBreaks') as HTMLInputElement).checked = + this.mdOptions.breaks; + (document.getElementById('mdOptLinkify') as HTMLInputElement).checked = + this.mdOptions.linkify; + (document.getElementById('mdOptTypographer') as HTMLInputElement).checked = + this.mdOptions.typographer; this.updateMarkdownIt(); } @@ -588,7 +634,6 @@ export class MarkdownEditor { } } - private createMarkdownIt(): MarkdownIt { // Use preset if commonmark or zero let md: MarkdownIt; @@ -604,28 +649,31 @@ export class MarkdownEditor { highlight: (str: string, lang: string) => { if (lang && hljs.getLanguage(lang)) { try { - return hljs.highlight(str, { language: lang, ignoreIllegals: true }).value; + return hljs.highlight(str, { + language: lang, + ignoreIllegals: true, + }).value; } catch { // Fall through to default } } return ''; // Use external default escaping - } + }, }); } // Apply plugins only for default preset (plugins may not work well with commonmark/zero) if (this.currentPreset === 'default') { - md.use(sub) // Subscript: ~text~ -> text - .use(sup) // Superscript: ^text^ -> text - .use(footnote) // Footnotes: [^1] and [^1]: footnote text - .use(deflist) // Definition lists - .use(abbr) // Abbreviations: *[abbr]: full text - .use(emoji) // Emoji: :smile: -> 😄 - .use(ins) // Inserted text: ++text++ -> text - .use(mark) // Marked text: ==text== -> text - .use(taskLists, { enabled: true, label: true, labelAfter: true }) // Task lists: - [x] done - .use(anchor, { permalink: false }) // Header anchors + md.use(sub) // Subscript: ~text~ -> text + .use(sup) // Superscript: ^text^ -> text + .use(footnote) // Footnotes: [^1] and [^1]: footnote text + .use(deflist) // Definition lists + .use(abbr) // Abbreviations: *[abbr]: full text + .use(emoji) // Emoji: :smile: -> 😄 + .use(ins) // Inserted text: ++text++ -> text + .use(mark) // Marked text: ==text== -> text + .use(taskLists, { enabled: true, label: true, labelAfter: true }) // Task lists: - [x] done + .use(anchor, { permalink: false }) // Header anchors .use(tocDoneRight); // Table of contents: ${toc} } @@ -650,7 +698,9 @@ export class MarkdownEditor { private async renderMermaidDiagrams(): Promise { if (!this.preview) return; - const mermaidBlocks = this.preview.querySelectorAll('pre > code.language-mermaid'); + const mermaidBlocks = this.preview.querySelectorAll( + 'pre > code.language-mermaid' + ); for (let i = 0; i < mermaidBlocks.length; i++) { const block = mermaidBlocks[i] as HTMLElement; @@ -948,14 +998,16 @@ ${content} applyTranslations(); // Special handling for select options (data-i18n on options doesn't work with applyTranslations) - const presetSelect = document.getElementById('mdPreset') as HTMLSelectElement; + const presetSelect = document.getElementById( + 'mdPreset' + ) as HTMLSelectElement; if (presetSelect) { const options = presetSelect.querySelectorAll('option[data-i18n]'); options.forEach((option) => { const key = option.getAttribute('data-i18n'); if (key) { // Use i18next directly for option text - const translated = (window as any).i18next?.t(key); + const translated = (window as WindowWithI18next).i18next?.t(key); if (translated && translated !== key) { option.textContent = translated; } diff --git a/src/js/utils/page-preview.ts b/src/js/utils/page-preview.ts index a0bac06..678397f 100644 --- a/src/js/utils/page-preview.ts +++ b/src/js/utils/page-preview.ts @@ -154,7 +154,7 @@ document.addEventListener('keydown', handleKeydown); export function initPagePreview( container: HTMLElement, pdfjsDoc: PDFDocumentProxy, - options: { pageAttr?: string } = {} + _options: { pageAttr?: string } = {} ): void { const totalPages = pdfjsDoc.numPages; diff --git a/src/js/utils/pdf-decrypt.ts b/src/js/utils/pdf-decrypt.ts index 44caf80..f5237eb 100644 --- a/src/js/utils/pdf-decrypt.ts +++ b/src/js/utils/pdf-decrypt.ts @@ -1,5 +1,6 @@ import { getCpdf, isCpdfAvailable } from './cpdf-helper'; import { isPyMuPDFAvailable, loadPyMuPDF } from './pymupdf-loader'; +import type { CpdfInstance } from '@/types'; export type PdfDecryptEngine = 'cpdf' | 'pymupdf'; @@ -37,13 +38,13 @@ function copyBytes(bytes: Uint8Array): Uint8Array { return Uint8Array.from(bytes); } -function cleanupCpdfDocument(cpdf: unknown, pdf: unknown): void { - if (!cpdf || !pdf || typeof cpdf !== 'object' || !('deletePdf' in cpdf)) { +function cleanupCpdfDocument(cpdf: CpdfInstance, pdf: unknown): void { + if (!cpdf || !pdf) { return; } try { - (cpdf as { deletePdf: (document: unknown) => void }).deletePdf(pdf); + cpdf.deletePdf(pdf); } catch (cleanupError) { console.warn( `${DECRYPT_LOG_PREFIX} Failed to cleanup CoherentPDF document: ${normalizeErrorMessage(cleanupError)}` diff --git a/src/js/utils/pymupdf-loader.ts b/src/js/utils/pymupdf-loader.ts index 58b74ba..a424383 100644 --- a/src/js/utils/pymupdf-loader.ts +++ b/src/js/utils/pymupdf-loader.ts @@ -1,25 +1,34 @@ import { WasmProvider } from './wasm-provider.js'; +import type { + PyMuPDFInstance, + PyMuPDFCompressOptions, + PyMuPDFExtractTextOptions, + PyMuPDFRasterizeOptions, +} from '@/types'; -let cachedPyMuPDF: any = null; -let loadPromise: Promise | null = null; +let cachedPyMuPDF: PyMuPDFInstance | null = null; +let loadPromise: Promise | null = null; export interface PyMuPDFInterface { load(): Promise; compressPdf( file: Blob, - options: any + options: PyMuPDFCompressOptions ): Promise<{ blob: Blob; compressedSize: number }>; convertToPdf(file: Blob, ext: string): Promise; - extractText(file: Blob, options?: any): Promise; + extractText(file: Blob, options?: PyMuPDFExtractTextOptions): Promise; extractImages(file: Blob): Promise>; - extractTables(file: Blob): Promise; + extractTables(file: Blob): Promise; toSvg(file: Blob, pageNum: number): Promise; renderPageToImage(file: Blob, pageNum: number, scale: number): Promise; getPageCount(file: Blob): Promise; - rasterizePdf(file: Blob | File, options: any): Promise; + rasterizePdf( + file: Blob | File, + options: PyMuPDFRasterizeOptions + ): Promise; } -export async function loadPyMuPDF(): Promise { +export async function loadPyMuPDF(): Promise { if (cachedPyMuPDF) { return cachedPyMuPDF; } @@ -65,9 +74,10 @@ export async function loadPyMuPDF(): Promise { console.log('[PyMuPDF Loader] Successfully loaded from CDN'); return cachedPyMuPDF; - } catch (error: any) { + } catch (error: unknown) { loadPromise = null; - throw new Error(`Failed to load PyMuPDF from CDN: ${error.message}`, { + const msg = error instanceof Error ? error.message : String(error); + throw new Error(`Failed to load PyMuPDF from CDN: ${msg}`, { cause: error, }); } diff --git a/src/js/utils/sanitize.ts b/src/js/utils/sanitize.ts index 6432b86..4fad95b 100644 --- a/src/js/utils/sanitize.ts +++ b/src/js/utils/sanitize.ts @@ -1,5 +1,34 @@ -import { PDFDocument, PDFName } from 'pdf-lib'; +import { PDFDocument, PDFName, PDFDict, PDFArray } from 'pdf-lib'; import { loadPdfDocument } from './load-pdf-document.js'; +import type { PDFDocumentInternal } from '@/types'; + +function getCatalogDict(pdfDoc: PDFDocument): PDFDict { + return pdfDoc.catalog; +} + +function lookupAsDict( + pdfDoc: PDFDocument, + ref: ReturnType +): PDFDict | undefined { + if (!ref) return undefined; + const result = pdfDoc.context.lookup(ref); + if (result instanceof PDFDict) return result; + return undefined; +} + +function lookupAsArray( + pdfDoc: PDFDocument, + ref: ReturnType +): PDFArray | undefined { + if (!ref) return undefined; + const result = pdfDoc.context.lookup(ref); + if (result instanceof PDFArray) return result; + return undefined; +} + +function getErrorMessage(e: unknown): string { + return e instanceof Error ? e.message : String(e); +} export interface SanitizeOptions { flattenForms: boolean; @@ -28,9 +57,9 @@ export const defaultSanitizeOptions: SanitizeOptions = { }; function removeMetadataFromDoc(pdfDoc: PDFDocument) { - const infoDict = (pdfDoc as any).getInfoDict(); + const infoDict = (pdfDoc as unknown as PDFDocumentInternal).getInfoDict(); const allKeys = infoDict.keys(); - allKeys.forEach((key: any) => { + allKeys.forEach((key: PDFName) => { infoDict.delete(key); }); @@ -42,30 +71,30 @@ function removeMetadataFromDoc(pdfDoc: PDFDocument) { pdfDoc.setProducer(''); try { - const catalogDict = (pdfDoc.catalog as any).dict; + const catalogDict = getCatalogDict(pdfDoc); if (catalogDict.has(PDFName.of('Metadata'))) { catalogDict.delete(PDFName.of('Metadata')); } - } catch (e: any) { - console.warn('Could not remove XMP metadata:', e.message); + } catch (e: unknown) { + console.warn('Could not remove XMP metadata:', getErrorMessage(e)); } try { const context = pdfDoc.context; - if ((context as any).trailerInfo) { - delete (context as any).trailerInfo.ID; + if (context.trailerInfo) { + delete context.trailerInfo.ID; } - } catch (e: any) { - console.warn('Could not remove document IDs:', e.message); + } catch (e: unknown) { + console.warn('Could not remove document IDs:', getErrorMessage(e)); } try { - const catalogDict = (pdfDoc.catalog as any).dict; + const catalogDict = getCatalogDict(pdfDoc); if (catalogDict.has(PDFName.of('PieceInfo'))) { catalogDict.delete(PDFName.of('PieceInfo')); } - } catch (e: any) { - console.warn('Could not remove PieceInfo:', e.message); + } catch (e: unknown) { + console.warn('Could not remove PieceInfo:', getErrorMessage(e)); } } @@ -74,8 +103,11 @@ function removeAnnotationsFromDoc(pdfDoc: PDFDocument) { for (const page of pages) { try { page.node.delete(PDFName.of('Annots')); - } catch (e: any) { - console.warn('Could not remove annotations from page:', e.message); + } catch (e: unknown) { + console.warn( + 'Could not remove annotations from page:', + getErrorMessage(e) + ); } } } @@ -86,21 +118,22 @@ function flattenFormsInDoc(pdfDoc: PDFDocument) { } function removeJavascriptFromDoc(pdfDoc: PDFDocument) { - if ((pdfDoc as any).javaScripts && (pdfDoc as any).javaScripts.length > 0) { - (pdfDoc as any).javaScripts = []; + const pdfDocInternal = pdfDoc as unknown as PDFDocumentInternal; + if (pdfDocInternal.javaScripts && pdfDocInternal.javaScripts.length > 0) { + pdfDocInternal.javaScripts = []; } - const catalogDict = (pdfDoc.catalog as any).dict; + const catalogDict = getCatalogDict(pdfDoc); const namesRef = catalogDict.get(PDFName.of('Names')); if (namesRef) { try { - const namesDict = pdfDoc.context.lookup(namesRef) as any; - if (namesDict.has(PDFName.of('JavaScript'))) { + const namesDict = lookupAsDict(pdfDoc, namesRef); + if (namesDict?.has(PDFName.of('JavaScript'))) { namesDict.delete(PDFName.of('JavaScript')); } - } catch (e: any) { - console.warn('Could not access Names/JavaScript:', e.message); + } catch (e: unknown) { + console.warn('Could not access Names/JavaScript:', getErrorMessage(e)); } } @@ -124,50 +157,55 @@ function removeJavascriptFromDoc(pdfDoc: PDFDocument) { const annotRefs = pageDict.Annots()?.asArray() || []; for (const annotRef of annotRefs) { try { - const annot = pdfDoc.context.lookup(annotRef) as any; + const annot = lookupAsDict(pdfDoc, annotRef); + if (!annot) continue; if (annot.has(PDFName.of('A'))) { const actionRef = annot.get(PDFName.of('A')); try { - const actionDict = pdfDoc.context.lookup(actionRef) as any; + const actionDict = lookupAsDict(pdfDoc, actionRef); const actionType = actionDict - .get(PDFName.of('S')) + ?.get(PDFName.of('S')) ?.toString() .substring(1); if (actionType === 'JavaScript') { annot.delete(PDFName.of('A')); } - } catch (e: any) { - console.warn('Could not read action:', e.message); + } catch (e: unknown) { + console.warn('Could not read action:', getErrorMessage(e)); } } if (annot.has(PDFName.of('AA'))) { annot.delete(PDFName.of('AA')); } - } catch (e: any) { - console.warn('Could not process annotation for JS:', e.message); + } catch (e: unknown) { + console.warn( + 'Could not process annotation for JS:', + getErrorMessage(e) + ); } } - } catch (e: any) { - console.warn('Could not remove page actions:', e.message); + } catch (e: unknown) { + console.warn('Could not remove page actions:', getErrorMessage(e)); } } try { const acroFormRef = catalogDict.get(PDFName.of('AcroForm')); if (acroFormRef) { - const acroFormDict = pdfDoc.context.lookup(acroFormRef) as any; - const fieldsRef = acroFormDict.get(PDFName.of('Fields')); + const acroFormDict = lookupAsDict(pdfDoc, acroFormRef); + const fieldsRef = acroFormDict?.get(PDFName.of('Fields')); if (fieldsRef) { - const fieldsArray = pdfDoc.context.lookup(fieldsRef) as any; - const fields = fieldsArray.asArray(); + const fieldsArray = lookupAsArray(pdfDoc, fieldsRef); + const fields = fieldsArray?.asArray() || []; for (const fieldRef of fields) { try { - const field = pdfDoc.context.lookup(fieldRef) as any; + const field = lookupAsDict(pdfDoc, fieldRef); + if (!field) continue; if (field.has(PDFName.of('A'))) { field.delete(PDFName.of('A')); @@ -176,29 +214,29 @@ function removeJavascriptFromDoc(pdfDoc: PDFDocument) { if (field.has(PDFName.of('AA'))) { field.delete(PDFName.of('AA')); } - } catch (e: any) { - console.warn('Could not process field for JS:', e.message); + } catch (e: unknown) { + console.warn('Could not process field for JS:', getErrorMessage(e)); } } } } - } catch (e: any) { - console.warn('Could not process form fields for JS:', e.message); + } catch (e: unknown) { + console.warn('Could not process form fields for JS:', getErrorMessage(e)); } } function removeEmbeddedFilesFromDoc(pdfDoc: PDFDocument) { - const catalogDict = (pdfDoc.catalog as any).dict; + const catalogDict = getCatalogDict(pdfDoc); const namesRef = catalogDict.get(PDFName.of('Names')); if (namesRef) { try { - const namesDict = pdfDoc.context.lookup(namesRef) as any; - if (namesDict.has(PDFName.of('EmbeddedFiles'))) { + const namesDict = lookupAsDict(pdfDoc, namesRef); + if (namesDict?.has(PDFName.of('EmbeddedFiles'))) { namesDict.delete(PDFName.of('EmbeddedFiles')); } - } catch (e: any) { - console.warn('Could not access Names/EmbeddedFiles:', e.message); + } catch (e: unknown) { + console.warn('Could not access Names/EmbeddedFiles:', getErrorMessage(e)); } } @@ -214,16 +252,16 @@ function removeEmbeddedFilesFromDoc(pdfDoc: PDFDocument) { for (const ref of annotRefs) { try { - const annot = pdfDoc.context.lookup(ref) as any; + const annot = lookupAsDict(pdfDoc, ref); const subtype = annot - .get(PDFName.of('Subtype')) + ?.get(PDFName.of('Subtype')) ?.toString() .substring(1); if (subtype !== 'FileAttachment') { annotsToKeep.push(ref); } - } catch (e) { + } catch { annotsToKeep.push(ref); } } @@ -236,18 +274,16 @@ function removeEmbeddedFilesFromDoc(pdfDoc: PDFDocument) { page.node.delete(PDFName.of('Annots')); } } - } catch (pageError: any) { + } catch (pageError: unknown) { console.warn( - `Could not process page for attachments: ${pageError.message}` + `Could not process page for attachments: ${getErrorMessage(pageError)}` ); } } - if ( - (pdfDoc as any).embeddedFiles && - (pdfDoc as any).embeddedFiles.length > 0 - ) { - (pdfDoc as any).embeddedFiles = []; + const pdfDocInternal = pdfDoc as unknown as PDFDocumentInternal; + if (pdfDocInternal.embeddedFiles && pdfDocInternal.embeddedFiles.length > 0) { + pdfDocInternal.embeddedFiles = []; } if (catalogDict.has(PDFName.of('Collection'))) { @@ -256,7 +292,7 @@ function removeEmbeddedFilesFromDoc(pdfDoc: PDFDocument) { } function removeLayersFromDoc(pdfDoc: PDFDocument) { - const catalogDict = (pdfDoc.catalog as any).dict; + const catalogDict = getCatalogDict(pdfDoc); if (catalogDict.has(PDFName.of('OCProperties'))) { catalogDict.delete(PDFName.of('OCProperties')); @@ -274,16 +310,16 @@ function removeLayersFromDoc(pdfDoc: PDFDocument) { const resourcesRef = pageDict.get(PDFName.of('Resources')); if (resourcesRef) { try { - const resourcesDict = pdfDoc.context.lookup(resourcesRef) as any; - if (resourcesDict.has(PDFName.of('Properties'))) { + const resourcesDict = lookupAsDict(pdfDoc, resourcesRef); + if (resourcesDict?.has(PDFName.of('Properties'))) { resourcesDict.delete(PDFName.of('Properties')); } - } catch (e: any) { - console.warn('Could not access Resources:', e.message); + } catch (e: unknown) { + console.warn('Could not access Resources:', getErrorMessage(e)); } } - } catch (e: any) { - console.warn('Could not remove page layers:', e.message); + } catch (e: unknown) { + console.warn('Could not remove page layers:', getErrorMessage(e)); } } } @@ -299,8 +335,9 @@ function removeLinksFromDoc(pdfDoc: PDFDocument) { const annotsRef = pageDict.get(PDFName.of('Annots')); if (!annotsRef) continue; - const annotsArray = pdfDoc.context.lookup(annotsRef) as any; - const annotRefs = annotsArray.asArray(); + const annotsArrayObj = lookupAsArray(pdfDoc, annotsRef); + if (!annotsArrayObj) continue; + const annotRefs = annotsArrayObj.asArray(); if (annotRefs.length === 0) continue; @@ -309,7 +346,11 @@ function removeLinksFromDoc(pdfDoc: PDFDocument) { for (const ref of annotRefs) { try { - const annot = pdfDoc.context.lookup(ref) as any; + const annot = lookupAsDict(pdfDoc, ref); + if (!annot) { + annotsToKeep.push(ref); + continue; + } const subtype = annot .get(PDFName.of('Subtype')) ?.toString() @@ -324,9 +365,9 @@ function removeLinksFromDoc(pdfDoc: PDFDocument) { const actionRef = annot.get(PDFName.of('A')); if (actionRef) { try { - const actionDict = pdfDoc.context.lookup(actionRef) as any; + const actionDict = lookupAsDict(pdfDoc, actionRef); const actionType = actionDict - .get(PDFName.of('S')) + ?.get(PDFName.of('S')) ?.toString() .substring(1); @@ -339,8 +380,8 @@ function removeLinksFromDoc(pdfDoc: PDFDocument) { isLink = true; linksRemoved++; } - } catch (e: any) { - console.warn('Could not read action:', e.message); + } catch (e: unknown) { + console.warn('Could not read action:', getErrorMessage(e)); } } @@ -354,8 +395,8 @@ function removeLinksFromDoc(pdfDoc: PDFDocument) { if (!isLink) { annotsToKeep.push(ref); } - } catch (e: any) { - console.warn('Could not process annotation:', e.message); + } catch (e: unknown) { + console.warn('Could not process annotation:', getErrorMessage(e)); annotsToKeep.push(ref); } } @@ -368,37 +409,37 @@ function removeLinksFromDoc(pdfDoc: PDFDocument) { pageDict.delete(PDFName.of('Annots')); } } - } catch (pageError: any) { + } catch (pageError: unknown) { console.warn( - `Could not process page ${pageIndex + 1} for links: ${pageError.message}` + `Could not process page ${pageIndex + 1} for links: ${getErrorMessage(pageError)}` ); } } try { - const catalogDict = (pdfDoc.catalog as any).dict; + const catalogDict = getCatalogDict(pdfDoc); const namesRef = catalogDict.get(PDFName.of('Names')); if (namesRef) { try { - const namesDict = pdfDoc.context.lookup(namesRef) as any; - if (namesDict.has(PDFName.of('Dests'))) { + const namesDict = lookupAsDict(pdfDoc, namesRef); + if (namesDict?.has(PDFName.of('Dests'))) { namesDict.delete(PDFName.of('Dests')); } - } catch (e: any) { - console.warn('Could not access Names/Dests:', e.message); + } catch (e: unknown) { + console.warn('Could not access Names/Dests:', getErrorMessage(e)); } } if (catalogDict.has(PDFName.of('Dests'))) { catalogDict.delete(PDFName.of('Dests')); } - } catch (e: any) { - console.warn('Could not remove named destinations:', e.message); + } catch (e: unknown) { + console.warn('Could not remove named destinations:', getErrorMessage(e)); } } function removeStructureTreeFromDoc(pdfDoc: PDFDocument) { - const catalogDict = (pdfDoc.catalog as any).dict; + const catalogDict = getCatalogDict(pdfDoc); if (catalogDict.has(PDFName.of('StructTreeRoot'))) { catalogDict.delete(PDFName.of('StructTreeRoot')); @@ -411,8 +452,8 @@ function removeStructureTreeFromDoc(pdfDoc: PDFDocument) { if (pageDict.has(PDFName.of('StructParents'))) { pageDict.delete(PDFName.of('StructParents')); } - } catch (e: any) { - console.warn('Could not remove page StructParents:', e.message); + } catch (e: unknown) { + console.warn('Could not remove page StructParents:', getErrorMessage(e)); } } @@ -422,7 +463,7 @@ function removeStructureTreeFromDoc(pdfDoc: PDFDocument) { } function removeMarkInfoFromDoc(pdfDoc: PDFDocument) { - const catalogDict = (pdfDoc.catalog as any).dict; + const catalogDict = getCatalogDict(pdfDoc); if (catalogDict.has(PDFName.of('MarkInfo'))) { catalogDict.delete(PDFName.of('MarkInfo')); @@ -444,29 +485,29 @@ function removeFontsFromDoc(pdfDoc: PDFDocument) { if (resourcesRef) { try { - const resourcesDict = pdfDoc.context.lookup(resourcesRef) as any; + const resourcesDict = lookupAsDict(pdfDoc, resourcesRef); + if (!resourcesDict) continue; if (resourcesDict.has(PDFName.of('Font'))) { const fontRef = resourcesDict.get(PDFName.of('Font')); try { - const fontDict = pdfDoc.context.lookup(fontRef) as any; + const fontDict = lookupAsDict(pdfDoc, fontRef); + if (!fontDict) continue; const fontKeys = fontDict.keys(); for (const fontKey of fontKeys) { try { const specificFontRef = fontDict.get(fontKey); - const specificFont = pdfDoc.context.lookup( - specificFontRef - ) as any; + const specificFont = lookupAsDict(pdfDoc, specificFontRef); + if (!specificFont) continue; if (specificFont.has(PDFName.of('FontDescriptor'))) { const descriptorRef = specificFont.get( PDFName.of('FontDescriptor') ); - const descriptor = pdfDoc.context.lookup( - descriptorRef - ) as any; + const descriptor = lookupAsDict(pdfDoc, descriptorRef); + if (!descriptor) continue; const fontFileKeys = ['FontFile', 'FontFile2', 'FontFile3']; for (const key of fontFileKeys) { @@ -475,28 +516,38 @@ function removeFontsFromDoc(pdfDoc: PDFDocument) { } } } - } catch (e: any) { - console.warn(`Could not process font ${fontKey}:`, e.message); + } catch (e: unknown) { + console.warn( + `Could not process font ${fontKey}:`, + getErrorMessage(e) + ); } } - } catch (e: any) { - console.warn('Could not access font dictionary:', e.message); + } catch (e: unknown) { + console.warn( + 'Could not access font dictionary:', + getErrorMessage(e) + ); } } - } catch (e: any) { - console.warn('Could not access Resources for fonts:', e.message); + } catch (e: unknown) { + console.warn( + 'Could not access Resources for fonts:', + getErrorMessage(e) + ); } } - } catch (e: any) { + } catch (e: unknown) { console.warn( `Could not remove fonts from page ${pageIndex + 1}:`, - e.message + getErrorMessage(e) ); } } - if ((pdfDoc as any).fonts && (pdfDoc as any).fonts.length > 0) { - (pdfDoc as any).fonts = []; + const pdfDocInternal = pdfDoc as unknown as PDFDocumentInternal; + if (pdfDocInternal.fonts && pdfDocInternal.fonts.length > 0) { + pdfDocInternal.fonts = []; } } @@ -509,15 +560,18 @@ export async function sanitizePdf( if (options.flattenForms) { try { flattenFormsInDoc(pdfDoc); - } catch (e: any) { - console.warn(`Could not flatten forms: ${e.message}`); + } catch (e: unknown) { + console.warn(`Could not flatten forms: ${getErrorMessage(e)}`); try { - const catalogDict = (pdfDoc.catalog as any).dict; + const catalogDict = getCatalogDict(pdfDoc); if (catalogDict.has(PDFName.of('AcroForm'))) { catalogDict.delete(PDFName.of('AcroForm')); } - } catch (removeError: any) { - console.warn('Could not remove AcroForm:', removeError.message); + } catch (removeError: unknown) { + console.warn( + 'Could not remove AcroForm:', + getErrorMessage(removeError) + ); } } } @@ -533,56 +587,56 @@ export async function sanitizePdf( if (options.removeJavascript) { try { removeJavascriptFromDoc(pdfDoc); - } catch (e: any) { - console.warn(`Could not remove JavaScript: ${e.message}`); + } catch (e: unknown) { + console.warn(`Could not remove JavaScript: ${getErrorMessage(e)}`); } } if (options.removeEmbeddedFiles) { try { removeEmbeddedFilesFromDoc(pdfDoc); - } catch (e: any) { - console.warn(`Could not remove embedded files: ${e.message}`); + } catch (e: unknown) { + console.warn(`Could not remove embedded files: ${getErrorMessage(e)}`); } } if (options.removeLayers) { try { removeLayersFromDoc(pdfDoc); - } catch (e: any) { - console.warn(`Could not remove layers: ${e.message}`); + } catch (e: unknown) { + console.warn(`Could not remove layers: ${getErrorMessage(e)}`); } } if (options.removeLinks) { try { removeLinksFromDoc(pdfDoc); - } catch (e: any) { - console.warn(`Could not remove links: ${e.message}`); + } catch (e: unknown) { + console.warn(`Could not remove links: ${getErrorMessage(e)}`); } } if (options.removeStructureTree) { try { removeStructureTreeFromDoc(pdfDoc); - } catch (e: any) { - console.warn(`Could not remove structure tree: ${e.message}`); + } catch (e: unknown) { + console.warn(`Could not remove structure tree: ${getErrorMessage(e)}`); } } if (options.removeMarkInfo) { try { removeMarkInfoFromDoc(pdfDoc); - } catch (e: any) { - console.warn(`Could not remove MarkInfo: ${e.message}`); + } catch (e: unknown) { + console.warn(`Could not remove MarkInfo: ${getErrorMessage(e)}`); } } if (options.removeFonts) { try { removeFontsFromDoc(pdfDoc); - } catch (e: any) { - console.warn(`Could not remove fonts: ${e.message}`); + } catch (e: unknown) { + console.warn(`Could not remove fonts: ${getErrorMessage(e)}`); } } diff --git a/src/js/utils/wasm-preloader.ts b/src/js/utils/wasm-preloader.ts index 818e281..451874d 100644 --- a/src/js/utils/wasm-preloader.ts +++ b/src/js/utils/wasm-preloader.ts @@ -1,4 +1,3 @@ -import { getLibreOfficeConverter } from './libreoffice-loader.js'; import { isWasmAvailable, getWasmBaseUrl } from '../config/wasm-cdn-config.js'; export enum PreloadStatus { @@ -77,7 +76,7 @@ async function preloadGhostscript(): Promise { await import('./ghostscript-loader.js'); const gsModule = await loadGsModule(); - setCachedGsModule(gsModule as any); + setCachedGsModule(gsModule); preloadState.ghostscript = PreloadStatus.READY; console.log('[Preloader] Ghostscript WASM ready'); } catch (e) { diff --git a/src/js/workflow/editor.ts b/src/js/workflow/editor.ts index 0725660..f702275 100644 --- a/src/js/workflow/editor.ts +++ b/src/js/workflow/editor.ts @@ -10,7 +10,6 @@ import { DataflowEngine } from 'rete-engine'; import type { DataflowEngineScheme } from 'rete-engine'; import { LitElement, html } from 'lit'; import type { BaseWorkflowNode } from './nodes/base-node'; -// @ts-ignore -- Vite ?inline import for injecting into Shadow DOM import phosphorCSS from '@phosphor-icons/web/regular?inline'; // Shared stylesheet for Phosphor icons (font-face already loaded globally, strip it) @@ -305,8 +304,8 @@ export async function createWorkflowEditor( // Override connection path to use vertical bezier curves (top-to-bottom flow) litPlugin.addPipe((context) => { - if ((context as any).type === 'connectionpath') { - const { points } = (context as any).data; + if ('type' in context && context.type === 'connectionpath') { + const { points } = context.data; const [start, end] = points as [ { x: number; y: number }, { x: number; y: number }, @@ -318,7 +317,7 @@ export async function createWorkflowEditor( const path = `M ${start.x} ${start.y} C ${start.x} ${start.y + dy} ${end.x} ${end.y - dy} ${end.x} ${end.y}`; return { ...context, - data: { ...(context as any).data, path }, + data: { ...context.data, path }, } as typeof context; } return context; diff --git a/src/js/workflow/serialization.ts b/src/js/workflow/serialization.ts index 2b15edb..e184023 100644 --- a/src/js/workflow/serialization.ts +++ b/src/js/workflow/serialization.ts @@ -4,26 +4,15 @@ import type { ClassicScheme, LitArea2D } from '@retejs/lit-plugin'; import type { BaseWorkflowNode } from './nodes/base-node'; import { createNodeByType } from './nodes/registry'; import { ClassicPreset } from 'rete'; -import type { SerializedWorkflow } from './types'; +import type { + SerializedWorkflow, + SerializedNode, + SerializedConnection, +} from './types'; import { WORKFLOW_VERSION } from './types'; type AreaExtra = LitArea2D; -interface SerializedNode { - id: string; - type: string; - position: { x: number; y: number }; - controls: Record; -} - -interface SerializedConnection { - id: string; - source: string; - sourceOutput: string; - target: string; - targetInput: string; -} - function getNodeType(node: BaseWorkflowNode): string | null { return node.nodeType || null; } @@ -78,19 +67,15 @@ async function deserializeWorkflow( editor: NodeEditor, area: AreaPlugin ): Promise { - if ( - !data || - !Array.isArray((data as any).nodes) || - !Array.isArray((data as any).connections) - ) { + if (!data || !Array.isArray(data.nodes) || !Array.isArray(data.connections)) { throw new Error( 'Invalid workflow file: missing nodes or connections array.' ); } - if ((data as any).version !== WORKFLOW_VERSION) { + if (data.version !== WORKFLOW_VERSION) { console.warn( - `Workflow version mismatch: expected ${WORKFLOW_VERSION}, got ${(data as any).version}. Attempting load anyway.` + `Workflow version mismatch: expected ${WORKFLOW_VERSION}, got ${data.version}. Attempting load anyway.` ); } @@ -104,7 +89,7 @@ async function deserializeWorkflow( const idMap = new Map(); const skippedTypes: string[] = []; - for (const serializedNode of (data as any).nodes) { + for (const serializedNode of data.nodes) { const node = createNodeByType(serializedNode.type); if (!node) { skippedTypes.push(serializedNode.type); @@ -114,17 +99,17 @@ async function deserializeWorkflow( for (const [key, value] of Object.entries(serializedNode.controls || {})) { const control = node.controls[key]; if (control && 'value' in control) { - (control as any).value = value; + (control as { value: unknown }).value = value; } } - await editor.addNode(node as any); + await editor.addNode(node as ClassicScheme['Node']); idMap.set(serializedNode.id, node.id); await area.translate(node.id, serializedNode.position); } - for (const serializedConn of (data as any).connections) { + for (const serializedConn of data.connections) { const sourceId = idMap.get(serializedConn.source); const targetId = idMap.get(serializedConn.target); if (!sourceId || !targetId) continue; @@ -139,7 +124,7 @@ async function deserializeWorkflow( targetNode, serializedConn.targetInput ); - await editor.addConnection(conn as any); + await editor.addConnection(conn as ClassicScheme['Connection']); } if (skippedTypes.length > 0) { diff --git a/src/tests/form-creator-extraction.test.ts b/src/tests/form-creator-extraction.test.ts index d2ff5c8..ae463a5 100644 --- a/src/tests/form-creator-extraction.test.ts +++ b/src/tests/form-creator-extraction.test.ts @@ -285,7 +285,7 @@ describe('form creator extraction regression', () => { }); it('skips fields whose widgets cannot be resolved to any page', async () => { - const { pdfDoc, page1Field, page2Field } = await buildTwoPageTextFieldPdf(); + const { pdfDoc, page2Field } = await buildTwoPageTextFieldPdf(); const page2Widget = page2Field.acroField.getWidgets()[0]; const page2WidgetRef = getWidgetRef(page2Widget, pdfDoc); diff --git a/src/tests/hocr-transform.test.ts b/src/tests/hocr-transform.test.ts index 844ce0a..0c6e031 100644 --- a/src/tests/hocr-transform.test.ts +++ b/src/tests/hocr-transform.test.ts @@ -9,6 +9,7 @@ import { calculateWordTransform, calculateSpaceTransform, } from '../js/utils/hocr-transform'; +import type { OcrWord } from '@/types'; describe('hocr-transform', () => { describe('parseBBox', () => { @@ -210,7 +211,7 @@ describe('hocr-transform', () => { bbox: { x0: 0, y0: 100, x1: 500, y1: 130 }, baseline: { slope: 0, intercept: 0 }, textangle: 0, - words: [], + words: [] as OcrWord[], direction: 'ltr' as const, injectWordBreaks: true, }; @@ -302,7 +303,7 @@ describe('hocr-transform', () => { bbox: { x0: 0, y0: 100, x1: 500, y1: 130 }, baseline: { slope: 0, intercept: 0 }, textangle: 0, - words: [], + words: [] as OcrWord[], direction: 'ltr' as const, injectWordBreaks: true, }; diff --git a/src/tests/image-effects.test.ts b/src/tests/image-effects.test.ts index 5850335..4aa7443 100644 --- a/src/tests/image-effects.test.ts +++ b/src/tests/image-effects.test.ts @@ -89,7 +89,7 @@ describe('image-effects', () => { }); it('should handle light colors (l > 0.5)', () => { - const [h, s, l] = rgbToHsl(255, 128, 128); + const [_h, s, l] = rgbToHsl(255, 128, 128); expect(l).toBeGreaterThan(0.5); expect(s).toBeGreaterThan(0); }); diff --git a/src/tests/state.test.ts b/src/tests/state.test.ts index 59ac5d5..f24360d 100644 --- a/src/tests/state.test.ts +++ b/src/tests/state.test.ts @@ -21,7 +21,9 @@ describe('State Management', () => { // 1. Modify the state properties to non-default values state.activeTool = 'merge'; state.files = [{ name: 'dummy.pdf', size: 1234 } as File]; - state.pdfDoc = { numPages: 5 }; // Mock PDF document object + state.pdfDoc = { + numPages: 5, + } as unknown as import('pdf-lib').PDFDocument; state.pdfPages = [{}, {}]; // Mock page objects state.currentPdfUrl = 'blob:http://localhost/some-uuid'; diff --git a/src/tests/timestamp-pdf-page.test.ts b/src/tests/timestamp-pdf-page.test.ts index dcc0cda..0a3821d 100644 --- a/src/tests/timestamp-pdf-page.test.ts +++ b/src/tests/timestamp-pdf-page.test.ts @@ -1,4 +1,4 @@ -import { describe, it, expect, beforeEach, vi } from 'vitest'; +import { describe, it, expect, beforeEach } from 'vitest'; import { TIMESTAMP_TSA_PRESETS } from '@/js/config/timestamp-tsa'; /** diff --git a/src/tests/tools.test.ts b/src/tests/tools.test.ts index f2d9704..b88ef34 100644 --- a/src/tests/tools.test.ts +++ b/src/tests/tools.test.ts @@ -23,10 +23,12 @@ describe('Tool Categories Configuration', () => { // **KEY CHANGE**: This test now ensures IDs are unique only WITHIN this specific category. it('should not contain any duplicate tool IDs within its own list', () => { const toolIds = category.tools.map((tool) => { - if ('id' in tool) return (tool as any).id; + if ('id' in tool) return tool.id; if ('href' in tool) { - const match = (tool as any).href.match(/\/([^/]+)\.html$/); - return match ? match[1] : (tool as any).href; + const match = (tool as { href: string }).href.match( + /\/([^/]+)\.html$/ + ); + return match ? match[1] : (tool as { href: string }).href; } return 'unknown'; }); diff --git a/src/tests/watermark.test.ts b/src/tests/watermark.test.ts index 8100760..b524c23 100644 --- a/src/tests/watermark.test.ts +++ b/src/tests/watermark.test.ts @@ -61,7 +61,7 @@ describe('Watermark Feature', () => { }); it('should reset pdfDoc to null', () => { - state.pdfDoc = {} as any; + state.pdfDoc = {} as unknown as typeof state.pdfDoc; resetState(); @@ -69,7 +69,7 @@ describe('Watermark Feature', () => { }); it('should reset pdfPages array', () => { - state.pdfPages = [1, 2, 3] as any; + state.pdfPages = [1, 2, 3] as unknown as typeof state.pdfPages; resetState(); diff --git a/src/types/markdown-it-plugins.d.ts b/src/types/markdown-it-plugins.d.ts new file mode 100644 index 0000000..6943493 --- /dev/null +++ b/src/types/markdown-it-plugins.d.ts @@ -0,0 +1,54 @@ +declare module 'markdown-it-sub' { + import type MarkdownIt from 'markdown-it'; + const plugin: MarkdownIt.PluginSimple; + export default plugin; +} + +declare module 'markdown-it-sup' { + import type MarkdownIt from 'markdown-it'; + const plugin: MarkdownIt.PluginSimple; + export default plugin; +} + +declare module 'markdown-it-footnote' { + import type MarkdownIt from 'markdown-it'; + const plugin: MarkdownIt.PluginSimple; + export default plugin; +} + +declare module 'markdown-it-deflist' { + import type MarkdownIt from 'markdown-it'; + const plugin: MarkdownIt.PluginSimple; + export default plugin; +} + +declare module 'markdown-it-abbr' { + import type MarkdownIt from 'markdown-it'; + const plugin: MarkdownIt.PluginSimple; + export default plugin; +} + +declare module 'markdown-it-emoji' { + import type MarkdownIt from 'markdown-it'; + const plugin: MarkdownIt.PluginSimple; + export { plugin as full }; + export default plugin; +} + +declare module 'markdown-it-ins' { + import type MarkdownIt from 'markdown-it'; + const plugin: MarkdownIt.PluginSimple; + export default plugin; +} + +declare module 'markdown-it-mark' { + import type MarkdownIt from 'markdown-it'; + const plugin: MarkdownIt.PluginSimple; + export default plugin; +} + +declare module 'markdown-it-task-lists' { + import type MarkdownIt from 'markdown-it'; + const plugin: MarkdownIt.PluginSimple; + export default plugin; +} diff --git a/tsconfig.json b/tsconfig.json index c547caf..be85c8a 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -42,7 +42,7 @@ "noFallthroughCasesInSwitch": false, "noUncheckedSideEffectImports": false, /* Fix for ArrayBuffer type issues */ - "noImplicitAny": false, + "noImplicitAny": true, /* Quality-of-life options */ "isolatedModules": true, "esModuleInterop": true,