Files
bentopdf/src/js/utils/helpers.ts

305 lines
8.6 KiB
TypeScript

import createModule from '@neslinesli93/qpdf-wasm';
import { showLoader, hideLoader, showAlert } from '../ui.js';
import { createIcons } from 'lucide';
import { state, resetState } from '../state.js';
import * as pdfjsLib from 'pdfjs-dist'
const STANDARD_SIZES = {
A4: { width: 595.28, height: 841.89 },
Letter: { width: 612, height: 792 },
Legal: { width: 612, height: 1008 },
Tabloid: { width: 792, height: 1224 },
A3: { width: 841.89, height: 1190.55 },
A5: { width: 419.53, height: 595.28 },
};
export function getStandardPageName(width: any, height: any) {
const tolerance = 1; // Allow for minor floating point variations
for (const [name, size] of Object.entries(STANDARD_SIZES)) {
if (
(Math.abs(width - size.width) < tolerance &&
Math.abs(height - size.height) < tolerance) ||
(Math.abs(width - size.height) < tolerance &&
Math.abs(height - size.width) < tolerance)
) {
return name;
}
}
return 'Custom';
}
export function convertPoints(points: any, unit: any) {
let result = 0;
switch (unit) {
case 'in':
result = points / 72;
break;
case 'mm':
result = (points / 72) * 25.4;
break;
case 'px':
result = points * (96 / 72); // Assuming 96 DPI
break;
default: // 'pt'
result = points;
break;
}
return result.toFixed(2);
}
// Convert hex color to RGB
export function hexToRgb(hex: string): { r: number; g: number; b: number } {
const result = /^#?([a-f\d]{2})([a-f\d]{2})([a-f\d]{2})$/i.exec(hex)
return result
? {
r: parseInt(result[1], 16) / 255,
g: parseInt(result[2], 16) / 255,
b: parseInt(result[3], 16) / 255,
}
: { r: 0, g: 0, b: 0 }
}
export const formatBytes = (bytes: any, decimals = 1) => {
if (bytes === 0) return '0 Bytes';
const k = 1024;
const dm = decimals < 0 ? 0 : decimals;
const sizes = ['Bytes', 'KB', 'MB', 'GB', 'TB'];
const i = Math.floor(Math.log(bytes) / Math.log(k));
return parseFloat((bytes / Math.pow(k, i)).toFixed(dm)) + ' ' + sizes[i];
};
export const downloadFile = (blob: Blob, filename: string): void => {
const url = URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url;
a.download = filename;
document.body.appendChild(a);
a.click();
document.body.removeChild(a);
URL.revokeObjectURL(url);
};
export const readFileAsArrayBuffer = (file: any) => {
return new Promise((resolve, reject) => {
const reader = new FileReader();
reader.onload = () => resolve(reader.result);
reader.onerror = (error) => reject(error);
reader.readAsArrayBuffer(file);
});
};
export function parsePageRanges(rangeString: string, totalPages: number): number[] {
if (!rangeString || rangeString.trim() === '') {
return Array.from({ length: totalPages }, (_, i) => i);
}
const indices = new Set<number>();
const parts = rangeString.split(',');
for (const part of parts) {
const trimmedPart = part.trim();
if (!trimmedPart) continue;
if (trimmedPart.includes('-')) {
const [start, end] = trimmedPart.split('-').map(Number);
if (
isNaN(start) ||
isNaN(end) ||
start < 1 ||
end > totalPages ||
start > end
) {
console.warn(`Invalid range skipped: ${trimmedPart}`);
continue;
}
for (let i = start; i <= end; i++) {
indices.add(i - 1);
}
} else {
const pageNum = Number(trimmedPart);
if (isNaN(pageNum) || pageNum < 1 || pageNum > totalPages) {
console.warn(`Invalid page number skipped: ${trimmedPart}`);
continue;
}
indices.add(pageNum - 1);
}
}
return Array.from(indices).sort((a, b) => a - b);
}
/**
* Formats an ISO 8601 date string (e.g., "2008-02-21T17:15:56-08:00")
* into a localized, human-readable string.
* @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) {
if (!isoDateString || typeof isoDateString !== 'string') {
return isoDateString; // Return original value if it's not a valid string
}
try {
const date = new Date(isoDateString);
// Check if the date object is valid
if (isNaN(date.getTime())) {
return isoDateString; // Return original string if the date is invalid
}
return date.toLocaleString();
} catch (e) {
console.error('Could not parse ISO date:', e);
return isoDateString; // Return original string on any error
}
}
let qpdfInstance: any = null;
/**
* Initialize qpdf-wasm singleton.
* Subsequent calls return the same instance.
*/
export async function initializeQpdf() {
if (qpdfInstance) return qpdfInstance;
showLoader('Initializing PDF engine...');
try {
qpdfInstance = await createModule({
locateFile: () => import.meta.env.BASE_URL + 'qpdf.wasm',
});
} catch (error) {
console.error('Failed to initialize qpdf-wasm:', error);
showAlert(
'Initialization Error',
'Could not load the PDF engine. Please refresh the page and try again.'
);
throw error;
} finally {
hideLoader();
}
return qpdfInstance;
}
export function initializeIcons(): void {
createIcons({
attrs: {
class: 'bento-icon',
'stroke-width': '1.5',
},
});
}
export function formatStars(num: number) {
if (num >= 1000) {
return (num / 1000).toFixed(1) + 'K';
}
return num.toLocaleString();
};
/**
* Truncates a filename to a maximum length, adding ellipsis if needed.
* Preserves the file extension.
* @param filename - The filename to truncate
* @param maxLength - Maximum length (default: 30)
* @returns Truncated filename with ellipsis if needed
*/
export function truncateFilename(filename: string, maxLength: number = 25): string {
if (filename.length <= maxLength) {
return filename;
}
const lastDotIndex = filename.lastIndexOf('.');
const extension = lastDotIndex !== -1 ? filename.substring(lastDotIndex) : '';
const nameWithoutExt = lastDotIndex !== -1 ? filename.substring(0, lastDotIndex) : filename;
const availableLength = maxLength - extension.length - 3; // 3 for '...'
if (availableLength <= 0) {
return filename.substring(0, maxLength - 3) + '...';
}
return nameWithoutExt.substring(0, availableLength) + '...' + extension;
}
export function formatShortcutDisplay(shortcut: string, isMac: boolean): string {
if (!shortcut) return '';
return shortcut
.replace('mod', isMac ? '⌘' : 'Ctrl')
.replace('ctrl', isMac ? '^' : 'Ctrl') // Control key on Mac shows as ^
.replace('alt', isMac ? '⌥' : 'Alt')
.replace('shift', 'Shift')
.split('+')
.map(k => k.charAt(0).toUpperCase() + k.slice(1))
.join(isMac ? '' : '+');
}
export function resetAndReloadTool(preResetCallback?: () => void) {
const toolid = state.activeTool;
if (preResetCallback) {
preResetCallback();
}
resetState();
if (toolid) {
const element = document.querySelector(
`[data-tool-id="${toolid}"]`
) as HTMLElement;
if (element) element.click();
}
}
/**
* Wrapper for pdfjsLib.getDocument that adds the required wasmUrl configuration.
* Use this instead of calling pdfjsLib.getDocument directly.
* @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;
// 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 };
}
// 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/',
});
}
/**
* Returns a sanitized PDF filename.
*
* The provided filename is processed as follows:
* - Removes a trailing `.pdf` file extension (case-insensitive)
* - Trims leading and trailing whitespace
* - Truncates the name to a maximum of 80 characters
*
* @param filename The original filename (including extension)
* @returns The sanitized filename without the `.pdf` extension, limited to 80 characters
*/
export function getCleanPdfFilename(filename: string): string {
let clean = filename.replace(/\.pdf$/i, '').trim();
if (clean.length > 80) {
clean = clean.slice(0, 80);
}
return clean;
}