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.
This commit is contained in:
@@ -1,47 +1,63 @@
|
|||||||
import globals from "globals";
|
import globals from 'globals';
|
||||||
import pluginJs from "@eslint/js";
|
import pluginJs from '@eslint/js';
|
||||||
import tseslint from "typescript-eslint";
|
import tseslint from 'typescript-eslint';
|
||||||
import eslintConfigPrettier from "eslint-config-prettier";
|
import eslintConfigPrettier from 'eslint-config-prettier';
|
||||||
|
|
||||||
export default [
|
export default [
|
||||||
{ files: ["**/*.{js,mjs,cjs,ts}"] },
|
{ files: ['**/*.{js,mjs,cjs,ts}'] },
|
||||||
{ ignores: ["dist/**", "coverage/**", "node_modules/**", "**/.vitepress/cache/**", "public/**/*.min.js"] },
|
{
|
||||||
{ languageOptions: { globals: { ...globals.browser, ...globals.node } } },
|
ignores: [
|
||||||
pluginJs.configs.recommended,
|
'dist/**',
|
||||||
...tseslint.configs.recommended,
|
'coverage/**',
|
||||||
eslintConfigPrettier,
|
'node_modules/**',
|
||||||
{
|
'**/.vitepress/cache/**',
|
||||||
rules: {
|
'public/**/*.min.js',
|
||||||
"@typescript-eslint/no-explicit-any": "warn",
|
],
|
||||||
"@typescript-eslint/no-unused-vars": ["warn", { "argsIgnorePattern": "^_" }],
|
},
|
||||||
"@typescript-eslint/no-unused-expressions": "warn",
|
{ languageOptions: { globals: { ...globals.browser, ...globals.node } } },
|
||||||
"no-redeclare": "warn",
|
pluginJs.configs.recommended,
|
||||||
"no-constant-condition": "warn",
|
...tseslint.configs.recommended,
|
||||||
"no-ex-assign": "warn",
|
eslintConfigPrettier,
|
||||||
"no-empty": "warn",
|
{
|
||||||
"no-case-declarations": "warn",
|
rules: {
|
||||||
"@typescript-eslint/no-this-alias": "warn",
|
'@typescript-eslint/no-explicit-any': 'warn',
|
||||||
"no-func-assign": "warn",
|
'@typescript-eslint/no-unused-vars': [
|
||||||
"no-fallthrough": "warn",
|
'warn',
|
||||||
"no-cond-assign": "warn",
|
{
|
||||||
"no-irregular-whitespace": "warn",
|
argsIgnorePattern: '^_',
|
||||||
"no-prototype-builtins": "warn",
|
varsIgnorePattern: '^_',
|
||||||
"no-undef": "warn",
|
caughtErrorsIgnorePattern: '^_',
|
||||||
"no-useless-escape": "warn",
|
destructuredArrayIgnorePattern: '^_',
|
||||||
"no-unsafe-finally": "warn",
|
},
|
||||||
"no-self-assign": "warn",
|
],
|
||||||
"no-control-regex": "warn",
|
'@typescript-eslint/no-unused-expressions': 'warn',
|
||||||
"@typescript-eslint/no-require-imports": "warn",
|
'no-redeclare': 'warn',
|
||||||
"getter-return": "warn",
|
'no-constant-condition': 'warn',
|
||||||
"no-constant-binary-expression": "warn",
|
'no-ex-assign': 'warn',
|
||||||
"@typescript-eslint/ban-ts-comment": "warn",
|
'no-empty': 'warn',
|
||||||
"no-unused-private-class-members": "warn",
|
'no-case-declarations': 'warn',
|
||||||
"no-unreachable": "warn",
|
'@typescript-eslint/no-this-alias': 'warn',
|
||||||
"no-setter-return": "warn",
|
'no-func-assign': 'warn',
|
||||||
"no-useless-catch": "warn",
|
'no-fallthrough': 'warn',
|
||||||
"valid-typeof": "warn",
|
'no-cond-assign': 'warn',
|
||||||
"no-sparse-arrays": "warn",
|
'no-irregular-whitespace': 'warn',
|
||||||
"no-misleading-character-class": "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',
|
||||||
|
},
|
||||||
|
},
|
||||||
];
|
];
|
||||||
|
|||||||
@@ -4,25 +4,34 @@ import { state } from './state.js';
|
|||||||
import { toolLogic } from './logic/index.js';
|
import { toolLogic } from './logic/index.js';
|
||||||
import { icons, createIcons } from 'lucide';
|
import { icons, createIcons } from 'lucide';
|
||||||
import * as pdfjsLib from 'pdfjs-dist';
|
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: {
|
const editorState: {
|
||||||
pdf: any;
|
pdf: PDFDocumentProxy | null;
|
||||||
canvas: any;
|
canvas: HTMLCanvasElement | null;
|
||||||
context: any;
|
context: CanvasRenderingContext2D | null;
|
||||||
container: any;
|
container: HTMLElement | null;
|
||||||
currentPageNum: number;
|
currentPageNum: number;
|
||||||
pageRendering: boolean;
|
pageRendering: boolean;
|
||||||
pageNumPending: number | null;
|
pageNumPending: number | null;
|
||||||
scale: number | 'fit';
|
scale: number | 'fit';
|
||||||
pageSnapshot: any;
|
pageSnapshot: ImageData | null;
|
||||||
isDrawing: boolean;
|
isDrawing: boolean;
|
||||||
startX: number;
|
startX: number;
|
||||||
startY: number;
|
startY: number;
|
||||||
cropBoxes: Record<number, any>;
|
cropBoxes: Record<number, CropBox>;
|
||||||
lastInteractionRect: { x: number; y: number; width: number; height: number } | null;
|
lastInteractionRect: {
|
||||||
|
x: number;
|
||||||
|
y: number;
|
||||||
|
width: number;
|
||||||
|
height: number;
|
||||||
|
} | null;
|
||||||
} = {
|
} = {
|
||||||
pdf: null,
|
pdf: null,
|
||||||
canvas: null,
|
canvas: null,
|
||||||
@@ -37,15 +46,15 @@ const editorState: {
|
|||||||
startX: 0,
|
startX: 0,
|
||||||
startY: 0,
|
startY: 0,
|
||||||
cropBoxes: {},
|
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.
|
* Calculates the best scale to fit the page within the container.
|
||||||
* @param {PDFPageProxy} page - The PDF.js page object.
|
* @param {PDFPageProxy} page - The PDF.js page object.
|
||||||
*/
|
*/
|
||||||
function calculateFitScale(page: any) {
|
function calculateFitScale(page: PDFPageProxy) {
|
||||||
const containerWidth = editorState.container.clientWidth;
|
const containerWidth = editorState.container!.clientWidth;
|
||||||
const viewport = page.getViewport({ scale: 1.0 });
|
const viewport = page.getViewport({ scale: 1.0 });
|
||||||
return containerWidth / viewport.width;
|
return containerWidth / viewport.width;
|
||||||
}
|
}
|
||||||
@@ -54,33 +63,34 @@ function calculateFitScale(page: any) {
|
|||||||
* Renders a specific page of the PDF onto the canvas.
|
* Renders a specific page of the PDF onto the canvas.
|
||||||
* @param {number} num The page number to render.
|
* @param {number} num The page number to render.
|
||||||
*/
|
*/
|
||||||
async function renderPage(num: any) {
|
async function renderPage(num: number) {
|
||||||
editorState.pageRendering = true;
|
editorState.pageRendering = true;
|
||||||
showLoader(`Loading page ${num}...`);
|
showLoader(`Loading page ${num}...`);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const page = await editorState.pdf.getPage(num);
|
const page = await editorState.pdf!.getPage(num);
|
||||||
|
|
||||||
if (editorState.scale === 'fit') {
|
if (editorState.scale === 'fit') {
|
||||||
editorState.scale = calculateFitScale(page);
|
editorState.scale = calculateFitScale(page);
|
||||||
}
|
}
|
||||||
|
|
||||||
const viewport = page.getViewport({ scale: editorState.scale });
|
const viewport = page.getViewport({ scale: editorState.scale as number });
|
||||||
editorState.canvas.height = viewport.height;
|
editorState.canvas!.height = viewport.height;
|
||||||
editorState.canvas.width = viewport.width;
|
editorState.canvas!.width = viewport.width;
|
||||||
|
|
||||||
const renderContext = {
|
const renderContext = {
|
||||||
canvasContext: editorState.context,
|
canvas: editorState.canvas,
|
||||||
|
canvasContext: editorState.context!,
|
||||||
viewport: viewport,
|
viewport: viewport,
|
||||||
};
|
};
|
||||||
|
|
||||||
await page.render(renderContext).promise;
|
await page.render(renderContext).promise;
|
||||||
|
|
||||||
editorState.pageSnapshot = editorState.context.getImageData(
|
editorState.pageSnapshot = editorState.context!.getImageData(
|
||||||
0,
|
0,
|
||||||
0,
|
0,
|
||||||
editorState.canvas.width,
|
editorState.canvas!.width,
|
||||||
editorState.canvas.height
|
editorState.canvas!.height
|
||||||
);
|
);
|
||||||
redrawShapes();
|
redrawShapes();
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
@@ -90,12 +100,11 @@ async function renderPage(num: any) {
|
|||||||
editorState.pageRendering = false;
|
editorState.pageRendering = false;
|
||||||
hideLoader();
|
hideLoader();
|
||||||
|
|
||||||
document.getElementById('current-page-display').textContent = num;
|
document.getElementById('current-page-display')!.textContent = String(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') as HTMLButtonElement).disabled =
|
||||||
document.getElementById('prev-page').disabled = num <= 1;
|
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') as HTMLButtonElement).disabled =
|
||||||
document.getElementById('next-page').disabled =
|
num >= editorState.pdf!.numPages;
|
||||||
num >= editorState.pdf.numPages;
|
|
||||||
|
|
||||||
if (editorState.pageNumPending !== null) {
|
if (editorState.pageNumPending !== null) {
|
||||||
const pendingPage = editorState.pageNumPending;
|
const pendingPage = editorState.pageNumPending;
|
||||||
@@ -105,7 +114,7 @@ async function renderPage(num: any) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function queueRenderPage(num: any) {
|
function queueRenderPage(num: number) {
|
||||||
if (editorState.pageRendering) {
|
if (editorState.pageRendering) {
|
||||||
editorState.pageNumPending = num;
|
editorState.pageNumPending = num;
|
||||||
} else {
|
} else {
|
||||||
@@ -116,36 +125,37 @@ function queueRenderPage(num: any) {
|
|||||||
|
|
||||||
function redrawShapes() {
|
function redrawShapes() {
|
||||||
if (editorState.pageSnapshot) {
|
if (editorState.pageSnapshot) {
|
||||||
editorState.context.putImageData(editorState.pageSnapshot, 0, 0);
|
editorState.context!.putImageData(editorState.pageSnapshot, 0, 0);
|
||||||
}
|
}
|
||||||
|
|
||||||
const currentCropBox = editorState.cropBoxes[editorState.currentPageNum - 1];
|
const currentCropBox = editorState.cropBoxes[editorState.currentPageNum - 1];
|
||||||
if (currentCropBox) {
|
if (currentCropBox) {
|
||||||
editorState.context.strokeStyle = 'rgba(79, 70, 229, 0.9)';
|
editorState.context!.strokeStyle = 'rgba(79, 70, 229, 0.9)';
|
||||||
editorState.context.lineWidth = 2;
|
editorState.context!.lineWidth = 2;
|
||||||
editorState.context.setLineDash([8, 4]);
|
editorState.context!.setLineDash([8, 4]);
|
||||||
editorState.context.strokeRect(
|
editorState.context!.strokeRect(
|
||||||
currentCropBox.x,
|
currentCropBox.x,
|
||||||
currentCropBox.y,
|
currentCropBox.y,
|
||||||
currentCropBox.width,
|
currentCropBox.width,
|
||||||
currentCropBox.height
|
currentCropBox.height
|
||||||
);
|
);
|
||||||
editorState.context.setLineDash([]);
|
editorState.context!.setLineDash([]);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function getEventCoordinates(e: any) {
|
function getEventCoordinates(e: MouseEvent | TouchEvent) {
|
||||||
const rect = editorState.canvas.getBoundingClientRect();
|
const rect = editorState.canvas!.getBoundingClientRect();
|
||||||
const touch = e.touches ? e.touches[0] : e;
|
const touch =
|
||||||
const scaleX = editorState.canvas.width / rect.width;
|
'touches' in e && e.touches.length > 0 ? e.touches[0] : (e as MouseEvent);
|
||||||
const scaleY = editorState.canvas.height / rect.height;
|
const scaleX = editorState.canvas!.width / rect.width;
|
||||||
|
const scaleY = editorState.canvas!.height / rect.height;
|
||||||
return {
|
return {
|
||||||
x: (touch.clientX - rect.left) * scaleX,
|
x: (touch.clientX - rect.left) * scaleX,
|
||||||
y: (touch.clientY - rect.top) * scaleY,
|
y: (touch.clientY - rect.top) * scaleY,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
function handleInteractionStart(e: any) {
|
function handleInteractionStart(e: MouseEvent | TouchEvent) {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
const coords = getEventCoordinates(e);
|
const coords = getEventCoordinates(e);
|
||||||
editorState.isDrawing = true;
|
editorState.isDrawing = true;
|
||||||
@@ -153,7 +163,7 @@ function handleInteractionStart(e: any) {
|
|||||||
editorState.startY = coords.y;
|
editorState.startY = coords.y;
|
||||||
}
|
}
|
||||||
|
|
||||||
function handleInteractionMove(e: any) {
|
function handleInteractionMove(e: MouseEvent | TouchEvent) {
|
||||||
if (!editorState.isDrawing) return;
|
if (!editorState.isDrawing) return;
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
|
|
||||||
@@ -165,13 +175,12 @@ function handleInteractionMove(e: any) {
|
|||||||
const width = Math.abs(editorState.startX - coords.x);
|
const width = Math.abs(editorState.startX - coords.x);
|
||||||
const height = Math.abs(editorState.startY - coords.y);
|
const height = Math.abs(editorState.startY - coords.y);
|
||||||
|
|
||||||
editorState.context.strokeStyle = 'rgba(79, 70, 229, 0.9)';
|
editorState.context!.strokeStyle = 'rgba(79, 70, 229, 0.9)';
|
||||||
editorState.context.lineWidth = 2;
|
editorState.context!.lineWidth = 2;
|
||||||
editorState.context.setLineDash([8, 4]);
|
editorState.context!.setLineDash([8, 4]);
|
||||||
editorState.context.strokeRect(x, y, width, height);
|
editorState.context!.strokeRect(x, y, width, height);
|
||||||
editorState.context.setLineDash([]);
|
editorState.context!.setLineDash([]);
|
||||||
|
|
||||||
// Store the last valid rectangle drawn during the move event
|
|
||||||
editorState.lastInteractionRect = { x, y, width, height };
|
editorState.lastInteractionRect = { x, y, width, height };
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -192,12 +201,14 @@ function handleInteractionEnd() {
|
|||||||
scale: editorState.scale,
|
scale: editorState.scale,
|
||||||
};
|
};
|
||||||
|
|
||||||
editorState.lastInteractionRect = null; // Reset for the next drawing action
|
editorState.lastInteractionRect = null;
|
||||||
redrawShapes();
|
redrawShapes();
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function setupCanvasEditor(toolId: any) {
|
export async function setupCanvasEditor(toolId: string) {
|
||||||
editorState.canvas = document.getElementById('canvas-editor');
|
editorState.canvas = document.getElementById(
|
||||||
|
'canvas-editor'
|
||||||
|
) as HTMLCanvasElement | null;
|
||||||
if (!editorState.canvas) return;
|
if (!editorState.canvas) return;
|
||||||
editorState.container = document.getElementById('canvas-container');
|
editorState.container = document.getElementById('canvas-container');
|
||||||
editorState.context = editorState.canvas.getContext('2d');
|
editorState.context = editorState.canvas.getContext('2d');
|
||||||
@@ -210,7 +221,7 @@ export async function setupCanvasEditor(toolId: any) {
|
|||||||
editorState.currentPageNum = 1;
|
editorState.currentPageNum = 1;
|
||||||
editorState.scale = 'fit';
|
editorState.scale = 'fit';
|
||||||
|
|
||||||
pageNav.textContent = '';
|
pageNav!.textContent = '';
|
||||||
|
|
||||||
const prevButton = document.createElement('button');
|
const prevButton = document.createElement('button');
|
||||||
prevButton.id = 'prev-page';
|
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';
|
'btn p-2 rounded-full bg-gray-700 hover:bg-gray-600 disabled:opacity-50';
|
||||||
nextButton.innerHTML = '<i data-lucide="chevron-right"></i>';
|
nextButton.innerHTML = '<i data-lucide="chevron-right"></i>';
|
||||||
|
|
||||||
pageNav.append(prevButton, pageInfo, nextButton);
|
pageNav!.append(prevButton, pageInfo, nextButton);
|
||||||
|
|
||||||
createIcons({ icons });
|
createIcons({ icons });
|
||||||
|
|
||||||
document.getElementById('prev-page').addEventListener('click', () => {
|
document.getElementById('prev-page')!.addEventListener('click', () => {
|
||||||
if (editorState.currentPageNum > 1)
|
if (editorState.currentPageNum > 1)
|
||||||
queueRenderPage(editorState.currentPageNum - 1);
|
queueRenderPage(editorState.currentPageNum - 1);
|
||||||
});
|
});
|
||||||
document.getElementById('next-page').addEventListener('click', () => {
|
document.getElementById('next-page')!.addEventListener('click', () => {
|
||||||
if (editorState.currentPageNum < editorState.pdf.numPages)
|
if (editorState.currentPageNum < editorState.pdf!.numPages)
|
||||||
queueRenderPage(editorState.currentPageNum + 1);
|
queueRenderPage(editorState.currentPageNum + 1);
|
||||||
});
|
});
|
||||||
|
|
||||||
// To prevent stacking multiple listeners, we replace the canvas element with a clone
|
const newCanvas = editorState.canvas.cloneNode(true) as HTMLCanvasElement;
|
||||||
const newCanvas = editorState.canvas.cloneNode(true);
|
editorState.canvas.parentNode!.replaceChild(newCanvas, editorState.canvas);
|
||||||
editorState.canvas.parentNode.replaceChild(newCanvas, editorState.canvas);
|
|
||||||
editorState.canvas = newCanvas;
|
editorState.canvas = newCanvas;
|
||||||
editorState.context = newCanvas.getContext('2d');
|
editorState.context = newCanvas.getContext('2d');
|
||||||
|
|
||||||
@@ -272,50 +282,55 @@ export async function setupCanvasEditor(toolId: any) {
|
|||||||
editorState.canvas.addEventListener('touchend', handleInteractionEnd);
|
editorState.canvas.addEventListener('touchend', handleInteractionEnd);
|
||||||
|
|
||||||
if (toolId === 'crop') {
|
if (toolId === 'crop') {
|
||||||
document.getElementById('zoom-in-btn').onclick = () => {
|
(document.getElementById('zoom-in-btn') as HTMLElement).onclick = () => {
|
||||||
if (typeof editorState.scale === 'number') {
|
if (typeof editorState.scale === 'number') {
|
||||||
editorState.scale += 0.25;
|
editorState.scale += 0.25;
|
||||||
}
|
}
|
||||||
renderPage(editorState.currentPageNum);
|
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) {
|
if (typeof editorState.scale === 'number' && editorState.scale > 0.25) {
|
||||||
editorState.scale -= 0.25;
|
editorState.scale -= 0.25;
|
||||||
renderPage(editorState.currentPageNum);
|
renderPage(editorState.currentPageNum);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
document.getElementById('fit-page-btn').onclick = async () => {
|
(document.getElementById('fit-page-btn') as HTMLElement).onclick =
|
||||||
const page = await editorState.pdf.getPage(editorState.currentPageNum);
|
async () => {
|
||||||
editorState.scale = calculateFitScale(page);
|
const page = await editorState.pdf!.getPage(editorState.currentPageNum);
|
||||||
renderPage(editorState.currentPageNum);
|
editorState.scale = calculateFitScale(page);
|
||||||
};
|
renderPage(editorState.currentPageNum);
|
||||||
document.getElementById('clear-crop-btn').onclick = () => {
|
};
|
||||||
|
(document.getElementById('clear-crop-btn') as HTMLElement).onclick = () => {
|
||||||
delete editorState.cropBoxes[editorState.currentPageNum - 1];
|
delete editorState.cropBoxes[editorState.currentPageNum - 1];
|
||||||
redrawShapes();
|
redrawShapes();
|
||||||
};
|
};
|
||||||
document.getElementById('clear-all-crops-btn').onclick = () => {
|
(document.getElementById('clear-all-crops-btn') as HTMLElement).onclick =
|
||||||
editorState.cropBoxes = {};
|
() => {
|
||||||
redrawShapes();
|
editorState.cropBoxes = {};
|
||||||
};
|
redrawShapes();
|
||||||
|
};
|
||||||
|
|
||||||
document.getElementById('process-btn').onclick = async () => {
|
(document.getElementById('process-btn') as HTMLElement).onclick =
|
||||||
if (Object.keys(editorState.cropBoxes).length === 0) {
|
async () => {
|
||||||
showAlert(
|
if (Object.keys(editorState.cropBoxes).length === 0) {
|
||||||
'No Area Selected',
|
showAlert(
|
||||||
'Please draw a rectangle on at least one page to select the crop area.'
|
'No Area Selected',
|
||||||
);
|
'Please draw a rectangle on at least one page to select the crop area.'
|
||||||
return;
|
);
|
||||||
}
|
return;
|
||||||
const success = await toolLogic['crop-pdf'].process(
|
}
|
||||||
editorState.cropBoxes
|
const cropLogic = toolLogic['crop-pdf'];
|
||||||
);
|
const success =
|
||||||
if (success) {
|
typeof cropLogic === 'object' && cropLogic.process
|
||||||
showAlert(
|
? await cropLogic.process(editorState.cropBoxes)
|
||||||
'Success!',
|
: false;
|
||||||
'Your PDF has been cropped and the download has started.'
|
if (success) {
|
||||||
);
|
showAlert(
|
||||||
}
|
'Success!',
|
||||||
};
|
'Your PDF has been cropped and the download has started.'
|
||||||
|
);
|
||||||
|
}
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
queueRenderPage(1);
|
queueRenderPage(1);
|
||||||
|
|||||||
@@ -4,7 +4,6 @@ import type {
|
|||||||
CompareTextItem,
|
CompareTextItem,
|
||||||
ComparePageSignature,
|
ComparePageSignature,
|
||||||
ComparePagePair,
|
ComparePagePair,
|
||||||
ComparePageResult,
|
|
||||||
CompareChangeSummary,
|
CompareChangeSummary,
|
||||||
CompareTextChange,
|
CompareTextChange,
|
||||||
} from '../types.ts';
|
} from '../types.ts';
|
||||||
@@ -44,8 +43,6 @@ interface ErrorResult {
|
|||||||
message: string;
|
message: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
type WorkerResult = DiffResult | PairResult | ErrorResult;
|
|
||||||
|
|
||||||
self.onmessage = function (e: MessageEvent<WorkerMessage>) {
|
self.onmessage = function (e: MessageEvent<WorkerMessage>) {
|
||||||
const msg = e.data;
|
const msg = e.data;
|
||||||
try {
|
try {
|
||||||
|
|||||||
@@ -58,13 +58,13 @@ async function handleSinglePdfUpload(toolId: string, file: File) {
|
|||||||
const processBtn = document.getElementById('process-btn');
|
const processBtn = document.getElementById('process-btn');
|
||||||
if (processBtn) {
|
if (processBtn) {
|
||||||
const logic = toolLogic[toolId];
|
const logic = toolLogic[toolId];
|
||||||
if (logic && logic.process) {
|
if (logic && typeof logic === 'object' && logic.process) {
|
||||||
processBtn.onclick = logic.process;
|
processBtn.onclick = logic.process;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const logic = toolLogic[toolId];
|
const logic = toolLogic[toolId];
|
||||||
if (logic && logic.setup) {
|
if (logic && typeof logic === 'object' && logic.setup) {
|
||||||
await logic.setup();
|
await logic.setup();
|
||||||
}
|
}
|
||||||
return;
|
return;
|
||||||
@@ -102,8 +102,12 @@ async function handleSinglePdfUpload(toolId: string, file: File) {
|
|||||||
const logic = toolLogic[toolId];
|
const logic = toolLogic[toolId];
|
||||||
if (logic) {
|
if (logic) {
|
||||||
const func =
|
const func =
|
||||||
typeof logic.process === 'function' ? logic.process : logic;
|
typeof logic === 'object' && typeof logic.process === 'function'
|
||||||
processBtn.onclick = func;
|
? 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');
|
const infoSection = createSection('Info Dictionary');
|
||||||
if (info && Object.keys(info).length > 0) {
|
if (info && Object.keys(info).length > 0) {
|
||||||
for (const key in info) {
|
for (const key in info) {
|
||||||
const value = info[key];
|
const value = (info as Record<string, unknown>)[key];
|
||||||
let displayValue;
|
let displayValue: string;
|
||||||
|
|
||||||
if (value === null || typeof value === 'undefined') {
|
if (value === null || typeof value === 'undefined') {
|
||||||
displayValue = '- Not Set -';
|
displayValue = '- Not Set -';
|
||||||
} else if (typeof value === 'object' && value.name) {
|
} else if (
|
||||||
displayValue = value.name;
|
typeof value === 'object' &&
|
||||||
|
'name' in (value as object) &&
|
||||||
|
(value as Record<string, unknown>).name
|
||||||
|
) {
|
||||||
|
displayValue = String((value as Record<string, unknown>).name);
|
||||||
} else if (typeof value === 'object') {
|
} else if (typeof value === 'object') {
|
||||||
try {
|
try {
|
||||||
displayValue = JSON.stringify(value);
|
displayValue = JSON.stringify(value);
|
||||||
@@ -524,7 +532,8 @@ async function handleSinglePdfUpload(toolId: string, file: File) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (toolId === 'page-dimensions') {
|
if (toolId === 'page-dimensions') {
|
||||||
toolLogic['page-dimensions']();
|
const pageDimLogic = toolLogic['page-dimensions'];
|
||||||
|
if (typeof pageDimLogic === 'function') pageDimLogic();
|
||||||
}
|
}
|
||||||
|
|
||||||
if (toolId === 'pdf-to-jpg') {
|
if (toolId === 'pdf-to-jpg') {
|
||||||
@@ -569,8 +578,13 @@ async function handleSinglePdfUpload(toolId: string, file: File) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (toolLogic[toolId] && typeof toolLogic[toolId].setup === 'function') {
|
const setupLogic = toolLogic[toolId];
|
||||||
toolLogic[toolId].setup();
|
if (
|
||||||
|
setupLogic &&
|
||||||
|
typeof setupLogic === 'object' &&
|
||||||
|
typeof setupLogic.setup === 'function'
|
||||||
|
) {
|
||||||
|
setupLogic.setup();
|
||||||
}
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
hideLoader();
|
hideLoader();
|
||||||
@@ -656,13 +670,19 @@ async function handleMultiFileUpload(toolId: string) {
|
|||||||
(processBtn as HTMLButtonElement).disabled = false;
|
(processBtn as HTMLButtonElement).disabled = false;
|
||||||
const logic = toolLogic[toolId];
|
const logic = toolLogic[toolId];
|
||||||
if (logic) {
|
if (logic) {
|
||||||
const func = typeof logic.process === 'function' ? logic.process : logic;
|
const func =
|
||||||
processBtn.onclick = func;
|
typeof logic === 'object' && typeof logic.process === 'function'
|
||||||
|
? logic.process
|
||||||
|
: typeof logic === 'function'
|
||||||
|
? logic
|
||||||
|
: null;
|
||||||
|
if (func) processBtn.onclick = func;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (toolId === 'alternate-merge') {
|
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') {
|
} else if (toolId === 'image-to-pdf') {
|
||||||
const imageList = document.getElementById('image-list');
|
const imageList = document.getElementById('image-list');
|
||||||
|
|
||||||
@@ -869,9 +889,8 @@ export function setupFileInputHandler(toolId: string) {
|
|||||||
processBtn.onclick = () => {
|
processBtn.onclick = () => {
|
||||||
const logic = toolLogic[toolId];
|
const logic = toolLogic[toolId];
|
||||||
if (logic) {
|
if (logic) {
|
||||||
const func =
|
if (typeof logic === 'function') logic();
|
||||||
typeof logic.process === 'function' ? logic.process : logic;
|
else if (logic.process) logic.process();
|
||||||
func();
|
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
@@ -893,9 +912,8 @@ export function setupFileInputHandler(toolId: string) {
|
|||||||
processBtn.onclick = () => {
|
processBtn.onclick = () => {
|
||||||
const logic = toolLogic[toolId];
|
const logic = toolLogic[toolId];
|
||||||
if (logic) {
|
if (logic) {
|
||||||
const func =
|
if (typeof logic === 'function') logic();
|
||||||
typeof logic.process === 'function' ? logic.process : logic;
|
else if (logic.process) logic.process();
|
||||||
func();
|
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ import type {
|
|||||||
AddPageLabelsState,
|
AddPageLabelsState,
|
||||||
LabelRule,
|
LabelRule,
|
||||||
PageLabelStyleName,
|
PageLabelStyleName,
|
||||||
|
CpdfInstance,
|
||||||
} from '@/types';
|
} from '@/types';
|
||||||
import { showAlert, showLoader, hideLoader } from '../ui.js';
|
import { showAlert, showLoader, hideLoader } from '../ui.js';
|
||||||
import { t } from '../i18n/index.js';
|
import { t } from '../i18n/index.js';
|
||||||
@@ -20,30 +21,6 @@ import {
|
|||||||
} from '../utils/page-labels.js';
|
} from '../utils/page-labels.js';
|
||||||
import { loadPdfDocument } from '../utils/load-pdf-document.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;
|
let labelRuleCounter = 0;
|
||||||
|
|
||||||
const translate = (
|
const translate = (
|
||||||
@@ -464,8 +441,8 @@ async function addPageLabels() {
|
|||||||
) as HTMLInputElement | null
|
) as HTMLInputElement | null
|
||||||
)?.checked ?? true;
|
)?.checked ?? true;
|
||||||
|
|
||||||
let cpdf: AddPageLabelsCpdf | null = null;
|
let cpdf: CpdfInstance | null = null;
|
||||||
let pdf: CoherentPdf | null = null;
|
let pdf: unknown = null;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
cpdf = await getCpdf();
|
cpdf = await getCpdf();
|
||||||
@@ -482,7 +459,7 @@ async function addPageLabels() {
|
|||||||
const rule = pageState.rules[index];
|
const rule = pageState.rules[index];
|
||||||
const trimmedRange = rule.pageRange.trim();
|
const trimmedRange = rule.pageRange.trim();
|
||||||
|
|
||||||
let range: CpdfPageRange;
|
let range: unknown;
|
||||||
try {
|
try {
|
||||||
range = trimmedRange
|
range = trimmedRange
|
||||||
? cpdf.parsePagespec(pdf, trimmedRange)
|
? cpdf.parsePagespec(pdf, trimmedRange)
|
||||||
|
|||||||
@@ -9,7 +9,6 @@ import { loadPdfWithPasswordPrompt } from '../utils/password-prompt.js';
|
|||||||
|
|
||||||
let selectedFile: File | null = null;
|
let selectedFile: File | null = null;
|
||||||
let viewerIframe: HTMLIFrameElement | null = null;
|
let viewerIframe: HTMLIFrameElement | null = null;
|
||||||
let viewerReady = false;
|
|
||||||
let currentBlobUrl: string | null = null;
|
let currentBlobUrl: string | null = null;
|
||||||
|
|
||||||
const pdfInput = document.getElementById('pdfFile') as HTMLInputElement;
|
const pdfInput = document.getElementById('pdfFile') as HTMLInputElement;
|
||||||
@@ -47,7 +46,7 @@ function resetState() {
|
|||||||
viewerContainer.removeChild(viewerIframe);
|
viewerContainer.removeChild(viewerIframe);
|
||||||
}
|
}
|
||||||
viewerIframe = null;
|
viewerIframe = null;
|
||||||
viewerReady = false;
|
|
||||||
if (viewerCard) viewerCard.classList.add('hidden');
|
if (viewerCard) viewerCard.classList.add('hidden');
|
||||||
if (saveStampedBtn) saveStampedBtn.classList.add('hidden');
|
if (saveStampedBtn) saveStampedBtn.classList.add('hidden');
|
||||||
|
|
||||||
@@ -157,7 +156,6 @@ async function loadPdfInViewer(file: File) {
|
|||||||
currentBlobUrl = null;
|
currentBlobUrl = null;
|
||||||
}
|
}
|
||||||
viewerIframe = null;
|
viewerIframe = null;
|
||||||
viewerReady = false;
|
|
||||||
|
|
||||||
// Calculate and apply dynamic height
|
// Calculate and apply dynamic height
|
||||||
await adjustViewerHeight(file);
|
await adjustViewerHeight(file);
|
||||||
@@ -169,7 +167,7 @@ async function loadPdfInViewer(file: File) {
|
|||||||
try {
|
try {
|
||||||
const existingPrefsRaw = localStorage.getItem('pdfjs.preferences');
|
const existingPrefsRaw = localStorage.getItem('pdfjs.preferences');
|
||||||
const existingPrefs = existingPrefsRaw ? JSON.parse(existingPrefsRaw) : {};
|
const existingPrefs = existingPrefsRaw ? JSON.parse(existingPrefsRaw) : {};
|
||||||
delete (existingPrefs as any).annotationEditorMode;
|
delete (existingPrefs as Record<string, unknown>).annotationEditorMode;
|
||||||
const newPrefs = {
|
const newPrefs = {
|
||||||
...existingPrefs,
|
...existingPrefs,
|
||||||
enablePermissions: false,
|
enablePermissions: false,
|
||||||
@@ -204,7 +202,14 @@ async function loadPdfInViewer(file: File) {
|
|||||||
|
|
||||||
function setupAnnotationViewer(iframe: HTMLIFrameElement) {
|
function setupAnnotationViewer(iframe: HTMLIFrameElement) {
|
||||||
try {
|
try {
|
||||||
const win = iframe.contentWindow as any;
|
const win = iframe.contentWindow as
|
||||||
|
| (Window & {
|
||||||
|
PDFViewerApplication?: {
|
||||||
|
initializedPromise?: Promise<void>;
|
||||||
|
eventBus?: { _on?: (event: string, callback: () => void) => void };
|
||||||
|
};
|
||||||
|
})
|
||||||
|
| null;
|
||||||
const doc = win?.document as Document | null;
|
const doc = win?.document as Document | null;
|
||||||
if (!win || !doc) return;
|
if (!win || !doc) return;
|
||||||
|
|
||||||
@@ -238,8 +243,6 @@ function setupAnnotationViewer(iframe: HTMLIFrameElement) {
|
|||||||
if (root) {
|
if (root) {
|
||||||
root.classList.add('PdfjsAnnotationExtension_Comment_hidden');
|
root.classList.add('PdfjsAnnotationExtension_Comment_hidden');
|
||||||
}
|
}
|
||||||
|
|
||||||
viewerReady = true;
|
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error(
|
console.error(
|
||||||
'Failed to initialize annotation viewer for Add Stamps:',
|
'Failed to initialize annotation viewer for Add Stamps:',
|
||||||
@@ -304,8 +307,14 @@ if (saveStampedBtn) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const win = viewerIframe.contentWindow as any;
|
const win = viewerIframe.contentWindow as
|
||||||
const extensionInstance = win?.pdfjsAnnotationExtensionInstance as any;
|
| (Window & {
|
||||||
|
pdfjsAnnotationExtensionInstance?: {
|
||||||
|
exportPdf?: () => Promise<void>;
|
||||||
|
};
|
||||||
|
})
|
||||||
|
| null;
|
||||||
|
const extensionInstance = win?.pdfjsAnnotationExtensionInstance;
|
||||||
|
|
||||||
if (
|
if (
|
||||||
extensionInstance &&
|
extensionInstance &&
|
||||||
|
|||||||
@@ -1013,9 +1013,12 @@ async function applyWatermark() {
|
|||||||
'watermarked.pdf'
|
'watermarked.pdf'
|
||||||
);
|
);
|
||||||
showAlert('Success', 'Watermark added successfully!', 'success');
|
showAlert('Success', 'Watermark added successfully!', 'success');
|
||||||
} catch (e: any) {
|
} catch (e: unknown) {
|
||||||
console.error(e);
|
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 {
|
} finally {
|
||||||
hideLoader();
|
hideLoader();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -7,7 +7,6 @@ import '../../css/bookmark.css';
|
|||||||
import { initializeGlobalShortcuts } from '../utils/shortcuts-init.js';
|
import { initializeGlobalShortcuts } from '../utils/shortcuts-init.js';
|
||||||
import {
|
import {
|
||||||
truncateFilename,
|
truncateFilename,
|
||||||
getPDFDocument,
|
|
||||||
formatBytes,
|
formatBytes,
|
||||||
downloadFile,
|
downloadFile,
|
||||||
escapeHtml,
|
escapeHtml,
|
||||||
@@ -46,7 +45,6 @@ let isPickingDestination = false;
|
|||||||
let currentPickingCallback: DestinationCallback | null = null;
|
let currentPickingCallback: DestinationCallback | null = null;
|
||||||
let destinationMarker: HTMLDivElement | null = null;
|
let destinationMarker: HTMLDivElement | null = null;
|
||||||
let savedModalOverlay: HTMLDivElement | null = null;
|
let savedModalOverlay: HTMLDivElement | null = null;
|
||||||
let savedModal: HTMLDivElement | null = null;
|
|
||||||
let currentViewport: PageViewport | null = null;
|
let currentViewport: PageViewport | null = null;
|
||||||
let currentZoom = 1.0;
|
let currentZoom = 1.0;
|
||||||
const fileInput = document.getElementById(
|
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) {
|
if (pickDestBtn) {
|
||||||
pickDestBtn.addEventListener('click', () => {
|
pickDestBtn.addEventListener('click', () => {
|
||||||
savedModalOverlay = overlay;
|
savedModalOverlay = overlay;
|
||||||
savedModal = modal;
|
|
||||||
overlay.style.display = 'none';
|
overlay.style.display = 'none';
|
||||||
|
|
||||||
startDestinationPicking((page: number, pdfX: number, pdfY: number) => {
|
startDestinationPicking((page: number, pdfX: number, pdfY: number) => {
|
||||||
@@ -699,7 +696,6 @@ function cancelDestinationPicking(): void {
|
|||||||
if (savedModalOverlay) {
|
if (savedModalOverlay) {
|
||||||
savedModalOverlay.style.display = '';
|
savedModalOverlay.style.display = '';
|
||||||
savedModalOverlay = null;
|
savedModalOverlay = null;
|
||||||
savedModal = null;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1317,7 +1313,7 @@ jsonInput?.addEventListener('change', async (e: Event) => {
|
|||||||
'JSON Loaded',
|
'JSON Loaded',
|
||||||
'Loaded bookmarks from JSON. Now upload your PDF.'
|
'Loaded bookmarks from JSON. Now upload your PDF.'
|
||||||
);
|
);
|
||||||
} catch (err) {
|
} catch {
|
||||||
await showAlertModal('Error', 'Invalid JSON format');
|
await showAlertModal('Error', 'Invalid JSON format');
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
@@ -2018,7 +2014,7 @@ jsonImportHidden?.addEventListener('change', async (e: Event) => {
|
|||||||
saveState();
|
saveState();
|
||||||
renderBookmarkTree();
|
renderBookmarkTree();
|
||||||
await showAlertModal('Success', 'Bookmarks imported from JSON!');
|
await showAlertModal('Success', 'Bookmarks imported from JSON!');
|
||||||
} catch (err) {
|
} catch {
|
||||||
await showAlertModal('Error', 'Invalid JSON format');
|
await showAlertModal('Error', 'Invalid JSON format');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -2,9 +2,8 @@ import { showLoader, hideLoader, showAlert } from '../ui.js';
|
|||||||
import { downloadFile, formatBytes } from '../utils/helpers.js';
|
import { downloadFile, formatBytes } from '../utils/helpers.js';
|
||||||
import { state } from '../state.js';
|
import { state } from '../state.js';
|
||||||
import { createIcons, icons } from 'lucide';
|
import { createIcons, icons } from 'lucide';
|
||||||
import { isWasmAvailable, getWasmBaseUrl } from '../config/wasm-cdn-config.js';
|
import { loadPyMuPDF } from '../utils/pymupdf-loader.js';
|
||||||
import { showWasmRequiredDialog } from '../utils/wasm-provider.js';
|
import type { PyMuPDFInstance } from '@/types';
|
||||||
import { loadPyMuPDF, isPyMuPDFAvailable } from '../utils/pymupdf-loader.js';
|
|
||||||
import JSZip from 'jszip';
|
import JSZip from 'jszip';
|
||||||
import { PDFDocument } from 'pdf-lib';
|
import { PDFDocument } from 'pdf-lib';
|
||||||
|
|
||||||
@@ -176,8 +175,8 @@ async function convertCbzToPdf(file: File): Promise<Blob> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async function convertCbrToPdf(file: File): Promise<Blob> {
|
async function convertCbrToPdf(file: File): Promise<Blob> {
|
||||||
const pymupdf = await loadPyMuPDF();
|
const pymupdf = (await loadPyMuPDF()) as PyMuPDFInstance;
|
||||||
return await (pymupdf as any).convertToPdf(file, { filetype: 'cbz' });
|
return await pymupdf.convertToPdf(file, { filetype: 'cbz' });
|
||||||
}
|
}
|
||||||
|
|
||||||
document.addEventListener('DOMContentLoaded', () => {
|
document.addEventListener('DOMContentLoaded', () => {
|
||||||
@@ -314,12 +313,12 @@ document.addEventListener('DOMContentLoaded', () => {
|
|||||||
() => resetState()
|
() => resetState()
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
} catch (e: any) {
|
} catch (e: unknown) {
|
||||||
console.error(`[${TOOL_NAME}2PDF] ERROR:`, e);
|
console.error(`[${TOOL_NAME}2PDF] ERROR:`, e);
|
||||||
hideLoader();
|
hideLoader();
|
||||||
showAlert(
|
showAlert(
|
||||||
'Error',
|
'Error',
|
||||||
`An error occurred during conversion. Error: ${e.message}`
|
`An error occurred during conversion. Error: ${e instanceof Error ? e.message : String(e)}`
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ import {
|
|||||||
readFileAsArrayBuffer,
|
readFileAsArrayBuffer,
|
||||||
} from '../utils/helpers.js';
|
} from '../utils/helpers.js';
|
||||||
import { icons, createIcons } from 'lucide';
|
import { icons, createIcons } from 'lucide';
|
||||||
import { ChangePermissionsState } from '@/types';
|
import { ChangePermissionsState, QpdfInstanceExtended } from '@/types';
|
||||||
|
|
||||||
const pageState: ChangePermissionsState = {
|
const pageState: ChangePermissionsState = {
|
||||||
file: null,
|
file: null,
|
||||||
@@ -114,7 +114,7 @@ async function changePermissions() {
|
|||||||
|
|
||||||
const inputPath = '/input.pdf';
|
const inputPath = '/input.pdf';
|
||||||
const outputPath = '/output.pdf';
|
const outputPath = '/output.pdf';
|
||||||
let qpdf: any;
|
let qpdf: QpdfInstanceExtended;
|
||||||
|
|
||||||
const loaderModal = document.getElementById('loader-modal');
|
const loaderModal = document.getElementById('loader-modal');
|
||||||
const loaderText = document.getElementById('loader-text');
|
const loaderText = document.getElementById('loader-text');
|
||||||
@@ -187,10 +187,10 @@ async function changePermissions() {
|
|||||||
args.push('--', outputPath);
|
args.push('--', outputPath);
|
||||||
try {
|
try {
|
||||||
qpdf.callMain(args);
|
qpdf.callMain(args);
|
||||||
} catch (qpdfError: any) {
|
} catch (qpdfError: unknown) {
|
||||||
console.error('qpdf execution error:', qpdfError);
|
console.error('qpdf execution error:', qpdfError);
|
||||||
|
|
||||||
const errorMsg = qpdfError.message || '';
|
const errorMsg = qpdfError instanceof Error ? qpdfError.message : '';
|
||||||
|
|
||||||
if (
|
if (
|
||||||
errorMsg.includes('invalid password') ||
|
errorMsg.includes('invalid password') ||
|
||||||
@@ -219,7 +219,9 @@ async function changePermissions() {
|
|||||||
throw new Error('Processing resulted in an empty file.');
|
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}`);
|
downloadFile(blob, `permissions-changed-${pageState.file.name}`);
|
||||||
|
|
||||||
if (loaderModal) loaderModal.classList.add('hidden');
|
if (loaderModal) loaderModal.classList.add('hidden');
|
||||||
@@ -233,16 +235,17 @@ async function changePermissions() {
|
|||||||
showAlert('Success', successMessage, 'success', () => {
|
showAlert('Success', successMessage, 'success', () => {
|
||||||
resetState();
|
resetState();
|
||||||
});
|
});
|
||||||
} catch (error: any) {
|
} catch (error: unknown) {
|
||||||
console.error('Error during PDF permission change:', error);
|
console.error('Error during PDF permission change:', error);
|
||||||
if (loaderModal) loaderModal.classList.add('hidden');
|
if (loaderModal) loaderModal.classList.add('hidden');
|
||||||
|
|
||||||
if (error.message === 'INVALID_PASSWORD') {
|
const errorMessage = error instanceof Error ? error.message : '';
|
||||||
|
if (errorMessage === 'INVALID_PASSWORD') {
|
||||||
showAlert(
|
showAlert(
|
||||||
'Incorrect Password',
|
'Incorrect Password',
|
||||||
'The current password you entered is incorrect. Please try again.'
|
'The current password you entered is incorrect. Please try again.'
|
||||||
);
|
);
|
||||||
} else if (error.message === 'PASSWORD_REQUIRED') {
|
} else if (errorMessage === 'PASSWORD_REQUIRED') {
|
||||||
showAlert(
|
showAlert(
|
||||||
'Password Required',
|
'Password Required',
|
||||||
'This PDF is password-protected. Please enter the current password to proceed.'
|
'This PDF is password-protected. Please enter the current password to proceed.'
|
||||||
@@ -250,7 +253,7 @@ async function changePermissions() {
|
|||||||
} else {
|
} else {
|
||||||
showAlert(
|
showAlert(
|
||||||
'Processing Failed',
|
'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 {
|
} finally {
|
||||||
|
|||||||
@@ -85,7 +85,10 @@ async function performCondenseCompression(
|
|||||||
scrub: {
|
scrub: {
|
||||||
metadata: customSettings?.removeMetadata ?? preset.scrub.metadata,
|
metadata: customSettings?.removeMetadata ?? preset.scrub.metadata,
|
||||||
thumbnails: customSettings?.removeThumbnails ?? preset.scrub.thumbnails,
|
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,
|
subsetFonts: customSettings?.subsetFonts ?? preset.subsetFonts,
|
||||||
save: {
|
save: {
|
||||||
@@ -99,8 +102,8 @@ async function performCondenseCompression(
|
|||||||
try {
|
try {
|
||||||
const result = await pymupdf.compressPdf(fileBlob, options);
|
const result = await pymupdf.compressPdf(fileBlob, options);
|
||||||
return result;
|
return result;
|
||||||
} catch (error: any) {
|
} catch (error: unknown) {
|
||||||
const errorMessage = error?.message || String(error);
|
const errorMessage = error instanceof Error ? error.message : String(error);
|
||||||
if (
|
if (
|
||||||
errorMessage.includes('PatternType') ||
|
errorMessage.includes('PatternType') ||
|
||||||
errorMessage.includes('pattern')
|
errorMessage.includes('pattern')
|
||||||
@@ -432,7 +435,7 @@ document.addEventListener('DOMContentLoaded', () => {
|
|||||||
usedMethod = 'Condense';
|
usedMethod = 'Condense';
|
||||||
|
|
||||||
// Check if fallback was used
|
// Check if fallback was used
|
||||||
if ((result as any).usedFallback) {
|
if ((result as { usedFallback?: boolean }).usedFallback) {
|
||||||
usedMethod +=
|
usedMethod +=
|
||||||
' (without image optimization due to unsupported patterns)';
|
' (without image optimization due to unsupported patterns)';
|
||||||
}
|
}
|
||||||
@@ -551,12 +554,12 @@ document.addEventListener('DOMContentLoaded', () => {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} catch (e: any) {
|
} catch (e: unknown) {
|
||||||
hideLoader();
|
hideLoader();
|
||||||
console.error('[CompressPDF] Error:', e);
|
console.error('[CompressPDF] Error:', e);
|
||||||
showAlert(
|
showAlert(
|
||||||
'Error',
|
'Error',
|
||||||
`An error occurred during compression. Error: ${e.message}`
|
`An error occurred during compression. Error: ${e instanceof Error ? e.message : String(e)}`
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ import { loadPdfWithPasswordPrompt } from '../utils/password-prompt.js';
|
|||||||
import Cropper from 'cropperjs';
|
import Cropper from 'cropperjs';
|
||||||
import * as pdfjsLib from 'pdfjs-dist';
|
import * as pdfjsLib from 'pdfjs-dist';
|
||||||
import { PDFDocument as PDFLibDocument } from 'pdf-lib';
|
import { PDFDocument as PDFLibDocument } from 'pdf-lib';
|
||||||
import { CropperState } from '@/types';
|
import { CropperState, CropPercentages } from '@/types';
|
||||||
import { loadPdfDocument } from '../utils/load-pdf-document.js';
|
import { loadPdfDocument } from '../utils/load-pdf-document.js';
|
||||||
|
|
||||||
pdfjsLib.GlobalWorkerOptions.workerSrc = new URL(
|
pdfjsLib.GlobalWorkerOptions.workerSrc = new URL(
|
||||||
@@ -167,7 +167,11 @@ async function displayPageAsImage(num: number) {
|
|||||||
const tempCtx = tempCanvas.getContext('2d');
|
const tempCtx = tempCanvas.getContext('2d');
|
||||||
tempCanvas.width = viewport.width;
|
tempCanvas.width = viewport.width;
|
||||||
tempCanvas.height = viewport.height;
|
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();
|
if (cropperState.cropper) cropperState.cropper.destroy();
|
||||||
|
|
||||||
@@ -251,7 +255,7 @@ async function performCrop() {
|
|||||||
document.getElementById('apply-to-all-toggle') as HTMLInputElement
|
document.getElementById('apply-to-all-toggle') as HTMLInputElement
|
||||||
)?.checked;
|
)?.checked;
|
||||||
|
|
||||||
let finalCropData: Record<number, any> = {};
|
let finalCropData: Record<number, CropPercentages> = {};
|
||||||
|
|
||||||
if (isApplyToAll) {
|
if (isApplyToAll) {
|
||||||
const currentCrop = cropperState.pageCrops[cropperState.currentPageNum];
|
const currentCrop = cropperState.pageCrops[cropperState.currentPageNum];
|
||||||
@@ -286,7 +290,7 @@ async function performCrop() {
|
|||||||
|
|
||||||
const fileName = isDestructive ? 'flattened_crop.pdf' : 'standard_crop.pdf';
|
const fileName = isDestructive ? 'flattened_crop.pdf' : 'standard_crop.pdf';
|
||||||
downloadFile(
|
downloadFile(
|
||||||
new Blob([finalPdfBytes], { type: 'application/pdf' }),
|
new Blob([new Uint8Array(finalPdfBytes)], { type: 'application/pdf' }),
|
||||||
fileName
|
fileName
|
||||||
);
|
);
|
||||||
showAlert(
|
showAlert(
|
||||||
@@ -304,7 +308,7 @@ async function performCrop() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async function performMetadataCrop(
|
async function performMetadataCrop(
|
||||||
cropData: Record<number, any>
|
cropData: Record<number, CropPercentages>
|
||||||
): Promise<Uint8Array> {
|
): Promise<Uint8Array> {
|
||||||
const pdfToModify = await loadPdfDocument(cropperState.originalPdfBytes!);
|
const pdfToModify = await loadPdfDocument(cropperState.originalPdfBytes!);
|
||||||
|
|
||||||
@@ -344,7 +348,7 @@ async function performMetadataCrop(
|
|||||||
}
|
}
|
||||||
|
|
||||||
async function performFlatteningCrop(
|
async function performFlatteningCrop(
|
||||||
cropData: Record<number, any>
|
cropData: Record<number, CropPercentages>
|
||||||
): Promise<Uint8Array> {
|
): Promise<Uint8Array> {
|
||||||
const newPdfDoc = await PDFLibDocument.create();
|
const newPdfDoc = await PDFLibDocument.create();
|
||||||
const sourcePdfDocForCopying = await loadPdfDocument(
|
const sourcePdfDocForCopying = await loadPdfDocument(
|
||||||
@@ -364,7 +368,11 @@ async function performFlatteningCrop(
|
|||||||
const tempCtx = tempCanvas.getContext('2d');
|
const tempCtx = tempCanvas.getContext('2d');
|
||||||
tempCanvas.width = viewport.width;
|
tempCanvas.width = viewport.width;
|
||||||
tempCanvas.height = viewport.height;
|
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 finalCanvas = document.createElement('canvas');
|
||||||
const finalCtx = finalCanvas.getContext('2d');
|
const finalCtx = finalCanvas.getContext('2d');
|
||||||
|
|||||||
@@ -16,7 +16,17 @@ pdfjsLib.GlobalWorkerOptions.workerSrc = new URL(
|
|||||||
).toString();
|
).toString();
|
||||||
|
|
||||||
// --- Global State for the Cropper Tool ---
|
// --- 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<number, CropPercentages>;
|
||||||
|
} = {
|
||||||
pdfDoc: null,
|
pdfDoc: null,
|
||||||
currentPageNum: 1,
|
currentPageNum: 1,
|
||||||
cropper: null,
|
cropper: null,
|
||||||
@@ -46,7 +56,8 @@ function saveCurrentCrop() {
|
|||||||
* Renders a PDF page to the Cropper UI as an image.
|
* Renders a PDF page to the Cropper UI as an image.
|
||||||
* @param {number} num The page number to render.
|
* @param {number} num The page number to render.
|
||||||
*/
|
*/
|
||||||
async function displayPageAsImage(num: any) {
|
|
||||||
|
async function displayPageAsImage(num: number) {
|
||||||
showLoader(`Rendering Page ${num}...`);
|
showLoader(`Rendering Page ${num}...`);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
@@ -57,7 +68,11 @@ async function displayPageAsImage(num: any) {
|
|||||||
const tempCtx = tempCanvas.getContext('2d');
|
const tempCtx = tempCanvas.getContext('2d');
|
||||||
tempCanvas.width = viewport.width;
|
tempCanvas.width = viewport.width;
|
||||||
tempCanvas.height = viewport.height;
|
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) {
|
if (cropperState.cropper) {
|
||||||
cropperState.cropper.destroy();
|
cropperState.cropper.destroy();
|
||||||
@@ -107,7 +122,7 @@ async function displayPageAsImage(num: any) {
|
|||||||
* Handles page navigation.
|
* Handles page navigation.
|
||||||
* @param {number} offset -1 for previous, 1 for next.
|
* @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
|
// Save the current page's crop before changing
|
||||||
saveCurrentCrop();
|
saveCurrentCrop();
|
||||||
|
|
||||||
@@ -137,7 +152,13 @@ function enableControls() {
|
|||||||
/**
|
/**
|
||||||
* Performs a non-destructive crop by updating the page's crop box.
|
* 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) {
|
for (const pageNum in cropData) {
|
||||||
const pdfJsPage = await cropperState.pdfDoc.getPage(Number(pageNum));
|
const pdfJsPage = await cropperState.pdfDoc.getPage(Number(pageNum));
|
||||||
const viewport = pdfJsPage.getViewport({ scale: 1 });
|
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.
|
* 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();
|
const newPdfDoc = await PDFLibDocument.create();
|
||||||
|
|
||||||
// Load the original PDF with pdf-lib to copy un-cropped pages from
|
// 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');
|
const tempCtx = tempCanvas.getContext('2d');
|
||||||
tempCanvas.width = viewport.width;
|
tempCanvas.width = viewport.width;
|
||||||
tempCanvas.height = viewport.height;
|
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 finalCanvas = document.createElement('canvas');
|
||||||
const finalCtx = finalCanvas.getContext('2d');
|
const finalCtx = finalCanvas.getContext('2d');
|
||||||
@@ -264,7 +294,7 @@ export async function setupCropperTool() {
|
|||||||
cropperState.pageCrops = {};
|
cropperState.pageCrops = {};
|
||||||
|
|
||||||
const arrayBuffer = await readFileAsArrayBuffer(state.files[0]);
|
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 arrayBufferForPdfJs = (arrayBuffer as ArrayBuffer).slice(0);
|
||||||
const loadingTask = getPDFDocument({ data: arrayBufferForPdfJs });
|
const loadingTask = getPDFDocument({ data: arrayBufferForPdfJs });
|
||||||
|
|
||||||
@@ -295,7 +325,7 @@ export async function setupCropperTool() {
|
|||||||
document.getElementById('apply-to-all-toggle') as HTMLInputElement
|
document.getElementById('apply-to-all-toggle') as HTMLInputElement
|
||||||
).checked;
|
).checked;
|
||||||
|
|
||||||
let finalCropData = {};
|
let finalCropData: Record<number, CropPercentages> = {};
|
||||||
if (isApplyToAll) {
|
if (isApplyToAll) {
|
||||||
const currentCrop = cropperState.pageCrops[cropperState.currentPageNum];
|
const currentCrop = cropperState.pageCrops[cropperState.currentPageNum];
|
||||||
if (!currentCrop) {
|
if (!currentCrop) {
|
||||||
@@ -308,10 +338,13 @@ export async function setupCropperTool() {
|
|||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
// If not applying to all, only process pages with saved crops
|
// If not applying to all, only process pages with saved crops
|
||||||
finalCropData = Object.keys(cropperState.pageCrops).reduce((obj, key) => {
|
finalCropData = Object.keys(cropperState.pageCrops).reduce(
|
||||||
obj[key] = cropperState.pageCrops[key];
|
(obj, key) => {
|
||||||
return obj;
|
obj[Number(key)] = cropperState.pageCrops[Number(key)];
|
||||||
});
|
return obj;
|
||||||
|
},
|
||||||
|
{} as Record<number, CropPercentages>
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (Object.keys(finalCropData).length === 0) {
|
if (Object.keys(finalCropData).length === 0) {
|
||||||
@@ -341,7 +374,7 @@ export async function setupCropperTool() {
|
|||||||
? 'flattened_crop.pdf'
|
? 'flattened_crop.pdf'
|
||||||
: 'standard_crop.pdf';
|
: 'standard_crop.pdf';
|
||||||
downloadFile(
|
downloadFile(
|
||||||
new Blob([finalPdfBytes], { type: 'application/pdf' }),
|
new Blob([new Uint8Array(finalPdfBytes)], { type: 'application/pdf' }),
|
||||||
fileName
|
fileName
|
||||||
);
|
);
|
||||||
showAlert('Success', 'Crop complete! Your download has started.');
|
showAlert('Success', 'Crop complete! Your download has started.');
|
||||||
|
|||||||
@@ -1,231 +1,255 @@
|
|||||||
import { showLoader, hideLoader, showAlert } from '../ui.js';
|
import { showLoader, hideLoader, showAlert } from '../ui.js';
|
||||||
import {
|
import { downloadFile, formatBytes } from '../utils/helpers.js';
|
||||||
downloadFile,
|
|
||||||
readFileAsArrayBuffer,
|
|
||||||
formatBytes,
|
|
||||||
} from '../utils/helpers.js';
|
|
||||||
import { state } from '../state.js';
|
import { state } from '../state.js';
|
||||||
import { createIcons, icons } from 'lucide';
|
import { createIcons, icons } from 'lucide';
|
||||||
|
|
||||||
document.addEventListener('DOMContentLoaded', () => {
|
document.addEventListener('DOMContentLoaded', () => {
|
||||||
state.files = [];
|
state.files = [];
|
||||||
|
|
||||||
const fileInput = document.getElementById('file-input') as HTMLInputElement;
|
const fileInput = document.getElementById('file-input') as HTMLInputElement;
|
||||||
const dropZone = document.getElementById('drop-zone');
|
const dropZone = document.getElementById('drop-zone');
|
||||||
const convertOptions = document.getElementById('convert-options');
|
const convertOptions = document.getElementById('convert-options');
|
||||||
const fileDisplayArea = document.getElementById('file-display-area');
|
const fileDisplayArea = document.getElementById('file-display-area');
|
||||||
const fileControls = document.getElementById('file-controls');
|
const fileControls = document.getElementById('file-controls');
|
||||||
const addMoreBtn = document.getElementById('add-more-btn');
|
const addMoreBtn = document.getElementById('add-more-btn');
|
||||||
const clearFilesBtn = document.getElementById('clear-files-btn');
|
const clearFilesBtn = document.getElementById('clear-files-btn');
|
||||||
const backBtn = document.getElementById('back-to-tools');
|
const backBtn = document.getElementById('back-to-tools');
|
||||||
const processBtn = document.getElementById('process-btn');
|
const processBtn = document.getElementById('process-btn');
|
||||||
|
|
||||||
if (backBtn) {
|
if (backBtn) {
|
||||||
backBtn.addEventListener('click', () => {
|
backBtn.addEventListener('click', () => {
|
||||||
window.location.href = import.meta.env.BASE_URL;
|
window.location.href = import.meta.env.BASE_URL;
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
const updateUI = async () => {
|
const updateUI = async () => {
|
||||||
if (!convertOptions) return;
|
if (!convertOptions) return;
|
||||||
|
|
||||||
if (state.files.length > 0) {
|
if (state.files.length > 0) {
|
||||||
if (fileDisplayArea) {
|
if (fileDisplayArea) {
|
||||||
fileDisplayArea.innerHTML = '';
|
fileDisplayArea.innerHTML = '';
|
||||||
|
|
||||||
for (let index = 0; index < state.files.length; index++) {
|
for (let index = 0; index < state.files.length; index++) {
|
||||||
const file = state.files[index];
|
const file = state.files[index];
|
||||||
const fileDiv = document.createElement('div');
|
const fileDiv = document.createElement('div');
|
||||||
fileDiv.className = 'flex items-center justify-between bg-gray-700 p-3 rounded-lg text-sm';
|
fileDiv.className =
|
||||||
|
'flex items-center justify-between bg-gray-700 p-3 rounded-lg text-sm';
|
||||||
|
|
||||||
const infoContainer = document.createElement('div');
|
const infoContainer = document.createElement('div');
|
||||||
infoContainer.className = 'flex flex-col overflow-hidden';
|
infoContainer.className = 'flex flex-col overflow-hidden';
|
||||||
|
|
||||||
const nameSpan = document.createElement('div');
|
const nameSpan = document.createElement('div');
|
||||||
nameSpan.className = 'truncate font-medium text-gray-200 text-sm mb-1';
|
nameSpan.className =
|
||||||
nameSpan.textContent = file.name;
|
'truncate font-medium text-gray-200 text-sm mb-1';
|
||||||
|
nameSpan.textContent = file.name;
|
||||||
|
|
||||||
const metaSpan = document.createElement('div');
|
const metaSpan = document.createElement('div');
|
||||||
metaSpan.className = 'text-xs text-gray-400';
|
metaSpan.className = 'text-xs text-gray-400';
|
||||||
metaSpan.textContent = formatBytes(file.size);
|
metaSpan.textContent = formatBytes(file.size);
|
||||||
|
|
||||||
infoContainer.append(nameSpan, metaSpan);
|
infoContainer.append(nameSpan, metaSpan);
|
||||||
|
|
||||||
const removeBtn = document.createElement('button');
|
const removeBtn = document.createElement('button');
|
||||||
removeBtn.className = 'ml-4 text-red-400 hover:text-red-300 flex-shrink-0';
|
removeBtn.className =
|
||||||
removeBtn.innerHTML = '<i data-lucide="trash-2" class="w-4 h-4"></i>';
|
'ml-4 text-red-400 hover:text-red-300 flex-shrink-0';
|
||||||
removeBtn.onclick = () => {
|
removeBtn.innerHTML = '<i data-lucide="trash-2" class="w-4 h-4"></i>';
|
||||||
state.files = state.files.filter((_, i) => i !== index);
|
removeBtn.onclick = () => {
|
||||||
updateUI();
|
state.files = state.files.filter((_, i) => i !== index);
|
||||||
};
|
|
||||||
|
|
||||||
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)];
|
|
||||||
updateUI();
|
updateUI();
|
||||||
|
};
|
||||||
|
|
||||||
|
fileDiv.append(infoContainer, removeBtn);
|
||||||
|
fileDisplayArea.appendChild(fileDiv);
|
||||||
}
|
}
|
||||||
};
|
|
||||||
|
|
||||||
if (fileInput && dropZone) {
|
createIcons({ icons });
|
||||||
fileInput.addEventListener('change', (e) => {
|
}
|
||||||
handleFileSelect((e.target as HTMLInputElement).files);
|
if (fileControls) fileControls.classList.remove('hidden');
|
||||||
});
|
convertOptions.classList.remove('hidden');
|
||||||
|
} else {
|
||||||
dropZone.addEventListener('dragover', (e) => {
|
if (fileDisplayArea) fileDisplayArea.innerHTML = '';
|
||||||
e.preventDefault();
|
if (fileControls) fileControls.classList.add('hidden');
|
||||||
dropZone.classList.add('bg-gray-700');
|
convertOptions.classList.add('hidden');
|
||||||
});
|
|
||||||
|
|
||||||
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);
|
|
||||||
}
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const resetState = () => {
|
||||||
|
state.files = [];
|
||||||
|
state.pdfDoc = null;
|
||||||
updateUI();
|
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();
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -170,7 +170,7 @@ async function decryptPdf() {
|
|||||||
|
|
||||||
zip.file(`unlocked-${file.name}`, decryptedBytes, { binary: true });
|
zip.file(`unlocked-${file.name}`, decryptedBytes, { binary: true });
|
||||||
successCount++;
|
successCount++;
|
||||||
} catch (fileError: any) {
|
} catch (fileError: unknown) {
|
||||||
errorCount++;
|
errorCount++;
|
||||||
console.error(`Failed to decrypt ${file.name}:`, fileError);
|
console.error(`Failed to decrypt ${file.name}:`, fileError);
|
||||||
}
|
}
|
||||||
@@ -194,15 +194,16 @@ async function decryptPdf() {
|
|||||||
resetState();
|
resetState();
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
} catch (error: any) {
|
} catch (error: unknown) {
|
||||||
console.error('Error during PDF decryption:', error);
|
console.error('Error during PDF decryption:', error);
|
||||||
|
|
||||||
if (error.message === 'INVALID_PASSWORD') {
|
const errorMessage = error instanceof Error ? error.message : '';
|
||||||
|
if (errorMessage === 'INVALID_PASSWORD') {
|
||||||
showAlert(
|
showAlert(
|
||||||
'Incorrect Password',
|
'Incorrect Password',
|
||||||
'The password you entered is incorrect. Please try again.'
|
'The password you entered is incorrect. Please try again.'
|
||||||
);
|
);
|
||||||
} else if (error.message?.includes('password')) {
|
} else if (errorMessage.includes('password')) {
|
||||||
showAlert(
|
showAlert(
|
||||||
'Password Error',
|
'Password Error',
|
||||||
'Unable to decrypt the PDF with the provided password.'
|
'Unable to decrypt the PDF with the provided password.'
|
||||||
@@ -210,7 +211,7 @@ async function decryptPdf() {
|
|||||||
} else {
|
} else {
|
||||||
showAlert(
|
showAlert(
|
||||||
'Decryption Failed',
|
'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 {
|
} finally {
|
||||||
|
|||||||
@@ -164,7 +164,7 @@ async function renderThumbnails() {
|
|||||||
canvas.width = viewport.width;
|
canvas.width = viewport.width;
|
||||||
canvas.height = viewport.height;
|
canvas.height = viewport.height;
|
||||||
const ctx = canvas.getContext('2d');
|
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');
|
const wrapper = document.createElement('div');
|
||||||
wrapper.className = 'relative cursor-pointer group';
|
wrapper.className = 'relative cursor-pointer group';
|
||||||
|
|||||||
@@ -1,9 +1,10 @@
|
|||||||
import { isWasmAvailable, getWasmBaseUrl } from '../config/wasm-cdn-config.js';
|
import { loadPyMuPDF } from '../utils/pymupdf-loader.js';
|
||||||
import { showWasmRequiredDialog } from '../utils/wasm-provider.js';
|
import type { PyMuPDFInstance } from '@/types';
|
||||||
import { loadPyMuPDF, isPyMuPDFAvailable } from '../utils/pymupdf-loader.js';
|
|
||||||
import { batchDecryptIfNeeded } from '../utils/password-prompt.js';
|
import { batchDecryptIfNeeded } from '../utils/password-prompt.js';
|
||||||
import { createIcons, icons } from 'lucide';
|
import { createIcons, icons } from 'lucide';
|
||||||
import { downloadFile } from '../utils/helpers';
|
import { downloadFile } from '../utils/helpers';
|
||||||
|
import { isWasmAvailable } from '../config/wasm-cdn-config.js';
|
||||||
|
import { showWasmRequiredDialog } from '../utils/wasm-provider.js';
|
||||||
|
|
||||||
interface DeskewResult {
|
interface DeskewResult {
|
||||||
totalPages: number;
|
totalPages: number;
|
||||||
@@ -13,11 +14,11 @@ interface DeskewResult {
|
|||||||
}
|
}
|
||||||
|
|
||||||
let selectedFiles: File[] = [];
|
let selectedFiles: File[] = [];
|
||||||
let pymupdf: any = null;
|
let pymupdf: PyMuPDFInstance | null = null;
|
||||||
|
|
||||||
async function initPyMuPDF(): Promise<any> {
|
async function initPyMuPDF(): Promise<PyMuPDFInstance> {
|
||||||
if (!pymupdf) {
|
if (!pymupdf) {
|
||||||
pymupdf = await loadPyMuPDF();
|
pymupdf = (await loadPyMuPDF()) as PyMuPDFInstance;
|
||||||
}
|
}
|
||||||
return pymupdf;
|
return pymupdf;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -400,7 +400,7 @@ async function handleCertFile(file: File): Promise<void> {
|
|||||||
certStatus.className = 'text-xs text-green-400';
|
certStatus.className = 'text-xs text-green-400';
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch {
|
||||||
const certStatus = getElement<HTMLDivElement>('cert-status');
|
const certStatus = getElement<HTMLDivElement>('cert-status');
|
||||||
if (certStatus) {
|
if (certStatus) {
|
||||||
certStatus.textContent = 'Failed to parse PEM file';
|
certStatus.textContent = 'Failed to parse PEM file';
|
||||||
|
|||||||
@@ -16,7 +16,9 @@ pdfjsLib.GlobalWorkerOptions.workerSrc = new URL(
|
|||||||
import.meta.url
|
import.meta.url
|
||||||
).toString();
|
).toString();
|
||||||
|
|
||||||
const duplicateOrganizeState = {
|
const duplicateOrganizeState: {
|
||||||
|
sortableInstances: { pageGrid?: Sortable };
|
||||||
|
} = {
|
||||||
sortableInstances: {},
|
sortableInstances: {},
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -24,13 +26,10 @@ function initializePageGridSortable() {
|
|||||||
const grid = document.getElementById('page-grid');
|
const grid = document.getElementById('page-grid');
|
||||||
if (!grid) return;
|
if (!grid) return;
|
||||||
|
|
||||||
// @ts-expect-error TS(2339) FIXME: Property 'pageGrid' does not exist on type '{}'.
|
|
||||||
if (duplicateOrganizeState.sortableInstances.pageGrid) {
|
if (duplicateOrganizeState.sortableInstances.pageGrid) {
|
||||||
// @ts-expect-error TS(2339) FIXME: Property 'pageGrid' does not exist on type '{}'.
|
|
||||||
duplicateOrganizeState.sortableInstances.pageGrid.destroy();
|
duplicateOrganizeState.sortableInstances.pageGrid.destroy();
|
||||||
}
|
}
|
||||||
|
|
||||||
// @ts-expect-error TS(2339) FIXME: Property 'pageGrid' does not exist on type '{}'.
|
|
||||||
duplicateOrganizeState.sortableInstances.pageGrid = Sortable.create(grid, {
|
duplicateOrganizeState.sortableInstances.pageGrid = Sortable.create(grid, {
|
||||||
animation: 150,
|
animation: 150,
|
||||||
ghostClass: 'sortable-ghost',
|
ghostClass: 'sortable-ghost',
|
||||||
@@ -38,10 +37,10 @@ function initializePageGridSortable() {
|
|||||||
dragClass: 'sortable-drag',
|
dragClass: 'sortable-drag',
|
||||||
filter: '.duplicate-btn, .delete-btn',
|
filter: '.duplicate-btn, .delete-btn',
|
||||||
preventOnFilter: true,
|
preventOnFilter: true,
|
||||||
onStart: function (evt: any) {
|
onStart: function (evt: Sortable.SortableEvent) {
|
||||||
evt.item.style.opacity = '0.5';
|
evt.item.style.opacity = '0.5';
|
||||||
},
|
},
|
||||||
onEnd: function (evt: any) {
|
onEnd: function (evt: Sortable.SortableEvent) {
|
||||||
evt.item.style.opacity = '1';
|
evt.item.style.opacity = '1';
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
@@ -51,7 +50,7 @@ function initializePageGridSortable() {
|
|||||||
* Attaches event listeners for duplicate and delete to a page thumbnail element.
|
* Attaches event listeners for duplicate and delete to a page thumbnail element.
|
||||||
* @param {HTMLElement} element The thumbnail element to attach listeners to.
|
* @param {HTMLElement} element The thumbnail element to attach listeners to.
|
||||||
*/
|
*/
|
||||||
function attachEventListeners(element: any) {
|
function attachEventListeners(element: HTMLElement) {
|
||||||
// Re-number all visible page labels
|
// Re-number all visible page labels
|
||||||
const renumberPages = () => {
|
const renumberPages = () => {
|
||||||
const grid = document.getElementById('page-grid');
|
const grid = document.getElementById('page-grid');
|
||||||
@@ -65,16 +64,16 @@ function attachEventListeners(element: any) {
|
|||||||
// Duplicate button listener
|
// Duplicate button listener
|
||||||
element
|
element
|
||||||
.querySelector('.duplicate-btn')
|
.querySelector('.duplicate-btn')
|
||||||
.addEventListener('click', (e: any) => {
|
.addEventListener('click', (e: Event) => {
|
||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
const clone = element.cloneNode(true);
|
const clone = element.cloneNode(true) as HTMLElement;
|
||||||
element.after(clone);
|
element.after(clone);
|
||||||
attachEventListeners(clone);
|
attachEventListeners(clone);
|
||||||
renumberPages();
|
renumberPages();
|
||||||
initializePageGridSortable();
|
initializePageGridSortable();
|
||||||
});
|
});
|
||||||
|
|
||||||
element.querySelector('.delete-btn').addEventListener('click', (e: any) => {
|
element.querySelector('.delete-btn').addEventListener('click', (e: Event) => {
|
||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
if (document.getElementById('page-grid').children.length > 1) {
|
if (document.getElementById('page-grid').children.length > 1) {
|
||||||
element.remove();
|
element.remove();
|
||||||
@@ -218,7 +217,7 @@ export async function processAndSave() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const copiedPages = await newPdfDoc.copyPages(state.pdfDoc, finalIndices);
|
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();
|
const newPdfBytes = await newPdfDoc.save();
|
||||||
downloadFile(
|
downloadFile(
|
||||||
|
|||||||
@@ -44,7 +44,11 @@ worker.onmessage = function (e) {
|
|||||||
const data = e.data;
|
const data = e.data;
|
||||||
|
|
||||||
if (data.status === 'success' && data.attachments !== undefined) {
|
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 {
|
return {
|
||||||
...att,
|
...att,
|
||||||
data: new Uint8Array(att.data),
|
data: new Uint8Array(att.data),
|
||||||
|
|||||||
@@ -11,8 +11,11 @@ const embedPdfWasmUrl = new URL(
|
|||||||
import.meta.url
|
import.meta.url
|
||||||
).href;
|
).href;
|
||||||
|
|
||||||
let viewerInstance: any = null;
|
import type { EmbedPdfContainer } from 'embedpdf-snippet';
|
||||||
let docManagerPlugin: any = null;
|
import type { DocManagerPlugin } from '@/types';
|
||||||
|
|
||||||
|
let viewerInstance: EmbedPdfContainer | null = null;
|
||||||
|
let docManagerPlugin: DocManagerPlugin | null = null;
|
||||||
let isViewerInitialized = false;
|
let isViewerInitialized = false;
|
||||||
const fileEntryMap = new Map<string, HTMLElement>();
|
const fileEntryMap = new Map<string, HTMLElement>();
|
||||||
|
|
||||||
@@ -148,33 +151,37 @@ async function handleFiles(files: FileList) {
|
|||||||
});
|
});
|
||||||
|
|
||||||
const registry = await viewerInstance.registry;
|
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) => {
|
docManagerPlugin.onDocumentClosed((data: { id?: string }) => {
|
||||||
const docId = data?.id || data;
|
const docId = data?.id || '';
|
||||||
removeFileEntry(docId);
|
removeFileEntry(docId);
|
||||||
});
|
});
|
||||||
|
|
||||||
docManagerPlugin.onDocumentOpened((data: any) => {
|
docManagerPlugin.onDocumentOpened(
|
||||||
const docId = data?.id;
|
(data: { id?: string; name?: string }) => {
|
||||||
const docKey = data?.name;
|
const docId = data?.id;
|
||||||
if (!docId) return;
|
const docKey = data?.name;
|
||||||
const pendingEntry = fileDisplayArea.querySelector(
|
if (!docId) return;
|
||||||
`[data-pending-name="${CSS.escape(docKey)}"]`
|
const pendingEntry = fileDisplayArea.querySelector(
|
||||||
) as HTMLElement;
|
`[data-pending-name="${CSS.escape(docKey)}"]`
|
||||||
if (pendingEntry) {
|
|
||||||
pendingEntry.removeAttribute('data-pending-name');
|
|
||||||
fileEntryMap.set(docId, pendingEntry);
|
|
||||||
const removeBtn = pendingEntry.querySelector(
|
|
||||||
'[data-remove-btn]'
|
|
||||||
) as HTMLElement;
|
) as HTMLElement;
|
||||||
if (removeBtn) {
|
if (pendingEntry) {
|
||||||
removeBtn.onclick = () => {
|
pendingEntry.removeAttribute('data-pending-name');
|
||||||
docManagerPlugin.closeDocument(docId);
|
fileEntryMap.set(docId, pendingEntry);
|
||||||
};
|
const removeBtn = pendingEntry.querySelector(
|
||||||
|
'[data-remove-btn]'
|
||||||
|
) as HTMLElement;
|
||||||
|
if (removeBtn) {
|
||||||
|
removeBtn.onclick = () => {
|
||||||
|
docManagerPlugin.closeDocument(docId);
|
||||||
|
};
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
);
|
||||||
|
|
||||||
addFileEntries(fileDisplayArea, decryptedFiles);
|
addFileEntries(fileDisplayArea, decryptedFiles);
|
||||||
|
|
||||||
|
|||||||
@@ -3,9 +3,7 @@ import { downloadFile, formatBytes } from '../utils/helpers.js';
|
|||||||
import { state } from '../state.js';
|
import { state } from '../state.js';
|
||||||
import { createIcons, icons } from 'lucide';
|
import { createIcons, icons } from 'lucide';
|
||||||
import { parseEmailFile, renderEmailToHtml } from './email-to-pdf.js';
|
import { parseEmailFile, renderEmailToHtml } from './email-to-pdf.js';
|
||||||
import { isWasmAvailable, getWasmBaseUrl } from '../config/wasm-cdn-config.js';
|
import { loadPyMuPDF } from '../utils/pymupdf-loader.js';
|
||||||
import { showWasmRequiredDialog } from '../utils/wasm-provider.js';
|
|
||||||
import { loadPyMuPDF, isPyMuPDFAvailable } from '../utils/pymupdf-loader.js';
|
|
||||||
|
|
||||||
const EXTENSIONS = ['.eml', '.msg'];
|
const EXTENSIONS = ['.eml', '.msg'];
|
||||||
const TOOL_NAME = 'Email';
|
const TOOL_NAME = 'Email';
|
||||||
@@ -125,7 +123,11 @@ document.addEventListener('DOMContentLoaded', () => {
|
|||||||
pageSize,
|
pageSize,
|
||||||
});
|
});
|
||||||
|
|
||||||
const pdfBlob = await (pymupdf as any).htmlToPdf(htmlContent, {
|
const pdfBlob = await (
|
||||||
|
pymupdf as unknown as {
|
||||||
|
htmlToPdf: (html: string, options: unknown) => Promise<Blob>;
|
||||||
|
}
|
||||||
|
).htmlToPdf(htmlContent, {
|
||||||
pageSize,
|
pageSize,
|
||||||
margins: { top: 50, right: 50, bottom: 50, left: 50 },
|
margins: { top: 50, right: 50, bottom: 50, left: 50 },
|
||||||
attachments: email.attachments
|
attachments: email.attachments
|
||||||
@@ -166,7 +168,11 @@ document.addEventListener('DOMContentLoaded', () => {
|
|||||||
pageSize,
|
pageSize,
|
||||||
});
|
});
|
||||||
|
|
||||||
const pdfBlob = await (pymupdf as any).htmlToPdf(htmlContent, {
|
const pdfBlob = await (
|
||||||
|
pymupdf as unknown as {
|
||||||
|
htmlToPdf: (html: string, options: unknown) => Promise<Blob>;
|
||||||
|
}
|
||||||
|
).htmlToPdf(htmlContent, {
|
||||||
pageSize,
|
pageSize,
|
||||||
margins: { top: 50, right: 50, bottom: 50, left: 50 },
|
margins: { top: 50, right: 50, bottom: 50, left: 50 },
|
||||||
attachments: email.attachments
|
attachments: email.attachments
|
||||||
@@ -179,7 +185,7 @@ document.addEventListener('DOMContentLoaded', () => {
|
|||||||
const baseName = file.name.replace(/\.[^.]+$/, '');
|
const baseName = file.name.replace(/\.[^.]+$/, '');
|
||||||
const pdfBuffer = await pdfBlob.arrayBuffer();
|
const pdfBuffer = await pdfBlob.arrayBuffer();
|
||||||
zip.file(`${baseName}.pdf`, pdfBuffer);
|
zip.file(`${baseName}.pdf`, pdfBuffer);
|
||||||
} catch (e: any) {
|
} catch (e: unknown) {
|
||||||
console.error(`Failed to convert ${file.name}:`, e);
|
console.error(`Failed to convert ${file.name}:`, e);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -196,12 +202,12 @@ document.addEventListener('DOMContentLoaded', () => {
|
|||||||
() => resetState()
|
() => resetState()
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
} catch (e: any) {
|
} catch (e: unknown) {
|
||||||
console.error(`[${TOOL_NAME}2PDF] ERROR:`, e);
|
console.error(`[${TOOL_NAME}2PDF] ERROR:`, e);
|
||||||
hideLoader();
|
hideLoader();
|
||||||
showAlert(
|
showAlert(
|
||||||
'Error',
|
'Error',
|
||||||
`An error occurred during conversion. Error: ${e.message}`
|
`An error occurred during conversion. Error: ${e instanceof Error ? e.message : String(e)}`
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -42,7 +42,14 @@ export async function parseEmlFile(file: File): Promise<ParsedEmail> {
|
|||||||
.filter(Boolean);
|
.filter(Boolean);
|
||||||
|
|
||||||
// Helper to map parsing result to EmailAttachment
|
// 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 content: Uint8Array | undefined;
|
||||||
let size = 0;
|
let size = 0;
|
||||||
if (att.content) {
|
if (att.content) {
|
||||||
@@ -67,7 +74,9 @@ export async function parseEmlFile(file: File): Promise<ParsedEmail> {
|
|||||||
|
|
||||||
const attachments: EmailAttachment[] = [
|
const attachments: EmailAttachment[] = [
|
||||||
...(email.attachments || []).map(mapAttachment),
|
...(email.attachments || []).map(mapAttachment),
|
||||||
...((email as any).inline || []).map(mapAttachment),
|
...((email as { inline?: RawAttachment[] }).inline || []).map(
|
||||||
|
mapAttachment
|
||||||
|
),
|
||||||
];
|
];
|
||||||
|
|
||||||
// Preserve original date string from headers
|
// Preserve original date string from headers
|
||||||
@@ -138,8 +147,16 @@ export async function parseMsgFile(file: File): Promise<ParsedEmail> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
interface MsgAttachment {
|
||||||
|
fileName?: string;
|
||||||
|
name?: string;
|
||||||
|
content?: ArrayLike<number>;
|
||||||
|
mimeType?: string;
|
||||||
|
pidContentId?: string;
|
||||||
|
}
|
||||||
|
|
||||||
const attachments: EmailAttachment[] = (msgData.attachments || []).map(
|
const attachments: EmailAttachment[] = (msgData.attachments || []).map(
|
||||||
(att: any) => ({
|
(att: MsgAttachment) => ({
|
||||||
filename: att.fileName || att.name || 'unnamed',
|
filename: att.fileName || att.name || 'unnamed',
|
||||||
size: att.content?.length || 0,
|
size: att.content?.length || 0,
|
||||||
contentType: att.mimeType || 'application/octet-stream',
|
contentType: att.mimeType || 'application/octet-stream',
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ import {
|
|||||||
readFileAsArrayBuffer,
|
readFileAsArrayBuffer,
|
||||||
} from '../utils/helpers.js';
|
} from '../utils/helpers.js';
|
||||||
import { icons, createIcons } from 'lucide';
|
import { icons, createIcons } from 'lucide';
|
||||||
import { EncryptPdfState } from '@/types';
|
import { EncryptPdfState, QpdfInstanceExtended } from '@/types';
|
||||||
|
|
||||||
const pageState: EncryptPdfState = {
|
const pageState: EncryptPdfState = {
|
||||||
file: null,
|
file: null,
|
||||||
@@ -114,7 +114,7 @@ async function encryptPdf() {
|
|||||||
|
|
||||||
const inputPath = '/input.pdf';
|
const inputPath = '/input.pdf';
|
||||||
const outputPath = '/output.pdf';
|
const outputPath = '/output.pdf';
|
||||||
let qpdf: any;
|
let qpdf: QpdfInstanceExtended;
|
||||||
|
|
||||||
const loaderModal = document.getElementById('loader-modal');
|
const loaderModal = document.getElementById('loader-modal');
|
||||||
const loaderText = document.getElementById('loader-text');
|
const loaderText = document.getElementById('loader-text');
|
||||||
@@ -154,10 +154,11 @@ async function encryptPdf() {
|
|||||||
|
|
||||||
try {
|
try {
|
||||||
qpdf.callMain(args);
|
qpdf.callMain(args);
|
||||||
} catch (qpdfError: any) {
|
} catch (qpdfError: unknown) {
|
||||||
console.error('qpdf execution error:', qpdfError);
|
console.error('qpdf execution error:', qpdfError);
|
||||||
throw new Error(
|
throw new Error(
|
||||||
'Encryption failed: ' + (qpdfError.message || 'Unknown error'),
|
'Encryption failed: ' +
|
||||||
|
(qpdfError instanceof Error ? qpdfError.message : 'Unknown error'),
|
||||||
{ cause: qpdfError }
|
{ cause: qpdfError }
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -169,7 +170,9 @@ async function encryptPdf() {
|
|||||||
throw new Error('Encryption resulted in an empty file.');
|
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}`);
|
downloadFile(blob, `encrypted-${pageState.file.name}`);
|
||||||
|
|
||||||
if (loaderModal) loaderModal.classList.add('hidden');
|
if (loaderModal) loaderModal.classList.add('hidden');
|
||||||
@@ -183,12 +186,12 @@ async function encryptPdf() {
|
|||||||
showAlert('Success', successMessage, 'success', () => {
|
showAlert('Success', successMessage, 'success', () => {
|
||||||
resetState();
|
resetState();
|
||||||
});
|
});
|
||||||
} catch (error: any) {
|
} catch (error: unknown) {
|
||||||
console.error('Error during PDF encryption:', error);
|
console.error('Error during PDF encryption:', error);
|
||||||
if (loaderModal) loaderModal.classList.add('hidden');
|
if (loaderModal) loaderModal.classList.add('hidden');
|
||||||
showAlert(
|
showAlert(
|
||||||
'Encryption Failed',
|
'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 {
|
} finally {
|
||||||
try {
|
try {
|
||||||
|
|||||||
@@ -2,9 +2,7 @@ import { showLoader, hideLoader, showAlert } from '../ui.js';
|
|||||||
import { downloadFile, formatBytes } from '../utils/helpers.js';
|
import { downloadFile, formatBytes } from '../utils/helpers.js';
|
||||||
import { state } from '../state.js';
|
import { state } from '../state.js';
|
||||||
import { createIcons, icons } from 'lucide';
|
import { createIcons, icons } from 'lucide';
|
||||||
import { isWasmAvailable, getWasmBaseUrl } from '../config/wasm-cdn-config.js';
|
import { loadPyMuPDF } from '../utils/pymupdf-loader.js';
|
||||||
import { showWasmRequiredDialog } from '../utils/wasm-provider.js';
|
|
||||||
import { loadPyMuPDF, isPyMuPDFAvailable } from '../utils/pymupdf-loader.js';
|
|
||||||
|
|
||||||
const FILETYPE = 'epub';
|
const FILETYPE = 'epub';
|
||||||
const EXTENSIONS = ['.epub'];
|
const EXTENSIONS = ['.epub'];
|
||||||
@@ -145,12 +143,12 @@ document.addEventListener('DOMContentLoaded', () => {
|
|||||||
() => resetState()
|
() => resetState()
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
} catch (e: any) {
|
} catch (e: unknown) {
|
||||||
console.error(`[${TOOL_NAME}2PDF] ERROR:`, e);
|
console.error(`[${TOOL_NAME}2PDF] ERROR:`, e);
|
||||||
hideLoader();
|
hideLoader();
|
||||||
showAlert(
|
showAlert(
|
||||||
'Error',
|
'Error',
|
||||||
`An error occurred during conversion. Error: ${e.message}`
|
`An error occurred during conversion. Error: ${e instanceof Error ? e.message : String(e)}`
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,216 +1,226 @@
|
|||||||
import { showLoader, hideLoader, showAlert } from '../ui.js';
|
import { showLoader, hideLoader, showAlert } from '../ui.js';
|
||||||
import {
|
import { downloadFile, formatBytes } from '../utils/helpers.js';
|
||||||
downloadFile,
|
|
||||||
readFileAsArrayBuffer,
|
|
||||||
formatBytes,
|
|
||||||
} from '../utils/helpers.js';
|
|
||||||
import { state } from '../state.js';
|
import { state } from '../state.js';
|
||||||
import { createIcons, icons } from 'lucide';
|
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', () => {
|
document.addEventListener('DOMContentLoaded', () => {
|
||||||
state.files = [];
|
state.files = [];
|
||||||
|
|
||||||
const fileInput = document.getElementById('file-input') as HTMLInputElement;
|
const fileInput = document.getElementById('file-input') as HTMLInputElement;
|
||||||
const dropZone = document.getElementById('drop-zone');
|
const dropZone = document.getElementById('drop-zone');
|
||||||
const convertOptions = document.getElementById('convert-options');
|
const convertOptions = document.getElementById('convert-options');
|
||||||
const fileDisplayArea = document.getElementById('file-display-area');
|
const fileDisplayArea = document.getElementById('file-display-area');
|
||||||
const fileControls = document.getElementById('file-controls');
|
const fileControls = document.getElementById('file-controls');
|
||||||
const addMoreBtn = document.getElementById('add-more-btn');
|
const addMoreBtn = document.getElementById('add-more-btn');
|
||||||
const clearFilesBtn = document.getElementById('clear-files-btn');
|
const clearFilesBtn = document.getElementById('clear-files-btn');
|
||||||
const backBtn = document.getElementById('back-to-tools');
|
const backBtn = document.getElementById('back-to-tools');
|
||||||
const processBtn = document.getElementById('process-btn');
|
const processBtn = document.getElementById('process-btn');
|
||||||
|
|
||||||
if (backBtn) {
|
if (backBtn) {
|
||||||
backBtn.addEventListener('click', () => {
|
backBtn.addEventListener('click', () => {
|
||||||
window.location.href = import.meta.env.BASE_URL;
|
window.location.href = import.meta.env.BASE_URL;
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
const updateUI = async () => {
|
const updateUI = async () => {
|
||||||
if (!convertOptions) return;
|
if (!convertOptions) return;
|
||||||
|
|
||||||
if (state.files.length > 0) {
|
if (state.files.length > 0) {
|
||||||
if (fileDisplayArea) {
|
if (fileDisplayArea) {
|
||||||
fileDisplayArea.innerHTML = '';
|
fileDisplayArea.innerHTML = '';
|
||||||
|
|
||||||
for (let index = 0; index < state.files.length; index++) {
|
for (let index = 0; index < state.files.length; index++) {
|
||||||
const file = state.files[index];
|
const file = state.files[index];
|
||||||
const fileDiv = document.createElement('div');
|
const fileDiv = document.createElement('div');
|
||||||
fileDiv.className = 'flex items-center justify-between bg-gray-700 p-3 rounded-lg text-sm';
|
fileDiv.className =
|
||||||
|
'flex items-center justify-between bg-gray-700 p-3 rounded-lg text-sm';
|
||||||
|
|
||||||
const infoContainer = document.createElement('div');
|
const infoContainer = document.createElement('div');
|
||||||
infoContainer.className = 'flex flex-col overflow-hidden';
|
infoContainer.className = 'flex flex-col overflow-hidden';
|
||||||
|
|
||||||
const nameSpan = document.createElement('div');
|
const nameSpan = document.createElement('div');
|
||||||
nameSpan.className = 'truncate font-medium text-gray-200 text-sm mb-1';
|
nameSpan.className =
|
||||||
nameSpan.textContent = file.name;
|
'truncate font-medium text-gray-200 text-sm mb-1';
|
||||||
|
nameSpan.textContent = file.name;
|
||||||
|
|
||||||
const metaSpan = document.createElement('div');
|
const metaSpan = document.createElement('div');
|
||||||
metaSpan.className = 'text-xs text-gray-400';
|
metaSpan.className = 'text-xs text-gray-400';
|
||||||
metaSpan.textContent = formatBytes(file.size);
|
metaSpan.textContent = formatBytes(file.size);
|
||||||
|
|
||||||
infoContainer.append(nameSpan, metaSpan);
|
infoContainer.append(nameSpan, metaSpan);
|
||||||
|
|
||||||
const removeBtn = document.createElement('button');
|
const removeBtn = document.createElement('button');
|
||||||
removeBtn.className = 'ml-4 text-red-400 hover:text-red-300 flex-shrink-0';
|
removeBtn.className =
|
||||||
removeBtn.innerHTML = '<i data-lucide="trash-2" class="w-4 h-4"></i>';
|
'ml-4 text-red-400 hover:text-red-300 flex-shrink-0';
|
||||||
removeBtn.onclick = () => {
|
removeBtn.innerHTML = '<i data-lucide="trash-2" class="w-4 h-4"></i>';
|
||||||
state.files = state.files.filter((_, i) => i !== index);
|
removeBtn.onclick = () => {
|
||||||
updateUI();
|
state.files = state.files.filter((_, i) => i !== index);
|
||||||
};
|
|
||||||
|
|
||||||
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)];
|
|
||||||
updateUI();
|
updateUI();
|
||||||
|
};
|
||||||
|
|
||||||
|
fileDiv.append(infoContainer, removeBtn);
|
||||||
|
fileDisplayArea.appendChild(fileDiv);
|
||||||
}
|
}
|
||||||
};
|
|
||||||
|
|
||||||
if (fileInput && dropZone) {
|
createIcons({ icons });
|
||||||
fileInput.addEventListener('change', (e) => {
|
}
|
||||||
handleFileSelect((e.target as HTMLInputElement).files);
|
if (fileControls) fileControls.classList.remove('hidden');
|
||||||
});
|
convertOptions.classList.remove('hidden');
|
||||||
|
} else {
|
||||||
dropZone.addEventListener('dragover', (e) => {
|
if (fileDisplayArea) fileDisplayArea.innerHTML = '';
|
||||||
e.preventDefault();
|
if (fileControls) fileControls.classList.add('hidden');
|
||||||
dropZone.classList.add('bg-gray-700');
|
convertOptions.classList.add('hidden');
|
||||||
});
|
|
||||||
|
|
||||||
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 = '';
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
};
|
||||||
|
|
||||||
if (addMoreBtn) {
|
const resetState = () => {
|
||||||
addMoreBtn.addEventListener('click', () => {
|
state.files = [];
|
||||||
fileInput.click();
|
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) {
|
// Clear value on click to allow re-selecting the same file
|
||||||
clearFilesBtn.addEventListener('click', () => {
|
fileInput.addEventListener('click', () => {
|
||||||
resetState();
|
fileInput.value = '';
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
if (processBtn) {
|
if (addMoreBtn) {
|
||||||
processBtn.addEventListener('click', convertToPdf);
|
addMoreBtn.addEventListener('click', () => {
|
||||||
}
|
fileInput.click();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (clearFilesBtn) {
|
||||||
|
clearFilesBtn.addEventListener('click', () => {
|
||||||
|
resetState();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (processBtn) {
|
||||||
|
processBtn.addEventListener('click', convertToPdf);
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -8,9 +8,7 @@ import {
|
|||||||
} from '../utils/helpers.js';
|
} from '../utils/helpers.js';
|
||||||
import { state } from '../state.js';
|
import { state } from '../state.js';
|
||||||
import { createIcons, icons } from 'lucide';
|
import { createIcons, icons } from 'lucide';
|
||||||
import { isWasmAvailable, getWasmBaseUrl } from '../config/wasm-cdn-config.js';
|
import { loadPyMuPDF } from '../utils/pymupdf-loader.js';
|
||||||
import { showWasmRequiredDialog } from '../utils/wasm-provider.js';
|
|
||||||
import { loadPyMuPDF, isPyMuPDFAvailable } from '../utils/pymupdf-loader.js';
|
|
||||||
import { batchDecryptIfNeeded } from '../utils/password-prompt.js';
|
import { batchDecryptIfNeeded } from '../utils/password-prompt.js';
|
||||||
|
|
||||||
interface ExtractedImage {
|
interface ExtractedImage {
|
||||||
@@ -88,7 +86,7 @@ document.addEventListener('DOMContentLoaded', () => {
|
|||||||
const arrayBuffer = await readFileAsArrayBuffer(file);
|
const arrayBuffer = await readFileAsArrayBuffer(file);
|
||||||
const pdfDoc = await getPDFDocument({ data: arrayBuffer }).promise;
|
const pdfDoc = await getPDFDocument({ data: arrayBuffer }).promise;
|
||||||
metaSpan.textContent = `${formatBytes(file.size)} • ${pdfDoc.numPages} pages`;
|
metaSpan.textContent = `${formatBytes(file.size)} • ${pdfDoc.numPages} pages`;
|
||||||
} catch (error) {
|
} catch {
|
||||||
metaSpan.textContent = `${formatBytes(file.size)} • Could not load page count`;
|
metaSpan.textContent = `${formatBytes(file.size)} • Could not load page count`;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -118,7 +116,7 @@ document.addEventListener('DOMContentLoaded', () => {
|
|||||||
if (!imagesGrid || !imagesContainer) return;
|
if (!imagesGrid || !imagesContainer) return;
|
||||||
imagesGrid.innerHTML = '';
|
imagesGrid.innerHTML = '';
|
||||||
|
|
||||||
extractedImages.forEach((img, index) => {
|
extractedImages.forEach((img) => {
|
||||||
const blob = new Blob([new Uint8Array(img.data)]);
|
const blob = new Blob([new Uint8Array(img.data)]);
|
||||||
const url = URL.createObjectURL(blob);
|
const url = URL.createObjectURL(blob);
|
||||||
|
|
||||||
@@ -212,11 +210,11 @@ document.addEventListener('DOMContentLoaded', () => {
|
|||||||
'success'
|
'success'
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
} catch (e: any) {
|
} catch (e: unknown) {
|
||||||
hideLoader();
|
hideLoader();
|
||||||
showAlert(
|
showAlert(
|
||||||
'Error',
|
'Error',
|
||||||
`An error occurred during extraction. Error: ${e.message}`
|
`An error occurred during extraction. Error: ${e instanceof Error ? e.message : String(e)}`
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -12,7 +12,7 @@ import { loadPdfDocument } from '../utils/load-pdf-document.js';
|
|||||||
|
|
||||||
interface ExtractState {
|
interface ExtractState {
|
||||||
file: File | null;
|
file: File | null;
|
||||||
pdfDoc: any;
|
pdfDoc: PDFDocument | null;
|
||||||
totalPages: number;
|
totalPages: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -2,9 +2,7 @@ import { showLoader, hideLoader, showAlert } from '../ui.js';
|
|||||||
import { downloadFile, formatBytes } from '../utils/helpers.js';
|
import { downloadFile, formatBytes } from '../utils/helpers.js';
|
||||||
import { createIcons, icons } from 'lucide';
|
import { createIcons, icons } from 'lucide';
|
||||||
import JSZip from 'jszip';
|
import JSZip from 'jszip';
|
||||||
import { isWasmAvailable, getWasmBaseUrl } from '../config/wasm-cdn-config.js';
|
import { loadPyMuPDF } from '../utils/pymupdf-loader.js';
|
||||||
import { showWasmRequiredDialog } from '../utils/wasm-provider.js';
|
|
||||||
import { loadPyMuPDF, isPyMuPDFAvailable } from '../utils/pymupdf-loader.js';
|
|
||||||
import { loadPdfWithPasswordPrompt } from '../utils/password-prompt.js';
|
import { loadPdfWithPasswordPrompt } from '../utils/password-prompt.js';
|
||||||
let file: File | null = null;
|
let file: File | null = null;
|
||||||
|
|
||||||
|
|||||||
@@ -2,9 +2,7 @@ import { showLoader, hideLoader, showAlert } from '../ui.js';
|
|||||||
import { downloadFile, formatBytes } from '../utils/helpers.js';
|
import { downloadFile, formatBytes } from '../utils/helpers.js';
|
||||||
import { state } from '../state.js';
|
import { state } from '../state.js';
|
||||||
import { createIcons, icons } from 'lucide';
|
import { createIcons, icons } from 'lucide';
|
||||||
import { isWasmAvailable, getWasmBaseUrl } from '../config/wasm-cdn-config.js';
|
import { loadPyMuPDF } from '../utils/pymupdf-loader.js';
|
||||||
import { showWasmRequiredDialog } from '../utils/wasm-provider.js';
|
|
||||||
import { loadPyMuPDF, isPyMuPDFAvailable } from '../utils/pymupdf-loader.js';
|
|
||||||
|
|
||||||
const FILETYPE = 'fb2';
|
const FILETYPE = 'fb2';
|
||||||
const EXTENSIONS = ['.fb2'];
|
const EXTENSIONS = ['.fb2'];
|
||||||
@@ -141,12 +139,12 @@ document.addEventListener('DOMContentLoaded', () => {
|
|||||||
() => resetState()
|
() => resetState()
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
} catch (e: any) {
|
} catch (e: unknown) {
|
||||||
console.error(`[${TOOL_NAME}2PDF] ERROR:`, e);
|
console.error(`[${TOOL_NAME}2PDF] ERROR:`, e);
|
||||||
hideLoader();
|
hideLoader();
|
||||||
showAlert(
|
showAlert(
|
||||||
'Error',
|
'Error',
|
||||||
`An error occurred during conversion. Error: ${e.message}`
|
`An error occurred during conversion. Error: ${e instanceof Error ? e.message : String(e)}`
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -28,7 +28,7 @@ type PdfViewerWindow = Window & {
|
|||||||
};
|
};
|
||||||
|
|
||||||
import { initializeGlobalShortcuts } from '../utils/shortcuts-init.js';
|
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 { loadPdfWithPasswordPrompt } from '../utils/password-prompt.js';
|
||||||
import { createIcons, icons } from 'lucide';
|
import { createIcons, icons } from 'lucide';
|
||||||
import * as pdfjsLib from 'pdfjs-dist';
|
import * as pdfjsLib from 'pdfjs-dist';
|
||||||
@@ -319,15 +319,15 @@ toolItems.forEach((item) => {
|
|||||||
let touchStartY = 0;
|
let touchStartY = 0;
|
||||||
let isTouchDragging = false;
|
let isTouchDragging = false;
|
||||||
|
|
||||||
item.addEventListener('touchstart', (e) => {
|
item.addEventListener('touchstart', (e: TouchEvent) => {
|
||||||
const touch = e.touches[0];
|
const touch = e.touches[0];
|
||||||
touchStartX = touch.clientX;
|
touchStartX = touch.clientX;
|
||||||
touchStartY = touch.clientY;
|
touchStartY = touch.clientY;
|
||||||
isTouchDragging = false;
|
isTouchDragging = false;
|
||||||
});
|
});
|
||||||
|
|
||||||
item.addEventListener('touchmove', (e) => {
|
item.addEventListener('touchmove', (e: TouchEvent) => {
|
||||||
e.preventDefault(); // Prevent scrolling while dragging
|
e.preventDefault();
|
||||||
const touch = e.touches[0];
|
const touch = e.touches[0];
|
||||||
const moveX = Math.abs(touch.clientX - touchStartX);
|
const moveX = Math.abs(touch.clientX - touchStartX);
|
||||||
const moveY = Math.abs(touch.clientY - touchStartY);
|
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();
|
e.preventDefault();
|
||||||
if (!isTouchDragging) {
|
if (!isTouchDragging) {
|
||||||
// It was a tap, treat as click
|
// It was a tap, treat as click
|
||||||
@@ -2131,8 +2131,7 @@ downloadBtn.addEventListener('click', async () => {
|
|||||||
nameCount.set(field.name, count + 1);
|
nameCount.set(field.name, count + 1);
|
||||||
|
|
||||||
if (existingFieldNames.has(field.name)) {
|
if (existingFieldNames.has(field.name)) {
|
||||||
if (field.type === 'radio' && existingRadioGroups.has(field.name)) {
|
if (!(field.type === 'radio' && existingRadioGroups.has(field.name))) {
|
||||||
} else {
|
|
||||||
conflictsWithPdf.push(field.name);
|
conflictsWithPdf.push(field.name);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -2337,7 +2336,7 @@ downloadBtn.addEventListener('click', async () => {
|
|||||||
const existingField = form.getFieldMaybe(groupName);
|
const existingField = form.getFieldMaybe(groupName);
|
||||||
|
|
||||||
if (existingField) {
|
if (existingField) {
|
||||||
radioGroup = existingField;
|
radioGroup = existingField as PDFRadioGroup;
|
||||||
radioGroups.set(groupName, radioGroup);
|
radioGroups.set(groupName, radioGroup);
|
||||||
console.log(`Using existing radio group from PDF: ${groupName}`);
|
console.log(`Using existing radio group from PDF: ${groupName}`);
|
||||||
} else {
|
} else {
|
||||||
@@ -2788,11 +2787,11 @@ function getPageDimensions(size: string): { width: number; height: number } {
|
|||||||
case 'a3':
|
case 'a3':
|
||||||
dimensions = PageSizes.A3;
|
dimensions = PageSizes.A3;
|
||||||
break;
|
break;
|
||||||
case 'custom':
|
case 'custom': {
|
||||||
// Get custom dimensions from inputs
|
|
||||||
const width = parseInt(customWidth.value) || 612;
|
const width = parseInt(customWidth.value) || 612;
|
||||||
const height = parseInt(customHeight.value) || 792;
|
const height = parseInt(customHeight.value) || 792;
|
||||||
return { width, height };
|
return { width, height };
|
||||||
|
}
|
||||||
default:
|
default:
|
||||||
dimensions = PageSizes.Letter;
|
dimensions = PageSizes.Letter;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,9 +1,8 @@
|
|||||||
import { createIcons, icons } from 'lucide';
|
import { createIcons, icons } from 'lucide';
|
||||||
import { showAlert, showLoader, hideLoader } from '../ui.js';
|
import { showAlert, showLoader, hideLoader } from '../ui.js';
|
||||||
import { downloadFile, formatBytes } from '../utils/helpers.js';
|
import { downloadFile, formatBytes } from '../utils/helpers.js';
|
||||||
import { isWasmAvailable, getWasmBaseUrl } from '../config/wasm-cdn-config.js';
|
import { loadPyMuPDF } from '../utils/pymupdf-loader.js';
|
||||||
import { showWasmRequiredDialog } from '../utils/wasm-provider.js';
|
import type { PyMuPDFInstance } from '@/types';
|
||||||
import { loadPyMuPDF, isPyMuPDFAvailable } from '../utils/pymupdf-loader.js';
|
|
||||||
import heic2any from 'heic2any';
|
import heic2any from 'heic2any';
|
||||||
import {
|
import {
|
||||||
getSelectedQuality,
|
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';
|
'JPG, PNG, BMP, GIF, TIFF, PNM, PGM, PBM, PPM, PAM, JXR, JPX, JP2, PSD, SVG, HEIC, WebP';
|
||||||
|
|
||||||
let files: File[] = [];
|
let files: File[] = [];
|
||||||
let pymupdf: any = null;
|
let pymupdf: PyMuPDFInstance | null = null;
|
||||||
|
|
||||||
if (document.readyState === 'loading') {
|
if (document.readyState === 'loading') {
|
||||||
document.addEventListener('DOMContentLoaded', initializePage);
|
document.addEventListener('DOMContentLoaded', initializePage);
|
||||||
@@ -177,9 +176,9 @@ function updateUI() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async function ensurePyMuPDF(): Promise<any> {
|
async function ensurePyMuPDF(): Promise<PyMuPDFInstance> {
|
||||||
if (!pymupdf) {
|
if (!pymupdf) {
|
||||||
pymupdf = await loadPyMuPDF();
|
pymupdf = (await loadPyMuPDF()) as PyMuPDFInstance;
|
||||||
}
|
}
|
||||||
return pymupdf;
|
return pymupdf;
|
||||||
}
|
}
|
||||||
@@ -274,7 +273,7 @@ async function convertToPdf() {
|
|||||||
const processed = await preprocessFile(file);
|
const processed = await preprocessFile(file);
|
||||||
const compressed = await compressImageFile(processed, quality);
|
const compressed = await compressImageFile(processed, quality);
|
||||||
processedFiles.push(compressed);
|
processedFiles.push(compressed);
|
||||||
} catch (error: any) {
|
} catch (error: unknown) {
|
||||||
console.warn(error);
|
console.warn(error);
|
||||||
throw error;
|
throw error;
|
||||||
}
|
}
|
||||||
@@ -291,11 +290,11 @@ async function convertToPdf() {
|
|||||||
showAlert('Success', 'PDF created successfully!', 'success', () => {
|
showAlert('Success', 'PDF created successfully!', 'success', () => {
|
||||||
resetState();
|
resetState();
|
||||||
});
|
});
|
||||||
} catch (e: any) {
|
} catch (e: unknown) {
|
||||||
console.error('[ImageToPDF]', e);
|
console.error('[ImageToPDF]', e);
|
||||||
showAlert(
|
showAlert(
|
||||||
'Conversion Error',
|
'Conversion Error',
|
||||||
e.message || 'Failed to convert images to PDF.'
|
e instanceof Error ? e.message : 'Failed to convert images to PDF.'
|
||||||
);
|
);
|
||||||
} finally {
|
} finally {
|
||||||
hideLoader();
|
hideLoader();
|
||||||
|
|||||||
@@ -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 { mdToPdf } from './md-to-pdf.js';
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
import { processAndSave } from './duplicate-organize.js';
|
import { processAndSave } from './duplicate-organize.js';
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
import { wordToPdf } from './word-to-pdf.js';
|
|
||||||
|
|
||||||
import { setupCropperTool } from './cropper.js';
|
import { setupCropperTool } from './cropper.js';
|
||||||
|
|
||||||
|
export const toolLogic: Record<
|
||||||
|
string,
|
||||||
|
| {
|
||||||
|
process?: (...args: unknown[]) => Promise<unknown>;
|
||||||
|
setup?: (...args: unknown[]) => Promise<unknown>;
|
||||||
|
}
|
||||||
|
| ((...args: unknown[]) => unknown)
|
||||||
|
> = {
|
||||||
export const toolLogic = {
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
'duplicate-organize': { process: processAndSave },
|
'duplicate-organize': { process: processAndSave },
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
cropper: { setup: setupCropperTool },
|
cropper: { setup: setupCropperTool },
|
||||||
|
|
||||||
|
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ import { createIcons, icons } from 'lucide';
|
|||||||
import { showAlert, showLoader, hideLoader } from '../ui.js';
|
import { showAlert, showLoader, hideLoader } from '../ui.js';
|
||||||
import { downloadFile, formatBytes } from '../utils/helpers.js';
|
import { downloadFile, formatBytes } from '../utils/helpers.js';
|
||||||
import { loadPyMuPDF } from '../utils/pymupdf-loader.js';
|
import { loadPyMuPDF } from '../utils/pymupdf-loader.js';
|
||||||
|
import type { PyMuPDFInstance } from '@/types';
|
||||||
import {
|
import {
|
||||||
getSelectedQuality,
|
getSelectedQuality,
|
||||||
compressImageFile,
|
compressImageFile,
|
||||||
@@ -11,7 +12,7 @@ const SUPPORTED_FORMATS = '.jpg,.jpeg,.jp2,.jpx';
|
|||||||
const SUPPORTED_MIME_TYPES = ['image/jpeg', 'image/jpg', 'image/jp2'];
|
const SUPPORTED_MIME_TYPES = ['image/jpeg', 'image/jpg', 'image/jp2'];
|
||||||
|
|
||||||
let files: File[] = [];
|
let files: File[] = [];
|
||||||
let pymupdf: any = null;
|
let pymupdf: PyMuPDFInstance | null = null;
|
||||||
|
|
||||||
if (document.readyState === 'loading') {
|
if (document.readyState === 'loading') {
|
||||||
document.addEventListener('DOMContentLoaded', initializePage);
|
document.addEventListener('DOMContentLoaded', initializePage);
|
||||||
@@ -168,9 +169,9 @@ function updateUI() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async function ensurePyMuPDF(): Promise<any> {
|
async function ensurePyMuPDF(): Promise<PyMuPDFInstance> {
|
||||||
if (!pymupdf) {
|
if (!pymupdf) {
|
||||||
pymupdf = await loadPyMuPDF();
|
pymupdf = (await loadPyMuPDF()) as PyMuPDFInstance;
|
||||||
}
|
}
|
||||||
return pymupdf;
|
return pymupdf;
|
||||||
}
|
}
|
||||||
@@ -200,11 +201,11 @@ async function convertToPdf() {
|
|||||||
showAlert('Success', 'PDF created successfully!', 'success', () => {
|
showAlert('Success', 'PDF created successfully!', 'success', () => {
|
||||||
resetState();
|
resetState();
|
||||||
});
|
});
|
||||||
} catch (e: any) {
|
} catch (e: unknown) {
|
||||||
console.error('[JpgToPdf]', e);
|
console.error('[JpgToPdf]', e);
|
||||||
showAlert(
|
showAlert(
|
||||||
'Conversion Error',
|
'Conversion Error',
|
||||||
e.message || 'Failed to convert images to PDF.'
|
e instanceof Error ? e.message : 'Failed to convert images to PDF.'
|
||||||
);
|
);
|
||||||
} finally {
|
} finally {
|
||||||
hideLoader();
|
hideLoader();
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ import {
|
|||||||
import { icons, createIcons } from 'lucide';
|
import { icons, createIcons } from 'lucide';
|
||||||
import JSZip from 'jszip';
|
import JSZip from 'jszip';
|
||||||
import { deduplicateFileName } from '../utils/deduplicate-filename.js';
|
import { deduplicateFileName } from '../utils/deduplicate-filename.js';
|
||||||
import { LinearizePdfState } from '@/types';
|
import { LinearizePdfState, QpdfInstanceExtended } from '@/types';
|
||||||
|
|
||||||
const pageState: LinearizePdfState = {
|
const pageState: LinearizePdfState = {
|
||||||
files: [],
|
files: [],
|
||||||
@@ -111,7 +111,7 @@ async function linearizePdf() {
|
|||||||
|
|
||||||
const zip = new JSZip();
|
const zip = new JSZip();
|
||||||
const usedNames = new Set<string>();
|
const usedNames = new Set<string>();
|
||||||
let qpdf: any;
|
let qpdf: QpdfInstanceExtended;
|
||||||
let successCount = 0;
|
let successCount = 0;
|
||||||
let errorCount = 0;
|
let errorCount = 0;
|
||||||
|
|
||||||
@@ -150,7 +150,7 @@ async function linearizePdf() {
|
|||||||
);
|
);
|
||||||
zip.file(zipEntryName, outputFile, { binary: true });
|
zip.file(zipEntryName, outputFile, { binary: true });
|
||||||
successCount++;
|
successCount++;
|
||||||
} catch (fileError: any) {
|
} catch (fileError: unknown) {
|
||||||
errorCount++;
|
errorCount++;
|
||||||
console.error(`Failed to linearize ${file.name}:`, fileError);
|
console.error(`Failed to linearize ${file.name}:`, fileError);
|
||||||
} finally {
|
} finally {
|
||||||
@@ -187,11 +187,11 @@ async function linearizePdf() {
|
|||||||
showAlert('Processing Complete', alertMessage, 'success', () => {
|
showAlert('Processing Complete', alertMessage, 'success', () => {
|
||||||
resetState();
|
resetState();
|
||||||
});
|
});
|
||||||
} catch (error: any) {
|
} catch (error: unknown) {
|
||||||
console.error('Linearization process error:', error);
|
console.error('Linearization process error:', error);
|
||||||
showAlert(
|
showAlert(
|
||||||
'Linearization Failed',
|
'Linearization Failed',
|
||||||
`An error occurred: ${error.message || 'Unknown error'}.`
|
`An error occurred: ${error instanceof Error ? error.message : 'Unknown error'}.`
|
||||||
);
|
);
|
||||||
} finally {
|
} finally {
|
||||||
if (loaderModal) loaderModal.classList.add('hidden');
|
if (loaderModal) loaderModal.classList.add('hidden');
|
||||||
|
|||||||
@@ -16,15 +16,15 @@ import {
|
|||||||
import { createIcons, icons } from 'lucide';
|
import { createIcons, icons } from 'lucide';
|
||||||
import * as pdfjsLib from 'pdfjs-dist';
|
import * as pdfjsLib from 'pdfjs-dist';
|
||||||
import Sortable from 'sortablejs';
|
import Sortable from 'sortablejs';
|
||||||
|
import type { MergeJob, MergeFile, MergeMessage, MergeResponse } from '@/types';
|
||||||
|
|
||||||
// @ts-ignore
|
|
||||||
pdfjsLib.GlobalWorkerOptions.workerSrc = new URL(
|
pdfjsLib.GlobalWorkerOptions.workerSrc = new URL(
|
||||||
'pdfjs-dist/build/pdf.worker.min.mjs',
|
'pdfjs-dist/build/pdf.worker.min.mjs',
|
||||||
import.meta.url
|
import.meta.url
|
||||||
).toString();
|
).toString();
|
||||||
|
|
||||||
interface MergeState {
|
interface MergeState {
|
||||||
pdfDocs: Record<string, any>;
|
pdfDocs: Record<string, pdfjsLib.PDFDocumentProxy>;
|
||||||
pdfBytes: Record<string, ArrayBuffer>;
|
pdfBytes: Record<string, ArrayBuffer>;
|
||||||
activeMode: 'file' | 'page';
|
activeMode: 'file' | 'page';
|
||||||
sortableInstances: {
|
sortableInstances: {
|
||||||
@@ -66,10 +66,10 @@ function initializeFileListSortable() {
|
|||||||
ghostClass: 'sortable-ghost',
|
ghostClass: 'sortable-ghost',
|
||||||
chosenClass: 'sortable-chosen',
|
chosenClass: 'sortable-chosen',
|
||||||
dragClass: 'sortable-drag',
|
dragClass: 'sortable-drag',
|
||||||
onStart: function (evt: any) {
|
onStart: function (evt: Sortable.SortableEvent) {
|
||||||
evt.item.style.opacity = '0.5';
|
evt.item.style.opacity = '0.5';
|
||||||
},
|
},
|
||||||
onEnd: function (evt: any) {
|
onEnd: function (evt: Sortable.SortableEvent) {
|
||||||
evt.item.style.opacity = '1';
|
evt.item.style.opacity = '1';
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
@@ -88,10 +88,10 @@ function initializePageThumbnailsSortable() {
|
|||||||
ghostClass: 'sortable-ghost',
|
ghostClass: 'sortable-ghost',
|
||||||
chosenClass: 'sortable-chosen',
|
chosenClass: 'sortable-chosen',
|
||||||
dragClass: 'sortable-drag',
|
dragClass: 'sortable-drag',
|
||||||
onStart: function (evt: any) {
|
onStart: function (evt: Sortable.SortableEvent) {
|
||||||
evt.item.style.opacity = '0.5';
|
evt.item.style.opacity = '0.5';
|
||||||
},
|
},
|
||||||
onEnd: function (evt: any) {
|
onEnd: function (evt: Sortable.SortableEvent) {
|
||||||
evt.item.style.opacity = '1';
|
evt.item.style.opacity = '1';
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
@@ -201,7 +201,7 @@ async function renderPageMergeThumbnails() {
|
|||||||
batchSize: 8,
|
batchSize: 8,
|
||||||
useLazyLoading: true,
|
useLazyLoading: true,
|
||||||
lazyLoadMargin: '300px',
|
lazyLoadMargin: '300px',
|
||||||
onProgress: (current, total) => {
|
onProgress: () => {
|
||||||
currentPageNumber++;
|
currentPageNumber++;
|
||||||
showLoader(`Rendering page previews...`);
|
showLoader(`Rendering page previews...`);
|
||||||
},
|
},
|
||||||
@@ -288,9 +288,7 @@ export async function merge() {
|
|||||||
|
|
||||||
showLoader('Merging PDFs...');
|
showLoader('Merging PDFs...');
|
||||||
try {
|
try {
|
||||||
// @ts-ignore
|
|
||||||
const jobs: MergeJob[] = [];
|
const jobs: MergeJob[] = [];
|
||||||
// @ts-ignore
|
|
||||||
const filesToMerge: MergeFile[] = [];
|
const filesToMerge: MergeFile[] = [];
|
||||||
const uniqueFileNames = new Set<string>();
|
const uniqueFileNames = new Set<string>();
|
||||||
|
|
||||||
@@ -387,7 +385,6 @@ export async function merge() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// @ts-ignore
|
|
||||||
const message: MergeMessage = {
|
const message: MergeMessage = {
|
||||||
command: 'merge',
|
command: 'merge',
|
||||||
files: filesToMerge,
|
files: filesToMerge,
|
||||||
@@ -400,7 +397,6 @@ export async function merge() {
|
|||||||
filesToMerge.map((f) => f.data)
|
filesToMerge.map((f) => f.data)
|
||||||
);
|
);
|
||||||
|
|
||||||
// @ts-ignore
|
|
||||||
mergeWorker.onmessage = (e: MessageEvent<MergeResponse>) => {
|
mergeWorker.onmessage = (e: MessageEvent<MergeResponse>) => {
|
||||||
hideLoader();
|
hideLoader();
|
||||||
if (e.data.status === 'success') {
|
if (e.data.status === 'success') {
|
||||||
|
|||||||
@@ -2,9 +2,7 @@ import { showLoader, hideLoader, showAlert } from '../ui.js';
|
|||||||
import { downloadFile, formatBytes } from '../utils/helpers.js';
|
import { downloadFile, formatBytes } from '../utils/helpers.js';
|
||||||
import { state } from '../state.js';
|
import { state } from '../state.js';
|
||||||
import { createIcons, icons } from 'lucide';
|
import { createIcons, icons } from 'lucide';
|
||||||
import { isWasmAvailable, getWasmBaseUrl } from '../config/wasm-cdn-config.js';
|
import { loadPyMuPDF } from '../utils/pymupdf-loader.js';
|
||||||
import { showWasmRequiredDialog } from '../utils/wasm-provider.js';
|
|
||||||
import { loadPyMuPDF, isPyMuPDFAvailable } from '../utils/pymupdf-loader.js';
|
|
||||||
|
|
||||||
const FILETYPE = 'mobi';
|
const FILETYPE = 'mobi';
|
||||||
const EXTENSIONS = ['.mobi'];
|
const EXTENSIONS = ['.mobi'];
|
||||||
@@ -141,12 +139,12 @@ document.addEventListener('DOMContentLoaded', () => {
|
|||||||
() => resetState()
|
() => resetState()
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
} catch (e: any) {
|
} catch (e: unknown) {
|
||||||
console.error(`[${TOOL_NAME}2PDF] ERROR:`, e);
|
console.error(`[${TOOL_NAME}2PDF] ERROR:`, e);
|
||||||
hideLoader();
|
hideLoader();
|
||||||
showAlert(
|
showAlert(
|
||||||
'Error',
|
'Error',
|
||||||
`An error occurred during conversion. Error: ${e.message}`
|
`An error occurred during conversion. Error: ${e instanceof Error ? e.message : String(e)}`
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,215 +1,223 @@
|
|||||||
import { showLoader, hideLoader, showAlert } from '../ui.js';
|
import { showLoader, hideLoader, showAlert } from '../ui.js';
|
||||||
import {
|
import { downloadFile, formatBytes } from '../utils/helpers.js';
|
||||||
downloadFile,
|
|
||||||
readFileAsArrayBuffer,
|
|
||||||
formatBytes,
|
|
||||||
} from '../utils/helpers.js';
|
|
||||||
import { state } from '../state.js';
|
import { state } from '../state.js';
|
||||||
import { createIcons, icons } from 'lucide';
|
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', () => {
|
document.addEventListener('DOMContentLoaded', () => {
|
||||||
state.files = [];
|
state.files = [];
|
||||||
|
|
||||||
const fileInput = document.getElementById('file-input') as HTMLInputElement;
|
const fileInput = document.getElementById('file-input') as HTMLInputElement;
|
||||||
const dropZone = document.getElementById('drop-zone');
|
const dropZone = document.getElementById('drop-zone');
|
||||||
const convertOptions = document.getElementById('convert-options');
|
const convertOptions = document.getElementById('convert-options');
|
||||||
const fileDisplayArea = document.getElementById('file-display-area');
|
const fileDisplayArea = document.getElementById('file-display-area');
|
||||||
const fileControls = document.getElementById('file-controls');
|
const fileControls = document.getElementById('file-controls');
|
||||||
const addMoreBtn = document.getElementById('add-more-btn');
|
const addMoreBtn = document.getElementById('add-more-btn');
|
||||||
const clearFilesBtn = document.getElementById('clear-files-btn');
|
const clearFilesBtn = document.getElementById('clear-files-btn');
|
||||||
const backBtn = document.getElementById('back-to-tools');
|
const backBtn = document.getElementById('back-to-tools');
|
||||||
const processBtn = document.getElementById('process-btn');
|
const processBtn = document.getElementById('process-btn');
|
||||||
|
|
||||||
if (backBtn) {
|
if (backBtn) {
|
||||||
backBtn.addEventListener('click', () => {
|
backBtn.addEventListener('click', () => {
|
||||||
window.location.href = import.meta.env.BASE_URL;
|
window.location.href = import.meta.env.BASE_URL;
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
const updateUI = async () => {
|
const updateUI = async () => {
|
||||||
if (!convertOptions) return;
|
if (!convertOptions) return;
|
||||||
|
|
||||||
if (state.files.length > 0) {
|
if (state.files.length > 0) {
|
||||||
if (fileDisplayArea) {
|
if (fileDisplayArea) {
|
||||||
fileDisplayArea.innerHTML = '';
|
fileDisplayArea.innerHTML = '';
|
||||||
|
|
||||||
for (let index = 0; index < state.files.length; index++) {
|
for (let index = 0; index < state.files.length; index++) {
|
||||||
const file = state.files[index];
|
const file = state.files[index];
|
||||||
const fileDiv = document.createElement('div');
|
const fileDiv = document.createElement('div');
|
||||||
fileDiv.className = 'flex items-center justify-between bg-gray-700 p-3 rounded-lg text-sm';
|
fileDiv.className =
|
||||||
|
'flex items-center justify-between bg-gray-700 p-3 rounded-lg text-sm';
|
||||||
|
|
||||||
const infoContainer = document.createElement('div');
|
const infoContainer = document.createElement('div');
|
||||||
infoContainer.className = 'flex flex-col overflow-hidden';
|
infoContainer.className = 'flex flex-col overflow-hidden';
|
||||||
|
|
||||||
const nameSpan = document.createElement('div');
|
const nameSpan = document.createElement('div');
|
||||||
nameSpan.className = 'truncate font-medium text-gray-200 text-sm mb-1';
|
nameSpan.className =
|
||||||
nameSpan.textContent = file.name;
|
'truncate font-medium text-gray-200 text-sm mb-1';
|
||||||
|
nameSpan.textContent = file.name;
|
||||||
|
|
||||||
const metaSpan = document.createElement('div');
|
const metaSpan = document.createElement('div');
|
||||||
metaSpan.className = 'text-xs text-gray-400';
|
metaSpan.className = 'text-xs text-gray-400';
|
||||||
metaSpan.textContent = formatBytes(file.size);
|
metaSpan.textContent = formatBytes(file.size);
|
||||||
|
|
||||||
infoContainer.append(nameSpan, metaSpan);
|
infoContainer.append(nameSpan, metaSpan);
|
||||||
|
|
||||||
const removeBtn = document.createElement('button');
|
const removeBtn = document.createElement('button');
|
||||||
removeBtn.className = 'ml-4 text-red-400 hover:text-red-300 flex-shrink-0';
|
removeBtn.className =
|
||||||
removeBtn.innerHTML = '<i data-lucide="trash-2" class="w-4 h-4"></i>';
|
'ml-4 text-red-400 hover:text-red-300 flex-shrink-0';
|
||||||
removeBtn.onclick = () => {
|
removeBtn.innerHTML = '<i data-lucide="trash-2" class="w-4 h-4"></i>';
|
||||||
state.files = state.files.filter((_, i) => i !== index);
|
removeBtn.onclick = () => {
|
||||||
updateUI();
|
state.files = state.files.filter((_, i) => i !== index);
|
||||||
};
|
|
||||||
|
|
||||||
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)];
|
|
||||||
updateUI();
|
updateUI();
|
||||||
|
};
|
||||||
|
|
||||||
|
fileDiv.append(infoContainer, removeBtn);
|
||||||
|
fileDisplayArea.appendChild(fileDiv);
|
||||||
}
|
}
|
||||||
};
|
|
||||||
|
|
||||||
if (fileInput && dropZone) {
|
createIcons({ icons });
|
||||||
fileInput.addEventListener('change', (e) => {
|
}
|
||||||
handleFileSelect((e.target as HTMLInputElement).files);
|
if (fileControls) fileControls.classList.remove('hidden');
|
||||||
});
|
convertOptions.classList.remove('hidden');
|
||||||
|
} else {
|
||||||
dropZone.addEventListener('dragover', (e) => {
|
if (fileDisplayArea) fileDisplayArea.innerHTML = '';
|
||||||
e.preventDefault();
|
if (fileControls) fileControls.classList.add('hidden');
|
||||||
dropZone.classList.add('bg-gray-700');
|
convertOptions.classList.add('hidden');
|
||||||
});
|
|
||||||
|
|
||||||
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);
|
|
||||||
}
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const resetState = () => {
|
||||||
|
state.files = [];
|
||||||
|
state.pdfDoc = null;
|
||||||
updateUI();
|
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();
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -15,10 +15,10 @@ pdfjsLib.GlobalWorkerOptions.workerSrc = new URL(
|
|||||||
|
|
||||||
interface OrganizeState {
|
interface OrganizeState {
|
||||||
file: File | null;
|
file: File | null;
|
||||||
pdfDoc: any;
|
pdfDoc: PDFDocument | null;
|
||||||
pdfJsDoc: any;
|
pdfJsDoc: pdfjsLib.PDFDocumentProxy | null;
|
||||||
totalPages: number;
|
totalPages: number;
|
||||||
sortableInstance: any;
|
sortableInstance: Sortable | null;
|
||||||
}
|
}
|
||||||
|
|
||||||
const organizeState: OrganizeState = {
|
const organizeState: OrganizeState = {
|
||||||
@@ -282,7 +282,7 @@ async function renderThumbnails() {
|
|||||||
canvas.width = viewport.width;
|
canvas.width = viewport.width;
|
||||||
canvas.height = viewport.height;
|
canvas.height = viewport.height;
|
||||||
const ctx = canvas.getContext('2d');
|
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');
|
const wrapper = document.createElement('div');
|
||||||
wrapper.className =
|
wrapper.className =
|
||||||
|
|||||||
@@ -150,12 +150,12 @@ document.addEventListener('DOMContentLoaded', () => {
|
|||||||
() => resetState()
|
() => resetState()
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
} catch (e: any) {
|
} catch (e: unknown) {
|
||||||
hideLoader();
|
hideLoader();
|
||||||
console.error(`[${FILETYPE_NAME}ToPDF] Error:`, e);
|
console.error(`[${FILETYPE_NAME}ToPDF] Error:`, e);
|
||||||
showAlert(
|
showAlert(
|
||||||
'Error',
|
'Error',
|
||||||
`An error occurred during conversion. Error: ${e.message}`
|
`An error occurred during conversion. Error: ${e instanceof Error ? e.message : String(e)}`
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -226,9 +226,9 @@ async function generatePreview() {
|
|||||||
const ctx = offscreen.getContext('2d')!;
|
const ctx = offscreen.getContext('2d')!;
|
||||||
|
|
||||||
await page.render({
|
await page.render({
|
||||||
canvasContext: ctx as any,
|
canvasContext: ctx as unknown as CanvasRenderingContext2D,
|
||||||
viewport: viewport,
|
viewport: viewport,
|
||||||
canvas: offscreen as any,
|
canvas: offscreen as unknown as HTMLCanvasElement,
|
||||||
}).promise;
|
}).promise;
|
||||||
|
|
||||||
const bitmap = await createImageBitmap(offscreen);
|
const bitmap = await createImageBitmap(offscreen);
|
||||||
|
|||||||
@@ -7,9 +7,7 @@ import {
|
|||||||
getPDFDocument,
|
getPDFDocument,
|
||||||
} from '../utils/helpers.js';
|
} from '../utils/helpers.js';
|
||||||
import { createIcons, icons } from 'lucide';
|
import { createIcons, icons } from 'lucide';
|
||||||
import { isWasmAvailable, getWasmBaseUrl } from '../config/wasm-cdn-config.js';
|
import { loadPyMuPDF } from '../utils/pymupdf-loader.js';
|
||||||
import { showWasmRequiredDialog } from '../utils/wasm-provider.js';
|
|
||||||
import { loadPyMuPDF, isPyMuPDFAvailable } from '../utils/pymupdf-loader.js';
|
|
||||||
import { loadPdfWithPasswordPrompt } from '../utils/password-prompt.js';
|
import { loadPdfWithPasswordPrompt } from '../utils/password-prompt.js';
|
||||||
|
|
||||||
interface LayerData {
|
interface LayerData {
|
||||||
@@ -24,7 +22,21 @@ interface LayerData {
|
|||||||
}
|
}
|
||||||
|
|
||||||
let currentFile: File | null = null;
|
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<number, LayerData>();
|
const layersMap = new Map<number, LayerData>();
|
||||||
let nextDisplayOrder = 0;
|
let nextDisplayOrder = 0;
|
||||||
|
|
||||||
@@ -274,7 +286,7 @@ document.addEventListener('DOMContentLoaded', () => {
|
|||||||
if (!childName || !childName.trim()) return;
|
if (!childName || !childName.trim()) return;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const childXref = currentDoc.addOCGWithParent(
|
const childResult = currentDoc.addOCGWithParent(
|
||||||
childName.trim(),
|
childName.trim(),
|
||||||
parentXref
|
parentXref
|
||||||
);
|
);
|
||||||
@@ -285,9 +297,9 @@ document.addEventListener('DOMContentLoaded', () => {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
layersMap.set(childXref, {
|
layersMap.set(childResult.xref, {
|
||||||
number: childXref,
|
number: childResult.number,
|
||||||
xref: childXref,
|
xref: childResult.xref,
|
||||||
text: childName.trim(),
|
text: childName.trim(),
|
||||||
on: true,
|
on: true,
|
||||||
locked: false,
|
locked: false,
|
||||||
@@ -316,16 +328,17 @@ document.addEventListener('DOMContentLoaded', () => {
|
|||||||
const pymupdf = await loadPyMuPDF();
|
const pymupdf = await loadPyMuPDF();
|
||||||
|
|
||||||
showLoader(`Loading layers from ${currentFile.name}...`);
|
showLoader(`Loading layers from ${currentFile.name}...`);
|
||||||
currentDoc = await pymupdf.open(currentFile);
|
currentDoc = await (
|
||||||
|
pymupdf as { open: (file: File) => Promise<PyMuPDFDocument> }
|
||||||
|
).open(currentFile);
|
||||||
|
|
||||||
showLoader('Reading layer configuration...');
|
showLoader('Reading layer configuration...');
|
||||||
const existingLayers = currentDoc.getLayerConfig();
|
const existingLayers = currentDoc.getLayerConfig();
|
||||||
|
|
||||||
// Reset and populate layers map
|
|
||||||
layersMap.clear();
|
layersMap.clear();
|
||||||
nextDisplayOrder = 0;
|
nextDisplayOrder = 0;
|
||||||
|
|
||||||
existingLayers.forEach((layer: any) => {
|
existingLayers.forEach((layer: LayerData) => {
|
||||||
layersMap.set(layer.number, {
|
layersMap.set(layer.number, {
|
||||||
number: layer.number,
|
number: layer.number,
|
||||||
xref: layer.xref ?? layer.number,
|
xref: layer.xref ?? layer.number,
|
||||||
@@ -350,9 +363,12 @@ document.addEventListener('DOMContentLoaded', () => {
|
|||||||
|
|
||||||
renderLayers();
|
renderLayers();
|
||||||
setupLayerHandlers();
|
setupLayerHandlers();
|
||||||
} catch (error: any) {
|
} catch (error: unknown) {
|
||||||
hideLoader();
|
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);
|
console.error('Layers error:', error);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
@@ -373,13 +389,13 @@ document.addEventListener('DOMContentLoaded', () => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const xref = currentDoc.addOCG(name);
|
const layerResult = currentDoc.addOCG(name);
|
||||||
newLayerInput.value = '';
|
newLayerInput.value = '';
|
||||||
|
|
||||||
const newDisplayOrder = nextDisplayOrder++;
|
const newDisplayOrder = nextDisplayOrder++;
|
||||||
layersMap.set(xref, {
|
layersMap.set(layerResult.xref, {
|
||||||
number: xref,
|
number: layerResult.number,
|
||||||
xref: xref,
|
xref: layerResult.xref,
|
||||||
text: name,
|
text: name,
|
||||||
on: true,
|
on: true,
|
||||||
locked: false,
|
locked: false,
|
||||||
@@ -389,8 +405,12 @@ document.addEventListener('DOMContentLoaded', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
renderLayers();
|
renderLayers();
|
||||||
} catch (err: any) {
|
} catch (err: unknown) {
|
||||||
showAlert('Error', 'Failed to add layer: ' + err.message);
|
showAlert(
|
||||||
|
'Error',
|
||||||
|
'Failed to add layer: ' +
|
||||||
|
(err instanceof Error ? err.message : String(err))
|
||||||
|
);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
@@ -409,9 +429,13 @@ document.addEventListener('DOMContentLoaded', () => {
|
|||||||
hideLoader();
|
hideLoader();
|
||||||
resetState();
|
resetState();
|
||||||
showAlert('Success', 'PDF with layer changes saved!', 'success');
|
showAlert('Success', 'PDF with layer changes saved!', 'success');
|
||||||
} catch (err: any) {
|
} catch (err: unknown) {
|
||||||
hideLoader();
|
hideLoader();
|
||||||
showAlert('Error', 'Failed to save PDF: ' + err.message);
|
showAlert(
|
||||||
|
'Error',
|
||||||
|
'Failed to save PDF: ' +
|
||||||
|
(err instanceof Error ? err.message : String(err))
|
||||||
|
);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -9,7 +9,6 @@ import {
|
|||||||
renderPagesProgressively,
|
renderPagesProgressively,
|
||||||
cleanupLazyRendering,
|
cleanupLazyRendering,
|
||||||
renderPageToCanvas,
|
renderPageToCanvas,
|
||||||
createPlaceholder,
|
|
||||||
} from '../utils/render-utils';
|
} from '../utils/render-utils';
|
||||||
import { initializeGlobalShortcuts } from '../utils/shortcuts-init.js';
|
import { initializeGlobalShortcuts } from '../utils/shortcuts-init.js';
|
||||||
import { repairPdfFile } from './repair-pdf.js';
|
import { repairPdfFile } from './repair-pdf.js';
|
||||||
@@ -841,7 +840,7 @@ function deletePage(index: number) {
|
|||||||
|
|
||||||
async function insertPdfAfter(index: number) {
|
async function insertPdfAfter(index: number) {
|
||||||
document.getElementById('insert-pdf-input')?.click();
|
document.getElementById('insert-pdf-input')?.click();
|
||||||
(window as any).__insertAfterIndex = index;
|
(window as unknown as Record<string, unknown>).__insertAfterIndex = index;
|
||||||
}
|
}
|
||||||
|
|
||||||
async function handleInsertPdf(e: Event) {
|
async function handleInsertPdf(e: Event) {
|
||||||
@@ -849,7 +848,8 @@ async function handleInsertPdf(e: Event) {
|
|||||||
const file = input.files?.[0];
|
const file = input.files?.[0];
|
||||||
if (!file) return;
|
if (!file) return;
|
||||||
|
|
||||||
const insertAfterIndex = (window as any).__insertAfterIndex;
|
const insertAfterIndex = (window as unknown as Record<string, unknown>)
|
||||||
|
.__insertAfterIndex as number | undefined;
|
||||||
if (insertAfterIndex === undefined) return;
|
if (insertAfterIndex === undefined) return;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
@@ -974,7 +974,7 @@ function addBlankPage() {
|
|||||||
rotation: 0,
|
rotation: 0,
|
||||||
visualRotation: 0,
|
visualRotation: 0,
|
||||||
canvas,
|
canvas,
|
||||||
pdfDoc: null as any,
|
pdfDoc: null!,
|
||||||
originalPageIndex: -1,
|
originalPageIndex: -1,
|
||||||
fileName: '', // Blank page has no file
|
fileName: '', // Blank page has no file
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,10 +1,7 @@
|
|||||||
import { showLoader, hideLoader, showAlert } from '../ui.js';
|
import { showLoader, hideLoader, showAlert } from '../ui.js';
|
||||||
import { downloadFile, formatBytes } from '../utils/helpers.js';
|
import { downloadFile, formatBytes } from '../utils/helpers.js';
|
||||||
import { createIcons, icons } from 'lucide';
|
import { createIcons, icons } from 'lucide';
|
||||||
import JSZip from 'jszip';
|
import { loadPyMuPDF } from '../utils/pymupdf-loader.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 { loadPdfWithPasswordPrompt } from '../utils/password-prompt.js';
|
import { loadPdfWithPasswordPrompt } from '../utils/password-prompt.js';
|
||||||
let file: File | null = null;
|
let file: File | null = null;
|
||||||
|
|
||||||
|
|||||||
@@ -8,9 +8,7 @@ import {
|
|||||||
} from '../utils/helpers.js';
|
} from '../utils/helpers.js';
|
||||||
import { state } from '../state.js';
|
import { state } from '../state.js';
|
||||||
import { createIcons, icons } from 'lucide';
|
import { createIcons, icons } from 'lucide';
|
||||||
import { isWasmAvailable, getWasmBaseUrl } from '../config/wasm-cdn-config.js';
|
import { loadPyMuPDF } from '../utils/pymupdf-loader.js';
|
||||||
import { showWasmRequiredDialog } from '../utils/wasm-provider.js';
|
|
||||||
import { loadPyMuPDF, isPyMuPDFAvailable } from '../utils/pymupdf-loader.js';
|
|
||||||
import { batchDecryptIfNeeded } from '../utils/password-prompt.js';
|
import { batchDecryptIfNeeded } from '../utils/password-prompt.js';
|
||||||
import { deduplicateFileName } from '../utils/deduplicate-filename.js';
|
import { deduplicateFileName } from '../utils/deduplicate-filename.js';
|
||||||
|
|
||||||
@@ -73,7 +71,7 @@ document.addEventListener('DOMContentLoaded', () => {
|
|||||||
const arrayBuffer = await readFileAsArrayBuffer(file);
|
const arrayBuffer = await readFileAsArrayBuffer(file);
|
||||||
const pdfDoc = await getPDFDocument({ data: arrayBuffer }).promise;
|
const pdfDoc = await getPDFDocument({ data: arrayBuffer }).promise;
|
||||||
metaSpan.textContent = `${formatBytes(file.size)} • ${pdfDoc.numPages} pages`;
|
metaSpan.textContent = `${formatBytes(file.size)} • ${pdfDoc.numPages} pages`;
|
||||||
} catch (error) {
|
} catch {
|
||||||
metaSpan.textContent = `${formatBytes(file.size)} • Could not load page count`;
|
metaSpan.textContent = `${formatBytes(file.size)} • Could not load page count`;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -160,11 +158,11 @@ document.addEventListener('DOMContentLoaded', () => {
|
|||||||
() => resetState()
|
() => resetState()
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
} catch (e: any) {
|
} catch (e: unknown) {
|
||||||
hideLoader();
|
hideLoader();
|
||||||
showAlert(
|
showAlert(
|
||||||
'Error',
|
'Error',
|
||||||
`An error occurred during conversion. Error: ${e.message}`
|
`An error occurred during conversion. Error: ${e instanceof Error ? e.message : String(e)}`
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,9 +1,7 @@
|
|||||||
import { showLoader, hideLoader, showAlert } from '../ui.js';
|
import { showLoader, hideLoader, showAlert } from '../ui.js';
|
||||||
import { downloadFile, formatBytes } from '../utils/helpers.js';
|
import { downloadFile, formatBytes } from '../utils/helpers.js';
|
||||||
import { createIcons, icons } from 'lucide';
|
import { createIcons, icons } from 'lucide';
|
||||||
import { isWasmAvailable, getWasmBaseUrl } from '../config/wasm-cdn-config.js';
|
import { loadPyMuPDF } from '../utils/pymupdf-loader.js';
|
||||||
import { showWasmRequiredDialog } from '../utils/wasm-provider.js';
|
|
||||||
import { loadPyMuPDF, isPyMuPDFAvailable } from '../utils/pymupdf-loader.js';
|
|
||||||
import { loadPdfWithPasswordPrompt } from '../utils/password-prompt.js';
|
import { loadPdfWithPasswordPrompt } from '../utils/password-prompt.js';
|
||||||
import * as XLSX from 'xlsx';
|
import * as XLSX from 'xlsx';
|
||||||
let file: File | null = null;
|
let file: File | null = null;
|
||||||
|
|||||||
@@ -8,9 +8,7 @@ import {
|
|||||||
} from '../utils/helpers.js';
|
} from '../utils/helpers.js';
|
||||||
import { state } from '../state.js';
|
import { state } from '../state.js';
|
||||||
import { createIcons, icons } from 'lucide';
|
import { createIcons, icons } from 'lucide';
|
||||||
import { isWasmAvailable, getWasmBaseUrl } from '../config/wasm-cdn-config.js';
|
import { loadPyMuPDF } from '../utils/pymupdf-loader.js';
|
||||||
import { showWasmRequiredDialog } from '../utils/wasm-provider.js';
|
|
||||||
import { loadPyMuPDF, isPyMuPDFAvailable } from '../utils/pymupdf-loader.js';
|
|
||||||
import { batchDecryptIfNeeded } from '../utils/password-prompt.js';
|
import { batchDecryptIfNeeded } from '../utils/password-prompt.js';
|
||||||
import { deduplicateFileName } from '../utils/deduplicate-filename.js';
|
import { deduplicateFileName } from '../utils/deduplicate-filename.js';
|
||||||
|
|
||||||
@@ -76,7 +74,7 @@ document.addEventListener('DOMContentLoaded', () => {
|
|||||||
const arrayBuffer = await readFileAsArrayBuffer(file);
|
const arrayBuffer = await readFileAsArrayBuffer(file);
|
||||||
const pdfDoc = await getPDFDocument({ data: arrayBuffer }).promise;
|
const pdfDoc = await getPDFDocument({ data: arrayBuffer }).promise;
|
||||||
metaSpan.textContent = `${formatBytes(file.size)} • ${pdfDoc.numPages} pages`;
|
metaSpan.textContent = `${formatBytes(file.size)} • ${pdfDoc.numPages} pages`;
|
||||||
} catch (error) {
|
} catch {
|
||||||
metaSpan.textContent = `${formatBytes(file.size)} • Could not load page count`;
|
metaSpan.textContent = `${formatBytes(file.size)} • Could not load page count`;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -162,11 +160,11 @@ document.addEventListener('DOMContentLoaded', () => {
|
|||||||
() => resetState()
|
() => resetState()
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
} catch (e: any) {
|
} catch (e: unknown) {
|
||||||
hideLoader();
|
hideLoader();
|
||||||
showAlert(
|
showAlert(
|
||||||
'Error',
|
'Error',
|
||||||
`An error occurred during conversion. Error: ${e.message}`
|
`An error occurred during conversion. Error: ${e instanceof Error ? e.message : String(e)}`
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,5 +1,9 @@
|
|||||||
import { showLoader, hideLoader, showAlert } from '../ui.js';
|
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';
|
import { state } from '../state.js';
|
||||||
|
|
||||||
export async function pdfToMarkdown() {
|
export async function pdfToMarkdown() {
|
||||||
@@ -14,7 +18,9 @@ export async function pdfToMarkdown() {
|
|||||||
const page = await pdf.getPage(i);
|
const page = await pdf.getPage(i);
|
||||||
const content = await page.getTextContent();
|
const content = await page.getTextContent();
|
||||||
// This is a simple text extraction. For more advanced formatting, more complex logic is needed.
|
// 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
|
markdown += text + '\n\n'; // Add double newline for paragraph breaks between pages
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ import { state } from '../state.js';
|
|||||||
import { createIcons, icons } from 'lucide';
|
import { createIcons, icons } from 'lucide';
|
||||||
import { convertFileToPdfA, type PdfALevel } from '../utils/ghostscript-loader';
|
import { convertFileToPdfA, type PdfALevel } from '../utils/ghostscript-loader';
|
||||||
import { loadPyMuPDF, isPyMuPDFAvailable } from '../utils/pymupdf-loader.js';
|
import { loadPyMuPDF, isPyMuPDFAvailable } from '../utils/pymupdf-loader.js';
|
||||||
|
import type { PyMuPDFInstance } from '@/types';
|
||||||
import { showWasmRequiredDialog } from '../utils/wasm-provider.js';
|
import { showWasmRequiredDialog } from '../utils/wasm-provider.js';
|
||||||
import { batchDecryptIfNeeded } from '../utils/password-prompt.js';
|
import { batchDecryptIfNeeded } from '../utils/password-prompt.js';
|
||||||
import { deduplicateFileName } from '../utils/deduplicate-filename.js';
|
import { deduplicateFileName } from '../utils/deduplicate-filename.js';
|
||||||
@@ -134,7 +135,7 @@ document.addEventListener('DOMContentLoaded', () => {
|
|||||||
const pymupdf = await loadPyMuPDF();
|
const pymupdf = await loadPyMuPDF();
|
||||||
|
|
||||||
// Rasterize PDF to images and back to PDF (300 DPI for quality)
|
// 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,
|
originalFile,
|
||||||
{
|
{
|
||||||
dpi: 300,
|
dpi: 300,
|
||||||
@@ -205,11 +206,11 @@ document.addEventListener('DOMContentLoaded', () => {
|
|||||||
() => resetState()
|
() => resetState()
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
} catch (e: any) {
|
} catch (e: unknown) {
|
||||||
hideLoader();
|
hideLoader();
|
||||||
showAlert(
|
showAlert(
|
||||||
'Error',
|
'Error',
|
||||||
`An error occurred during conversion. Error: ${e.message}`
|
`An error occurred during conversion. Error: ${e instanceof Error ? e.message : String(e)}`
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -2,12 +2,12 @@ import { showLoader, hideLoader, showAlert } from '../ui.js';
|
|||||||
import { downloadFile, formatBytes } from '../utils/helpers.js';
|
import { downloadFile, formatBytes } from '../utils/helpers.js';
|
||||||
import { createIcons, icons } from 'lucide';
|
import { createIcons, icons } from 'lucide';
|
||||||
import JSZip from 'jszip';
|
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, isPyMuPDFAvailable } from '../utils/pymupdf-loader.js';
|
||||||
|
import type { PyMuPDFInstance } from '@/types';
|
||||||
import { batchDecryptIfNeeded } from '../utils/password-prompt.js';
|
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[] = [];
|
let files: File[] = [];
|
||||||
|
|
||||||
const updateUI = () => {
|
const updateUI = () => {
|
||||||
|
|||||||
@@ -1,14 +1,13 @@
|
|||||||
import { createIcons, icons } from 'lucide';
|
import { createIcons, icons } from 'lucide';
|
||||||
import { showAlert, showLoader, hideLoader } from '../ui.js';
|
import { showAlert, showLoader, hideLoader } from '../ui.js';
|
||||||
import { downloadFile, formatBytes } from '../utils/helpers.js';
|
import { downloadFile, formatBytes } from '../utils/helpers.js';
|
||||||
import { isWasmAvailable, getWasmBaseUrl } from '../config/wasm-cdn-config.js';
|
import { loadPyMuPDF } from '../utils/pymupdf-loader.js';
|
||||||
import { showWasmRequiredDialog } from '../utils/wasm-provider.js';
|
import type { PyMuPDFInstance } from '@/types';
|
||||||
import { loadPyMuPDF, isPyMuPDFAvailable } from '../utils/pymupdf-loader.js';
|
|
||||||
import { batchDecryptIfNeeded } from '../utils/password-prompt.js';
|
import { batchDecryptIfNeeded } from '../utils/password-prompt.js';
|
||||||
import { deduplicateFileName } from '../utils/deduplicate-filename.js';
|
import { deduplicateFileName } from '../utils/deduplicate-filename.js';
|
||||||
|
|
||||||
let files: File[] = [];
|
let files: File[] = [];
|
||||||
let pymupdf: any = null;
|
let pymupdf: PyMuPDFInstance | null = null;
|
||||||
|
|
||||||
if (document.readyState === 'loading') {
|
if (document.readyState === 'loading') {
|
||||||
document.addEventListener('DOMContentLoaded', initializePage);
|
document.addEventListener('DOMContentLoaded', initializePage);
|
||||||
@@ -159,9 +158,9 @@ function updateUI() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async function ensurePyMuPDF(): Promise<any> {
|
async function ensurePyMuPDF(): Promise<PyMuPDFInstance> {
|
||||||
if (!pymupdf) {
|
if (!pymupdf) {
|
||||||
pymupdf = await loadPyMuPDF();
|
pymupdf = (await loadPyMuPDF()) as PyMuPDFInstance;
|
||||||
}
|
}
|
||||||
return pymupdf;
|
return pymupdf;
|
||||||
}
|
}
|
||||||
@@ -230,12 +229,12 @@ async function extractText() {
|
|||||||
}
|
}
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
} catch (e: any) {
|
} catch (e: unknown) {
|
||||||
console.error('[PDFToText]', e);
|
console.error('[PDFToText]', e);
|
||||||
hideLoader();
|
hideLoader();
|
||||||
showAlert(
|
showAlert(
|
||||||
'Extraction Error',
|
'Extraction Error',
|
||||||
e.message || 'Failed to extract text from PDF.'
|
e instanceof Error ? e.message : 'Failed to extract text from PDF.'
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -913,8 +913,15 @@ function showNodeSettings(node: BaseWorkflowNode) {
|
|||||||
content.appendChild(divider);
|
content.appendChild(divider);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
interface FileInputNode extends BaseWorkflowNode {
|
||||||
|
hasFile(): boolean;
|
||||||
|
getFilenames(): string[];
|
||||||
|
removeFile(index: number): void;
|
||||||
|
addFiles(files: File[]): Promise<void>;
|
||||||
|
}
|
||||||
|
|
||||||
const fileInputConfigs: {
|
const fileInputConfigs: {
|
||||||
cls: any;
|
cls: new (...args: unknown[]) => FileInputNode;
|
||||||
label: string;
|
label: string;
|
||||||
accept: string;
|
accept: string;
|
||||||
btnLabel: 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
|
const ctrl = controlEntries.find(([k]) => k === dropdownKey)?.[1] as
|
||||||
| { value?: unknown }
|
| { value?: unknown }
|
||||||
| undefined;
|
| undefined;
|
||||||
|
|||||||
@@ -160,9 +160,13 @@ function updateUI() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function sanitizeImageAsJpeg(imageBytes: any) {
|
function sanitizeImageAsJpeg(imageBytes: Uint8Array | ArrayBuffer) {
|
||||||
return new Promise((resolve, reject) => {
|
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 imageUrl = URL.createObjectURL(blob);
|
||||||
const img = new Image();
|
const img = new Image();
|
||||||
|
|
||||||
@@ -248,9 +252,9 @@ async function convertToPdf() {
|
|||||||
showAlert('Success', 'PDF created successfully!', 'success', () => {
|
showAlert('Success', 'PDF created successfully!', 'success', () => {
|
||||||
resetState();
|
resetState();
|
||||||
});
|
});
|
||||||
} catch (e: any) {
|
} catch (e: unknown) {
|
||||||
console.error(e);
|
console.error(e);
|
||||||
showAlert('Conversion Error', e.message);
|
showAlert('Conversion Error', e instanceof Error ? e.message : String(e));
|
||||||
} finally {
|
} finally {
|
||||||
hideLoader();
|
hideLoader();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,9 +1,5 @@
|
|||||||
import { showLoader, hideLoader, showAlert } from '../ui.js';
|
import { showLoader, hideLoader, showAlert } from '../ui.js';
|
||||||
import {
|
import { downloadFile, formatBytes } from '../utils/helpers.js';
|
||||||
downloadFile,
|
|
||||||
readFileAsArrayBuffer,
|
|
||||||
formatBytes,
|
|
||||||
} from '../utils/helpers.js';
|
|
||||||
import { state } from '../state.js';
|
import { state } from '../state.js';
|
||||||
import { createIcons, icons } from 'lucide';
|
import { createIcons, icons } from 'lucide';
|
||||||
import {
|
import {
|
||||||
@@ -159,11 +155,11 @@ document.addEventListener('DOMContentLoaded', () => {
|
|||||||
() => resetState()
|
() => resetState()
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
} catch (e: any) {
|
} catch (e: unknown) {
|
||||||
hideLoader();
|
hideLoader();
|
||||||
showAlert(
|
showAlert(
|
||||||
'Error',
|
'Error',
|
||||||
`An error occurred during conversion. Error: ${e.message}`
|
`An error occurred during conversion. Error: ${e instanceof Error ? e.message : String(e)}`
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ import {
|
|||||||
import { state } from '../state.js';
|
import { state } from '../state.js';
|
||||||
import { createIcons, icons } from 'lucide';
|
import { createIcons, icons } from 'lucide';
|
||||||
import { loadPyMuPDF } 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 { batchDecryptIfNeeded } from '../utils/password-prompt.js';
|
||||||
import { deduplicateFileName } from '../utils/deduplicate-filename.js';
|
import { deduplicateFileName } from '../utils/deduplicate-filename.js';
|
||||||
|
|
||||||
@@ -117,7 +118,9 @@ document.addEventListener('DOMContentLoaded', () => {
|
|||||||
const file = state.files[0];
|
const file = state.files[0];
|
||||||
showLoader(`Extracting ${file.name} for AI...`);
|
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 outName = file.name.replace(/\.pdf$/i, '') + '_llm.json';
|
||||||
const jsonContent = JSON.stringify(llamaDocs, null, 2);
|
const jsonContent = JSON.stringify(llamaDocs, null, 2);
|
||||||
downloadFile(
|
downloadFile(
|
||||||
@@ -144,7 +147,9 @@ document.addEventListener('DOMContentLoaded', () => {
|
|||||||
`Extracting ${file.name} for AI (${completed + 1}/${total})...`
|
`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 outName = file.name.replace(/\.pdf$/i, '') + '_llm.json';
|
||||||
const jsonContent = JSON.stringify(llamaDocs, null, 2);
|
const jsonContent = JSON.stringify(llamaDocs, null, 2);
|
||||||
const zipEntryName = deduplicateFileName(outName, usedNames);
|
const zipEntryName = deduplicateFileName(outName, usedNames);
|
||||||
@@ -180,11 +185,11 @@ document.addEventListener('DOMContentLoaded', () => {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} catch (e: any) {
|
} catch (e: unknown) {
|
||||||
hideLoader();
|
hideLoader();
|
||||||
showAlert(
|
showAlert(
|
||||||
'Error',
|
'Error',
|
||||||
`An error occurred during extraction. Error: ${e.message}`
|
`An error occurred during extraction. Error: ${e instanceof Error ? e.message : String(e)}`
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -2,18 +2,17 @@ import { showLoader, hideLoader, showAlert } from '../ui.js';
|
|||||||
import { downloadFile, formatBytes } from '../utils/helpers.js';
|
import { downloadFile, formatBytes } from '../utils/helpers.js';
|
||||||
import { state } from '../state.js';
|
import { state } from '../state.js';
|
||||||
import { createIcons, icons } from 'lucide';
|
import { createIcons, icons } from 'lucide';
|
||||||
import { isWasmAvailable, getWasmBaseUrl } from '../config/wasm-cdn-config.js';
|
import { loadPyMuPDF } from '../utils/pymupdf-loader.js';
|
||||||
import { showWasmRequiredDialog } from '../utils/wasm-provider.js';
|
import type { PyMuPDFInstance } from '@/types';
|
||||||
import { loadPyMuPDF, isPyMuPDFAvailable } from '../utils/pymupdf-loader.js';
|
|
||||||
|
|
||||||
const ACCEPTED_EXTENSIONS = ['.psd'];
|
const ACCEPTED_EXTENSIONS = ['.psd'];
|
||||||
const FILETYPE_NAME = 'PSD';
|
const FILETYPE_NAME = 'PSD';
|
||||||
|
|
||||||
let pymupdf: any = null;
|
let pymupdf: PyMuPDFInstance | null = null;
|
||||||
|
|
||||||
async function ensurePyMuPDF(): Promise<any> {
|
async function ensurePyMuPDF(): Promise<PyMuPDFInstance> {
|
||||||
if (!pymupdf) {
|
if (!pymupdf) {
|
||||||
pymupdf = await loadPyMuPDF();
|
pymupdf = (await loadPyMuPDF()) as PyMuPDFInstance;
|
||||||
}
|
}
|
||||||
return pymupdf;
|
return pymupdf;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -8,11 +8,11 @@ import {
|
|||||||
} from '../utils/helpers.js';
|
} from '../utils/helpers.js';
|
||||||
import { state } from '../state.js';
|
import { state } from '../state.js';
|
||||||
import { createIcons, icons } from 'lucide';
|
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, isPyMuPDFAvailable } from '../utils/pymupdf-loader.js';
|
||||||
|
import type { PyMuPDFInstance } from '@/types';
|
||||||
import { batchDecryptIfNeeded } from '../utils/password-prompt.js';
|
import { batchDecryptIfNeeded } from '../utils/password-prompt.js';
|
||||||
import { deduplicateFileName } from '../utils/deduplicate-filename.js';
|
import { deduplicateFileName } from '../utils/deduplicate-filename.js';
|
||||||
|
import { showWasmRequiredDialog } from '../utils/wasm-provider.js';
|
||||||
|
|
||||||
document.addEventListener('DOMContentLoaded', () => {
|
document.addEventListener('DOMContentLoaded', () => {
|
||||||
const fileInput = document.getElementById('file-input') as HTMLInputElement;
|
const fileInput = document.getElementById('file-input') as HTMLInputElement;
|
||||||
@@ -136,12 +136,15 @@ document.addEventListener('DOMContentLoaded', () => {
|
|||||||
const file = state.files[0];
|
const file = state.files[0];
|
||||||
showLoader(`Rasterizing ${file.name}...`);
|
showLoader(`Rasterizing ${file.name}...`);
|
||||||
|
|
||||||
const rasterizedBlob = await (pymupdf as any).rasterizePdf(file, {
|
const rasterizedBlob = await (pymupdf as PyMuPDFInstance).rasterizePdf(
|
||||||
dpi,
|
file,
|
||||||
format,
|
{
|
||||||
grayscale,
|
dpi,
|
||||||
quality: 95,
|
format,
|
||||||
});
|
grayscale,
|
||||||
|
quality: 95,
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
const outName = file.name.replace(/\.pdf$/i, '') + '_rasterized.pdf';
|
const outName = file.name.replace(/\.pdf$/i, '') + '_rasterized.pdf';
|
||||||
downloadFile(rasterizedBlob, outName);
|
downloadFile(rasterizedBlob, outName);
|
||||||
@@ -165,7 +168,9 @@ document.addEventListener('DOMContentLoaded', () => {
|
|||||||
`Rasterizing ${file.name} (${completed + 1}/${total})...`
|
`Rasterizing ${file.name} (${completed + 1}/${total})...`
|
||||||
);
|
);
|
||||||
|
|
||||||
const rasterizedBlob = await (pymupdf as any).rasterizePdf(file, {
|
const rasterizedBlob = await (
|
||||||
|
pymupdf as PyMuPDFInstance
|
||||||
|
).rasterizePdf(file, {
|
||||||
dpi,
|
dpi,
|
||||||
format,
|
format,
|
||||||
grayscale,
|
grayscale,
|
||||||
@@ -210,11 +215,11 @@ document.addEventListener('DOMContentLoaded', () => {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} catch (e: any) {
|
} catch (e: unknown) {
|
||||||
hideLoader();
|
hideLoader();
|
||||||
showAlert(
|
showAlert(
|
||||||
'Error',
|
'Error',
|
||||||
`An error occurred during rasterization. Error: ${e.message}`
|
`An error occurred during rasterization. Error: ${e instanceof Error ? e.message : String(e)}`
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -2,16 +2,18 @@ import { showLoader, hideLoader, showAlert } from '../ui.js';
|
|||||||
import { downloadFile } from '../utils/helpers.js';
|
import { downloadFile } from '../utils/helpers.js';
|
||||||
import { state } from '../state.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
|
// @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;
|
const { rgb } = window.PDFLib;
|
||||||
|
|
||||||
export async function redact(redactions: any, canvasScale: any) {
|
export async function redact(redactions: RedactionRect[], canvasScale: number) {
|
||||||
showLoader('Applying redactions...');
|
showLoader('Applying redactions...');
|
||||||
try {
|
try {
|
||||||
const pdfPages = state.pdfDoc.getPages();
|
const pdfPages = state.pdfDoc.getPages();
|
||||||
const conversionScale = 1 / canvasScale;
|
const conversionScale = 1 / canvasScale;
|
||||||
|
|
||||||
redactions.forEach((r: any) => {
|
redactions.forEach((r: RedactionRect) => {
|
||||||
const page = pdfPages[r.pageIndex];
|
const page = pdfPages[r.pageIndex];
|
||||||
const { height: pageHeight } = page.getSize();
|
const { height: pageHeight } = page.getSize();
|
||||||
|
|
||||||
@@ -32,7 +34,7 @@ export async function redact(redactions: any, canvasScale: any) {
|
|||||||
|
|
||||||
const redactedBytes = await state.pdfDoc.save();
|
const redactedBytes = await state.pdfDoc.save();
|
||||||
downloadFile(
|
downloadFile(
|
||||||
new Blob([redactedBytes], { type: 'application/pdf' }),
|
new Blob([new Uint8Array(redactedBytes)], { type: 'application/pdf' }),
|
||||||
'redacted.pdf'
|
'redacted.pdf'
|
||||||
);
|
);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
|
|||||||
@@ -36,7 +36,7 @@ function hideLoader() {
|
|||||||
function showAlert(
|
function showAlert(
|
||||||
title: string,
|
title: string,
|
||||||
msg: string,
|
msg: string,
|
||||||
type = 'error',
|
_type = 'error',
|
||||||
cb?: () => void
|
cb?: () => void
|
||||||
) {
|
) {
|
||||||
const modal = document.getElementById('alert-modal');
|
const modal = document.getElementById('alert-modal');
|
||||||
@@ -138,7 +138,7 @@ async function handleFileUpload(file: File) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async function isPageBlank(
|
async function isPageBlank(
|
||||||
page: any,
|
page: pdfjsLib.PDFPageProxy,
|
||||||
maxNonWhitePercent = 0.5
|
maxNonWhitePercent = 0.5
|
||||||
): Promise<boolean> {
|
): Promise<boolean> {
|
||||||
const viewport = page.getViewport({ scale: 0.5 });
|
const viewport = page.getViewport({ scale: 0.5 });
|
||||||
@@ -149,7 +149,7 @@ async function isPageBlank(
|
|||||||
canvas.width = viewport.width;
|
canvas.width = viewport.width;
|
||||||
canvas.height = viewport.height;
|
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 imageData = ctx.getImageData(0, 0, canvas.width, canvas.height);
|
||||||
const data = imageData.data;
|
const data = imageData.data;
|
||||||
@@ -165,7 +165,7 @@ async function isPageBlank(
|
|||||||
return nonWhitePercent <= maxNonWhitePercent;
|
return nonWhitePercent <= maxNonWhitePercent;
|
||||||
}
|
}
|
||||||
|
|
||||||
async function generateThumbnail(page: any): Promise<string> {
|
async function generateThumbnail(page: pdfjsLib.PDFPageProxy): Promise<string> {
|
||||||
const viewport = page.getViewport({ scale: 1 });
|
const viewport = page.getViewport({ scale: 1 });
|
||||||
const canvas = document.createElement('canvas');
|
const canvas = document.createElement('canvas');
|
||||||
const ctx = canvas.getContext('2d');
|
const ctx = canvas.getContext('2d');
|
||||||
@@ -174,7 +174,7 @@ async function generateThumbnail(page: any): Promise<string> {
|
|||||||
canvas.width = viewport.width;
|
canvas.width = viewport.width;
|
||||||
canvas.height = viewport.height;
|
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);
|
return canvas.toDataURL('image/jpeg', 0.7);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ import {
|
|||||||
readFileAsArrayBuffer,
|
readFileAsArrayBuffer,
|
||||||
} from '../utils/helpers.js';
|
} from '../utils/helpers.js';
|
||||||
import { icons, createIcons } from 'lucide';
|
import { icons, createIcons } from 'lucide';
|
||||||
import { RemoveRestrictionsState } from '@/types';
|
import { RemoveRestrictionsState, QpdfInstanceExtended } from '@/types';
|
||||||
|
|
||||||
const pageState: RemoveRestrictionsState = {
|
const pageState: RemoveRestrictionsState = {
|
||||||
file: null,
|
file: null,
|
||||||
@@ -98,7 +98,7 @@ async function removeRestrictions() {
|
|||||||
|
|
||||||
const inputPath = '/input.pdf';
|
const inputPath = '/input.pdf';
|
||||||
const outputPath = '/output.pdf';
|
const outputPath = '/output.pdf';
|
||||||
let qpdf: any;
|
let qpdf: QpdfInstanceExtended;
|
||||||
|
|
||||||
const loaderModal = document.getElementById('loader-modal');
|
const loaderModal = document.getElementById('loader-modal');
|
||||||
const loaderText = document.getElementById('loader-text');
|
const loaderText = document.getElementById('loader-text');
|
||||||
@@ -127,12 +127,10 @@ async function removeRestrictions() {
|
|||||||
|
|
||||||
try {
|
try {
|
||||||
qpdf.callMain(args);
|
qpdf.callMain(args);
|
||||||
} catch (qpdfError: any) {
|
} catch (qpdfError: unknown) {
|
||||||
console.error('qpdf execution error:', qpdfError);
|
console.error('qpdf execution error:', qpdfError);
|
||||||
if (
|
const qpdfMsg = qpdfError instanceof Error ? qpdfError.message : '';
|
||||||
qpdfError.message?.includes('password') ||
|
if (qpdfMsg.includes('password') || qpdfMsg.includes('encrypt')) {
|
||||||
qpdfError.message?.includes('encrypt')
|
|
||||||
) {
|
|
||||||
throw new Error(
|
throw new Error(
|
||||||
'Failed to remove restrictions. The PDF may require the correct owner password.',
|
'Failed to remove restrictions. The PDF may require the correct owner password.',
|
||||||
{ cause: qpdfError }
|
{ cause: qpdfError }
|
||||||
@@ -140,8 +138,7 @@ async function removeRestrictions() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
throw new Error(
|
throw new Error(
|
||||||
'Failed to remove restrictions: ' +
|
'Failed to remove restrictions: ' + (qpdfMsg || 'Unknown error'),
|
||||||
(qpdfError.message || 'Unknown error'),
|
|
||||||
{ cause: qpdfError }
|
{ cause: qpdfError }
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -153,7 +150,9 @@ async function removeRestrictions() {
|
|||||||
throw new Error('Operation resulted in an empty file.');
|
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}`);
|
downloadFile(blob, `unrestricted-${pageState.file.name}`);
|
||||||
|
|
||||||
if (loaderModal) loaderModal.classList.add('hidden');
|
if (loaderModal) loaderModal.classList.add('hidden');
|
||||||
@@ -166,12 +165,12 @@ async function removeRestrictions() {
|
|||||||
resetState();
|
resetState();
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
} catch (error: any) {
|
} catch (error: unknown) {
|
||||||
console.error('Error during restriction removal:', error);
|
console.error('Error during restriction removal:', error);
|
||||||
if (loaderModal) loaderModal.classList.add('hidden');
|
if (loaderModal) loaderModal.classList.add('hidden');
|
||||||
showAlert(
|
showAlert(
|
||||||
'Operation Failed',
|
'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 {
|
} finally {
|
||||||
try {
|
try {
|
||||||
|
|||||||
@@ -8,11 +8,12 @@ import { state } from '../state.js';
|
|||||||
import JSZip from 'jszip';
|
import JSZip from 'jszip';
|
||||||
import { deduplicateFileName } from '../utils/deduplicate-filename.js';
|
import { deduplicateFileName } from '../utils/deduplicate-filename.js';
|
||||||
import { batchDecryptIfNeeded } from '../utils/password-prompt.js';
|
import { batchDecryptIfNeeded } from '../utils/password-prompt.js';
|
||||||
|
import type { QpdfInstanceExtended } from '@/types';
|
||||||
|
|
||||||
export async function repairPdfFile(file: File): Promise<Uint8Array | null> {
|
export async function repairPdfFile(file: File): Promise<Uint8Array | null> {
|
||||||
const inputPath = '/input.pdf';
|
const inputPath = '/input.pdf';
|
||||||
const outputPath = '/repaired_form.pdf';
|
const outputPath = '/repaired_form.pdf';
|
||||||
let qpdf: any;
|
let qpdf: QpdfInstanceExtended;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
qpdf = await initializeQpdf();
|
qpdf = await initializeQpdf();
|
||||||
|
|||||||
@@ -1,9 +1,5 @@
|
|||||||
import { showLoader, hideLoader, showAlert } from '../ui.js';
|
import { showLoader, hideLoader, showAlert } from '../ui.js';
|
||||||
import {
|
import { downloadFile, formatBytes } from '../utils/helpers.js';
|
||||||
downloadFile,
|
|
||||||
readFileAsArrayBuffer,
|
|
||||||
formatBytes,
|
|
||||||
} from '../utils/helpers.js';
|
|
||||||
import { state } from '../state.js';
|
import { state } from '../state.js';
|
||||||
import { createIcons, icons } from 'lucide';
|
import { createIcons, icons } from 'lucide';
|
||||||
import {
|
import {
|
||||||
@@ -158,11 +154,11 @@ document.addEventListener('DOMContentLoaded', () => {
|
|||||||
() => resetState()
|
() => resetState()
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
} catch (e: any) {
|
} catch (e: unknown) {
|
||||||
hideLoader();
|
hideLoader();
|
||||||
showAlert(
|
showAlert(
|
||||||
'Error',
|
'Error',
|
||||||
`An error occurred during conversion. Error: ${e.message}`
|
`An error occurred during conversion. Error: ${e instanceof Error ? e.message : String(e)}`
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,192 +1,202 @@
|
|||||||
import { categories } from '../config/tools.js';
|
import { categories } from '../config/tools.js';
|
||||||
|
import type { ToolEntry } from '@/types';
|
||||||
|
|
||||||
export class ShortcutsManager {
|
export class ShortcutsManager {
|
||||||
private static STORAGE_KEY = 'bentopdf_shortcuts';
|
private static STORAGE_KEY = 'bentopdf_shortcuts';
|
||||||
private static shortcuts: Map<string, string> = new Map();
|
private static shortcuts: Map<string, string> = new Map();
|
||||||
private static defaultShortcuts: Map<string, string> = new Map();
|
private static defaultShortcuts: Map<string, string> = new Map();
|
||||||
|
|
||||||
private static getToolId(tool: any): string {
|
private static getToolId(tool: ToolEntry): string {
|
||||||
if (tool.id) return tool.id;
|
if (tool.id) return tool.id;
|
||||||
if (tool.href) {
|
if (tool.href) {
|
||||||
// Extract filename without extension from href
|
// Extract filename without extension from href
|
||||||
const match = tool.href.match(/\/([^/]+)\.html$/);
|
const match = tool.href.match(/\/([^/]+)\.html$/);
|
||||||
return match ? match[1] : tool.href;
|
return match ? match[1] : tool.href;
|
||||||
}
|
|
||||||
return 'unknown';
|
|
||||||
}
|
}
|
||||||
|
return 'unknown';
|
||||||
|
}
|
||||||
|
|
||||||
static init() {
|
static init() {
|
||||||
this.loadDefaults();
|
this.loadDefaults();
|
||||||
this.loadFromStorage();
|
this.loadFromStorage();
|
||||||
this.setupGlobalListener();
|
this.setupGlobalListener();
|
||||||
}
|
}
|
||||||
|
|
||||||
private static loadDefaults() {
|
private static loadDefaults() {
|
||||||
this.defaultShortcuts.set('merge', 'mod+shift+m');
|
this.defaultShortcuts.set('merge', 'mod+shift+m');
|
||||||
this.defaultShortcuts.set('split', 'mod+shift+s');
|
this.defaultShortcuts.set('split', 'mod+shift+s');
|
||||||
this.defaultShortcuts.set('compress', 'mod+shift+c');
|
this.defaultShortcuts.set('compress', 'mod+shift+c');
|
||||||
}
|
}
|
||||||
|
|
||||||
private static loadFromStorage() {
|
private static loadFromStorage() {
|
||||||
const stored = localStorage.getItem(this.STORAGE_KEY);
|
const stored = localStorage.getItem(this.STORAGE_KEY);
|
||||||
if (stored) {
|
if (stored) {
|
||||||
try {
|
try {
|
||||||
const parsed = JSON.parse(stored);
|
const parsed = JSON.parse(stored);
|
||||||
this.shortcuts = new Map(Object.entries(parsed));
|
this.shortcuts = new Map(Object.entries(parsed));
|
||||||
|
|
||||||
const allTools = categories.flatMap(c => c.tools);
|
const allTools = categories.flatMap((c) => c.tools);
|
||||||
const validToolIds = allTools.map(t => this.getToolId(t));
|
const validToolIds = allTools.map((t) => this.getToolId(t));
|
||||||
for (const [toolId, _] of this.shortcuts.entries()) {
|
for (const [toolId] of this.shortcuts.entries()) {
|
||||||
if (!validToolIds.includes(toolId)) {
|
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 {
|
|
||||||
this.shortcuts.delete(toolId);
|
this.shortcuts.delete(toolId);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
this.save();
|
|
||||||
window.dispatchEvent(new CustomEvent('shortcuts-updated'));
|
|
||||||
}
|
|
||||||
|
|
||||||
static getAllShortcuts(): Map<string, string> {
|
if (this.shortcuts.size !== Object.keys(parsed).length) {
|
||||||
return this.shortcuts;
|
this.save();
|
||||||
}
|
|
||||||
|
|
||||||
static exportSettings() {
|
|
||||||
// Create a map with all tools, defaulting to empty string if not set
|
|
||||||
const exportObj: Record<string, string> = {};
|
|
||||||
|
|
||||||
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;
|
|
||||||
}
|
}
|
||||||
|
} 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() {
|
static save() {
|
||||||
window.addEventListener('keydown', (e) => {
|
const obj = Object.fromEntries(this.shortcuts);
|
||||||
// Ignore if typing in an input or textarea
|
localStorage.setItem(this.STORAGE_KEY, JSON.stringify(obj));
|
||||||
const target = e.target as HTMLElement;
|
}
|
||||||
if (target.tagName === 'INPUT' || target.tagName === 'TEXTAREA' || target.isContentEditable) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Build key string
|
static reset() {
|
||||||
const keys: string[] = [];
|
this.shortcuts = new Map(this.defaultShortcuts);
|
||||||
const isMac = navigator.userAgent.toUpperCase().includes('MAC');
|
this.save();
|
||||||
|
// Dispatch event to update UI if needed
|
||||||
|
window.dispatchEvent(new CustomEvent('shortcuts-updated'));
|
||||||
|
}
|
||||||
|
|
||||||
// On Mac: metaKey = Command, ctrlKey = Control
|
static getShortcut(toolId: string): string | undefined {
|
||||||
// On Windows/Linux: metaKey is rare, ctrlKey = Ctrl
|
return this.shortcuts.get(toolId);
|
||||||
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();
|
static findToolByShortcut(key: string): string | undefined {
|
||||||
|
for (const [id, k] of this.shortcuts.entries()) {
|
||||||
if (e.altKey && e.code) {
|
if (k === key) {
|
||||||
if (e.code.startsWith('Key')) {
|
return id;
|
||||||
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 });
|
|
||||||
}
|
}
|
||||||
|
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<string, string> {
|
||||||
|
return this.shortcuts;
|
||||||
|
}
|
||||||
|
|
||||||
|
static exportSettings() {
|
||||||
|
// Create a map with all tools, defaulting to empty string if not set
|
||||||
|
const exportObj: Record<string, string> = {};
|
||||||
|
|
||||||
|
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 }
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -16,7 +16,6 @@ import JSZip from 'jszip';
|
|||||||
import { PDFDocument as PDFLibDocument } from 'pdf-lib';
|
import { PDFDocument as PDFLibDocument } from 'pdf-lib';
|
||||||
import { loadPdfDocument } from '../utils/load-pdf-document.js';
|
import { loadPdfDocument } from '../utils/load-pdf-document.js';
|
||||||
|
|
||||||
// @ts-ignore
|
|
||||||
pdfjsLib.GlobalWorkerOptions.workerSrc = new URL(
|
pdfjsLib.GlobalWorkerOptions.workerSrc = new URL(
|
||||||
'pdfjs-dist/build/pdf.worker.min.mjs',
|
'pdfjs-dist/build/pdf.worker.min.mjs',
|
||||||
import.meta.url
|
import.meta.url
|
||||||
@@ -178,7 +177,7 @@ document.addEventListener('DOMContentLoaded', () => {
|
|||||||
imgContainer.append(img, pageNumDiv);
|
imgContainer.append(img, pageNumDiv);
|
||||||
wrapper.appendChild(imgContainer);
|
wrapper.appendChild(imgContainer);
|
||||||
|
|
||||||
const handleSelection = (e: any) => {
|
const handleSelection = (e: Event) => {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
|
|
||||||
@@ -286,7 +285,7 @@ document.addEventListener('DOMContentLoaded', () => {
|
|||||||
let indicesToExtract: number[] = [];
|
let indicesToExtract: number[] = [];
|
||||||
|
|
||||||
switch (splitMode) {
|
switch (splitMode) {
|
||||||
case 'range':
|
case 'range': {
|
||||||
const pageRangeInput = (
|
const pageRangeInput = (
|
||||||
document.getElementById('page-range') as HTMLInputElement
|
document.getElementById('page-range') as HTMLInputElement
|
||||||
).value;
|
).value;
|
||||||
@@ -331,7 +330,7 @@ document.addEventListener('DOMContentLoaded', () => {
|
|||||||
const group = rangeGroups[i];
|
const group = rangeGroups[i];
|
||||||
const newPdf = await PDFLibDocument.create();
|
const newPdf = await PDFLibDocument.create();
|
||||||
const copiedPages = await newPdf.copyPages(state.pdfDoc, group);
|
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 pdfBytes = await newPdf.save();
|
||||||
|
|
||||||
const minPage = Math.min(...group) + 1;
|
const minPage = Math.min(...group) + 1;
|
||||||
@@ -357,8 +356,9 @@ document.addEventListener('DOMContentLoaded', () => {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
case 'even-odd':
|
case 'even-odd': {
|
||||||
const choiceElement = document.querySelector(
|
const choiceElement = document.querySelector(
|
||||||
'input[name="even-odd-choice"]:checked'
|
'input[name="even-odd-choice"]:checked'
|
||||||
) as HTMLInputElement;
|
) as HTMLInputElement;
|
||||||
@@ -371,6 +371,7 @@ document.addEventListener('DOMContentLoaded', () => {
|
|||||||
if (choice === 'odd' && (i + 1) % 2 !== 0) indicesToExtract.push(i);
|
if (choice === 'odd' && (i + 1) % 2 !== 0) indicesToExtract.push(i);
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
|
}
|
||||||
case 'all':
|
case 'all':
|
||||||
indicesToExtract = Array.from({ length: totalPages }, (_, i) => i);
|
indicesToExtract = Array.from({ length: totalPages }, (_, i) => i);
|
||||||
break;
|
break;
|
||||||
@@ -379,8 +380,7 @@ document.addEventListener('DOMContentLoaded', () => {
|
|||||||
document.querySelectorAll('.page-thumbnail-wrapper.selected')
|
document.querySelectorAll('.page-thumbnail-wrapper.selected')
|
||||||
).map((el) => parseInt((el as HTMLElement).dataset.pageIndex || '0'));
|
).map((el) => parseInt((el as HTMLElement).dataset.pageIndex || '0'));
|
||||||
break;
|
break;
|
||||||
case 'bookmarks':
|
case 'bookmarks': {
|
||||||
// Check if CPDF is configured
|
|
||||||
if (!isCpdfAvailable()) {
|
if (!isCpdfAvailable()) {
|
||||||
showWasmRequiredDialog('cpdf');
|
showWasmRequiredDialog('cpdf');
|
||||||
hideLoader();
|
hideLoader();
|
||||||
@@ -404,7 +404,7 @@ document.addEventListener('DOMContentLoaded', () => {
|
|||||||
|
|
||||||
if (bookmarkLevel === 'all' || level === parseInt(bookmarkLevel)) {
|
if (bookmarkLevel === 'all' || level === parseInt(bookmarkLevel)) {
|
||||||
if (page > 1 && !splitPages.includes(page - 1)) {
|
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,
|
state.pdfDoc,
|
||||||
pageIndices
|
pageIndices
|
||||||
);
|
);
|
||||||
copiedPages.forEach((page: any) => newPdf.addPage(page));
|
copiedPages.forEach((page) => newPdf.addPage(page));
|
||||||
const pdfBytes2 = await newPdf.save();
|
const pdfBytes2 = await newPdf.save();
|
||||||
zip.file(`split-${i + 1}.pdf`, pdfBytes2);
|
zip.file(`split-${i + 1}.pdf`, pdfBytes2);
|
||||||
}
|
}
|
||||||
@@ -446,8 +446,9 @@ document.addEventListener('DOMContentLoaded', () => {
|
|||||||
resetState();
|
resetState();
|
||||||
});
|
});
|
||||||
return;
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
case 'n-times':
|
case 'n-times': {
|
||||||
const nValue = parseInt(
|
const nValue = parseInt(
|
||||||
(document.getElementById('split-n-value') as HTMLInputElement)
|
(document.getElementById('split-n-value') as HTMLInputElement)
|
||||||
?.value || '5'
|
?.value || '5'
|
||||||
@@ -470,7 +471,7 @@ document.addEventListener('DOMContentLoaded', () => {
|
|||||||
state.pdfDoc,
|
state.pdfDoc,
|
||||||
pageIndices
|
pageIndices
|
||||||
);
|
);
|
||||||
copiedPages.forEach((page: any) => newPdf.addPage(page));
|
copiedPages.forEach((page) => newPdf.addPage(page));
|
||||||
const pdfBytes3 = await newPdf.save();
|
const pdfBytes3 = await newPdf.save();
|
||||||
zip2.file(`split-${i + 1}.pdf`, pdfBytes3);
|
zip2.file(`split-${i + 1}.pdf`, pdfBytes3);
|
||||||
}
|
}
|
||||||
@@ -482,6 +483,7 @@ document.addEventListener('DOMContentLoaded', () => {
|
|||||||
resetState();
|
resetState();
|
||||||
});
|
});
|
||||||
return;
|
return;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const uniqueIndices = [...new Set(indicesToExtract)];
|
const uniqueIndices = [...new Set(indicesToExtract)];
|
||||||
@@ -506,8 +508,7 @@ document.addEventListener('DOMContentLoaded', () => {
|
|||||||
]);
|
]);
|
||||||
newPdf.addPage(copiedPage);
|
newPdf.addPage(copiedPage);
|
||||||
const pdfBytes = await newPdf.save();
|
const pdfBytes = await newPdf.save();
|
||||||
// @ts-ignore
|
zip.file(`page-${index + 1}.pdf`, new Uint8Array(pdfBytes));
|
||||||
zip.file(`page-${index + 1}.pdf`, pdfBytes);
|
|
||||||
}
|
}
|
||||||
const zipBlob = await zip.generateAsync({ type: 'blob' });
|
const zipBlob = await zip.generateAsync({ type: 'blob' });
|
||||||
downloadFile(zipBlob, 'split-pages.zip');
|
downloadFile(zipBlob, 'split-pages.zip');
|
||||||
@@ -517,7 +518,7 @@ document.addEventListener('DOMContentLoaded', () => {
|
|||||||
state.pdfDoc,
|
state.pdfDoc,
|
||||||
uniqueIndices as number[]
|
uniqueIndices as number[]
|
||||||
);
|
);
|
||||||
copiedPages.forEach((page: any) => newPdf.addPage(page));
|
copiedPages.forEach((page) => newPdf.addPage(page));
|
||||||
const pdfBytes = await newPdf.save();
|
const pdfBytes = await newPdf.save();
|
||||||
downloadFile(
|
downloadFile(
|
||||||
new Blob([new Uint8Array(pdfBytes)], { type: 'application/pdf' }),
|
new Blob([new Uint8Array(pdfBytes)], { type: 'application/pdf' }),
|
||||||
@@ -532,11 +533,13 @@ document.addEventListener('DOMContentLoaded', () => {
|
|||||||
showAlert('Success', 'PDF split successfully!', 'success', () => {
|
showAlert('Success', 'PDF split successfully!', 'success', () => {
|
||||||
resetState();
|
resetState();
|
||||||
});
|
});
|
||||||
} catch (e: any) {
|
} catch (e: unknown) {
|
||||||
console.error(e);
|
console.error(e);
|
||||||
showAlert(
|
showAlert(
|
||||||
'Error',
|
'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 {
|
} finally {
|
||||||
hideLoader();
|
hideLoader();
|
||||||
|
|||||||
@@ -1,10 +1,6 @@
|
|||||||
import { createIcons, icons } from 'lucide';
|
import { createIcons, icons } from 'lucide';
|
||||||
import { showAlert, showLoader, hideLoader } from '../ui.js';
|
import { showAlert, showLoader, hideLoader } from '../ui.js';
|
||||||
import {
|
import { downloadFile, formatBytes } from '../utils/helpers.js';
|
||||||
downloadFile,
|
|
||||||
readFileAsArrayBuffer,
|
|
||||||
formatBytes,
|
|
||||||
} from '../utils/helpers.js';
|
|
||||||
import { PDFDocument as PDFLibDocument } from 'pdf-lib';
|
import { PDFDocument as PDFLibDocument } from 'pdf-lib';
|
||||||
import {
|
import {
|
||||||
getSelectedQuality,
|
getSelectedQuality,
|
||||||
@@ -261,9 +257,9 @@ async function convertToPdf() {
|
|||||||
showAlert('Success', 'PDF created successfully!', 'success', () => {
|
showAlert('Success', 'PDF created successfully!', 'success', () => {
|
||||||
resetState();
|
resetState();
|
||||||
});
|
});
|
||||||
} catch (e: any) {
|
} catch (e: unknown) {
|
||||||
console.error(e);
|
console.error(e);
|
||||||
showAlert('Conversion Error', e.message);
|
showAlert('Conversion Error', e instanceof Error ? e.message : String(e));
|
||||||
} finally {
|
} finally {
|
||||||
hideLoader();
|
hideLoader();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -36,15 +36,6 @@ const backToToolsBtn = document.getElementById(
|
|||||||
'back-to-tools'
|
'back-to-tools'
|
||||||
) as HTMLButtonElement;
|
) as HTMLButtonElement;
|
||||||
|
|
||||||
interface GenerateTOCMessage {
|
|
||||||
command: 'generate-toc';
|
|
||||||
pdfData: ArrayBuffer;
|
|
||||||
title: string;
|
|
||||||
fontSize: number;
|
|
||||||
fontFamily: number;
|
|
||||||
addBookmark: boolean;
|
|
||||||
}
|
|
||||||
|
|
||||||
interface TOCSuccessResponse {
|
interface TOCSuccessResponse {
|
||||||
status: 'success';
|
status: 'success';
|
||||||
pdfBytes: ArrayBuffer;
|
pdfBytes: ArrayBuffer;
|
||||||
|
|||||||
@@ -1,9 +1,7 @@
|
|||||||
import { showLoader, hideLoader, showAlert } from '../ui.js';
|
import { showLoader, hideLoader, showAlert } from '../ui.js';
|
||||||
import { downloadFile, formatBytes } from '../utils/helpers.js';
|
import { downloadFile, formatBytes } from '../utils/helpers.js';
|
||||||
import { createIcons, icons } from 'lucide';
|
import { createIcons, icons } from 'lucide';
|
||||||
import { isWasmAvailable, getWasmBaseUrl } from '../config/wasm-cdn-config.js';
|
import { loadPyMuPDF } from '../utils/pymupdf-loader.js';
|
||||||
import { showWasmRequiredDialog } from '../utils/wasm-provider.js';
|
|
||||||
import { loadPyMuPDF, isPyMuPDFAvailable } from '../utils/pymupdf-loader.js';
|
|
||||||
|
|
||||||
let files: File[] = [];
|
let files: File[] = [];
|
||||||
let currentMode: 'upload' | 'text' = 'upload';
|
let currentMode: 'upload' | 'text' = 'upload';
|
||||||
@@ -140,9 +138,12 @@ async function convert() {
|
|||||||
resetState();
|
resetState();
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
} catch (e: any) {
|
} catch (e: unknown) {
|
||||||
console.error('[TxtToPDF] Error:', e);
|
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 {
|
} finally {
|
||||||
hideLoader();
|
hideLoader();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -100,7 +100,13 @@ export function validateSignature(
|
|||||||
Array.from(signature.contents)
|
Array.from(signature.contents)
|
||||||
);
|
);
|
||||||
const asn1 = forge.asn1.fromDer(binaryString);
|
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) {
|
if (!p7.certificates || p7.certificates.length === 0) {
|
||||||
result.errorMessage = 'No certificates found in signature';
|
result.errorMessage = 'No certificates found in signature';
|
||||||
@@ -166,10 +172,8 @@ export function validateSignature(
|
|||||||
} else {
|
} else {
|
||||||
// Try to extract from authenticated attributes
|
// Try to extract from authenticated attributes
|
||||||
try {
|
try {
|
||||||
const signedData = p7 as any;
|
if (p7.rawCapture?.authenticatedAttributes) {
|
||||||
if (signedData.rawCapture?.authenticatedAttributes) {
|
for (const attr of p7.rawCapture.authenticatedAttributes) {
|
||||||
// Look for signing time attribute
|
|
||||||
for (const attr of signedData.rawCapture.authenticatedAttributes) {
|
|
||||||
if (attr.type === forge.pki.oids.signingTime) {
|
if (attr.type === forge.pki.oids.signingTime) {
|
||||||
result.signatureDate = attr.value;
|
result.signatureDate = attr.value;
|
||||||
break;
|
break;
|
||||||
@@ -185,7 +189,7 @@ export function validateSignature(
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (signature.byteRange && signature.byteRange.length === 4) {
|
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 totalCovered = len1 + len2;
|
||||||
const expectedEnd = start2 + len2;
|
const expectedEnd = start2 + len2;
|
||||||
|
|
||||||
|
|||||||
@@ -160,9 +160,13 @@ function updateUI() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function sanitizeImageAsJpeg(imageBytes: any) {
|
function sanitizeImageAsJpeg(imageBytes: Uint8Array | ArrayBuffer) {
|
||||||
return new Promise((resolve, reject) => {
|
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 imageUrl = URL.createObjectURL(blob);
|
||||||
const img = new Image();
|
const img = new Image();
|
||||||
|
|
||||||
@@ -248,9 +252,9 @@ async function convertToPdf() {
|
|||||||
showAlert('Success', 'PDF created successfully!', 'success', () => {
|
showAlert('Success', 'PDF created successfully!', 'success', () => {
|
||||||
resetState();
|
resetState();
|
||||||
});
|
});
|
||||||
} catch (e: any) {
|
} catch (e: unknown) {
|
||||||
console.error(e);
|
console.error(e);
|
||||||
showAlert('Conversion Error', e.message);
|
showAlert('Conversion Error', e instanceof Error ? e.message : String(e));
|
||||||
} finally {
|
} finally {
|
||||||
hideLoader();
|
hideLoader();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,9 +1,5 @@
|
|||||||
import { showLoader, hideLoader, showAlert } from '../ui.js';
|
import { showLoader, hideLoader, showAlert } from '../ui.js';
|
||||||
import {
|
import { downloadFile, formatBytes } from '../utils/helpers.js';
|
||||||
downloadFile,
|
|
||||||
readFileAsArrayBuffer,
|
|
||||||
formatBytes,
|
|
||||||
} from '../utils/helpers.js';
|
|
||||||
import { state } from '../state.js';
|
import { state } from '../state.js';
|
||||||
import { createIcons, icons } from 'lucide';
|
import { createIcons, icons } from 'lucide';
|
||||||
import {
|
import {
|
||||||
@@ -187,13 +183,16 @@ document.addEventListener('DOMContentLoaded', () => {
|
|||||||
() => resetState()
|
() => resetState()
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
} catch (e: any) {
|
} catch (e: unknown) {
|
||||||
console.error('[Word2PDF] ERROR:', e);
|
console.error('[Word2PDF] ERROR:', e);
|
||||||
console.error('[Word2PDF] Error stack:', e.stack);
|
console.error(
|
||||||
|
'[Word2PDF] Error stack:',
|
||||||
|
e instanceof Error ? e.stack : ''
|
||||||
|
);
|
||||||
hideLoader();
|
hideLoader();
|
||||||
showAlert(
|
showAlert(
|
||||||
'Error',
|
'Error',
|
||||||
`An error occurred during conversion. Error: ${e.message}`
|
`An error occurred during conversion. Error: ${e instanceof Error ? e.message : String(e)}`
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -15,13 +15,18 @@ export async function wordToPdf() {
|
|||||||
try {
|
try {
|
||||||
const mammothOptions = {
|
const mammothOptions = {
|
||||||
// @ts-expect-error TS(2304) FIXME: Cannot find name 'mammoth'.
|
// @ts-expect-error TS(2304) FIXME: Cannot find name 'mammoth'.
|
||||||
convertImage: mammoth.images.inline((element: any) => {
|
convertImage: mammoth.images.inline(
|
||||||
return element.read('base64').then((imageBuffer: any) => {
|
(element: {
|
||||||
return {
|
read: (encoding: string) => Promise<string>;
|
||||||
src: `data:${element.contentType};base64,${imageBuffer}`,
|
contentType: string;
|
||||||
};
|
}) => {
|
||||||
});
|
return element.read('base64').then((imageBuffer: string) => {
|
||||||
}),
|
return {
|
||||||
|
src: `data:${element.contentType};base64,${imageBuffer}`,
|
||||||
|
};
|
||||||
|
});
|
||||||
|
}
|
||||||
|
),
|
||||||
};
|
};
|
||||||
const arrayBuffer = await readFileAsArrayBuffer(file);
|
const arrayBuffer = await readFileAsArrayBuffer(file);
|
||||||
// @ts-expect-error TS(2304) FIXME: Cannot find name 'mammoth'.
|
// @ts-expect-error TS(2304) FIXME: Cannot find name 'mammoth'.
|
||||||
@@ -77,7 +82,18 @@ export async function wordToPdf() {
|
|||||||
});
|
});
|
||||||
|
|
||||||
await doc.html(previewContent, {
|
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 links = previewContent.querySelectorAll('a');
|
||||||
const pageHeight = doc.internal.pageSize.getHeight();
|
const pageHeight = doc.internal.pageSize.getHeight();
|
||||||
const containerRect = previewContent.getBoundingClientRect(); // Get container's position
|
const containerRect = previewContent.getBoundingClientRect(); // Get container's position
|
||||||
|
|||||||
@@ -150,12 +150,12 @@ document.addEventListener('DOMContentLoaded', () => {
|
|||||||
() => resetState()
|
() => resetState()
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
} catch (e: any) {
|
} catch (e: unknown) {
|
||||||
hideLoader();
|
hideLoader();
|
||||||
console.error(`[${FILETYPE_NAME}ToPDF] Error:`, e);
|
console.error(`[${FILETYPE_NAME}ToPDF] Error:`, e);
|
||||||
showAlert(
|
showAlert(
|
||||||
'Error',
|
'Error',
|
||||||
`An error occurred during conversion. Error: ${e.message}`
|
`An error occurred during conversion. Error: ${e instanceof Error ? e.message : String(e)}`
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -150,12 +150,12 @@ document.addEventListener('DOMContentLoaded', () => {
|
|||||||
() => resetState()
|
() => resetState()
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
} catch (e: any) {
|
} catch (e: unknown) {
|
||||||
hideLoader();
|
hideLoader();
|
||||||
console.error(`[${FILETYPE_NAME}ToPDF] Error:`, e);
|
console.error(`[${FILETYPE_NAME}ToPDF] Error:`, e);
|
||||||
showAlert(
|
showAlert(
|
||||||
'Error',
|
'Error',
|
||||||
`An error occurred during conversion. Error: ${e.message}`
|
`An error occurred during conversion. Error: ${e instanceof Error ? e.message : String(e)}`
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -2,9 +2,7 @@ import { showLoader, hideLoader, showAlert } from '../ui.js';
|
|||||||
import { downloadFile, formatBytes } from '../utils/helpers.js';
|
import { downloadFile, formatBytes } from '../utils/helpers.js';
|
||||||
import { state } from '../state.js';
|
import { state } from '../state.js';
|
||||||
import { createIcons, icons } from 'lucide';
|
import { createIcons, icons } from 'lucide';
|
||||||
import { isWasmAvailable, getWasmBaseUrl } from '../config/wasm-cdn-config.js';
|
import { loadPyMuPDF } from '../utils/pymupdf-loader.js';
|
||||||
import { showWasmRequiredDialog } from '../utils/wasm-provider.js';
|
|
||||||
import { loadPyMuPDF, isPyMuPDFAvailable } from '../utils/pymupdf-loader.js';
|
|
||||||
import { deduplicateFileName } from '../utils/deduplicate-filename.js';
|
import { deduplicateFileName } from '../utils/deduplicate-filename.js';
|
||||||
|
|
||||||
const FILETYPE = 'xps';
|
const FILETYPE = 'xps';
|
||||||
@@ -147,12 +145,12 @@ document.addEventListener('DOMContentLoaded', () => {
|
|||||||
() => resetState()
|
() => resetState()
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
} catch (e: any) {
|
} catch (e: unknown) {
|
||||||
console.error(`[${TOOL_NAME}2PDF] ERROR:`, e);
|
console.error(`[${TOOL_NAME}2PDF] ERROR:`, e);
|
||||||
hideLoader();
|
hideLoader();
|
||||||
showAlert(
|
showAlert(
|
||||||
'Error',
|
'Error',
|
||||||
`An error occurred during conversion. Error: ${e.message}`
|
`An error occurred during conversion. Error: ${e instanceof Error ? e.message : String(e)}`
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -6,13 +6,11 @@ import '@phosphor-icons/web/regular';
|
|||||||
import * as pdfjsLib from 'pdfjs-dist';
|
import * as pdfjsLib from 'pdfjs-dist';
|
||||||
import '../css/styles.css';
|
import '../css/styles.css';
|
||||||
import { formatShortcutDisplay, formatStars } from './utils/helpers.js';
|
import { formatShortcutDisplay, formatStars } from './utils/helpers.js';
|
||||||
import { APP_VERSION } from '../version.js';
|
|
||||||
import {
|
import {
|
||||||
initI18n,
|
initI18n,
|
||||||
applyTranslations,
|
applyTranslations,
|
||||||
rewriteLinks,
|
rewriteLinks,
|
||||||
injectLanguageSwitcher,
|
injectLanguageSwitcher,
|
||||||
createLanguageSwitcher,
|
|
||||||
t,
|
t,
|
||||||
} from './i18n/index.js';
|
} from './i18n/index.js';
|
||||||
import {
|
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
|
// 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.id) return tool.id;
|
||||||
if (tool.href) {
|
if (tool.href) {
|
||||||
const match = tool.href.match(/\/([^/]+)\.html$/);
|
const match = tool.href.match(/\/([^/]+)\.html$/);
|
||||||
|
|||||||
@@ -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,
|
activeTool: null,
|
||||||
files: [],
|
files: [],
|
||||||
pdfDoc: null,
|
pdfDoc: null,
|
||||||
|
|||||||
@@ -1,5 +1,7 @@
|
|||||||
|
import type { PDFDocumentProxy } from 'pdfjs-dist';
|
||||||
|
|
||||||
export interface AlternateMergeState {
|
export interface AlternateMergeState {
|
||||||
files: File[];
|
files: File[];
|
||||||
pdfBytes: Map<string, ArrayBuffer>;
|
pdfBytes: Map<string, ArrayBuffer>;
|
||||||
pdfDocs: Map<string, any>;
|
pdfDocs: Map<string, PDFDocumentProxy>;
|
||||||
}
|
}
|
||||||
7
src/js/types/canvas-editor-type.ts
Normal file
7
src/js/types/canvas-editor-type.ts
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
export interface CropBox {
|
||||||
|
x: number;
|
||||||
|
y: number;
|
||||||
|
width: number;
|
||||||
|
height: number;
|
||||||
|
scale: number | 'fit';
|
||||||
|
}
|
||||||
@@ -1,8 +1,18 @@
|
|||||||
export interface CropperState {
|
import type { PDFDocumentProxy } from 'pdfjs-dist';
|
||||||
pdfDoc: any;
|
import Cropper from 'cropperjs';
|
||||||
currentPageNum: number;
|
|
||||||
cropper: any;
|
export interface CropPercentages {
|
||||||
originalPdfBytes: ArrayBuffer | null;
|
x: number;
|
||||||
pageCrops: Record<number, any>;
|
y: number;
|
||||||
file: File | null;
|
width: number;
|
||||||
|
height: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface CropperState {
|
||||||
|
pdfDoc: PDFDocumentProxy | null;
|
||||||
|
currentPageNum: number;
|
||||||
|
cropper: Cropper | null;
|
||||||
|
originalPdfBytes: ArrayBuffer | null;
|
||||||
|
pageCrops: Record<number, CropPercentages>;
|
||||||
|
file: File | null;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,7 +1,10 @@
|
|||||||
|
import { PDFDocument as PDFLibDocument } from 'pdf-lib';
|
||||||
|
import type { PDFDocumentProxy } from 'pdfjs-dist';
|
||||||
|
|
||||||
export interface DeletePagesState {
|
export interface DeletePagesState {
|
||||||
file: File | null;
|
file: File | null;
|
||||||
pdfDoc: any;
|
pdfDoc: PDFLibDocument | null;
|
||||||
pdfJsDoc: any;
|
pdfJsDoc: PDFDocumentProxy | null;
|
||||||
totalPages: number;
|
totalPages: number;
|
||||||
pagesToDelete: Set<number>;
|
pagesToDelete: Set<number>;
|
||||||
}
|
}
|
||||||
|
|||||||
15
src/js/types/edit-pdf-type.ts
Normal file
15
src/js/types/edit-pdf-type.ts
Normal file
@@ -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<Uint8Array>;
|
||||||
|
}
|
||||||
@@ -56,3 +56,12 @@ export * from './pdf-to-tiff-type.ts';
|
|||||||
export * from './pdf-to-cbz-type.ts';
|
export * from './pdf-to-cbz-type.ts';
|
||||||
export * from './password-prompt-type.ts';
|
export * from './password-prompt-type.ts';
|
||||||
export * from './config-types.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';
|
||||||
|
|||||||
28
src/js/types/markdown-editor-type.ts
Normal file
28
src/js/types/markdown-editor-type.ts
Normal file
@@ -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;
|
||||||
@@ -1,5 +1,7 @@
|
|||||||
|
import type { PDFDocumentProxy } from 'pdfjs-dist';
|
||||||
|
|
||||||
export interface MergeState {
|
export interface MergeState {
|
||||||
files: File[];
|
files: File[];
|
||||||
pdfBytes: Map<string, ArrayBuffer>;
|
pdfBytes: Map<string, ArrayBuffer>;
|
||||||
pdfDocs: Map<string, any>;
|
pdfDocs: Map<string, PDFDocumentProxy>;
|
||||||
}
|
}
|
||||||
|
|||||||
32
src/js/types/merge-worker-type.ts
Normal file
32
src/js/types/merge-worker-type.ts
Normal file
@@ -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;
|
||||||
7
src/js/types/redact-type.ts
Normal file
7
src/js/types/redact-type.ts
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
export interface RedactionRect {
|
||||||
|
pageIndex: number;
|
||||||
|
canvasX: number;
|
||||||
|
canvasY: number;
|
||||||
|
canvasWidth: number;
|
||||||
|
canvasHeight: number;
|
||||||
|
}
|
||||||
8
src/js/types/sanitize-type.ts
Normal file
8
src/js/types/sanitize-type.ts
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
import type { PDFDict } from 'pdf-lib';
|
||||||
|
|
||||||
|
export interface PDFDocumentInternal {
|
||||||
|
getInfoDict(): PDFDict;
|
||||||
|
javaScripts?: unknown[];
|
||||||
|
embeddedFiles?: unknown[];
|
||||||
|
fonts?: unknown[];
|
||||||
|
}
|
||||||
5
src/js/types/shortcuts-type.ts
Normal file
5
src/js/types/shortcuts-type.ts
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
export interface ToolEntry {
|
||||||
|
id?: string;
|
||||||
|
href?: string;
|
||||||
|
name?: string;
|
||||||
|
}
|
||||||
5
src/js/types/ui-type.ts
Normal file
5
src/js/types/ui-type.ts
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
export interface FileInputOptions {
|
||||||
|
multiple?: boolean;
|
||||||
|
accept?: string;
|
||||||
|
showControls?: boolean;
|
||||||
|
}
|
||||||
235
src/js/types/utils-types.ts
Normal file
235
src/js/types/utils-types.ts
Normal file
@@ -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<GhostscriptDynamicInstance>;
|
||||||
|
GhostscriptWASM?: new (url: string) => GhostscriptDynamicInstance;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface GhostscriptDynamicInstance {
|
||||||
|
convertToPDFA?(pdfBuffer: ArrayBuffer, profile: string): Promise<ArrayBuffer>;
|
||||||
|
fontToOutline?(pdfBuffer: ArrayBuffer): Promise<ArrayBuffer>;
|
||||||
|
init?(): Promise<void>;
|
||||||
|
}
|
||||||
|
|
||||||
|
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<void>;
|
||||||
|
compressPdf(
|
||||||
|
file: Blob,
|
||||||
|
options: PyMuPDFCompressOptions
|
||||||
|
): Promise<{ blob: Blob; compressedSize: number; usedFallback?: boolean }>;
|
||||||
|
convertToPdf(
|
||||||
|
file: Blob | File,
|
||||||
|
ext: string | { filetype: string }
|
||||||
|
): Promise<Blob>;
|
||||||
|
extractText(file: Blob, options?: PyMuPDFExtractTextOptions): Promise<string>;
|
||||||
|
extractImages(file: Blob): Promise<Array<{ data: Uint8Array; ext: string }>>;
|
||||||
|
extractTables(file: Blob): Promise<unknown[]>;
|
||||||
|
toSvg(file: Blob, pageNum: number): Promise<string>;
|
||||||
|
renderPageToImage(file: Blob, pageNum: number, scale: number): Promise<Blob>;
|
||||||
|
getPageCount(file: Blob): Promise<number>;
|
||||||
|
rasterizePdf(
|
||||||
|
file: Blob | File,
|
||||||
|
options: PyMuPDFRasterizeOptions
|
||||||
|
): Promise<Blob>;
|
||||||
|
open(file: Blob | File, password?: string): Promise<PyMuPDFDocument>;
|
||||||
|
textToPdf(text: string, options?: PyMuPDFTextToPdfOptions): Promise<Blob>;
|
||||||
|
pdfToDocx(file: Blob | File): Promise<Blob>;
|
||||||
|
pdfToMarkdown(
|
||||||
|
file: Blob | File,
|
||||||
|
options?: { includeImages?: boolean }
|
||||||
|
): Promise<string>;
|
||||||
|
pdfToText(file: Blob | File): Promise<string>;
|
||||||
|
deskewPdf(
|
||||||
|
file: Blob,
|
||||||
|
options?: PyMuPDFDeskewOptions
|
||||||
|
): Promise<{
|
||||||
|
pdf: Blob;
|
||||||
|
result: {
|
||||||
|
totalPages: number;
|
||||||
|
correctedPages: number;
|
||||||
|
angles: number[];
|
||||||
|
corrected: boolean[];
|
||||||
|
};
|
||||||
|
}>;
|
||||||
|
imageToPdf(file: File, options?: { imageType?: string }): Promise<Blob>;
|
||||||
|
imagesToPdf(files: File[]): Promise<Blob>;
|
||||||
|
htmlToPdf(html: string, options: unknown): Promise<Blob>;
|
||||||
|
pdfToLlamaIndex(file: File): Promise<unknown>;
|
||||||
|
}
|
||||||
|
|
||||||
|
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;
|
||||||
|
}
|
||||||
37
src/js/ui.ts
37
src/js/ui.ts
@@ -1,6 +1,5 @@
|
|||||||
import { resetState } from './state.js';
|
import { resetState } from './state.js';
|
||||||
import { formatBytes, getPDFDocument } from './utils/helpers.js';
|
import { formatBytes, getPDFDocument } from './utils/helpers.js';
|
||||||
import { tesseractLanguages } from './config/tesseract-languages.js';
|
|
||||||
import {
|
import {
|
||||||
renderPagesProgressively,
|
renderPagesProgressively,
|
||||||
cleanupLazyRendering,
|
cleanupLazyRendering,
|
||||||
@@ -14,6 +13,7 @@ import {
|
|||||||
} from './utils/rotation-state.js';
|
} from './utils/rotation-state.js';
|
||||||
import * as pdfjsLib from 'pdfjs-dist';
|
import * as pdfjsLib from 'pdfjs-dist';
|
||||||
import { t } from './i18n/i18n';
|
import { t } from './i18n/i18n';
|
||||||
|
import type { FileInputOptions } from '@/types';
|
||||||
|
|
||||||
pdfjsLib.GlobalWorkerOptions.workerSrc = new URL(
|
pdfjsLib.GlobalWorkerOptions.workerSrc = new URL(
|
||||||
'pdfjs-dist/build/pdf.worker.min.mjs',
|
'pdfjs-dist/build/pdf.worker.min.mjs',
|
||||||
@@ -112,8 +112,8 @@ export const hideLoader = () => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
export const showAlert = (
|
export const showAlert = (
|
||||||
title: any,
|
title: string,
|
||||||
message: any,
|
message: string,
|
||||||
type: string = 'error',
|
type: string = 'error',
|
||||||
callback?: () => void
|
callback?: () => void
|
||||||
) => {
|
) => {
|
||||||
@@ -137,7 +137,7 @@ export const hideAlert = () => {
|
|||||||
if (dom.alertModal) dom.alertModal.classList.add('hidden');
|
if (dom.alertModal) dom.alertModal.classList.add('hidden');
|
||||||
};
|
};
|
||||||
|
|
||||||
export const switchView = (view: any) => {
|
export const switchView = (view: string) => {
|
||||||
if (view === 'grid') {
|
if (view === 'grid') {
|
||||||
dom.gridView.classList.remove('hidden');
|
dom.gridView.classList.remove('hidden');
|
||||||
dom.toolInterface.classList.add('hidden');
|
dom.toolInterface.classList.add('hidden');
|
||||||
@@ -170,11 +170,13 @@ export const switchView = (view: any) => {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const thumbnailState = {
|
const thumbnailState: {
|
||||||
|
sortableInstances: Record<string, Sortable>;
|
||||||
|
} = {
|
||||||
sortableInstances: {},
|
sortableInstances: {},
|
||||||
};
|
};
|
||||||
|
|
||||||
function initializeOrganizeSortable(containerId: any) {
|
function initializeOrganizeSortable(containerId: string) {
|
||||||
const container = document.getElementById(containerId);
|
const container = document.getElementById(containerId);
|
||||||
if (!container) return;
|
if (!container) return;
|
||||||
|
|
||||||
@@ -189,10 +191,10 @@ function initializeOrganizeSortable(containerId: any) {
|
|||||||
dragClass: 'sortable-drag',
|
dragClass: 'sortable-drag',
|
||||||
filter: '.delete-page-btn',
|
filter: '.delete-page-btn',
|
||||||
preventOnFilter: true,
|
preventOnFilter: true,
|
||||||
onStart: function (evt: any) {
|
onStart: function (evt: Sortable.SortableEvent) {
|
||||||
evt.item.style.opacity = '0.5';
|
evt.item.style.opacity = '0.5';
|
||||||
},
|
},
|
||||||
onEnd: function (evt: any) {
|
onEnd: function (evt: Sortable.SortableEvent) {
|
||||||
evt.item.style.opacity = '1';
|
evt.item.style.opacity = '1';
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
@@ -203,7 +205,10 @@ function initializeOrganizeSortable(containerId: any) {
|
|||||||
* @param {string} toolId The ID of the active tool.
|
* @param {string} toolId The ID of the active tool.
|
||||||
* @param {object} pdfDoc The loaded pdf-lib document instance.
|
* @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<Uint8Array> }
|
||||||
|
) => {
|
||||||
const containerId =
|
const containerId =
|
||||||
toolId === 'organize'
|
toolId === 'organize'
|
||||||
? 'page-organizer'
|
? 'page-organizer'
|
||||||
@@ -229,8 +234,7 @@ export const renderPageThumbnails = async (toolId: any, pdfDoc: any) => {
|
|||||||
// Function to create wrapper element for each page
|
// Function to create wrapper element for each page
|
||||||
const createWrapper = (canvas: HTMLCanvasElement, pageNumber: number) => {
|
const createWrapper = (canvas: HTMLCanvasElement, pageNumber: number) => {
|
||||||
const wrapper = document.createElement('div');
|
const wrapper = document.createElement('div');
|
||||||
// @ts-expect-error TS(2322) FIXME: Type 'number' is not assignable to type 'string'.
|
wrapper.dataset.pageIndex = String(pageNumber - 1);
|
||||||
wrapper.dataset.pageIndex = pageNumber - 1;
|
|
||||||
|
|
||||||
const imgContainer = document.createElement('div');
|
const imgContainer = document.createElement('div');
|
||||||
imgContainer.className = 'relative';
|
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 {HTMLElement} container The DOM element to render the list into.
|
||||||
* @param {File[]} files The array of file objects.
|
* @param {File[]} files The array of file objects.
|
||||||
*/
|
*/
|
||||||
export const renderFileDisplay = (container: any, files: any) => {
|
export const renderFileDisplay = (container: HTMLElement, files: File[]) => {
|
||||||
container.textContent = '';
|
container.textContent = '';
|
||||||
if (files.length > 0) {
|
if (files.length > 0) {
|
||||||
files.forEach((file: any) => {
|
files.forEach((file: File) => {
|
||||||
const fileDiv = document.createElement('div');
|
const fileDiv = document.createElement('div');
|
||||||
fileDiv.className =
|
fileDiv.className =
|
||||||
'flex items-center justify-between bg-gray-700 p-3 rounded-lg text-sm';
|
'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 = {}) => {
|
const createFileInputHTML = (options: FileInputOptions = {}) => {
|
||||||
// @ts-expect-error TS(2339) FIXME: Property 'multiple' does not exist on type '{}'.
|
|
||||||
const multiple = options.multiple ? 'multiple' : '';
|
const multiple = options.multiple ? 'multiple' : '';
|
||||||
// @ts-expect-error TS(2339) FIXME: Property 'accept' does not exist on type '{}'.
|
|
||||||
const acceptedFiles = options.accept || 'application/pdf';
|
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;
|
||||||
const showControls = options.showControls || false; // NEW: Add this parameter
|
|
||||||
|
|
||||||
return `
|
return `
|
||||||
<div id="drop-zone" class="relative flex flex-col items-center justify-center w-full h-48 border-2 border-dashed border-gray-600 rounded-xl cursor-pointer bg-gray-900 hover:bg-gray-700 transition-colors duration-300">
|
<div id="drop-zone" class="relative flex flex-col items-center justify-center w-full h-48 border-2 border-dashed border-gray-600 rounded-xl cursor-pointer bg-gray-900 hover:bg-gray-700 transition-colors duration-300">
|
||||||
|
|||||||
@@ -69,7 +69,10 @@ export async function performCondenseCompression(
|
|||||||
scrub: {
|
scrub: {
|
||||||
metadata: customSettings?.removeMetadata ?? preset.scrub.metadata,
|
metadata: customSettings?.removeMetadata ?? preset.scrub.metadata,
|
||||||
thumbnails: customSettings?.removeThumbnails ?? preset.scrub.thumbnails,
|
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,
|
subsetFonts: customSettings?.subsetFonts ?? preset.subsetFonts,
|
||||||
save: {
|
save: {
|
||||||
@@ -95,8 +98,11 @@ export async function performCondenseCompression(
|
|||||||
try {
|
try {
|
||||||
const result = await pymupdf.compressPdf(fileBlob, fallbackOptions);
|
const result = await pymupdf.compressPdf(fileBlob, fallbackOptions);
|
||||||
return { ...result, usedFallback: true };
|
return { ...result, usedFallback: true };
|
||||||
} catch (fallbackError: any) {
|
} catch (fallbackError: unknown) {
|
||||||
const msg = fallbackError?.message || String(fallbackError);
|
const msg =
|
||||||
|
fallbackError instanceof Error
|
||||||
|
? fallbackError.message
|
||||||
|
: String(fallbackError);
|
||||||
throw new Error(`PDF compression failed: ${msg}`, {
|
throw new Error(`PDF compression failed: ${msg}`, {
|
||||||
cause: fallbackError,
|
cause: fallbackError,
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import { WasmProvider } from './wasm-provider';
|
import { WasmProvider } from './wasm-provider';
|
||||||
|
import type { WindowWithCoherentPdf, CpdfInstance } from '@/types';
|
||||||
|
|
||||||
let cpdfLoaded = false;
|
let cpdfLoaded = false;
|
||||||
let cpdfLoadPromise: Promise<void> | null = null;
|
let cpdfLoadPromise: Promise<void> | null = null;
|
||||||
@@ -31,7 +32,7 @@ export async function isCpdfLoaded(): Promise<void> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
cpdfLoadPromise = new Promise((resolve, reject) => {
|
cpdfLoadPromise = new Promise((resolve, reject) => {
|
||||||
if (typeof (window as any).coherentpdf !== 'undefined') {
|
if (typeof (window as WindowWithCoherentPdf).coherentpdf !== 'undefined') {
|
||||||
cpdfLoaded = true;
|
cpdfLoaded = true;
|
||||||
resolve();
|
resolve();
|
||||||
return;
|
return;
|
||||||
@@ -53,7 +54,7 @@ export async function isCpdfLoaded(): Promise<void> {
|
|||||||
return cpdfLoadPromise;
|
return cpdfLoadPromise;
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function getCpdf(): Promise<any> {
|
export async function getCpdf(): Promise<CpdfInstance> {
|
||||||
await isCpdfLoaded();
|
await isCpdfLoaded();
|
||||||
return (window as any).coherentpdf;
|
return (window as WindowWithCoherentPdf).coherentpdf as CpdfInstance;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -27,7 +27,9 @@ export async function loadRuntimeConfig(): Promise<void> {
|
|||||||
(c): c is string => typeof c === 'string'
|
(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 {
|
export function isToolDisabled(toolId: string): boolean {
|
||||||
|
|||||||
@@ -1,7 +1,11 @@
|
|||||||
import { WasmProvider } from './wasm-provider.js';
|
import { WasmProvider } from './wasm-provider.js';
|
||||||
|
import type {
|
||||||
|
GlobalScopeWithGhostscript,
|
||||||
|
GhostscriptDynamicInstance,
|
||||||
|
} from '@/types';
|
||||||
|
|
||||||
let cachedGS: any = null;
|
let cachedGS: GhostscriptInterface | null = null;
|
||||||
let loadPromise: Promise<any> | null = null;
|
let loadPromise: Promise<GhostscriptInterface> | null = null;
|
||||||
|
|
||||||
export interface GhostscriptInterface {
|
export interface GhostscriptInterface {
|
||||||
convertToPDFA(pdfBuffer: ArrayBuffer, profile: string): Promise<ArrayBuffer>;
|
convertToPDFA(pdfBuffer: ArrayBuffer, profile: string): Promise<ArrayBuffer>;
|
||||||
@@ -32,16 +36,20 @@ export async function loadGhostscript(): Promise<GhostscriptInterface> {
|
|||||||
|
|
||||||
await loadScript(wrapperUrl);
|
await loadScript(wrapperUrl);
|
||||||
|
|
||||||
const globalScope =
|
const globalScope = (
|
||||||
typeof globalThis !== 'undefined' ? globalThis : window;
|
typeof globalThis !== 'undefined' ? globalThis : window
|
||||||
|
) as typeof globalThis & GlobalScopeWithGhostscript;
|
||||||
|
|
||||||
if (typeof (globalScope as any).loadGS === 'function') {
|
if (typeof globalScope.loadGS === 'function') {
|
||||||
cachedGS = await (globalScope as any).loadGS({
|
const instance = await globalScope.loadGS({
|
||||||
baseUrl: normalizedUrl,
|
baseUrl: normalizedUrl,
|
||||||
});
|
});
|
||||||
} else if (typeof (globalScope as any).GhostscriptWASM === 'function') {
|
cachedGS = instance as unknown as GhostscriptInterface;
|
||||||
cachedGS = new (globalScope as any).GhostscriptWASM(normalizedUrl);
|
} else if (typeof globalScope.GhostscriptWASM === 'function') {
|
||||||
await cachedGS.init?.();
|
const instance: GhostscriptDynamicInstance =
|
||||||
|
new globalScope.GhostscriptWASM(normalizedUrl);
|
||||||
|
await instance.init?.();
|
||||||
|
cachedGS = instance as unknown as GhostscriptInterface;
|
||||||
} else {
|
} else {
|
||||||
throw new Error(
|
throw new Error(
|
||||||
'Ghostscript wrapper did not expose expected interface. Expected loadGS() or GhostscriptWASM class.'
|
'Ghostscript wrapper did not expose expected interface. Expected loadGS() or GhostscriptWASM class.'
|
||||||
@@ -49,10 +57,11 @@ export async function loadGhostscript(): Promise<GhostscriptInterface> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return cachedGS;
|
return cachedGS;
|
||||||
} catch (error: any) {
|
} catch (error: unknown) {
|
||||||
loadPromise = null;
|
loadPromise = null;
|
||||||
|
const msg = error instanceof Error ? error.message : String(error);
|
||||||
throw new Error(
|
throw new Error(
|
||||||
`Failed to load Ghostscript from ${normalizedUrl}: ${error.message}`,
|
`Failed to load Ghostscript from ${normalizedUrl}: ${msg}`,
|
||||||
{ cause: error }
|
{ cause: error }
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,8 +1,10 @@
|
|||||||
import createModule from '@neslinesli93/qpdf-wasm';
|
import createModule from '@neslinesli93/qpdf-wasm';
|
||||||
|
import type { QpdfInstanceExtended } from '@/types';
|
||||||
import { showLoader, hideLoader, showAlert } from '../ui.js';
|
import { showLoader, hideLoader, showAlert } from '../ui.js';
|
||||||
import { createIcons } from 'lucide';
|
import { createIcons } from 'lucide';
|
||||||
import { state, resetState } from '../state.js';
|
import { state, resetState } from '../state.js';
|
||||||
import * as pdfjsLib from 'pdfjs-dist';
|
import * as pdfjsLib from 'pdfjs-dist';
|
||||||
|
import type { DocumentInitParameters } from 'pdfjs-dist/types/src/display/api';
|
||||||
|
|
||||||
const STANDARD_SIZES = {
|
const STANDARD_SIZES = {
|
||||||
A4: { width: 595.28, height: 841.89 },
|
A4: { width: 595.28, height: 841.89 },
|
||||||
@@ -13,7 +15,7 @@ const STANDARD_SIZES = {
|
|||||||
A5: { width: 419.53, height: 595.28 },
|
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
|
const tolerance = 1; // Allow for minor floating point variations
|
||||||
for (const [name, size] of Object.entries(STANDARD_SIZES)) {
|
for (const [name, size] of Object.entries(STANDARD_SIZES)) {
|
||||||
if (
|
if (
|
||||||
@@ -28,7 +30,7 @@ export function getStandardPageName(width: any, height: any) {
|
|||||||
return 'Custom';
|
return 'Custom';
|
||||||
}
|
}
|
||||||
|
|
||||||
export function convertPoints(points: any, unit: any) {
|
export function convertPoints(points: number, unit: string) {
|
||||||
let result: number;
|
let result: number;
|
||||||
switch (unit) {
|
switch (unit) {
|
||||||
case 'in':
|
case 'in':
|
||||||
@@ -59,7 +61,7 @@ export function hexToRgb(hex: string): { r: number; g: number; b: number } {
|
|||||||
: { r: 0, g: 0, b: 0 };
|
: { 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';
|
if (bytes === 0) return '0 Bytes';
|
||||||
const k = 1024;
|
const k = 1024;
|
||||||
const dm = decimals < 0 ? 0 : decimals;
|
const dm = decimals < 0 ? 0 : decimals;
|
||||||
@@ -79,10 +81,12 @@ export const downloadFile = (blob: Blob, filename: string): void => {
|
|||||||
URL.revokeObjectURL(url);
|
URL.revokeObjectURL(url);
|
||||||
};
|
};
|
||||||
|
|
||||||
export const readFileAsArrayBuffer = (file: any) => {
|
export const readFileAsArrayBuffer = (
|
||||||
|
file: Blob
|
||||||
|
): Promise<ArrayBuffer | null> => {
|
||||||
return new Promise((resolve, reject) => {
|
return new Promise((resolve, reject) => {
|
||||||
const reader = new FileReader();
|
const reader = new FileReader();
|
||||||
reader.onload = () => resolve(reader.result);
|
reader.onload = () => resolve(reader.result as ArrayBuffer | null);
|
||||||
reader.onerror = (error) => reject(error);
|
reader.onerror = (error) => reject(error);
|
||||||
reader.readAsArrayBuffer(file);
|
reader.readAsArrayBuffer(file);
|
||||||
});
|
});
|
||||||
@@ -139,7 +143,7 @@ export function parsePageRanges(
|
|||||||
* @param {string} isoDateString - The ISO 8601 date string.
|
* @param {string} isoDateString - The ISO 8601 date string.
|
||||||
* @returns {string} A localized date and time string, or the original string if parsing fails.
|
* @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') {
|
if (!isoDateString || typeof isoDateString !== 'string') {
|
||||||
return isoDateString; // Return original value if it's not a valid 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.
|
* Initialize qpdf-wasm singleton.
|
||||||
* Subsequent calls return the same instance.
|
* Subsequent calls return the same instance.
|
||||||
*/
|
*/
|
||||||
export async function initializeQpdf() {
|
export async function initializeQpdf(): Promise<QpdfInstanceExtended> {
|
||||||
if (qpdfInstance) return qpdfInstance;
|
if (qpdfInstance) return qpdfInstance;
|
||||||
|
|
||||||
showLoader('Initializing PDF engine...');
|
showLoader('Initializing PDF engine...');
|
||||||
try {
|
try {
|
||||||
qpdfInstance = await createModule({
|
qpdfInstance = (await createModule({
|
||||||
locateFile: () => import.meta.env.BASE_URL + 'qpdf.wasm',
|
locateFile: () => import.meta.env.BASE_URL + 'qpdf.wasm',
|
||||||
});
|
})) as unknown as QpdfInstanceExtended;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Failed to initialize qpdf-wasm:', error);
|
console.error('Failed to initialize qpdf-wasm:', error);
|
||||||
showAlert(
|
showAlert(
|
||||||
@@ -267,24 +271,19 @@ export function resetAndReloadTool(preResetCallback?: () => void) {
|
|||||||
* @param src The source to load (url string, typed array, or parameters object)
|
* @param src The source to load (url string, typed array, or parameters object)
|
||||||
* @returns The PDF loading task
|
* @returns The PDF loading task
|
||||||
*/
|
*/
|
||||||
export function getPDFDocument(src: any) {
|
export function getPDFDocument(
|
||||||
let params = src;
|
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') {
|
if (typeof src === 'string') {
|
||||||
params = { url: src };
|
params = { url: src };
|
||||||
} else if (src instanceof Uint8Array || src instanceof ArrayBuffer) {
|
} else if (src instanceof Uint8Array || src instanceof ArrayBuffer) {
|
||||||
params = { data: src };
|
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({
|
return pdfjsLib.getDocument({
|
||||||
...params,
|
...params,
|
||||||
wasmUrl: import.meta.env.BASE_URL + 'pdfjs-viewer/wasm/',
|
wasmUrl: import.meta.env.BASE_URL + 'pdfjs-viewer/wasm/',
|
||||||
@@ -413,7 +412,7 @@ export function formatRawDate(raw: string): string {
|
|||||||
year,
|
year,
|
||||||
hoursStr,
|
hoursStr,
|
||||||
minsStr,
|
minsStr,
|
||||||
secsStr,
|
_secsStr,
|
||||||
timezone,
|
timezone,
|
||||||
] = match;
|
] = match;
|
||||||
|
|
||||||
@@ -455,8 +454,8 @@ export function formatRawDate(raw: string): string {
|
|||||||
|
|
||||||
return `${fullDay}, ${fullMonth} ${dom}, ${year} at ${hours}:${minsStr} ${ampm} (${formattedTz})`;
|
return `${fullDay}, ${fullMonth} ${dom}, ${year} at ${hours}:${minsStr} ${ampm} (${formattedTz})`;
|
||||||
}
|
}
|
||||||
} catch (e) {
|
} catch {
|
||||||
// Fallback to raw string if parsing fails
|
console.error('Error parsing date string:', raw);
|
||||||
}
|
}
|
||||||
return raw;
|
return raw;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,13 +6,14 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
import { WorkerBrowserConverter } from '@matbee/libreoffice-converter/browser';
|
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/';
|
const LIBREOFFICE_LOCAL_PATH = import.meta.env.BASE_URL + 'libreoffice-wasm/';
|
||||||
|
|
||||||
export interface LoadProgress {
|
export interface LoadProgress {
|
||||||
phase: 'loading' | 'initializing' | 'converting' | 'complete' | 'ready';
|
phase: 'loading' | 'initializing' | 'converting' | 'complete' | 'ready';
|
||||||
percent: number;
|
percent: number;
|
||||||
message: string;
|
message: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export type ProgressCallback = (progress: LoadProgress) => void;
|
export type ProgressCallback = (progress: LoadProgress) => void;
|
||||||
@@ -21,140 +22,161 @@ export type ProgressCallback = (progress: LoadProgress) => void;
|
|||||||
let converterInstance: LibreOfficeConverter | null = null;
|
let converterInstance: LibreOfficeConverter | null = null;
|
||||||
|
|
||||||
export class LibreOfficeConverter {
|
export class LibreOfficeConverter {
|
||||||
private converter: WorkerBrowserConverter | null = null;
|
private converter: WorkerBrowserConverter | null = null;
|
||||||
private initialized = false;
|
private initialized = false;
|
||||||
private initializing = false;
|
private initializing = false;
|
||||||
private basePath: string;
|
private basePath: string;
|
||||||
|
|
||||||
constructor(basePath?: string) {
|
constructor(basePath?: string) {
|
||||||
this.basePath = basePath || LIBREOFFICE_LOCAL_PATH;
|
this.basePath = basePath || LIBREOFFICE_LOCAL_PATH;
|
||||||
|
}
|
||||||
|
|
||||||
|
async initialize(onProgress?: ProgressCallback): Promise<void> {
|
||||||
|
if (this.initialized) return;
|
||||||
|
|
||||||
|
if (this.initializing) {
|
||||||
|
while (this.initializing) {
|
||||||
|
await new Promise((r) => setTimeout(r, 100));
|
||||||
|
}
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
async initialize(onProgress?: ProgressCallback): Promise<void> {
|
this.initializing = true;
|
||||||
if (this.initialized) return;
|
let progressCallback = onProgress; // Store original callback
|
||||||
|
|
||||||
if (this.initializing) {
|
try {
|
||||||
while (this.initializing) {
|
progressCallback?.({
|
||||||
await new Promise(r => setTimeout(r, 100));
|
phase: 'loading',
|
||||||
}
|
percent: 0,
|
||||||
return;
|
message: 'Loading conversion engine...',
|
||||||
}
|
});
|
||||||
|
|
||||||
|
this.converter = new WorkerBrowserConverter({
|
||||||
this.initializing = true;
|
sofficeJs: `${this.basePath}soffice.js`,
|
||||||
let progressCallback = onProgress; // Store original callback
|
sofficeWasm: `${this.basePath}soffice.wasm.gz`,
|
||||||
|
sofficeData: `${this.basePath}soffice.data.gz`,
|
||||||
try {
|
sofficeWorkerJs: `${this.basePath}soffice.worker.js`,
|
||||||
progressCallback?.({ phase: 'loading', percent: 0, message: 'Loading conversion engine...' });
|
browserWorkerJs: `${this.basePath}browser.worker.global.js`,
|
||||||
|
verbose: false,
|
||||||
this.converter = new WorkerBrowserConverter({
|
onProgress: (info: {
|
||||||
sofficeJs: `${this.basePath}soffice.js`,
|
phase: string;
|
||||||
sofficeWasm: `${this.basePath}soffice.wasm.gz`,
|
percent: number;
|
||||||
sofficeData: `${this.basePath}soffice.data.gz`,
|
message: string;
|
||||||
sofficeWorkerJs: `${this.basePath}soffice.worker.js`,
|
}) => {
|
||||||
browserWorkerJs: `${this.basePath}browser.worker.global.js`,
|
if (progressCallback && !this.initialized) {
|
||||||
verbose: false,
|
const simplifiedMessage = `Loading conversion engine (${Math.round(info.percent)}%)...`;
|
||||||
onProgress: (info: { phase: string; percent: number; message: string }) => {
|
progressCallback({
|
||||||
if (progressCallback && !this.initialized) {
|
phase: info.phase as LoadProgress['phase'],
|
||||||
const simplifiedMessage = `Loading conversion engine (${Math.round(info.percent)}%)...`;
|
percent: info.percent,
|
||||||
progressCallback({
|
message: simplifiedMessage,
|
||||||
phase: info.phase as LoadProgress['phase'],
|
|
||||||
percent: info.percent,
|
|
||||||
message: simplifiedMessage
|
|
||||||
});
|
|
||||||
}
|
|
||||||
},
|
|
||||||
onReady: () => {
|
|
||||||
console.log('[LibreOffice] Ready!');
|
|
||||||
},
|
|
||||||
onError: (error: Error) => {
|
|
||||||
console.error('[LibreOffice] Error:', error);
|
|
||||||
},
|
|
||||||
});
|
});
|
||||||
|
}
|
||||||
|
},
|
||||||
|
onReady: () => {
|
||||||
|
console.log('[LibreOffice] Ready!');
|
||||||
|
},
|
||||||
|
onError: (error: Error) => {
|
||||||
|
console.error('[LibreOffice] Error:', error);
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
await this.converter.initialize();
|
await this.converter.initialize();
|
||||||
this.initialized = true;
|
this.initialized = true;
|
||||||
|
|
||||||
// Call completion message
|
// Call completion message
|
||||||
progressCallback?.({ phase: 'ready', percent: 100, message: 'Conversion engine ready!' });
|
progressCallback?.({
|
||||||
|
phase: 'ready',
|
||||||
|
percent: 100,
|
||||||
|
message: 'Conversion engine ready!',
|
||||||
|
});
|
||||||
|
|
||||||
// Null out the callback to prevent any late-firing progress updates
|
// Null out the callback to prevent any late-firing progress updates
|
||||||
progressCallback = undefined;
|
progressCallback = undefined;
|
||||||
} finally {
|
} finally {
|
||||||
this.initializing = false;
|
this.initializing = false;
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
isReady(): boolean {
|
||||||
|
return this.initialized && this.converter !== null;
|
||||||
|
}
|
||||||
|
|
||||||
|
async convertToPdf(file: File): Promise<Blob> {
|
||||||
|
if (!this.converter) {
|
||||||
|
throw new Error('Converter not initialized');
|
||||||
}
|
}
|
||||||
|
|
||||||
isReady(): boolean {
|
console.log(`[LibreOffice] Converting ${file.name} to PDF...`);
|
||||||
return this.initialized && this.converter !== null;
|
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<Blob> {
|
async wordToPdf(file: File): Promise<Blob> {
|
||||||
if (!this.converter) {
|
return this.convertToPdf(file);
|
||||||
throw new Error('Converter not initialized');
|
}
|
||||||
}
|
|
||||||
|
|
||||||
console.log(`[LibreOffice] Converting ${file.name} to PDF...`);
|
async pptToPdf(file: File): Promise<Blob> {
|
||||||
console.log(`[LibreOffice] File type: ${file.type}, Size: ${file.size} bytes`);
|
return this.convertToPdf(file);
|
||||||
|
}
|
||||||
|
|
||||||
try {
|
async excelToPdf(file: File): Promise<Blob> {
|
||||||
console.log(`[LibreOffice] Reading file as ArrayBuffer...`);
|
return this.convertToPdf(file);
|
||||||
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...`);
|
async destroy(): Promise<void> {
|
||||||
const startTime = Date.now();
|
if (this.converter) {
|
||||||
|
await this.converter.destroy();
|
||||||
// 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<Blob> {
|
|
||||||
return this.convertToPdf(file);
|
|
||||||
}
|
|
||||||
|
|
||||||
async pptToPdf(file: File): Promise<Blob> {
|
|
||||||
return this.convertToPdf(file);
|
|
||||||
}
|
|
||||||
|
|
||||||
async excelToPdf(file: File): Promise<Blob> {
|
|
||||||
return this.convertToPdf(file);
|
|
||||||
}
|
|
||||||
|
|
||||||
async destroy(): Promise<void> {
|
|
||||||
if (this.converter) {
|
|
||||||
await this.converter.destroy();
|
|
||||||
}
|
|
||||||
this.converter = null;
|
|
||||||
this.initialized = false;
|
|
||||||
}
|
}
|
||||||
|
this.converter = null;
|
||||||
|
this.initialized = false;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export function getLibreOfficeConverter(basePath?: string): LibreOfficeConverter {
|
export function getLibreOfficeConverter(
|
||||||
if (!converterInstance) {
|
basePath?: string
|
||||||
converterInstance = new LibreOfficeConverter(basePath);
|
): LibreOfficeConverter {
|
||||||
}
|
if (!converterInstance) {
|
||||||
return converterInstance;
|
converterInstance = new LibreOfficeConverter(basePath);
|
||||||
|
}
|
||||||
|
return converterInstance;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -29,8 +29,14 @@ import taskLists from 'markdown-it-task-lists';
|
|||||||
import anchor from 'markdown-it-anchor';
|
import anchor from 'markdown-it-anchor';
|
||||||
import tocDoneRight from 'markdown-it-toc-done-right';
|
import tocDoneRight from 'markdown-it-toc-done-right';
|
||||||
import { applyTranslations } from '../i18n/i18n';
|
import { applyTranslations } from '../i18n/i18n';
|
||||||
|
import type {
|
||||||
|
WindowWithLucide,
|
||||||
|
WindowWithI18next,
|
||||||
|
MarkdownItOptions,
|
||||||
|
MarkdownItToken,
|
||||||
|
MarkdownItRendererSelf,
|
||||||
|
MarkdownItRenderRule,
|
||||||
|
} from '@/types';
|
||||||
|
|
||||||
// Register highlight.js languages
|
// Register highlight.js languages
|
||||||
hljs.registerLanguage('javascript', javascript);
|
hljs.registerLanguage('javascript', javascript);
|
||||||
@@ -66,18 +72,7 @@ export interface MarkdownEditorOptions {
|
|||||||
onBack?: () => void;
|
onBack?: () => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface MarkdownItOptions {
|
export type { MarkdownItOptions } from '@/types';
|
||||||
/** Enable HTML tags in source */
|
|
||||||
html: boolean;
|
|
||||||
/** Convert '\n' in paragraphs into <br> */
|
|
||||||
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;
|
|
||||||
}
|
|
||||||
|
|
||||||
const DEFAULT_MARKDOWN = `# Welcome to BentoPDF Markdown Editor
|
const DEFAULT_MARKDOWN = `# Welcome to BentoPDF Markdown Editor
|
||||||
|
|
||||||
@@ -277,7 +272,7 @@ export class MarkdownEditor {
|
|||||||
html: true,
|
html: true,
|
||||||
breaks: false,
|
breaks: false,
|
||||||
linkify: true,
|
linkify: true,
|
||||||
typographer: true
|
typographer: true,
|
||||||
};
|
};
|
||||||
|
|
||||||
constructor(container: HTMLElement, options: MarkdownEditorOptions) {
|
constructor(container: HTMLElement, options: MarkdownEditorOptions) {
|
||||||
@@ -303,22 +298,36 @@ export class MarkdownEditor {
|
|||||||
startOnLoad: false,
|
startOnLoad: false,
|
||||||
theme: 'default',
|
theme: 'default',
|
||||||
securityLevel: 'loose',
|
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;
|
this.mermaidInitialized = true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private configureLinkRenderer(): void {
|
private configureLinkRenderer(): void {
|
||||||
// Override link renderer to add target="_blank" and rel="noopener"
|
const existingRule = this.md.renderer.rules.link_open;
|
||||||
const defaultRender = this.md.renderer.rules.link_open ||
|
const defaultRender: MarkdownItRenderRule = existingRule
|
||||||
((tokens: any[], idx: number, options: any, _env: any, self: any) => self.renderToken(tokens, idx, options));
|
? (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) => {
|
this.md.renderer.rules.link_open = (tokens, idx, options, env, self) => {
|
||||||
const token = tokens[idx];
|
const token = tokens[idx] as unknown as MarkdownItToken;
|
||||||
token.attrSet('target', '_blank');
|
token.attrSet('target', '_blank');
|
||||||
token.attrSet('rel', 'noopener noreferrer');
|
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();
|
this.applyI18n();
|
||||||
|
|
||||||
// Initialize Lucide icons
|
// Initialize Lucide icons
|
||||||
if (typeof (window as any).lucide !== 'undefined') {
|
if (typeof (window as WindowWithLucide).lucide !== 'undefined') {
|
||||||
(window as any).lucide.createIcons();
|
(window as WindowWithLucide).lucide?.createIcons();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
private setupEventListeners(): void {
|
private setupEventListeners(): void {
|
||||||
// Editor input
|
// Editor input
|
||||||
this.editor?.addEventListener('input', () => {
|
this.editor?.addEventListener('input', () => {
|
||||||
@@ -441,9 +449,13 @@ export class MarkdownEditor {
|
|||||||
this.editor?.addEventListener('scroll', () => {
|
this.editor?.addEventListener('scroll', () => {
|
||||||
if (this.syncScroll && !this.isSyncing && this.editor && this.preview) {
|
if (this.syncScroll && !this.isSyncing && this.editor && this.preview) {
|
||||||
this.isSyncing = true;
|
this.isSyncing = true;
|
||||||
const scrollPercentage = this.editor.scrollTop / (this.editor.scrollHeight - this.editor.clientHeight);
|
const scrollPercentage =
|
||||||
this.preview.scrollTop = scrollPercentage * (this.preview.scrollHeight - this.preview.clientHeight);
|
this.editor.scrollTop /
|
||||||
setTimeout(() => this.isSyncing = false, 10);
|
(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', () => {
|
this.preview?.addEventListener('scroll', () => {
|
||||||
if (this.syncScroll && !this.isSyncing && this.editor && this.preview) {
|
if (this.syncScroll && !this.isSyncing && this.editor && this.preview) {
|
||||||
this.isSyncing = true;
|
this.isSyncing = true;
|
||||||
const scrollPercentage = this.preview.scrollTop / (this.preview.scrollHeight - this.preview.clientHeight);
|
const scrollPercentage =
|
||||||
this.editor.scrollTop = scrollPercentage * (this.editor.scrollHeight - this.editor.clientHeight);
|
this.preview.scrollTop /
|
||||||
setTimeout(() => this.isSyncing = false, 10);
|
(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
|
// Settings modal close
|
||||||
document.getElementById('mdCloseSettings')?.addEventListener('click', () => {
|
document
|
||||||
const modal = document.getElementById('mdSettingsModal');
|
.getElementById('mdCloseSettings')
|
||||||
if (modal) {
|
?.addEventListener('click', () => {
|
||||||
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');
|
const modal = document.getElementById('mdSettingsModal');
|
||||||
if (modal) {
|
if (modal) {
|
||||||
modal.style.display = 'none';
|
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
|
// Settings checkboxes
|
||||||
document.getElementById('mdOptHtml')?.addEventListener('change', (e) => {
|
document.getElementById('mdOptHtml')?.addEventListener('change', (e) => {
|
||||||
@@ -507,10 +531,12 @@ export class MarkdownEditor {
|
|||||||
this.updateMarkdownIt();
|
this.updateMarkdownIt();
|
||||||
});
|
});
|
||||||
|
|
||||||
document.getElementById('mdOptTypographer')?.addEventListener('change', (e) => {
|
document
|
||||||
this.mdOptions.typographer = (e.target as HTMLInputElement).checked;
|
.getElementById('mdOptTypographer')
|
||||||
this.updateMarkdownIt();
|
?.addEventListener('change', (e) => {
|
||||||
});
|
this.mdOptions.typographer = (e.target as HTMLInputElement).checked;
|
||||||
|
this.updateMarkdownIt();
|
||||||
|
});
|
||||||
|
|
||||||
// Preset selector
|
// Preset selector
|
||||||
document.getElementById('mdPreset')?.addEventListener('change', (e) => {
|
document.getElementById('mdPreset')?.addEventListener('change', (e) => {
|
||||||
@@ -549,7 +575,8 @@ export class MarkdownEditor {
|
|||||||
const start = this.editor!.selectionStart;
|
const start = this.editor!.selectionStart;
|
||||||
const end = this.editor!.selectionEnd;
|
const end = this.editor!.selectionEnd;
|
||||||
const value = this.editor!.value;
|
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.editor!.selectionStart = this.editor!.selectionEnd = start + 2;
|
||||||
this.updatePreview();
|
this.updatePreview();
|
||||||
}
|
}
|
||||||
@@ -563,18 +590,37 @@ export class MarkdownEditor {
|
|||||||
|
|
||||||
// Update options based on preset
|
// Update options based on preset
|
||||||
if (preset === 'commonmark') {
|
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') {
|
} 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 {
|
} else {
|
||||||
this.mdOptions = { html: true, breaks: false, linkify: true, typographer: true };
|
this.mdOptions = {
|
||||||
|
html: true,
|
||||||
|
breaks: false,
|
||||||
|
linkify: true,
|
||||||
|
typographer: true,
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
// Update UI checkboxes
|
// Update UI checkboxes
|
||||||
(document.getElementById('mdOptHtml') as HTMLInputElement).checked = this.mdOptions.html;
|
(document.getElementById('mdOptHtml') as HTMLInputElement).checked =
|
||||||
(document.getElementById('mdOptBreaks') as HTMLInputElement).checked = this.mdOptions.breaks;
|
this.mdOptions.html;
|
||||||
(document.getElementById('mdOptLinkify') as HTMLInputElement).checked = this.mdOptions.linkify;
|
(document.getElementById('mdOptBreaks') as HTMLInputElement).checked =
|
||||||
(document.getElementById('mdOptTypographer') as HTMLInputElement).checked = this.mdOptions.typographer;
|
this.mdOptions.breaks;
|
||||||
|
(document.getElementById('mdOptLinkify') as HTMLInputElement).checked =
|
||||||
|
this.mdOptions.linkify;
|
||||||
|
(document.getElementById('mdOptTypographer') as HTMLInputElement).checked =
|
||||||
|
this.mdOptions.typographer;
|
||||||
|
|
||||||
this.updateMarkdownIt();
|
this.updateMarkdownIt();
|
||||||
}
|
}
|
||||||
@@ -588,7 +634,6 @@ export class MarkdownEditor {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
private createMarkdownIt(): MarkdownIt {
|
private createMarkdownIt(): MarkdownIt {
|
||||||
// Use preset if commonmark or zero
|
// Use preset if commonmark or zero
|
||||||
let md: MarkdownIt;
|
let md: MarkdownIt;
|
||||||
@@ -604,28 +649,31 @@ export class MarkdownEditor {
|
|||||||
highlight: (str: string, lang: string) => {
|
highlight: (str: string, lang: string) => {
|
||||||
if (lang && hljs.getLanguage(lang)) {
|
if (lang && hljs.getLanguage(lang)) {
|
||||||
try {
|
try {
|
||||||
return hljs.highlight(str, { language: lang, ignoreIllegals: true }).value;
|
return hljs.highlight(str, {
|
||||||
|
language: lang,
|
||||||
|
ignoreIllegals: true,
|
||||||
|
}).value;
|
||||||
} catch {
|
} catch {
|
||||||
// Fall through to default
|
// Fall through to default
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return ''; // Use external default escaping
|
return ''; // Use external default escaping
|
||||||
}
|
},
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// Apply plugins only for default preset (plugins may not work well with commonmark/zero)
|
// Apply plugins only for default preset (plugins may not work well with commonmark/zero)
|
||||||
if (this.currentPreset === 'default') {
|
if (this.currentPreset === 'default') {
|
||||||
md.use(sub) // Subscript: ~text~ -> <sub>text</sub>
|
md.use(sub) // Subscript: ~text~ -> <sub>text</sub>
|
||||||
.use(sup) // Superscript: ^text^ -> <sup>text</sup>
|
.use(sup) // Superscript: ^text^ -> <sup>text</sup>
|
||||||
.use(footnote) // Footnotes: [^1] and [^1]: footnote text
|
.use(footnote) // Footnotes: [^1] and [^1]: footnote text
|
||||||
.use(deflist) // Definition lists
|
.use(deflist) // Definition lists
|
||||||
.use(abbr) // Abbreviations: *[abbr]: full text
|
.use(abbr) // Abbreviations: *[abbr]: full text
|
||||||
.use(emoji) // Emoji: :smile: -> 😄
|
.use(emoji) // Emoji: :smile: -> 😄
|
||||||
.use(ins) // Inserted text: ++text++ -> <ins>text</ins>
|
.use(ins) // Inserted text: ++text++ -> <ins>text</ins>
|
||||||
.use(mark) // Marked text: ==text== -> <mark>text</mark>
|
.use(mark) // Marked text: ==text== -> <mark>text</mark>
|
||||||
.use(taskLists, { enabled: true, label: true, labelAfter: true }) // Task lists: - [x] done
|
.use(taskLists, { enabled: true, label: true, labelAfter: true }) // Task lists: - [x] done
|
||||||
.use(anchor, { permalink: false }) // Header anchors
|
.use(anchor, { permalink: false }) // Header anchors
|
||||||
.use(tocDoneRight); // Table of contents: ${toc}
|
.use(tocDoneRight); // Table of contents: ${toc}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -650,7 +698,9 @@ export class MarkdownEditor {
|
|||||||
private async renderMermaidDiagrams(): Promise<void> {
|
private async renderMermaidDiagrams(): Promise<void> {
|
||||||
if (!this.preview) return;
|
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++) {
|
for (let i = 0; i < mermaidBlocks.length; i++) {
|
||||||
const block = mermaidBlocks[i] as HTMLElement;
|
const block = mermaidBlocks[i] as HTMLElement;
|
||||||
@@ -948,14 +998,16 @@ ${content}
|
|||||||
applyTranslations();
|
applyTranslations();
|
||||||
|
|
||||||
// Special handling for select options (data-i18n on options doesn't work with 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) {
|
if (presetSelect) {
|
||||||
const options = presetSelect.querySelectorAll('option[data-i18n]');
|
const options = presetSelect.querySelectorAll('option[data-i18n]');
|
||||||
options.forEach((option) => {
|
options.forEach((option) => {
|
||||||
const key = option.getAttribute('data-i18n');
|
const key = option.getAttribute('data-i18n');
|
||||||
if (key) {
|
if (key) {
|
||||||
// Use i18next directly for option text
|
// 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) {
|
if (translated && translated !== key) {
|
||||||
option.textContent = translated;
|
option.textContent = translated;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -154,7 +154,7 @@ document.addEventListener('keydown', handleKeydown);
|
|||||||
export function initPagePreview(
|
export function initPagePreview(
|
||||||
container: HTMLElement,
|
container: HTMLElement,
|
||||||
pdfjsDoc: PDFDocumentProxy,
|
pdfjsDoc: PDFDocumentProxy,
|
||||||
options: { pageAttr?: string } = {}
|
_options: { pageAttr?: string } = {}
|
||||||
): void {
|
): void {
|
||||||
const totalPages = pdfjsDoc.numPages;
|
const totalPages = pdfjsDoc.numPages;
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import { getCpdf, isCpdfAvailable } from './cpdf-helper';
|
import { getCpdf, isCpdfAvailable } from './cpdf-helper';
|
||||||
import { isPyMuPDFAvailable, loadPyMuPDF } from './pymupdf-loader';
|
import { isPyMuPDFAvailable, loadPyMuPDF } from './pymupdf-loader';
|
||||||
|
import type { CpdfInstance } from '@/types';
|
||||||
|
|
||||||
export type PdfDecryptEngine = 'cpdf' | 'pymupdf';
|
export type PdfDecryptEngine = 'cpdf' | 'pymupdf';
|
||||||
|
|
||||||
@@ -37,13 +38,13 @@ function copyBytes(bytes: Uint8Array): Uint8Array {
|
|||||||
return Uint8Array.from(bytes);
|
return Uint8Array.from(bytes);
|
||||||
}
|
}
|
||||||
|
|
||||||
function cleanupCpdfDocument(cpdf: unknown, pdf: unknown): void {
|
function cleanupCpdfDocument(cpdf: CpdfInstance, pdf: unknown): void {
|
||||||
if (!cpdf || !pdf || typeof cpdf !== 'object' || !('deletePdf' in cpdf)) {
|
if (!cpdf || !pdf) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
(cpdf as { deletePdf: (document: unknown) => void }).deletePdf(pdf);
|
cpdf.deletePdf(pdf);
|
||||||
} catch (cleanupError) {
|
} catch (cleanupError) {
|
||||||
console.warn(
|
console.warn(
|
||||||
`${DECRYPT_LOG_PREFIX} Failed to cleanup CoherentPDF document: ${normalizeErrorMessage(cleanupError)}`
|
`${DECRYPT_LOG_PREFIX} Failed to cleanup CoherentPDF document: ${normalizeErrorMessage(cleanupError)}`
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user