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:
alam00000
2026-03-31 17:59:49 +05:30
parent a1fc2fc3c6
commit 9d0b68e18c
114 changed files with 2577 additions and 1868 deletions
+9 -3
View File
@@ -69,7 +69,10 @@ export async function performCondenseCompression(
scrub: {
metadata: customSettings?.removeMetadata ?? preset.scrub.metadata,
thumbnails: customSettings?.removeThumbnails ?? preset.scrub.thumbnails,
xmlMetadata: (preset.scrub as any).xmlMetadata ?? false,
xmlMetadata:
('xmlMetadata' in preset.scrub
? (preset.scrub as { xmlMetadata?: boolean }).xmlMetadata
: undefined) ?? false,
},
subsetFonts: customSettings?.subsetFonts ?? preset.subsetFonts,
save: {
@@ -95,8 +98,11 @@ export async function performCondenseCompression(
try {
const result = await pymupdf.compressPdf(fileBlob, fallbackOptions);
return { ...result, usedFallback: true };
} catch (fallbackError: any) {
const msg = fallbackError?.message || String(fallbackError);
} catch (fallbackError: unknown) {
const msg =
fallbackError instanceof Error
? fallbackError.message
: String(fallbackError);
throw new Error(`PDF compression failed: ${msg}`, {
cause: fallbackError,
});
+4 -3
View File
@@ -1,4 +1,5 @@
import { WasmProvider } from './wasm-provider';
import type { WindowWithCoherentPdf, CpdfInstance } from '@/types';
let cpdfLoaded = false;
let cpdfLoadPromise: Promise<void> | null = null;
@@ -31,7 +32,7 @@ export async function isCpdfLoaded(): Promise<void> {
}
cpdfLoadPromise = new Promise((resolve, reject) => {
if (typeof (window as any).coherentpdf !== 'undefined') {
if (typeof (window as WindowWithCoherentPdf).coherentpdf !== 'undefined') {
cpdfLoaded = true;
resolve();
return;
@@ -53,7 +54,7 @@ export async function isCpdfLoaded(): Promise<void> {
return cpdfLoadPromise;
}
export async function getCpdf(): Promise<any> {
export async function getCpdf(): Promise<CpdfInstance> {
await isCpdfLoaded();
return (window as any).coherentpdf;
return (window as WindowWithCoherentPdf).coherentpdf as CpdfInstance;
}
+3 -1
View File
@@ -27,7 +27,9 @@ export async function loadRuntimeConfig(): Promise<void> {
(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 {
+20 -11
View File
@@ -1,7 +1,11 @@
import { WasmProvider } from './wasm-provider.js';
import type {
GlobalScopeWithGhostscript,
GhostscriptDynamicInstance,
} from '@/types';
let cachedGS: any = null;
let loadPromise: Promise<any> | null = null;
let cachedGS: GhostscriptInterface | null = null;
let loadPromise: Promise<GhostscriptInterface> | null = null;
export interface GhostscriptInterface {
convertToPDFA(pdfBuffer: ArrayBuffer, profile: string): Promise<ArrayBuffer>;
@@ -32,16 +36,20 @@ export async function loadGhostscript(): Promise<GhostscriptInterface> {
await loadScript(wrapperUrl);
const globalScope =
typeof globalThis !== 'undefined' ? globalThis : window;
const globalScope = (
typeof globalThis !== 'undefined' ? globalThis : window
) as typeof globalThis & GlobalScopeWithGhostscript;
if (typeof (globalScope as any).loadGS === 'function') {
cachedGS = await (globalScope as any).loadGS({
if (typeof globalScope.loadGS === 'function') {
const instance = await globalScope.loadGS({
baseUrl: normalizedUrl,
});
} else if (typeof (globalScope as any).GhostscriptWASM === 'function') {
cachedGS = new (globalScope as any).GhostscriptWASM(normalizedUrl);
await cachedGS.init?.();
cachedGS = instance as unknown as GhostscriptInterface;
} else if (typeof globalScope.GhostscriptWASM === 'function') {
const instance: GhostscriptDynamicInstance =
new globalScope.GhostscriptWASM(normalizedUrl);
await instance.init?.();
cachedGS = instance as unknown as GhostscriptInterface;
} else {
throw new Error(
'Ghostscript wrapper did not expose expected interface. Expected loadGS() or GhostscriptWASM class.'
@@ -49,10 +57,11 @@ export async function loadGhostscript(): Promise<GhostscriptInterface> {
}
return cachedGS;
} catch (error: any) {
} catch (error: unknown) {
loadPromise = null;
const msg = error instanceof Error ? error.message : String(error);
throw new Error(
`Failed to load Ghostscript from ${normalizedUrl}: ${error.message}`,
`Failed to load Ghostscript from ${normalizedUrl}: ${msg}`,
{ cause: error }
);
}
+23 -24
View File
@@ -1,8 +1,10 @@
import createModule from '@neslinesli93/qpdf-wasm';
import type { QpdfInstanceExtended } from '@/types';
import { showLoader, hideLoader, showAlert } from '../ui.js';
import { createIcons } from 'lucide';
import { state, resetState } from '../state.js';
import * as pdfjsLib from 'pdfjs-dist';
import type { DocumentInitParameters } from 'pdfjs-dist/types/src/display/api';
const STANDARD_SIZES = {
A4: { width: 595.28, height: 841.89 },
@@ -13,7 +15,7 @@ const STANDARD_SIZES = {
A5: { width: 419.53, height: 595.28 },
};
export function getStandardPageName(width: any, height: any) {
export function getStandardPageName(width: number, height: number) {
const tolerance = 1; // Allow for minor floating point variations
for (const [name, size] of Object.entries(STANDARD_SIZES)) {
if (
@@ -28,7 +30,7 @@ export function getStandardPageName(width: any, height: any) {
return 'Custom';
}
export function convertPoints(points: any, unit: any) {
export function convertPoints(points: number, unit: string) {
let result: number;
switch (unit) {
case 'in':
@@ -59,7 +61,7 @@ export function hexToRgb(hex: string): { r: number; g: number; b: number } {
: { r: 0, g: 0, b: 0 };
}
export const formatBytes = (bytes: any, decimals = 1) => {
export const formatBytes = (bytes: number, decimals = 1) => {
if (bytes === 0) return '0 Bytes';
const k = 1024;
const dm = decimals < 0 ? 0 : decimals;
@@ -79,10 +81,12 @@ export const downloadFile = (blob: Blob, filename: string): void => {
URL.revokeObjectURL(url);
};
export const readFileAsArrayBuffer = (file: any) => {
export const readFileAsArrayBuffer = (
file: Blob
): Promise<ArrayBuffer | null> => {
return new Promise((resolve, reject) => {
const reader = new FileReader();
reader.onload = () => resolve(reader.result);
reader.onload = () => resolve(reader.result as ArrayBuffer | null);
reader.onerror = (error) => reject(error);
reader.readAsArrayBuffer(file);
});
@@ -139,7 +143,7 @@ export function parsePageRanges(
* @param {string} isoDateString - The ISO 8601 date string.
* @returns {string} A localized date and time string, or the original string if parsing fails.
*/
export function formatIsoDate(isoDateString) {
export function formatIsoDate(isoDateString: string) {
if (!isoDateString || typeof isoDateString !== 'string') {
return isoDateString; // Return original value if it's not a valid string
}
@@ -156,20 +160,20 @@ export function formatIsoDate(isoDateString) {
}
}
let qpdfInstance: any = null;
let qpdfInstance: QpdfInstanceExtended | null = null;
/**
* Initialize qpdf-wasm singleton.
* Subsequent calls return the same instance.
*/
export async function initializeQpdf() {
export async function initializeQpdf(): Promise<QpdfInstanceExtended> {
if (qpdfInstance) return qpdfInstance;
showLoader('Initializing PDF engine...');
try {
qpdfInstance = await createModule({
qpdfInstance = (await createModule({
locateFile: () => import.meta.env.BASE_URL + 'qpdf.wasm',
});
})) as unknown as QpdfInstanceExtended;
} catch (error) {
console.error('Failed to initialize qpdf-wasm:', error);
showAlert(
@@ -267,24 +271,19 @@ export function resetAndReloadTool(preResetCallback?: () => void) {
* @param src The source to load (url string, typed array, or parameters object)
* @returns The PDF loading task
*/
export function getPDFDocument(src: any) {
let params = src;
export function getPDFDocument(
src: string | Uint8Array | ArrayBuffer | DocumentInitParameters
) {
let params: DocumentInitParameters;
// Handle different input types similar to how getDocument handles them,
// but we ensure we have an object to attach wasmUrl to.
if (typeof src === 'string') {
params = { url: src };
} else if (src instanceof Uint8Array || src instanceof ArrayBuffer) {
params = { data: src };
} else {
params = src;
}
// Ensure params is an object
if (typeof params !== 'object' || params === null) {
params = {};
}
// Add wasmUrl pointing to our public/wasm directory
// This is required for PDF.js v5+ to load OpenJPEG for certain images
return pdfjsLib.getDocument({
...params,
wasmUrl: import.meta.env.BASE_URL + 'pdfjs-viewer/wasm/',
@@ -413,7 +412,7 @@ export function formatRawDate(raw: string): string {
year,
hoursStr,
minsStr,
secsStr,
_secsStr,
timezone,
] = match;
@@ -455,8 +454,8 @@ export function formatRawDate(raw: string): string {
return `${fullDay}, ${fullMonth} ${dom}, ${year} at ${hours}:${minsStr} ${ampm} (${formattedTz})`;
}
} catch (e) {
// Fallback to raw string if parsing fails
} catch {
console.error('Error parsing date string:', raw);
}
return raw;
}
+143 -121
View File
@@ -1,18 +1,19 @@
/**
* LibreOffice WASM Converter Wrapper
*
*
* Uses @matbee/libreoffice-converter package for document conversion.
* Handles progress tracking and provides simpler API.
*/
import { WorkerBrowserConverter } from '@matbee/libreoffice-converter/browser';
import type { InputFormat } from '@matbee/libreoffice-converter/browser';
const LIBREOFFICE_LOCAL_PATH = import.meta.env.BASE_URL + 'libreoffice-wasm/';
export interface LoadProgress {
phase: 'loading' | 'initializing' | 'converting' | 'complete' | 'ready';
percent: number;
message: string;
phase: 'loading' | 'initializing' | 'converting' | 'complete' | 'ready';
percent: number;
message: string;
}
export type ProgressCallback = (progress: LoadProgress) => void;
@@ -21,140 +22,161 @@ export type ProgressCallback = (progress: LoadProgress) => void;
let converterInstance: LibreOfficeConverter | null = null;
export class LibreOfficeConverter {
private converter: WorkerBrowserConverter | null = null;
private initialized = false;
private initializing = false;
private basePath: string;
private converter: WorkerBrowserConverter | null = null;
private initialized = false;
private initializing = false;
private basePath: string;
constructor(basePath?: string) {
this.basePath = basePath || LIBREOFFICE_LOCAL_PATH;
constructor(basePath?: string) {
this.basePath = basePath || LIBREOFFICE_LOCAL_PATH;
}
async initialize(onProgress?: ProgressCallback): Promise<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> {
if (this.initialized) return;
this.initializing = true;
let progressCallback = onProgress; // Store original callback
if (this.initializing) {
while (this.initializing) {
await new Promise(r => setTimeout(r, 100));
}
return;
}
try {
progressCallback?.({
phase: 'loading',
percent: 0,
message: 'Loading conversion engine...',
});
this.initializing = true;
let progressCallback = onProgress; // Store original callback
try {
progressCallback?.({ phase: 'loading', percent: 0, message: 'Loading conversion engine...' });
this.converter = new WorkerBrowserConverter({
sofficeJs: `${this.basePath}soffice.js`,
sofficeWasm: `${this.basePath}soffice.wasm.gz`,
sofficeData: `${this.basePath}soffice.data.gz`,
sofficeWorkerJs: `${this.basePath}soffice.worker.js`,
browserWorkerJs: `${this.basePath}browser.worker.global.js`,
verbose: false,
onProgress: (info: { phase: string; percent: number; message: string }) => {
if (progressCallback && !this.initialized) {
const simplifiedMessage = `Loading conversion engine (${Math.round(info.percent)}%)...`;
progressCallback({
phase: info.phase as LoadProgress['phase'],
percent: info.percent,
message: simplifiedMessage
});
}
},
onReady: () => {
console.log('[LibreOffice] Ready!');
},
onError: (error: Error) => {
console.error('[LibreOffice] Error:', error);
},
this.converter = new WorkerBrowserConverter({
sofficeJs: `${this.basePath}soffice.js`,
sofficeWasm: `${this.basePath}soffice.wasm.gz`,
sofficeData: `${this.basePath}soffice.data.gz`,
sofficeWorkerJs: `${this.basePath}soffice.worker.js`,
browserWorkerJs: `${this.basePath}browser.worker.global.js`,
verbose: false,
onProgress: (info: {
phase: string;
percent: number;
message: string;
}) => {
if (progressCallback && !this.initialized) {
const simplifiedMessage = `Loading conversion engine (${Math.round(info.percent)}%)...`;
progressCallback({
phase: info.phase as LoadProgress['phase'],
percent: info.percent,
message: simplifiedMessage,
});
}
},
onReady: () => {
console.log('[LibreOffice] Ready!');
},
onError: (error: Error) => {
console.error('[LibreOffice] Error:', error);
},
});
await this.converter.initialize();
this.initialized = true;
await this.converter.initialize();
this.initialized = true;
// Call completion message
progressCallback?.({ phase: 'ready', percent: 100, message: 'Conversion engine ready!' });
// Call completion message
progressCallback?.({
phase: 'ready',
percent: 100,
message: 'Conversion engine ready!',
});
// Null out the callback to prevent any late-firing progress updates
progressCallback = undefined;
} finally {
this.initializing = false;
}
// Null out the callback to prevent any late-firing progress updates
progressCallback = undefined;
} finally {
this.initializing = false;
}
}
isReady(): boolean {
return this.initialized && this.converter !== null;
}
async convertToPdf(file: File): Promise<Blob> {
if (!this.converter) {
throw new Error('Converter not initialized');
}
isReady(): boolean {
return this.initialized && this.converter !== null;
console.log(`[LibreOffice] Converting ${file.name} to PDF...`);
console.log(
`[LibreOffice] File type: ${file.type}, Size: ${file.size} bytes`
);
try {
console.log(`[LibreOffice] Reading file as ArrayBuffer...`);
const arrayBuffer = await file.arrayBuffer();
const uint8Array = new Uint8Array(arrayBuffer);
console.log(`[LibreOffice] File loaded, ${uint8Array.length} bytes`);
console.log(`[LibreOffice] Calling converter.convert() with buffer...`);
const startTime = Date.now();
// Detect input format - critical for CSV to apply import filters
const ext = file.name.split('.').pop()?.toLowerCase() || '';
console.log(`[LibreOffice] Detected format from extension: ${ext}`);
const result = await this.converter.convert(
uint8Array,
{
outputFormat: 'pdf',
inputFormat: ext as InputFormat,
},
file.name
);
const duration = Date.now() - startTime;
console.log(
`[LibreOffice] Conversion complete! Duration: ${duration}ms, Size: ${result.data.length} bytes`
);
// Create a copy to avoid SharedArrayBuffer type issues
const data = new Uint8Array(result.data);
return new Blob([data], { type: result.mimeType });
} catch (error) {
console.error(`[LibreOffice] Conversion FAILED for ${file.name}:`, error);
console.error(`[LibreOffice] Error details:`, {
message: error instanceof Error ? error.message : String(error),
stack: error instanceof Error ? error.stack : undefined,
});
throw error;
}
}
async convertToPdf(file: File): Promise<Blob> {
if (!this.converter) {
throw new Error('Converter not initialized');
}
async wordToPdf(file: File): Promise<Blob> {
return this.convertToPdf(file);
}
console.log(`[LibreOffice] Converting ${file.name} to PDF...`);
console.log(`[LibreOffice] File type: ${file.type}, Size: ${file.size} bytes`);
async pptToPdf(file: File): Promise<Blob> {
return this.convertToPdf(file);
}
try {
console.log(`[LibreOffice] Reading file as ArrayBuffer...`);
const arrayBuffer = await file.arrayBuffer();
const uint8Array = new Uint8Array(arrayBuffer);
console.log(`[LibreOffice] File loaded, ${uint8Array.length} bytes`);
async excelToPdf(file: File): Promise<Blob> {
return this.convertToPdf(file);
}
console.log(`[LibreOffice] Calling converter.convert() with buffer...`);
const startTime = Date.now();
// Detect input format - critical for CSV to apply import filters
const ext = file.name.split('.').pop()?.toLowerCase() || '';
console.log(`[LibreOffice] Detected format from extension: ${ext}`);
const result = await this.converter.convert(uint8Array, {
outputFormat: 'pdf',
inputFormat: ext as any, // Explicitly specify format for CSV import filters
}, file.name);
const duration = Date.now() - startTime;
console.log(`[LibreOffice] Conversion complete! Duration: ${duration}ms, Size: ${result.data.length} bytes`);
// Create a copy to avoid SharedArrayBuffer type issues
const data = new Uint8Array(result.data);
return new Blob([data], { type: result.mimeType });
} catch (error) {
console.error(`[LibreOffice] Conversion FAILED for ${file.name}:`, error);
console.error(`[LibreOffice] Error details:`, {
message: error instanceof Error ? error.message : String(error),
stack: error instanceof Error ? error.stack : undefined
});
throw error;
}
}
async wordToPdf(file: File): Promise<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;
async destroy(): Promise<void> {
if (this.converter) {
await this.converter.destroy();
}
this.converter = null;
this.initialized = false;
}
}
export function getLibreOfficeConverter(basePath?: string): LibreOfficeConverter {
if (!converterInstance) {
converterInstance = new LibreOfficeConverter(basePath);
}
return converterInstance;
export function getLibreOfficeConverter(
basePath?: string
): LibreOfficeConverter {
if (!converterInstance) {
converterInstance = new LibreOfficeConverter(basePath);
}
return converterInstance;
}
+123 -71
View File
@@ -29,8 +29,14 @@ import taskLists from 'markdown-it-task-lists';
import anchor from 'markdown-it-anchor';
import tocDoneRight from 'markdown-it-toc-done-right';
import { applyTranslations } from '../i18n/i18n';
import type {
WindowWithLucide,
WindowWithI18next,
MarkdownItOptions,
MarkdownItToken,
MarkdownItRendererSelf,
MarkdownItRenderRule,
} from '@/types';
// Register highlight.js languages
hljs.registerLanguage('javascript', javascript);
@@ -66,18 +72,7 @@ export interface MarkdownEditorOptions {
onBack?: () => void;
}
export interface MarkdownItOptions {
/** Enable HTML tags in source */
html: boolean;
/** Convert '\n' in paragraphs into <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;
}
export type { MarkdownItOptions } from '@/types';
const DEFAULT_MARKDOWN = `# Welcome to BentoPDF Markdown Editor
@@ -277,7 +272,7 @@ export class MarkdownEditor {
html: true,
breaks: false,
linkify: true,
typographer: true
typographer: true,
};
constructor(container: HTMLElement, options: MarkdownEditorOptions) {
@@ -303,22 +298,36 @@ export class MarkdownEditor {
startOnLoad: false,
theme: 'default',
securityLevel: 'loose',
fontFamily: '-apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif'
fontFamily:
'-apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif',
});
this.mermaidInitialized = true;
}
}
private configureLinkRenderer(): void {
// Override link renderer to add target="_blank" and rel="noopener"
const defaultRender = this.md.renderer.rules.link_open ||
((tokens: any[], idx: number, options: any, _env: any, self: any) => self.renderToken(tokens, idx, options));
const existingRule = this.md.renderer.rules.link_open;
const defaultRender: MarkdownItRenderRule = existingRule
? (existingRule as unknown as MarkdownItRenderRule)
: (
tokens: MarkdownItToken[],
idx: number,
options: MarkdownItOptions,
env: unknown,
self: MarkdownItRendererSelf
) => self.renderToken(tokens, idx, options);
this.md.renderer.rules.link_open = (tokens: any[], idx: number, options: any, env: any, self: any) => {
const token = tokens[idx];
this.md.renderer.rules.link_open = (tokens, idx, options, env, self) => {
const token = tokens[idx] as unknown as MarkdownItToken;
token.attrSet('target', '_blank');
token.attrSet('rel', 'noopener noreferrer');
return defaultRender(tokens, idx, options, env, self);
return defaultRender(
tokens as unknown as MarkdownItToken[],
idx,
options as unknown as MarkdownItOptions,
env as unknown,
self as unknown as MarkdownItRendererSelf
);
};
}
@@ -417,12 +426,11 @@ export class MarkdownEditor {
this.applyI18n();
// Initialize Lucide icons
if (typeof (window as any).lucide !== 'undefined') {
(window as any).lucide.createIcons();
if (typeof (window as WindowWithLucide).lucide !== 'undefined') {
(window as WindowWithLucide).lucide?.createIcons();
}
}
private setupEventListeners(): void {
// Editor input
this.editor?.addEventListener('input', () => {
@@ -441,9 +449,13 @@ export class MarkdownEditor {
this.editor?.addEventListener('scroll', () => {
if (this.syncScroll && !this.isSyncing && this.editor && this.preview) {
this.isSyncing = true;
const scrollPercentage = this.editor.scrollTop / (this.editor.scrollHeight - this.editor.clientHeight);
this.preview.scrollTop = scrollPercentage * (this.preview.scrollHeight - this.preview.clientHeight);
setTimeout(() => this.isSyncing = false, 10);
const scrollPercentage =
this.editor.scrollTop /
(this.editor.scrollHeight - this.editor.clientHeight);
this.preview.scrollTop =
scrollPercentage *
(this.preview.scrollHeight - this.preview.clientHeight);
setTimeout(() => (this.isSyncing = false), 10);
}
});
@@ -451,9 +463,13 @@ export class MarkdownEditor {
this.preview?.addEventListener('scroll', () => {
if (this.syncScroll && !this.isSyncing && this.editor && this.preview) {
this.isSyncing = true;
const scrollPercentage = this.preview.scrollTop / (this.preview.scrollHeight - this.preview.clientHeight);
this.editor.scrollTop = scrollPercentage * (this.editor.scrollHeight - this.editor.clientHeight);
setTimeout(() => this.isSyncing = false, 10);
const scrollPercentage =
this.preview.scrollTop /
(this.preview.scrollHeight - this.preview.clientHeight);
this.editor.scrollTop =
scrollPercentage *
(this.editor.scrollHeight - this.editor.clientHeight);
setTimeout(() => (this.isSyncing = false), 10);
}
});
@@ -474,22 +490,30 @@ export class MarkdownEditor {
});
// Settings modal close
document.getElementById('mdCloseSettings')?.addEventListener('click', () => {
const modal = document.getElementById('mdSettingsModal');
if (modal) {
modal.style.display = 'none';
}
});
// Close modal on overlay click
document.getElementById('mdSettingsModal')?.addEventListener('click', (e) => {
if ((e.target as HTMLElement).classList.contains('md-editor-modal-overlay')) {
document
.getElementById('mdCloseSettings')
?.addEventListener('click', () => {
const modal = document.getElementById('mdSettingsModal');
if (modal) {
modal.style.display = 'none';
}
}
});
});
// Close modal on overlay click
document
.getElementById('mdSettingsModal')
?.addEventListener('click', (e) => {
if (
(e.target as HTMLElement).classList.contains(
'md-editor-modal-overlay'
)
) {
const modal = document.getElementById('mdSettingsModal');
if (modal) {
modal.style.display = 'none';
}
}
});
// Settings checkboxes
document.getElementById('mdOptHtml')?.addEventListener('change', (e) => {
@@ -507,10 +531,12 @@ export class MarkdownEditor {
this.updateMarkdownIt();
});
document.getElementById('mdOptTypographer')?.addEventListener('change', (e) => {
this.mdOptions.typographer = (e.target as HTMLInputElement).checked;
this.updateMarkdownIt();
});
document
.getElementById('mdOptTypographer')
?.addEventListener('change', (e) => {
this.mdOptions.typographer = (e.target as HTMLInputElement).checked;
this.updateMarkdownIt();
});
// Preset selector
document.getElementById('mdPreset')?.addEventListener('change', (e) => {
@@ -549,7 +575,8 @@ export class MarkdownEditor {
const start = this.editor!.selectionStart;
const end = this.editor!.selectionEnd;
const value = this.editor!.value;
this.editor!.value = value.substring(0, start) + ' ' + value.substring(end);
this.editor!.value =
value.substring(0, start) + ' ' + value.substring(end);
this.editor!.selectionStart = this.editor!.selectionEnd = start + 2;
this.updatePreview();
}
@@ -563,18 +590,37 @@ export class MarkdownEditor {
// Update options based on preset
if (preset === 'commonmark') {
this.mdOptions = { html: false, breaks: false, linkify: false, typographer: false };
this.mdOptions = {
html: false,
breaks: false,
linkify: false,
typographer: false,
};
} else if (preset === 'zero') {
this.mdOptions = { html: false, breaks: false, linkify: false, typographer: false };
this.mdOptions = {
html: false,
breaks: false,
linkify: false,
typographer: false,
};
} else {
this.mdOptions = { html: true, breaks: false, linkify: true, typographer: true };
this.mdOptions = {
html: true,
breaks: false,
linkify: true,
typographer: true,
};
}
// Update UI checkboxes
(document.getElementById('mdOptHtml') as HTMLInputElement).checked = this.mdOptions.html;
(document.getElementById('mdOptBreaks') as HTMLInputElement).checked = this.mdOptions.breaks;
(document.getElementById('mdOptLinkify') as HTMLInputElement).checked = this.mdOptions.linkify;
(document.getElementById('mdOptTypographer') as HTMLInputElement).checked = this.mdOptions.typographer;
(document.getElementById('mdOptHtml') as HTMLInputElement).checked =
this.mdOptions.html;
(document.getElementById('mdOptBreaks') as HTMLInputElement).checked =
this.mdOptions.breaks;
(document.getElementById('mdOptLinkify') as HTMLInputElement).checked =
this.mdOptions.linkify;
(document.getElementById('mdOptTypographer') as HTMLInputElement).checked =
this.mdOptions.typographer;
this.updateMarkdownIt();
}
@@ -588,7 +634,6 @@ export class MarkdownEditor {
}
}
private createMarkdownIt(): MarkdownIt {
// Use preset if commonmark or zero
let md: MarkdownIt;
@@ -604,28 +649,31 @@ export class MarkdownEditor {
highlight: (str: string, lang: string) => {
if (lang && hljs.getLanguage(lang)) {
try {
return hljs.highlight(str, { language: lang, ignoreIllegals: true }).value;
return hljs.highlight(str, {
language: lang,
ignoreIllegals: true,
}).value;
} catch {
// Fall through to default
}
}
return ''; // Use external default escaping
}
},
});
}
// Apply plugins only for default preset (plugins may not work well with commonmark/zero)
if (this.currentPreset === 'default') {
md.use(sub) // Subscript: ~text~ -> <sub>text</sub>
.use(sup) // Superscript: ^text^ -> <sup>text</sup>
.use(footnote) // Footnotes: [^1] and [^1]: footnote text
.use(deflist) // Definition lists
.use(abbr) // Abbreviations: *[abbr]: full text
.use(emoji) // Emoji: :smile: -> 😄
.use(ins) // Inserted text: ++text++ -> <ins>text</ins>
.use(mark) // Marked text: ==text== -> <mark>text</mark>
.use(taskLists, { enabled: true, label: true, labelAfter: true }) // Task lists: - [x] done
.use(anchor, { permalink: false }) // Header anchors
md.use(sub) // Subscript: ~text~ -> <sub>text</sub>
.use(sup) // Superscript: ^text^ -> <sup>text</sup>
.use(footnote) // Footnotes: [^1] and [^1]: footnote text
.use(deflist) // Definition lists
.use(abbr) // Abbreviations: *[abbr]: full text
.use(emoji) // Emoji: :smile: -> 😄
.use(ins) // Inserted text: ++text++ -> <ins>text</ins>
.use(mark) // Marked text: ==text== -> <mark>text</mark>
.use(taskLists, { enabled: true, label: true, labelAfter: true }) // Task lists: - [x] done
.use(anchor, { permalink: false }) // Header anchors
.use(tocDoneRight); // Table of contents: ${toc}
}
@@ -650,7 +698,9 @@ export class MarkdownEditor {
private async renderMermaidDiagrams(): Promise<void> {
if (!this.preview) return;
const mermaidBlocks = this.preview.querySelectorAll('pre > code.language-mermaid');
const mermaidBlocks = this.preview.querySelectorAll(
'pre > code.language-mermaid'
);
for (let i = 0; i < mermaidBlocks.length; i++) {
const block = mermaidBlocks[i] as HTMLElement;
@@ -948,14 +998,16 @@ ${content}
applyTranslations();
// Special handling for select options (data-i18n on options doesn't work with applyTranslations)
const presetSelect = document.getElementById('mdPreset') as HTMLSelectElement;
const presetSelect = document.getElementById(
'mdPreset'
) as HTMLSelectElement;
if (presetSelect) {
const options = presetSelect.querySelectorAll('option[data-i18n]');
options.forEach((option) => {
const key = option.getAttribute('data-i18n');
if (key) {
// Use i18next directly for option text
const translated = (window as any).i18next?.t(key);
const translated = (window as WindowWithI18next).i18next?.t(key);
if (translated && translated !== key) {
option.textContent = translated;
}
+1 -1
View File
@@ -154,7 +154,7 @@ document.addEventListener('keydown', handleKeydown);
export function initPagePreview(
container: HTMLElement,
pdfjsDoc: PDFDocumentProxy,
options: { pageAttr?: string } = {}
_options: { pageAttr?: string } = {}
): void {
const totalPages = pdfjsDoc.numPages;
+4 -3
View File
@@ -1,5 +1,6 @@
import { getCpdf, isCpdfAvailable } from './cpdf-helper';
import { isPyMuPDFAvailable, loadPyMuPDF } from './pymupdf-loader';
import type { CpdfInstance } from '@/types';
export type PdfDecryptEngine = 'cpdf' | 'pymupdf';
@@ -37,13 +38,13 @@ function copyBytes(bytes: Uint8Array): Uint8Array {
return Uint8Array.from(bytes);
}
function cleanupCpdfDocument(cpdf: unknown, pdf: unknown): void {
if (!cpdf || !pdf || typeof cpdf !== 'object' || !('deletePdf' in cpdf)) {
function cleanupCpdfDocument(cpdf: CpdfInstance, pdf: unknown): void {
if (!cpdf || !pdf) {
return;
}
try {
(cpdf as { deletePdf: (document: unknown) => void }).deletePdf(pdf);
cpdf.deletePdf(pdf);
} catch (cleanupError) {
console.warn(
`${DECRYPT_LOG_PREFIX} Failed to cleanup CoherentPDF document: ${normalizeErrorMessage(cleanupError)}`
+19 -9
View File
@@ -1,25 +1,34 @@
import { WasmProvider } from './wasm-provider.js';
import type {
PyMuPDFInstance,
PyMuPDFCompressOptions,
PyMuPDFExtractTextOptions,
PyMuPDFRasterizeOptions,
} from '@/types';
let cachedPyMuPDF: any = null;
let loadPromise: Promise<any> | null = null;
let cachedPyMuPDF: PyMuPDFInstance | null = null;
let loadPromise: Promise<PyMuPDFInstance> | null = null;
export interface PyMuPDFInterface {
load(): Promise<void>;
compressPdf(
file: Blob,
options: any
options: PyMuPDFCompressOptions
): Promise<{ blob: Blob; compressedSize: number }>;
convertToPdf(file: Blob, ext: string): Promise<Blob>;
extractText(file: Blob, options?: any): Promise<string>;
extractText(file: Blob, options?: PyMuPDFExtractTextOptions): Promise<string>;
extractImages(file: Blob): Promise<Array<{ data: Uint8Array; ext: string }>>;
extractTables(file: Blob): Promise<any[]>;
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: any): Promise<Blob>;
rasterizePdf(
file: Blob | File,
options: PyMuPDFRasterizeOptions
): Promise<Blob>;
}
export async function loadPyMuPDF(): Promise<any> {
export async function loadPyMuPDF(): Promise<PyMuPDFInstance> {
if (cachedPyMuPDF) {
return cachedPyMuPDF;
}
@@ -65,9 +74,10 @@ export async function loadPyMuPDF(): Promise<any> {
console.log('[PyMuPDF Loader] Successfully loaded from CDN');
return cachedPyMuPDF;
} catch (error: any) {
} catch (error: unknown) {
loadPromise = null;
throw new Error(`Failed to load PyMuPDF from CDN: ${error.message}`, {
const msg = error instanceof Error ? error.message : String(error);
throw new Error(`Failed to load PyMuPDF from CDN: ${msg}`, {
cause: error,
});
}
+175 -121
View File
@@ -1,5 +1,34 @@
import { PDFDocument, PDFName } from 'pdf-lib';
import { PDFDocument, PDFName, PDFDict, PDFArray } from 'pdf-lib';
import { loadPdfDocument } from './load-pdf-document.js';
import type { PDFDocumentInternal } from '@/types';
function getCatalogDict(pdfDoc: PDFDocument): PDFDict {
return pdfDoc.catalog;
}
function lookupAsDict(
pdfDoc: PDFDocument,
ref: ReturnType<PDFDict['get']>
): PDFDict | undefined {
if (!ref) return undefined;
const result = pdfDoc.context.lookup(ref);
if (result instanceof PDFDict) return result;
return undefined;
}
function lookupAsArray(
pdfDoc: PDFDocument,
ref: ReturnType<PDFDict['get']>
): PDFArray | undefined {
if (!ref) return undefined;
const result = pdfDoc.context.lookup(ref);
if (result instanceof PDFArray) return result;
return undefined;
}
function getErrorMessage(e: unknown): string {
return e instanceof Error ? e.message : String(e);
}
export interface SanitizeOptions {
flattenForms: boolean;
@@ -28,9 +57,9 @@ export const defaultSanitizeOptions: SanitizeOptions = {
};
function removeMetadataFromDoc(pdfDoc: PDFDocument) {
const infoDict = (pdfDoc as any).getInfoDict();
const infoDict = (pdfDoc as unknown as PDFDocumentInternal).getInfoDict();
const allKeys = infoDict.keys();
allKeys.forEach((key: any) => {
allKeys.forEach((key: PDFName) => {
infoDict.delete(key);
});
@@ -42,30 +71,30 @@ function removeMetadataFromDoc(pdfDoc: PDFDocument) {
pdfDoc.setProducer('');
try {
const catalogDict = (pdfDoc.catalog as any).dict;
const catalogDict = getCatalogDict(pdfDoc);
if (catalogDict.has(PDFName.of('Metadata'))) {
catalogDict.delete(PDFName.of('Metadata'));
}
} catch (e: any) {
console.warn('Could not remove XMP metadata:', e.message);
} catch (e: unknown) {
console.warn('Could not remove XMP metadata:', getErrorMessage(e));
}
try {
const context = pdfDoc.context;
if ((context as any).trailerInfo) {
delete (context as any).trailerInfo.ID;
if (context.trailerInfo) {
delete context.trailerInfo.ID;
}
} catch (e: any) {
console.warn('Could not remove document IDs:', e.message);
} catch (e: unknown) {
console.warn('Could not remove document IDs:', getErrorMessage(e));
}
try {
const catalogDict = (pdfDoc.catalog as any).dict;
const catalogDict = getCatalogDict(pdfDoc);
if (catalogDict.has(PDFName.of('PieceInfo'))) {
catalogDict.delete(PDFName.of('PieceInfo'));
}
} catch (e: any) {
console.warn('Could not remove PieceInfo:', e.message);
} catch (e: unknown) {
console.warn('Could not remove PieceInfo:', getErrorMessage(e));
}
}
@@ -74,8 +103,11 @@ function removeAnnotationsFromDoc(pdfDoc: PDFDocument) {
for (const page of pages) {
try {
page.node.delete(PDFName.of('Annots'));
} catch (e: any) {
console.warn('Could not remove annotations from page:', e.message);
} catch (e: unknown) {
console.warn(
'Could not remove annotations from page:',
getErrorMessage(e)
);
}
}
}
@@ -86,21 +118,22 @@ function flattenFormsInDoc(pdfDoc: PDFDocument) {
}
function removeJavascriptFromDoc(pdfDoc: PDFDocument) {
if ((pdfDoc as any).javaScripts && (pdfDoc as any).javaScripts.length > 0) {
(pdfDoc as any).javaScripts = [];
const pdfDocInternal = pdfDoc as unknown as PDFDocumentInternal;
if (pdfDocInternal.javaScripts && pdfDocInternal.javaScripts.length > 0) {
pdfDocInternal.javaScripts = [];
}
const catalogDict = (pdfDoc.catalog as any).dict;
const catalogDict = getCatalogDict(pdfDoc);
const namesRef = catalogDict.get(PDFName.of('Names'));
if (namesRef) {
try {
const namesDict = pdfDoc.context.lookup(namesRef) as any;
if (namesDict.has(PDFName.of('JavaScript'))) {
const namesDict = lookupAsDict(pdfDoc, namesRef);
if (namesDict?.has(PDFName.of('JavaScript'))) {
namesDict.delete(PDFName.of('JavaScript'));
}
} catch (e: any) {
console.warn('Could not access Names/JavaScript:', e.message);
} catch (e: unknown) {
console.warn('Could not access Names/JavaScript:', getErrorMessage(e));
}
}
@@ -124,50 +157,55 @@ function removeJavascriptFromDoc(pdfDoc: PDFDocument) {
const annotRefs = pageDict.Annots()?.asArray() || [];
for (const annotRef of annotRefs) {
try {
const annot = pdfDoc.context.lookup(annotRef) as any;
const annot = lookupAsDict(pdfDoc, annotRef);
if (!annot) continue;
if (annot.has(PDFName.of('A'))) {
const actionRef = annot.get(PDFName.of('A'));
try {
const actionDict = pdfDoc.context.lookup(actionRef) as any;
const actionDict = lookupAsDict(pdfDoc, actionRef);
const actionType = actionDict
.get(PDFName.of('S'))
?.get(PDFName.of('S'))
?.toString()
.substring(1);
if (actionType === 'JavaScript') {
annot.delete(PDFName.of('A'));
}
} catch (e: any) {
console.warn('Could not read action:', e.message);
} catch (e: unknown) {
console.warn('Could not read action:', getErrorMessage(e));
}
}
if (annot.has(PDFName.of('AA'))) {
annot.delete(PDFName.of('AA'));
}
} catch (e: any) {
console.warn('Could not process annotation for JS:', e.message);
} catch (e: unknown) {
console.warn(
'Could not process annotation for JS:',
getErrorMessage(e)
);
}
}
} catch (e: any) {
console.warn('Could not remove page actions:', e.message);
} catch (e: unknown) {
console.warn('Could not remove page actions:', getErrorMessage(e));
}
}
try {
const acroFormRef = catalogDict.get(PDFName.of('AcroForm'));
if (acroFormRef) {
const acroFormDict = pdfDoc.context.lookup(acroFormRef) as any;
const fieldsRef = acroFormDict.get(PDFName.of('Fields'));
const acroFormDict = lookupAsDict(pdfDoc, acroFormRef);
const fieldsRef = acroFormDict?.get(PDFName.of('Fields'));
if (fieldsRef) {
const fieldsArray = pdfDoc.context.lookup(fieldsRef) as any;
const fields = fieldsArray.asArray();
const fieldsArray = lookupAsArray(pdfDoc, fieldsRef);
const fields = fieldsArray?.asArray() || [];
for (const fieldRef of fields) {
try {
const field = pdfDoc.context.lookup(fieldRef) as any;
const field = lookupAsDict(pdfDoc, fieldRef);
if (!field) continue;
if (field.has(PDFName.of('A'))) {
field.delete(PDFName.of('A'));
@@ -176,29 +214,29 @@ function removeJavascriptFromDoc(pdfDoc: PDFDocument) {
if (field.has(PDFName.of('AA'))) {
field.delete(PDFName.of('AA'));
}
} catch (e: any) {
console.warn('Could not process field for JS:', e.message);
} catch (e: unknown) {
console.warn('Could not process field for JS:', getErrorMessage(e));
}
}
}
}
} catch (e: any) {
console.warn('Could not process form fields for JS:', e.message);
} catch (e: unknown) {
console.warn('Could not process form fields for JS:', getErrorMessage(e));
}
}
function removeEmbeddedFilesFromDoc(pdfDoc: PDFDocument) {
const catalogDict = (pdfDoc.catalog as any).dict;
const catalogDict = getCatalogDict(pdfDoc);
const namesRef = catalogDict.get(PDFName.of('Names'));
if (namesRef) {
try {
const namesDict = pdfDoc.context.lookup(namesRef) as any;
if (namesDict.has(PDFName.of('EmbeddedFiles'))) {
const namesDict = lookupAsDict(pdfDoc, namesRef);
if (namesDict?.has(PDFName.of('EmbeddedFiles'))) {
namesDict.delete(PDFName.of('EmbeddedFiles'));
}
} catch (e: any) {
console.warn('Could not access Names/EmbeddedFiles:', e.message);
} catch (e: unknown) {
console.warn('Could not access Names/EmbeddedFiles:', getErrorMessage(e));
}
}
@@ -214,16 +252,16 @@ function removeEmbeddedFilesFromDoc(pdfDoc: PDFDocument) {
for (const ref of annotRefs) {
try {
const annot = pdfDoc.context.lookup(ref) as any;
const annot = lookupAsDict(pdfDoc, ref);
const subtype = annot
.get(PDFName.of('Subtype'))
?.get(PDFName.of('Subtype'))
?.toString()
.substring(1);
if (subtype !== 'FileAttachment') {
annotsToKeep.push(ref);
}
} catch (e) {
} catch {
annotsToKeep.push(ref);
}
}
@@ -236,18 +274,16 @@ function removeEmbeddedFilesFromDoc(pdfDoc: PDFDocument) {
page.node.delete(PDFName.of('Annots'));
}
}
} catch (pageError: any) {
} catch (pageError: unknown) {
console.warn(
`Could not process page for attachments: ${pageError.message}`
`Could not process page for attachments: ${getErrorMessage(pageError)}`
);
}
}
if (
(pdfDoc as any).embeddedFiles &&
(pdfDoc as any).embeddedFiles.length > 0
) {
(pdfDoc as any).embeddedFiles = [];
const pdfDocInternal = pdfDoc as unknown as PDFDocumentInternal;
if (pdfDocInternal.embeddedFiles && pdfDocInternal.embeddedFiles.length > 0) {
pdfDocInternal.embeddedFiles = [];
}
if (catalogDict.has(PDFName.of('Collection'))) {
@@ -256,7 +292,7 @@ function removeEmbeddedFilesFromDoc(pdfDoc: PDFDocument) {
}
function removeLayersFromDoc(pdfDoc: PDFDocument) {
const catalogDict = (pdfDoc.catalog as any).dict;
const catalogDict = getCatalogDict(pdfDoc);
if (catalogDict.has(PDFName.of('OCProperties'))) {
catalogDict.delete(PDFName.of('OCProperties'));
@@ -274,16 +310,16 @@ function removeLayersFromDoc(pdfDoc: PDFDocument) {
const resourcesRef = pageDict.get(PDFName.of('Resources'));
if (resourcesRef) {
try {
const resourcesDict = pdfDoc.context.lookup(resourcesRef) as any;
if (resourcesDict.has(PDFName.of('Properties'))) {
const resourcesDict = lookupAsDict(pdfDoc, resourcesRef);
if (resourcesDict?.has(PDFName.of('Properties'))) {
resourcesDict.delete(PDFName.of('Properties'));
}
} catch (e: any) {
console.warn('Could not access Resources:', e.message);
} catch (e: unknown) {
console.warn('Could not access Resources:', getErrorMessage(e));
}
}
} catch (e: any) {
console.warn('Could not remove page layers:', e.message);
} catch (e: unknown) {
console.warn('Could not remove page layers:', getErrorMessage(e));
}
}
}
@@ -299,8 +335,9 @@ function removeLinksFromDoc(pdfDoc: PDFDocument) {
const annotsRef = pageDict.get(PDFName.of('Annots'));
if (!annotsRef) continue;
const annotsArray = pdfDoc.context.lookup(annotsRef) as any;
const annotRefs = annotsArray.asArray();
const annotsArrayObj = lookupAsArray(pdfDoc, annotsRef);
if (!annotsArrayObj) continue;
const annotRefs = annotsArrayObj.asArray();
if (annotRefs.length === 0) continue;
@@ -309,7 +346,11 @@ function removeLinksFromDoc(pdfDoc: PDFDocument) {
for (const ref of annotRefs) {
try {
const annot = pdfDoc.context.lookup(ref) as any;
const annot = lookupAsDict(pdfDoc, ref);
if (!annot) {
annotsToKeep.push(ref);
continue;
}
const subtype = annot
.get(PDFName.of('Subtype'))
?.toString()
@@ -324,9 +365,9 @@ function removeLinksFromDoc(pdfDoc: PDFDocument) {
const actionRef = annot.get(PDFName.of('A'));
if (actionRef) {
try {
const actionDict = pdfDoc.context.lookup(actionRef) as any;
const actionDict = lookupAsDict(pdfDoc, actionRef);
const actionType = actionDict
.get(PDFName.of('S'))
?.get(PDFName.of('S'))
?.toString()
.substring(1);
@@ -339,8 +380,8 @@ function removeLinksFromDoc(pdfDoc: PDFDocument) {
isLink = true;
linksRemoved++;
}
} catch (e: any) {
console.warn('Could not read action:', e.message);
} catch (e: unknown) {
console.warn('Could not read action:', getErrorMessage(e));
}
}
@@ -354,8 +395,8 @@ function removeLinksFromDoc(pdfDoc: PDFDocument) {
if (!isLink) {
annotsToKeep.push(ref);
}
} catch (e: any) {
console.warn('Could not process annotation:', e.message);
} catch (e: unknown) {
console.warn('Could not process annotation:', getErrorMessage(e));
annotsToKeep.push(ref);
}
}
@@ -368,37 +409,37 @@ function removeLinksFromDoc(pdfDoc: PDFDocument) {
pageDict.delete(PDFName.of('Annots'));
}
}
} catch (pageError: any) {
} catch (pageError: unknown) {
console.warn(
`Could not process page ${pageIndex + 1} for links: ${pageError.message}`
`Could not process page ${pageIndex + 1} for links: ${getErrorMessage(pageError)}`
);
}
}
try {
const catalogDict = (pdfDoc.catalog as any).dict;
const catalogDict = getCatalogDict(pdfDoc);
const namesRef = catalogDict.get(PDFName.of('Names'));
if (namesRef) {
try {
const namesDict = pdfDoc.context.lookup(namesRef) as any;
if (namesDict.has(PDFName.of('Dests'))) {
const namesDict = lookupAsDict(pdfDoc, namesRef);
if (namesDict?.has(PDFName.of('Dests'))) {
namesDict.delete(PDFName.of('Dests'));
}
} catch (e: any) {
console.warn('Could not access Names/Dests:', e.message);
} catch (e: unknown) {
console.warn('Could not access Names/Dests:', getErrorMessage(e));
}
}
if (catalogDict.has(PDFName.of('Dests'))) {
catalogDict.delete(PDFName.of('Dests'));
}
} catch (e: any) {
console.warn('Could not remove named destinations:', e.message);
} catch (e: unknown) {
console.warn('Could not remove named destinations:', getErrorMessage(e));
}
}
function removeStructureTreeFromDoc(pdfDoc: PDFDocument) {
const catalogDict = (pdfDoc.catalog as any).dict;
const catalogDict = getCatalogDict(pdfDoc);
if (catalogDict.has(PDFName.of('StructTreeRoot'))) {
catalogDict.delete(PDFName.of('StructTreeRoot'));
@@ -411,8 +452,8 @@ function removeStructureTreeFromDoc(pdfDoc: PDFDocument) {
if (pageDict.has(PDFName.of('StructParents'))) {
pageDict.delete(PDFName.of('StructParents'));
}
} catch (e: any) {
console.warn('Could not remove page StructParents:', e.message);
} catch (e: unknown) {
console.warn('Could not remove page StructParents:', getErrorMessage(e));
}
}
@@ -422,7 +463,7 @@ function removeStructureTreeFromDoc(pdfDoc: PDFDocument) {
}
function removeMarkInfoFromDoc(pdfDoc: PDFDocument) {
const catalogDict = (pdfDoc.catalog as any).dict;
const catalogDict = getCatalogDict(pdfDoc);
if (catalogDict.has(PDFName.of('MarkInfo'))) {
catalogDict.delete(PDFName.of('MarkInfo'));
@@ -444,29 +485,29 @@ function removeFontsFromDoc(pdfDoc: PDFDocument) {
if (resourcesRef) {
try {
const resourcesDict = pdfDoc.context.lookup(resourcesRef) as any;
const resourcesDict = lookupAsDict(pdfDoc, resourcesRef);
if (!resourcesDict) continue;
if (resourcesDict.has(PDFName.of('Font'))) {
const fontRef = resourcesDict.get(PDFName.of('Font'));
try {
const fontDict = pdfDoc.context.lookup(fontRef) as any;
const fontDict = lookupAsDict(pdfDoc, fontRef);
if (!fontDict) continue;
const fontKeys = fontDict.keys();
for (const fontKey of fontKeys) {
try {
const specificFontRef = fontDict.get(fontKey);
const specificFont = pdfDoc.context.lookup(
specificFontRef
) as any;
const specificFont = lookupAsDict(pdfDoc, specificFontRef);
if (!specificFont) continue;
if (specificFont.has(PDFName.of('FontDescriptor'))) {
const descriptorRef = specificFont.get(
PDFName.of('FontDescriptor')
);
const descriptor = pdfDoc.context.lookup(
descriptorRef
) as any;
const descriptor = lookupAsDict(pdfDoc, descriptorRef);
if (!descriptor) continue;
const fontFileKeys = ['FontFile', 'FontFile2', 'FontFile3'];
for (const key of fontFileKeys) {
@@ -475,28 +516,38 @@ function removeFontsFromDoc(pdfDoc: PDFDocument) {
}
}
}
} catch (e: any) {
console.warn(`Could not process font ${fontKey}:`, e.message);
} catch (e: unknown) {
console.warn(
`Could not process font ${fontKey}:`,
getErrorMessage(e)
);
}
}
} catch (e: any) {
console.warn('Could not access font dictionary:', e.message);
} catch (e: unknown) {
console.warn(
'Could not access font dictionary:',
getErrorMessage(e)
);
}
}
} catch (e: any) {
console.warn('Could not access Resources for fonts:', e.message);
} catch (e: unknown) {
console.warn(
'Could not access Resources for fonts:',
getErrorMessage(e)
);
}
}
} catch (e: any) {
} catch (e: unknown) {
console.warn(
`Could not remove fonts from page ${pageIndex + 1}:`,
e.message
getErrorMessage(e)
);
}
}
if ((pdfDoc as any).fonts && (pdfDoc as any).fonts.length > 0) {
(pdfDoc as any).fonts = [];
const pdfDocInternal = pdfDoc as unknown as PDFDocumentInternal;
if (pdfDocInternal.fonts && pdfDocInternal.fonts.length > 0) {
pdfDocInternal.fonts = [];
}
}
@@ -509,15 +560,18 @@ export async function sanitizePdf(
if (options.flattenForms) {
try {
flattenFormsInDoc(pdfDoc);
} catch (e: any) {
console.warn(`Could not flatten forms: ${e.message}`);
} catch (e: unknown) {
console.warn(`Could not flatten forms: ${getErrorMessage(e)}`);
try {
const catalogDict = (pdfDoc.catalog as any).dict;
const catalogDict = getCatalogDict(pdfDoc);
if (catalogDict.has(PDFName.of('AcroForm'))) {
catalogDict.delete(PDFName.of('AcroForm'));
}
} catch (removeError: any) {
console.warn('Could not remove AcroForm:', removeError.message);
} catch (removeError: unknown) {
console.warn(
'Could not remove AcroForm:',
getErrorMessage(removeError)
);
}
}
}
@@ -533,56 +587,56 @@ export async function sanitizePdf(
if (options.removeJavascript) {
try {
removeJavascriptFromDoc(pdfDoc);
} catch (e: any) {
console.warn(`Could not remove JavaScript: ${e.message}`);
} catch (e: unknown) {
console.warn(`Could not remove JavaScript: ${getErrorMessage(e)}`);
}
}
if (options.removeEmbeddedFiles) {
try {
removeEmbeddedFilesFromDoc(pdfDoc);
} catch (e: any) {
console.warn(`Could not remove embedded files: ${e.message}`);
} catch (e: unknown) {
console.warn(`Could not remove embedded files: ${getErrorMessage(e)}`);
}
}
if (options.removeLayers) {
try {
removeLayersFromDoc(pdfDoc);
} catch (e: any) {
console.warn(`Could not remove layers: ${e.message}`);
} catch (e: unknown) {
console.warn(`Could not remove layers: ${getErrorMessage(e)}`);
}
}
if (options.removeLinks) {
try {
removeLinksFromDoc(pdfDoc);
} catch (e: any) {
console.warn(`Could not remove links: ${e.message}`);
} catch (e: unknown) {
console.warn(`Could not remove links: ${getErrorMessage(e)}`);
}
}
if (options.removeStructureTree) {
try {
removeStructureTreeFromDoc(pdfDoc);
} catch (e: any) {
console.warn(`Could not remove structure tree: ${e.message}`);
} catch (e: unknown) {
console.warn(`Could not remove structure tree: ${getErrorMessage(e)}`);
}
}
if (options.removeMarkInfo) {
try {
removeMarkInfoFromDoc(pdfDoc);
} catch (e: any) {
console.warn(`Could not remove MarkInfo: ${e.message}`);
} catch (e: unknown) {
console.warn(`Could not remove MarkInfo: ${getErrorMessage(e)}`);
}
}
if (options.removeFonts) {
try {
removeFontsFromDoc(pdfDoc);
} catch (e: any) {
console.warn(`Could not remove fonts: ${e.message}`);
} catch (e: unknown) {
console.warn(`Could not remove fonts: ${getErrorMessage(e)}`);
}
}
+1 -2
View File
@@ -1,4 +1,3 @@
import { getLibreOfficeConverter } from './libreoffice-loader.js';
import { isWasmAvailable, getWasmBaseUrl } from '../config/wasm-cdn-config.js';
export enum PreloadStatus {
@@ -77,7 +76,7 @@ async function preloadGhostscript(): Promise<void> {
await import('./ghostscript-loader.js');
const gsModule = await loadGsModule();
setCachedGsModule(gsModule as any);
setCachedGsModule(gsModule);
preloadState.ghostscript = PreloadStatus.READY;
console.log('[Preloader] Ghostscript WASM ready');
} catch (e) {