feat:Setup Prettier for code formatting

This commit is contained in:
NanditaPatil-dotcom
2025-10-17 11:37:32 +05:30
parent 87c191213c
commit f1d830d81d
96 changed files with 9420 additions and 7154 deletions

View File

@@ -4,48 +4,53 @@ import { state } from '../state.js';
import { PDFDocument as PDFLibDocument } from 'pdf-lib';
export async function addBlankPage() {
// @ts-expect-error TS(2339) FIXME: Property 'value' does not exist on type 'HTMLEleme... Remove this comment to see the full error message
const pageNumberInput = document.getElementById('page-number').value;
if (pageNumberInput.trim() === '') {
showAlert('Invalid Input', 'Please enter a page number.');
return;
// @ts-expect-error TS(2339) FIXME: Property 'value' does not exist on type 'HTMLEleme... Remove this comment to see the full error message
const pageNumberInput = document.getElementById('page-number').value;
if (pageNumberInput.trim() === '') {
showAlert('Invalid Input', 'Please enter a page number.');
return;
}
const position = parseInt(pageNumberInput);
const totalPages = state.pdfDoc.getPageCount();
if (isNaN(position) || position < 0 || position > totalPages) {
showAlert(
'Invalid Input',
`Please enter a number between 0 and ${totalPages}.`
);
return;
}
showLoader('Adding page...');
try {
const newPdf = await PDFLibDocument.create();
const { width, height } = state.pdfDoc.getPage(0).getSize();
const allIndices = Array.from({ length: totalPages }, (_, i) => i);
const indicesBefore = allIndices.slice(0, position);
const indicesAfter = allIndices.slice(position);
if (indicesBefore.length > 0) {
const copied = await newPdf.copyPages(state.pdfDoc, indicesBefore);
copied.forEach((p: any) => newPdf.addPage(p));
}
const position = parseInt(pageNumberInput);
const totalPages = state.pdfDoc.getPageCount();
if (isNaN(position) || position < 0 || position > totalPages) {
showAlert('Invalid Input', `Please enter a number between 0 and ${totalPages}.`);
return;
newPdf.addPage([width, height]);
if (indicesAfter.length > 0) {
const copied = await newPdf.copyPages(state.pdfDoc, indicesAfter);
copied.forEach((p: any) => newPdf.addPage(p));
}
showLoader('Adding page...');
try {
const newPdf = await PDFLibDocument.create();
const { width, height } = state.pdfDoc.getPage(0).getSize();
const allIndices = Array.from({ length: totalPages }, (_, i) => i);
const indicesBefore = allIndices.slice(0, position);
const indicesAfter = allIndices.slice(position);
if (indicesBefore.length > 0) {
const copied = await newPdf.copyPages(state.pdfDoc, indicesBefore);
copied.forEach((p: any) => newPdf.addPage(p));
}
newPdf.addPage([width, height]);
if (indicesAfter.length > 0) {
const copied = await newPdf.copyPages(state.pdfDoc, indicesAfter);
copied.forEach((p: any) => newPdf.addPage(p));
}
const newPdfBytes = await newPdf.save();
downloadFile(new Blob([new Uint8Array(newPdfBytes)], { type: 'application/pdf' }), 'page-added.pdf');
} catch (e) {
console.error(e);
showAlert('Error', 'Could not add a blank page.');
} finally {
hideLoader();
}
}
const newPdfBytes = await newPdf.save();
downloadFile(
new Blob([new Uint8Array(newPdfBytes)], { type: 'application/pdf' }),
'page-added.pdf'
);
} catch (e) {
console.error(e);
showAlert('Error', 'Could not add a blank page.');
} finally {
hideLoader();
}
}

View File

@@ -4,98 +4,154 @@ import { state } from '../state.js';
import { PDFDocument as PDFLibDocument, rgb, StandardFonts } from 'pdf-lib';
export function setupHeaderFooterUI() {
const totalPagesSpan = document.getElementById('total-pages');
if (totalPagesSpan && state.pdfDoc) {
totalPagesSpan.textContent = state.pdfDoc.getPageCount();
}
const totalPagesSpan = document.getElementById('total-pages');
if (totalPagesSpan && state.pdfDoc) {
totalPagesSpan.textContent = state.pdfDoc.getPageCount();
}
}
export async function addHeaderFooter() {
showLoader('Adding header & footer...');
try {
const helveticaFont = await state.pdfDoc.embedFont(StandardFonts.Helvetica);
const allPages = state.pdfDoc.getPages();
const totalPages = allPages.length;
const margin = 40;
showLoader('Adding header & footer...');
try {
const helveticaFont = await state.pdfDoc.embedFont(StandardFonts.Helvetica);
const allPages = state.pdfDoc.getPages();
const totalPages = allPages.length;
const margin = 40;
// --- 1. Get new formatting options from the UI ---
// @ts-expect-error TS(2339) FIXME: Property 'value' does not exist on type 'HTMLEleme... Remove this comment to see the full error message
const fontSize = parseInt(document.getElementById('font-size').value) || 10;
// @ts-expect-error TS(2339) FIXME: Property 'value' does not exist on type 'HTMLEleme... Remove this comment to see the full error message
const colorHex = document.getElementById('font-color').value;
const fontColor = hexToRgb(colorHex);
// @ts-expect-error TS(2339) FIXME: Property 'value' does not exist on type 'HTMLEleme... Remove this comment to see the full error message
const pageRangeInput = document.getElementById('page-range').value;
// --- 1. Get new formatting options from the UI ---
// @ts-expect-error TS(2339) FIXME: Property 'value' does not exist on type 'HTMLEleme... Remove this comment to see the full error message
const fontSize = parseInt(document.getElementById('font-size').value) || 10;
// @ts-expect-error TS(2339) FIXME: Property 'value' does not exist on type 'HTMLEleme... Remove this comment to see the full error message
const colorHex = document.getElementById('font-color').value;
const fontColor = hexToRgb(colorHex);
// @ts-expect-error TS(2339) FIXME: Property 'value' does not exist on type 'HTMLEleme... Remove this comment to see the full error message
const pageRangeInput = document.getElementById('page-range').value;
// --- 2. Get text values ---
const texts = {
// @ts-expect-error TS(2339) FIXME: Property 'value' does not exist on type 'HTMLEleme... Remove this comment to see the full error message
headerLeft: document.getElementById('header-left').value,
// @ts-expect-error TS(2339) FIXME: Property 'value' does not exist on type 'HTMLEleme... Remove this comment to see the full error message
headerCenter: document.getElementById('header-center').value,
// @ts-expect-error TS(2339) FIXME: Property 'value' does not exist on type 'HTMLEleme... Remove this comment to see the full error message
headerRight: document.getElementById('header-right').value,
// @ts-expect-error TS(2339) FIXME: Property 'value' does not exist on type 'HTMLEleme... Remove this comment to see the full error message
footerLeft: document.getElementById('footer-left').value,
// @ts-expect-error TS(2339) FIXME: Property 'value' does not exist on type 'HTMLEleme... Remove this comment to see the full error message
footerCenter: document.getElementById('footer-center').value,
// @ts-expect-error TS(2339) FIXME: Property 'value' does not exist on type 'HTMLEleme... Remove this comment to see the full error message
footerRight: document.getElementById('footer-right').value,
};
// --- 2. Get text values ---
const texts = {
// @ts-expect-error TS(2339) FIXME: Property 'value' does not exist on type 'HTMLEleme... Remove this comment to see the full error message
headerLeft: document.getElementById('header-left').value,
// @ts-expect-error TS(2339) FIXME: Property 'value' does not exist on type 'HTMLEleme... Remove this comment to see the full error message
headerCenter: document.getElementById('header-center').value,
// @ts-expect-error TS(2339) FIXME: Property 'value' does not exist on type 'HTMLEleme... Remove this comment to see the full error message
headerRight: document.getElementById('header-right').value,
// @ts-expect-error TS(2339) FIXME: Property 'value' does not exist on type 'HTMLEleme... Remove this comment to see the full error message
footerLeft: document.getElementById('footer-left').value,
// @ts-expect-error TS(2339) FIXME: Property 'value' does not exist on type 'HTMLEleme... Remove this comment to see the full error message
footerCenter: document.getElementById('footer-center').value,
// @ts-expect-error TS(2339) FIXME: Property 'value' does not exist on type 'HTMLEleme... Remove this comment to see the full error message
footerRight: document.getElementById('footer-right').value,
};
// --- 3. Parse page range to determine which pages to modify ---
const indicesToProcess = parsePageRanges(pageRangeInput, totalPages);
if (indicesToProcess.length === 0) {
throw new Error("Invalid page range specified. Please check your input (e.g., '1-3, 5').");
}
// --- 4. Define drawing options with new values ---
const drawOptions = {
font: helveticaFont,
size: fontSize,
color: rgb(fontColor.r, fontColor.g, fontColor.b)
};
// --- 5. Loop over only the selected pages ---
for (const pageIndex of indicesToProcess) {
// @ts-expect-error TS(2538) FIXME: Type 'unknown' cannot be used as an index type.
const page = allPages[pageIndex];
const { width, height } = page.getSize();
// @ts-expect-error TS(2365) FIXME: Operator '+' cannot be applied to types 'unknown' ... Remove this comment to see the full error message
const pageNumber = pageIndex + 1; // For dynamic text
// Helper to replace placeholders like {page} and {total}
const processText = (text: any) => text
.replace(/{page}/g, pageNumber)
.replace(/{total}/g, totalPages);
// Get processed text for the current page
const processedTexts = {
headerLeft: processText(texts.headerLeft),
headerCenter: processText(texts.headerCenter),
headerRight: processText(texts.headerRight),
footerLeft: processText(texts.footerLeft),
footerCenter: processText(texts.footerCenter),
footerRight: processText(texts.footerRight),
};
if (processedTexts.headerLeft) page.drawText(processedTexts.headerLeft, { ...drawOptions, x: margin, y: height - margin });
if (processedTexts.headerCenter) page.drawText(processedTexts.headerCenter, { ...drawOptions, x: (width / 2) - helveticaFont.widthOfTextAtSize(processedTexts.headerCenter, fontSize) / 2, y: height - margin });
if (processedTexts.headerRight) page.drawText(processedTexts.headerRight, { ...drawOptions, x: width - margin - helveticaFont.widthOfTextAtSize(processedTexts.headerRight, fontSize), y: height - margin });
if (processedTexts.footerLeft) page.drawText(processedTexts.footerLeft, { ...drawOptions, x: margin, y: margin });
if (processedTexts.footerCenter) page.drawText(processedTexts.footerCenter, { ...drawOptions, x: (width / 2) - helveticaFont.widthOfTextAtSize(processedTexts.footerCenter, fontSize) / 2, y: margin });
if (processedTexts.footerRight) page.drawText(processedTexts.footerRight, { ...drawOptions, x: width - margin - helveticaFont.widthOfTextAtSize(processedTexts.footerRight, fontSize), y: margin });
}
const newPdfBytes = await state.pdfDoc.save();
downloadFile(new Blob([newPdfBytes], { type: 'application/pdf' }), 'header-footer-added.pdf');
} catch (e) {
console.error(e);
showAlert('Error', e.message || 'Could not add header or footer.');
} finally {
hideLoader();
// --- 3. Parse page range to determine which pages to modify ---
const indicesToProcess = parsePageRanges(pageRangeInput, totalPages);
if (indicesToProcess.length === 0) {
throw new Error(
"Invalid page range specified. Please check your input (e.g., '1-3, 5')."
);
}
}
// --- 4. Define drawing options with new values ---
const drawOptions = {
font: helveticaFont,
size: fontSize,
color: rgb(fontColor.r, fontColor.g, fontColor.b),
};
// --- 5. Loop over only the selected pages ---
for (const pageIndex of indicesToProcess) {
// @ts-expect-error TS(2538) FIXME: Type 'unknown' cannot be used as an index type.
const page = allPages[pageIndex];
const { width, height } = page.getSize();
// @ts-expect-error TS(2365) FIXME: Operator '+' cannot be applied to types 'unknown' ... Remove this comment to see the full error message
const pageNumber = pageIndex + 1; // For dynamic text
// Helper to replace placeholders like {page} and {total}
const processText = (text: any) =>
text.replace(/{page}/g, pageNumber).replace(/{total}/g, totalPages);
// Get processed text for the current page
const processedTexts = {
headerLeft: processText(texts.headerLeft),
headerCenter: processText(texts.headerCenter),
headerRight: processText(texts.headerRight),
footerLeft: processText(texts.footerLeft),
footerCenter: processText(texts.footerCenter),
footerRight: processText(texts.footerRight),
};
if (processedTexts.headerLeft)
page.drawText(processedTexts.headerLeft, {
...drawOptions,
x: margin,
y: height - margin,
});
if (processedTexts.headerCenter)
page.drawText(processedTexts.headerCenter, {
...drawOptions,
x:
width / 2 -
helveticaFont.widthOfTextAtSize(
processedTexts.headerCenter,
fontSize
) /
2,
y: height - margin,
});
if (processedTexts.headerRight)
page.drawText(processedTexts.headerRight, {
...drawOptions,
x:
width -
margin -
helveticaFont.widthOfTextAtSize(
processedTexts.headerRight,
fontSize
),
y: height - margin,
});
if (processedTexts.footerLeft)
page.drawText(processedTexts.footerLeft, {
...drawOptions,
x: margin,
y: margin,
});
if (processedTexts.footerCenter)
page.drawText(processedTexts.footerCenter, {
...drawOptions,
x:
width / 2 -
helveticaFont.widthOfTextAtSize(
processedTexts.footerCenter,
fontSize
) /
2,
y: margin,
});
if (processedTexts.footerRight)
page.drawText(processedTexts.footerRight, {
...drawOptions,
x:
width -
margin -
helveticaFont.widthOfTextAtSize(
processedTexts.footerRight,
fontSize
),
y: margin,
});
}
const newPdfBytes = await state.pdfDoc.save();
downloadFile(
new Blob([newPdfBytes], { type: 'application/pdf' }),
'header-footer-added.pdf'
);
} catch (e) {
console.error(e);
showAlert('Error', e.message || 'Could not add header or footer.');
} finally {
hideLoader();
}
}

View File

@@ -5,97 +5,133 @@ import { state } from '../state.js';
import { rgb, StandardFonts } from 'pdf-lib';
export async function addPageNumbers() {
showLoader('Adding page numbers...');
try {
// @ts-expect-error TS(2339) FIXME: Property 'value' does not exist on type 'HTMLEleme... Remove this comment to see the full error message
const position = document.getElementById('position').value;
// @ts-expect-error TS(2339) FIXME: Property 'value' does not exist on type 'HTMLEleme... Remove this comment to see the full error message
const fontSize = parseInt(document.getElementById('font-size').value) || 12;
// @ts-expect-error TS(2339) FIXME: Property 'value' does not exist on type 'HTMLEleme... Remove this comment to see the full error message
const format = document.getElementById('number-format').value;
// @ts-expect-error TS(2339) FIXME: Property 'value' does not exist on type 'HTMLEleme... Remove this comment to see the full error message
const colorHex = document.getElementById('text-color').value;
const textColor = hexToRgb(colorHex);
showLoader('Adding page numbers...');
try {
// @ts-expect-error TS(2339) FIXME: Property 'value' does not exist on type 'HTMLEleme... Remove this comment to see the full error message
const position = document.getElementById('position').value;
// @ts-expect-error TS(2339) FIXME: Property 'value' does not exist on type 'HTMLEleme... Remove this comment to see the full error message
const fontSize = parseInt(document.getElementById('font-size').value) || 12;
// @ts-expect-error TS(2339) FIXME: Property 'value' does not exist on type 'HTMLEleme... Remove this comment to see the full error message
const format = document.getElementById('number-format').value;
// @ts-expect-error TS(2339) FIXME: Property 'value' does not exist on type 'HTMLEleme... Remove this comment to see the full error message
const colorHex = document.getElementById('text-color').value;
const textColor = hexToRgb(colorHex);
const pages = state.pdfDoc.getPages();
const totalPages = pages.length;
const helveticaFont = await state.pdfDoc.embedFont(StandardFonts.Helvetica);
const pages = state.pdfDoc.getPages();
const totalPages = pages.length;
const helveticaFont = await state.pdfDoc.embedFont(StandardFonts.Helvetica);
for (let i = 0; i < totalPages; i++) {
const page = pages[i];
const mediaBox = page.getMediaBox();
const cropBox = page.getCropBox();
const bounds = cropBox || mediaBox;
const width = bounds.width;
const height = bounds.height;
const xOffset = bounds.x || 0;
const yOffset = bounds.y || 0;
let pageNumText = (format === 'page_x_of_y') ? `${i + 1} / ${totalPages}` : `${i + 1}`;
for (let i = 0; i < totalPages; i++) {
const page = pages[i];
const textWidth = helveticaFont.widthOfTextAtSize(pageNumText, fontSize);
const textHeight = fontSize;
const minMargin = 8;
const maxMargin = 40;
const marginPercentage = 0.04;
const horizontalMargin = Math.max(minMargin, Math.min(maxMargin, width * marginPercentage));
const verticalMargin = Math.max(minMargin, Math.min(maxMargin, height * marginPercentage));
// Ensure text doesn't go outside visible page boundaries
const safeHorizontalMargin = Math.max(horizontalMargin, textWidth / 2 + 3);
const safeVerticalMargin = Math.max(verticalMargin, textHeight + 3);
let x, y;
const mediaBox = page.getMediaBox();
const cropBox = page.getCropBox();
const bounds = cropBox || mediaBox;
const width = bounds.width;
const height = bounds.height;
const xOffset = bounds.x || 0;
const yOffset = bounds.y || 0;
switch (position) {
case 'bottom-center':
x = Math.max(safeHorizontalMargin, Math.min(width - safeHorizontalMargin - textWidth, (width - textWidth) / 2)) + xOffset;
y = safeVerticalMargin + yOffset;
break;
case 'bottom-left':
x = safeHorizontalMargin + xOffset;
y = safeVerticalMargin + yOffset;
break;
case 'bottom-right':
x = Math.max(safeHorizontalMargin, width - safeHorizontalMargin - textWidth) + xOffset;
y = safeVerticalMargin + yOffset;
break;
case 'top-center':
x = Math.max(safeHorizontalMargin, Math.min(width - safeHorizontalMargin - textWidth, (width - textWidth) / 2)) + xOffset;
y = height - safeVerticalMargin - textHeight + yOffset;
break;
case 'top-left':
x = safeHorizontalMargin + xOffset;
y = height - safeVerticalMargin - textHeight + yOffset;
break;
case 'top-right':
x = Math.max(safeHorizontalMargin, width - safeHorizontalMargin - textWidth) + xOffset;
y = height - safeVerticalMargin - textHeight + yOffset;
break;
}
let pageNumText =
format === 'page_x_of_y' ? `${i + 1} / ${totalPages}` : `${i + 1}`;
// Final safety check to ensure coordinates are within visible page bounds
x = Math.max(xOffset + 3, Math.min(xOffset + width - textWidth - 3, x));
y = Math.max(yOffset + 3, Math.min(yOffset + height - textHeight - 3, y));
const textWidth = helveticaFont.widthOfTextAtSize(pageNumText, fontSize);
const textHeight = fontSize;
page.drawText(pageNumText, {
x, y,
font: helveticaFont,
size: fontSize,
color: rgb(textColor.r, textColor.g, textColor.b)
});
}
const minMargin = 8;
const maxMargin = 40;
const marginPercentage = 0.04;
const newPdfBytes = await state.pdfDoc.save();
downloadFile(new Blob([newPdfBytes], { type: 'application/pdf' }), 'paginated.pdf');
showAlert('Success', 'Page numbers added successfully!');
} catch (e) {
console.error(e);
showAlert('Error', 'Could not add page numbers.');
} finally {
hideLoader();
const horizontalMargin = Math.max(
minMargin,
Math.min(maxMargin, width * marginPercentage)
);
const verticalMargin = Math.max(
minMargin,
Math.min(maxMargin, height * marginPercentage)
);
// Ensure text doesn't go outside visible page boundaries
const safeHorizontalMargin = Math.max(
horizontalMargin,
textWidth / 2 + 3
);
const safeVerticalMargin = Math.max(verticalMargin, textHeight + 3);
let x, y;
switch (position) {
case 'bottom-center':
x =
Math.max(
safeHorizontalMargin,
Math.min(
width - safeHorizontalMargin - textWidth,
(width - textWidth) / 2
)
) + xOffset;
y = safeVerticalMargin + yOffset;
break;
case 'bottom-left':
x = safeHorizontalMargin + xOffset;
y = safeVerticalMargin + yOffset;
break;
case 'bottom-right':
x =
Math.max(
safeHorizontalMargin,
width - safeHorizontalMargin - textWidth
) + xOffset;
y = safeVerticalMargin + yOffset;
break;
case 'top-center':
x =
Math.max(
safeHorizontalMargin,
Math.min(
width - safeHorizontalMargin - textWidth,
(width - textWidth) / 2
)
) + xOffset;
y = height - safeVerticalMargin - textHeight + yOffset;
break;
case 'top-left':
x = safeHorizontalMargin + xOffset;
y = height - safeVerticalMargin - textHeight + yOffset;
break;
case 'top-right':
x =
Math.max(
safeHorizontalMargin,
width - safeHorizontalMargin - textWidth
) + xOffset;
y = height - safeVerticalMargin - textHeight + yOffset;
break;
}
// Final safety check to ensure coordinates are within visible page bounds
x = Math.max(xOffset + 3, Math.min(xOffset + width - textWidth - 3, x));
y = Math.max(yOffset + 3, Math.min(yOffset + height - textHeight - 3, y));
page.drawText(pageNumText, {
x,
y,
font: helveticaFont,
size: fontSize,
color: rgb(textColor.r, textColor.g, textColor.b),
});
}
}
const newPdfBytes = await state.pdfDoc.save();
downloadFile(
new Blob([newPdfBytes], { type: 'application/pdf' }),
'paginated.pdf'
);
showAlert('Success', 'Page numbers added successfully!');
} catch (e) {
console.error(e);
showAlert('Error', 'Could not add page numbers.');
} finally {
hideLoader();
}
}

View File

@@ -1,132 +1,172 @@
import { showLoader, hideLoader, showAlert } from '../ui.js';
import { downloadFile, readFileAsArrayBuffer, hexToRgb } from '../utils/helpers.js';
import {
downloadFile,
readFileAsArrayBuffer,
hexToRgb,
} from '../utils/helpers.js';
import { state } from '../state.js';
import { PDFDocument as PDFLibDocument, rgb, degrees, StandardFonts } from 'pdf-lib';
import {
PDFDocument as PDFLibDocument,
rgb,
degrees,
StandardFonts,
} from 'pdf-lib';
export function setupWatermarkUI() {
const watermarkTypeRadios = document.querySelectorAll('input[name="watermark-type"]');
const textOptions = document.getElementById('text-watermark-options');
const imageOptions = document.getElementById('image-watermark-options');
const watermarkTypeRadios = document.querySelectorAll(
'input[name="watermark-type"]'
);
const textOptions = document.getElementById('text-watermark-options');
const imageOptions = document.getElementById('image-watermark-options');
watermarkTypeRadios.forEach(radio => {
radio.addEventListener('change', (e) => {
// @ts-expect-error TS(2339) FIXME: Property 'value' does not exist on type 'EventTarg... Remove this comment to see the full error message
if (e.target.value === 'text') {
textOptions.classList.remove('hidden');
imageOptions.classList.add('hidden');
} else {
textOptions.classList.add('hidden');
imageOptions.classList.remove('hidden');
}
});
watermarkTypeRadios.forEach((radio) => {
radio.addEventListener('change', (e) => {
// @ts-expect-error TS(2339) FIXME: Property 'value' does not exist on type 'EventTarg... Remove this comment to see the full error message
if (e.target.value === 'text') {
textOptions.classList.remove('hidden');
imageOptions.classList.add('hidden');
} else {
textOptions.classList.add('hidden');
imageOptions.classList.remove('hidden');
}
});
});
const opacitySliderText = document.getElementById('opacity-text');
const opacityValueText = document.getElementById('opacity-value-text');
const angleSliderText = document.getElementById('angle-text');
const angleValueText = document.getElementById('angle-value-text');
// @ts-expect-error TS(2339) FIXME: Property 'value' does not exist on type 'HTMLEleme... Remove this comment to see the full error message
opacitySliderText.addEventListener('input', () => opacityValueText.textContent = opacitySliderText.value);
// @ts-expect-error TS(2339) FIXME: Property 'value' does not exist on type 'HTMLEleme... Remove this comment to see the full error message
angleSliderText.addEventListener('input', () => angleValueText.textContent = angleSliderText.value);
const opacitySliderText = document.getElementById('opacity-text');
const opacityValueText = document.getElementById('opacity-value-text');
const angleSliderText = document.getElementById('angle-text');
const angleValueText = document.getElementById('angle-value-text');
const opacitySliderImage = document.getElementById('opacity-image');
const opacityValueImage = document.getElementById('opacity-value-image');
const angleSliderImage = document.getElementById('angle-image');
const angleValueImage = document.getElementById('angle-value-image');
// @ts-expect-error TS(2339) FIXME: Property 'value' does not exist on type 'HTMLEleme... Remove this comment to see the full error message
opacitySliderText.addEventListener(
'input',
() => (opacityValueText.textContent = opacitySliderText.value)
);
// @ts-expect-error TS(2339) FIXME: Property 'value' does not exist on type 'HTMLEleme... Remove this comment to see the full error message
angleSliderText.addEventListener(
'input',
() => (angleValueText.textContent = angleSliderText.value)
);
// @ts-expect-error TS(2339) FIXME: Property 'value' does not exist on type 'HTMLEleme... Remove this comment to see the full error message
opacitySliderImage.addEventListener('input', () => opacityValueImage.textContent = opacitySliderImage.value);
// @ts-expect-error TS(2339) FIXME: Property 'value' does not exist on type 'HTMLEleme... Remove this comment to see the full error message
angleSliderImage.addEventListener('input', () => angleValueImage.textContent = angleSliderImage.value);
const opacitySliderImage = document.getElementById('opacity-image');
const opacityValueImage = document.getElementById('opacity-value-image');
const angleSliderImage = document.getElementById('angle-image');
const angleValueImage = document.getElementById('angle-value-image');
// @ts-expect-error TS(2339) FIXME: Property 'value' does not exist on type 'HTMLEleme... Remove this comment to see the full error message
opacitySliderImage.addEventListener(
'input',
() => (opacityValueImage.textContent = opacitySliderImage.value)
);
// @ts-expect-error TS(2339) FIXME: Property 'value' does not exist on type 'HTMLEleme... Remove this comment to see the full error message
angleSliderImage.addEventListener(
'input',
() => (angleValueImage.textContent = angleSliderImage.value)
);
}
export async function addWatermark() {
// @ts-expect-error TS(2339) FIXME: Property 'value' does not exist on type 'Element'.
const watermarkType = document.querySelector('input[name="watermark-type"]:checked').value;
showLoader('Adding watermark...');
// @ts-expect-error TS(2339) FIXME: Property 'value' does not exist on type 'Element'.
const watermarkType = document.querySelector(
'input[name="watermark-type"]:checked'
).value;
try {
const pages = state.pdfDoc.getPages();
let watermarkAsset = null;
showLoader('Adding watermark...');
if (watermarkType === 'text') {
watermarkAsset = await state.pdfDoc.embedFont(StandardFonts.Helvetica);
} else { // 'image'
// @ts-expect-error TS(2339) FIXME: Property 'files' does not exist on type 'HTMLEleme... Remove this comment to see the full error message
const imageFile = document.getElementById('image-watermark-input').files[0];
if (!imageFile) throw new Error('Please select an image file for the watermark.');
const imageBytes = await readFileAsArrayBuffer(imageFile);
if (imageFile.type === 'image/png') {
watermarkAsset = await state.pdfDoc.embedPng(imageBytes);
} else if (imageFile.type === 'image/jpeg') {
watermarkAsset = await state.pdfDoc.embedJpg(imageBytes);
} else {
throw new Error('Unsupported Image. Please use a PNG or JPG for the watermark.');
}
}
try {
const pages = state.pdfDoc.getPages();
let watermarkAsset = null;
for (const page of pages) {
const { width, height } = page.getSize();
if (watermarkType === 'text') {
watermarkAsset = await state.pdfDoc.embedFont(StandardFonts.Helvetica);
} else {
// 'image'
// @ts-expect-error TS(2339) FIXME: Property 'files' does not exist on type 'HTMLEleme... Remove this comment to see the full error message
const imageFile = document.getElementById('image-watermark-input')
.files[0];
if (!imageFile)
throw new Error('Please select an image file for the watermark.');
if (watermarkType === 'text') {
// @ts-expect-error TS(2339) FIXME: Property 'value' does not exist on type 'HTMLEleme... Remove this comment to see the full error message
const text = document.getElementById('watermark-text').value;
if (!text.trim()) throw new Error('Please enter text for the watermark.');
// @ts-expect-error TS(2339) FIXME: Property 'value' does not exist on type 'HTMLEleme... Remove this comment to see the full error message
const fontSize = parseInt(document.getElementById('font-size').value) || 72;
// @ts-expect-error TS(2339) FIXME: Property 'value' does not exist on type 'HTMLEleme... Remove this comment to see the full error message
const angle = parseInt(document.getElementById('angle-text').value) || 0;
// @ts-expect-error TS(2339) FIXME: Property 'value' does not exist on type 'HTMLEleme... Remove this comment to see the full error message
const opacity = parseFloat(document.getElementById('opacity-text').value) || 0.3;
// @ts-expect-error TS(2339) FIXME: Property 'value' does not exist on type 'HTMLEleme... Remove this comment to see the full error message
const colorHex = document.getElementById('text-color').value;
const textColor = hexToRgb(colorHex);
const textWidth = watermarkAsset.widthOfTextAtSize(text, fontSize);
page.drawText(text, {
x: (width - textWidth) / 2,
y: height / 2,
font: watermarkAsset,
size: fontSize,
color: rgb(textColor.r, textColor.g, textColor.b),
opacity: opacity,
rotate: degrees(angle),
});
} else {
// @ts-expect-error TS(2339) FIXME: Property 'value' does not exist on type 'HTMLEleme... Remove this comment to see the full error message
const angle = parseInt(document.getElementById('angle-image').value) || 0;
// @ts-expect-error TS(2339) FIXME: Property 'value' does not exist on type 'HTMLEleme... Remove this comment to see the full error message
const opacity = parseFloat(document.getElementById('opacity-image').value) || 0.3;
const scale = 0.5;
const imgWidth = watermarkAsset.width * scale;
const imgHeight = watermarkAsset.height * scale;
page.drawImage(watermarkAsset, {
x: (width - imgWidth) / 2,
y: (height - imgHeight) / 2,
width: imgWidth,
height: imgHeight,
opacity: opacity,
rotate: degrees(angle),
});
}
}
const newPdfBytes = await state.pdfDoc.save();
downloadFile(new Blob([newPdfBytes], { type: 'application/pdf' }), 'watermarked.pdf');
} catch (e) {
console.error(e);
showAlert('Error', e.message || 'Could not add the watermark. Please check your inputs.');
} finally {
hideLoader();
const imageBytes = await readFileAsArrayBuffer(imageFile);
if (imageFile.type === 'image/png') {
watermarkAsset = await state.pdfDoc.embedPng(imageBytes);
} else if (imageFile.type === 'image/jpeg') {
watermarkAsset = await state.pdfDoc.embedJpg(imageBytes);
} else {
throw new Error(
'Unsupported Image. Please use a PNG or JPG for the watermark.'
);
}
}
for (const page of pages) {
const { width, height } = page.getSize();
if (watermarkType === 'text') {
// @ts-expect-error TS(2339) FIXME: Property 'value' does not exist on type 'HTMLEleme... Remove this comment to see the full error message
const text = document.getElementById('watermark-text').value;
if (!text.trim())
throw new Error('Please enter text for the watermark.');
// @ts-expect-error TS(2339) FIXME: Property 'value' does not exist on type 'HTMLEleme... Remove this comment to see the full error message
const fontSize =
parseInt(document.getElementById('font-size').value) || 72;
// @ts-expect-error TS(2339) FIXME: Property 'value' does not exist on type 'HTMLEleme... Remove this comment to see the full error message
const angle =
parseInt(document.getElementById('angle-text').value) || 0;
// @ts-expect-error TS(2339) FIXME: Property 'value' does not exist on type 'HTMLEleme... Remove this comment to see the full error message
const opacity =
parseFloat(document.getElementById('opacity-text').value) || 0.3;
// @ts-expect-error TS(2339) FIXME: Property 'value' does not exist on type 'HTMLEleme... Remove this comment to see the full error message
const colorHex = document.getElementById('text-color').value;
const textColor = hexToRgb(colorHex);
const textWidth = watermarkAsset.widthOfTextAtSize(text, fontSize);
page.drawText(text, {
x: (width - textWidth) / 2,
y: height / 2,
font: watermarkAsset,
size: fontSize,
color: rgb(textColor.r, textColor.g, textColor.b),
opacity: opacity,
rotate: degrees(angle),
});
} else {
// @ts-expect-error TS(2339) FIXME: Property 'value' does not exist on type 'HTMLEleme... Remove this comment to see the full error message
const angle =
parseInt(document.getElementById('angle-image').value) || 0;
// @ts-expect-error TS(2339) FIXME: Property 'value' does not exist on type 'HTMLEleme... Remove this comment to see the full error message
const opacity =
parseFloat(document.getElementById('opacity-image').value) || 0.3;
const scale = 0.5;
const imgWidth = watermarkAsset.width * scale;
const imgHeight = watermarkAsset.height * scale;
page.drawImage(watermarkAsset, {
x: (width - imgWidth) / 2,
y: (height - imgHeight) / 2,
width: imgWidth,
height: imgHeight,
opacity: opacity,
rotate: degrees(angle),
});
}
}
const newPdfBytes = await state.pdfDoc.save();
downloadFile(
new Blob([newPdfBytes], { type: 'application/pdf' }),
'watermarked.pdf'
);
} catch (e) {
console.error(e);
showAlert(
'Error',
e.message || 'Could not add the watermark. Please check your inputs.'
);
} finally {
hideLoader();
}
}

View File

@@ -5,103 +5,121 @@ import { PDFDocument } from 'pdf-lib';
import Sortable from 'sortablejs';
const alternateMergeState = {
pdfDocs: {},
pdfDocs: {},
};
export async function setupAlternateMergeTool() {
const optionsDiv = document.getElementById('alternate-merge-options');
const processBtn = document.getElementById('process-btn') as HTMLButtonElement;
const fileList = document.getElementById('alternate-file-list');
const optionsDiv = document.getElementById('alternate-merge-options');
const processBtn = document.getElementById(
'process-btn'
) as HTMLButtonElement;
const fileList = document.getElementById('alternate-file-list');
if (!optionsDiv || !processBtn || !fileList) return;
if (!optionsDiv || !processBtn || !fileList) return;
optionsDiv.classList.remove('hidden');
processBtn.disabled = false;
processBtn.onclick = alternateMerge;
fileList.innerHTML = '';
alternateMergeState.pdfDocs = {};
optionsDiv.classList.remove('hidden');
processBtn.disabled = false;
processBtn.onclick = alternateMerge;
showLoader('Loading PDF documents...');
try {
for (const file of state.files) {
const pdfBytes = await readFileAsArrayBuffer(file);
alternateMergeState.pdfDocs[file.name] = await PDFDocument.load(pdfBytes as ArrayBuffer, {
ignoreEncryption: true
});
const pageCount = alternateMergeState.pdfDocs[file.name].getPageCount();
fileList.innerHTML = '';
alternateMergeState.pdfDocs = {};
const li = document.createElement('li');
li.className = 'bg-gray-700 p-3 rounded-lg border border-gray-600 flex items-center justify-between';
li.dataset.fileName = file.name;
const infoDiv = document.createElement('div');
infoDiv.className = 'flex items-center gap-2 truncate';
const nameSpan = document.createElement('span');
nameSpan.className = 'truncate font-medium text-white';
nameSpan.textContent = file.name;
const pagesSpan = document.createElement('span');
pagesSpan.className = 'text-sm text-gray-400 flex-shrink-0';
pagesSpan.textContent = `(${pageCount} pages)`;
infoDiv.append(nameSpan, pagesSpan);
const dragHandle = document.createElement('div');
dragHandle.className = 'drag-handle cursor-move text-gray-400 hover:text-white p-1 rounded';
dragHandle.innerHTML = `<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><circle cx="9" cy="5" r="1"/><circle cx="15" cy="5" r="1"/><circle cx="9" cy="12" r="1"/><circle cx="15" cy="12" r="1"/><circle cx="9" cy="19" r="1"/><circle cx="15" cy="19" r="1"/></svg>`;
li.append(infoDiv, dragHandle);
fileList.appendChild(li);
showLoader('Loading PDF documents...');
try {
for (const file of state.files) {
const pdfBytes = await readFileAsArrayBuffer(file);
alternateMergeState.pdfDocs[file.name] = await PDFDocument.load(
pdfBytes as ArrayBuffer,
{
ignoreEncryption: true,
}
);
const pageCount = alternateMergeState.pdfDocs[file.name].getPageCount();
Sortable.create(fileList, {
handle: '.drag-handle',
animation: 150,
});
const li = document.createElement('li');
li.className =
'bg-gray-700 p-3 rounded-lg border border-gray-600 flex items-center justify-between';
li.dataset.fileName = file.name;
} catch (error) {
showAlert('Error', 'Failed to load one or more PDF files. They may be corrupted or password-protected.');
console.error(error);
} finally {
hideLoader();
const infoDiv = document.createElement('div');
infoDiv.className = 'flex items-center gap-2 truncate';
const nameSpan = document.createElement('span');
nameSpan.className = 'truncate font-medium text-white';
nameSpan.textContent = file.name;
const pagesSpan = document.createElement('span');
pagesSpan.className = 'text-sm text-gray-400 flex-shrink-0';
pagesSpan.textContent = `(${pageCount} pages)`;
infoDiv.append(nameSpan, pagesSpan);
const dragHandle = document.createElement('div');
dragHandle.className =
'drag-handle cursor-move text-gray-400 hover:text-white p-1 rounded';
dragHandle.innerHTML = `<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><circle cx="9" cy="5" r="1"/><circle cx="15" cy="5" r="1"/><circle cx="9" cy="12" r="1"/><circle cx="15" cy="12" r="1"/><circle cx="9" cy="19" r="1"/><circle cx="15" cy="19" r="1"/></svg>`;
li.append(infoDiv, dragHandle);
fileList.appendChild(li);
}
Sortable.create(fileList, {
handle: '.drag-handle',
animation: 150,
});
} catch (error) {
showAlert(
'Error',
'Failed to load one or more PDF files. They may be corrupted or password-protected.'
);
console.error(error);
} finally {
hideLoader();
}
}
export async function alternateMerge() {
if (Object.keys(alternateMergeState.pdfDocs).length < 2) {
showAlert('Not Enough Files', 'Please upload at least two PDF files to alternate and mix.');
return;
}
if (Object.keys(alternateMergeState.pdfDocs).length < 2) {
showAlert(
'Not Enough Files',
'Please upload at least two PDF files to alternate and mix.'
);
return;
}
showLoader('Alternating and mixing pages...');
try {
const newPdfDoc = await PDFDocument.create();
const fileList = document.getElementById('alternate-file-list');
const sortedFileNames = Array.from(fileList.children).map(li => (li as HTMLElement).dataset.fileName);
showLoader('Alternating and mixing pages...');
try {
const newPdfDoc = await PDFDocument.create();
const fileList = document.getElementById('alternate-file-list');
const sortedFileNames = Array.from(fileList.children).map(
(li) => (li as HTMLElement).dataset.fileName
);
const loadedDocs = sortedFileNames.map(name => alternateMergeState.pdfDocs[name]);
const pageCounts = loadedDocs.map(doc => doc.getPageCount());
const maxPages = Math.max(...pageCounts);
const loadedDocs = sortedFileNames.map(
(name) => alternateMergeState.pdfDocs[name]
);
const pageCounts = loadedDocs.map((doc) => doc.getPageCount());
const maxPages = Math.max(...pageCounts);
for (let i = 0; i < maxPages; i++) {
for (const doc of loadedDocs) {
if (i < doc.getPageCount()) {
const [copiedPage] = await newPdfDoc.copyPages(doc, [i]);
newPdfDoc.addPage(copiedPage);
}
}
for (let i = 0; i < maxPages; i++) {
for (const doc of loadedDocs) {
if (i < doc.getPageCount()) {
const [copiedPage] = await newPdfDoc.copyPages(doc, [i]);
newPdfDoc.addPage(copiedPage);
}
const mergedPdfBytes = await newPdfDoc.save();
downloadFile(new Blob([new Uint8Array(mergedPdfBytes)], { type: 'application/pdf' }), 'alternated-mixed.pdf');
showAlert('Success', 'PDFs have been mixed successfully!');
} catch (e) {
console.error('Alternate Merge error:', e);
showAlert('Error', 'An error occurred while mixing the PDFs.');
} finally {
hideLoader();
}
}
}
const mergedPdfBytes = await newPdfDoc.save();
downloadFile(
new Blob([new Uint8Array(mergedPdfBytes)], { type: 'application/pdf' }),
'alternated-mixed.pdf'
);
showAlert('Success', 'PDFs have been mixed successfully!');
} catch (e) {
console.error('Alternate Merge error:', e);
showAlert('Error', 'An error occurred while mixing the PDFs.');
} finally {
hideLoader();
}
}

View File

@@ -5,51 +5,64 @@ import { state } from '../state.js';
import { PDFDocument as PDFLibDocument } from 'pdf-lib';
async function convertImageToPngBytes(file: any) {
return new Promise((resolve, reject) => {
const img = new Image();
const reader = new FileReader();
return new Promise((resolve, reject) => {
const img = new Image();
const reader = new FileReader();
reader.onload = (e) => {
img.onload = async () => {
const canvas = document.createElement('canvas');
canvas.width = img.width;
canvas.height = img.height;
const ctx = canvas.getContext('2d');
ctx.drawImage(img, 0, 0);
const pngBlob = await new Promise(res => canvas.toBlob(res, 'image/png'));
// @ts-expect-error TS(2339) FIXME: Property 'arrayBuffer' does not exist on type 'unk... Remove this comment to see the full error message
const pngBytes = await pngBlob.arrayBuffer();
resolve(pngBytes);
};
img.onerror = () => reject(new Error('Failed to load image.'));
// @ts-expect-error TS(2322) FIXME: Type 'string | ArrayBuffer' is not assignable to t... Remove this comment to see the full error message
img.src = e.target.result;
};
reader.onerror = () => reject(new Error('Failed to read file.'));
reader.readAsDataURL(file);
});
reader.onload = (e) => {
img.onload = async () => {
const canvas = document.createElement('canvas');
canvas.width = img.width;
canvas.height = img.height;
const ctx = canvas.getContext('2d');
ctx.drawImage(img, 0, 0);
const pngBlob = await new Promise((res) =>
canvas.toBlob(res, 'image/png')
);
// @ts-expect-error TS(2339) FIXME: Property 'arrayBuffer' does not exist on type 'unk... Remove this comment to see the full error message
const pngBytes = await pngBlob.arrayBuffer();
resolve(pngBytes);
};
img.onerror = () => reject(new Error('Failed to load image.'));
// @ts-expect-error TS(2322) FIXME: Type 'string | ArrayBuffer' is not assignable to t... Remove this comment to see the full error message
img.src = e.target.result;
};
reader.onerror = () => reject(new Error('Failed to read file.'));
reader.readAsDataURL(file);
});
}
export async function bmpToPdf() {
if (state.files.length === 0) {
showAlert('No Files', 'Please select at least one BMP file.');
return;
if (state.files.length === 0) {
showAlert('No Files', 'Please select at least one BMP file.');
return;
}
showLoader('Converting BMP to PDF...');
try {
const pdfDoc = await PDFLibDocument.create();
for (const file of state.files) {
const pngBytes = await convertImageToPngBytes(file);
const pngImage = await pdfDoc.embedPng(pngBytes as ArrayBuffer);
const page = pdfDoc.addPage([pngImage.width, pngImage.height]);
page.drawImage(pngImage, {
x: 0,
y: 0,
width: pngImage.width,
height: pngImage.height,
});
}
showLoader('Converting BMP to PDF...');
try {
const pdfDoc = await PDFLibDocument.create();
for (const file of state.files) {
const pngBytes = await convertImageToPngBytes(file);
const pngImage = await pdfDoc.embedPng(pngBytes as ArrayBuffer);
const page = pdfDoc.addPage([pngImage.width, pngImage.height]);
page.drawImage(pngImage, { x: 0, y: 0, width: pngImage.width, height: pngImage.height });
}
const pdfBytes = await pdfDoc.save();
downloadFile(new Blob([new Uint8Array(pdfBytes)], { type: 'application/pdf' }), 'from_bmps.pdf');
} catch (e) {
console.error(e);
showAlert('Error', 'Failed to convert BMP to PDF. One of the files may be invalid.');
} finally {
hideLoader();
}
}
const pdfBytes = await pdfDoc.save();
downloadFile(
new Blob([new Uint8Array(pdfBytes)], { type: 'application/pdf' }),
'from_bmps.pdf'
);
} catch (e) {
console.error(e);
showAlert(
'Error',
'Failed to convert BMP to PDF. One of the files may be invalid.'
);
} finally {
hideLoader();
}
}

View File

@@ -1,4 +1,3 @@
import { showLoader, hideLoader, showAlert } from '../ui.js';
import { downloadFile, hexToRgb } from '../utils/helpers.js';
import { state } from '../state.js';
@@ -6,48 +5,51 @@ import { state } from '../state.js';
import { PDFDocument as PDFLibDocument, rgb } from 'pdf-lib';
export async function changeBackgroundColor() {
if (!state.pdfDoc) {
showAlert('Error', 'PDF not loaded.');
return;
if (!state.pdfDoc) {
showAlert('Error', 'PDF not loaded.');
return;
}
// @ts-expect-error TS(2339) FIXME: Property 'value' does not exist on type 'HTMLEleme... Remove this comment to see the full error message
const colorHex = document.getElementById('background-color').value;
const color = hexToRgb(colorHex);
showLoader('Changing background color...');
try {
const newPdfDoc = await PDFLibDocument.create();
for (let i = 0; i < state.pdfDoc.getPageCount(); i++) {
const [originalPage] = await newPdfDoc.copyPages(state.pdfDoc, [i]);
const { width, height } = originalPage.getSize();
const newPage = newPdfDoc.addPage([width, height]);
newPage.drawRectangle({
x: 0,
y: 0,
width,
height,
color: rgb(color.r, color.g, color.b),
});
const embeddedPage = await newPdfDoc.embedPage(originalPage);
newPage.drawPage(embeddedPage, {
x: 0,
y: 0,
width,
height,
});
}
// @ts-expect-error TS(2339) FIXME: Property 'value' does not exist on type 'HTMLEleme... Remove this comment to see the full error message
const colorHex = document.getElementById('background-color').value;
const color = hexToRgb(colorHex);
showLoader('Changing background color...');
try {
const newPdfDoc = await PDFLibDocument.create();
for (let i = 0; i < state.pdfDoc.getPageCount(); i++) {
const [originalPage] = await newPdfDoc.copyPages(state.pdfDoc, [i]);
const { width, height } = originalPage.getSize();
const newPage = newPdfDoc.addPage([width, height]);
newPage.drawRectangle({
x: 0,
y: 0,
width,
height,
color: rgb(color.r, color.g, color.b),
});
const embeddedPage = await newPdfDoc.embedPage(originalPage);
newPage.drawPage(embeddedPage, {
x: 0,
y: 0,
width,
height,
});
}
const newPdfBytes = await newPdfDoc.save();
downloadFile(new Blob([new Uint8Array(newPdfBytes)], { type: 'application/pdf' }), 'background-changed.pdf');
} catch (e) {
console.error(e);
showAlert('Error', 'Could not change the background color.');
} finally {
hideLoader();
}
}
const newPdfBytes = await newPdfDoc.save();
downloadFile(
new Blob([new Uint8Array(newPdfBytes)], { type: 'application/pdf' }),
'background-changed.pdf'
);
} catch (e) {
console.error(e);
showAlert('Error', 'Could not change the background color.');
} finally {
hideLoader();
}
}

View File

@@ -1,115 +1,146 @@
import { showLoader, hideLoader, showAlert } from '../ui.js';
import { downloadFile, readFileAsArrayBuffer } from '../utils/helpers.js';
import { state } from '../state.js';
import PDFDocument from 'pdfkit/js/pdfkit.standalone';
import blobStream from 'blob-stream';
import * as pdfjsLib from "pdfjs-dist";
import * as pdfjsLib from 'pdfjs-dist';
export async function changePermissions() {
const currentPassword = (document.getElementById('current-password') as HTMLInputElement).value;
const newUserPassword = (document.getElementById('new-user-password') as HTMLInputElement).value;
const newOwnerPassword = (document.getElementById('new-owner-password') as HTMLInputElement).value;
// An owner password is required to enforce any permissions.
if (!newOwnerPassword && (newUserPassword || document.querySelectorAll('input[type="checkbox"]:not(:checked)').length > 0)) {
showAlert('Input Required', 'You must set a "New Owner Password" to enforce specific permissions or to set a user password.');
return;
}
showLoader('Preparing to process...');
const currentPassword = (
document.getElementById('current-password') as HTMLInputElement
).value;
const newUserPassword = (
document.getElementById('new-user-password') as HTMLInputElement
).value;
const newOwnerPassword = (
document.getElementById('new-owner-password') as HTMLInputElement
).value;
// An owner password is required to enforce any permissions.
if (
!newOwnerPassword &&
(newUserPassword ||
document.querySelectorAll('input[type="checkbox"]:not(:checked)').length >
0)
) {
showAlert(
'Input Required',
'You must set a "New Owner Password" to enforce specific permissions or to set a user password.'
);
return;
}
showLoader('Preparing to process...');
try {
const file = state.files[0];
const pdfData = await readFileAsArrayBuffer(file);
let pdf;
try {
const file = state.files[0];
const pdfData = await readFileAsArrayBuffer(file);
let pdf;
try {
pdf = await pdfjsLib.getDocument({
data: pdfData as ArrayBuffer,
password: currentPassword
}).promise;
} catch (e) {
// This catch is specific to password errors in pdf.js
if (e.name === 'PasswordException') {
hideLoader();
showAlert('Incorrect Password', 'The current password you entered is incorrect.');
return;
}
throw e;
}
const numPages = pdf.numPages;
const pageImages = [];
for (let i = 1; i <= numPages; i++) {
document.getElementById('loader-text').textContent = `Processing page ${i} of ${numPages}...`;
const page = await pdf.getPage(i);
const viewport = page.getViewport({ scale: 2.0 });
const canvas = document.createElement('canvas');
canvas.height = viewport.height;
canvas.width = viewport.width;
const context = canvas.getContext('2d');
await page.render({ canvasContext: context, viewport: viewport }).promise;
pageImages.push({
data: canvas.toDataURL('image/jpeg', 0.8),
width: viewport.width,
height: viewport.height
});
}
document.getElementById('loader-text').textContent = 'Applying new permissions...';
const allowPrinting = (document.getElementById('allow-printing') as HTMLInputElement).checked;
const allowCopying = (document.getElementById('allow-copying') as HTMLInputElement).checked;
const allowModifying = (document.getElementById('allow-modifying') as HTMLInputElement).checked;
const allowAnnotating = (document.getElementById('allow-annotating') as HTMLInputElement).checked;
const allowFillingForms = (document.getElementById('allow-filling-forms') as HTMLInputElement).checked;
const allowContentAccessibility = (document.getElementById('allow-content-accessibility') as HTMLInputElement).checked;
const allowDocumentAssembly = (document.getElementById('allow-document-assembly') as HTMLInputElement).checked;
const doc = new PDFDocument({
size: [pageImages[0].width, pageImages[0].height],
pdfVersion: '1.7ext3', // Uses 256-bit AES encryption
// Apply the new, separate user and owner passwords
userPassword: newUserPassword,
ownerPassword: newOwnerPassword,
// Apply all seven permissions from the checkboxes
permissions: {
printing: allowPrinting ? 'highResolution' : false,
modifying: allowModifying,
copying: allowCopying,
annotating: allowAnnotating,
fillingForms: allowFillingForms,
contentAccessibility: allowContentAccessibility,
documentAssembly: allowDocumentAssembly
}
});
const stream = doc.pipe(blobStream());
for (let i = 0; i < pageImages.length; i++) {
if (i > 0) doc.addPage({ size: [pageImages[i].width, pageImages[i].height] });
doc.image(pageImages[i].data, 0, 0, {
width: pageImages[i].width,
height: pageImages[i].height
});
}
doc.end();
stream.on('finish', function () {
const blob = stream.toBlob('application/pdf');
downloadFile(blob, `permissions-changed-${file.name}`);
hideLoader();
showAlert('Success', 'Permissions changed successfully!');
});
pdf = await pdfjsLib.getDocument({
data: pdfData as ArrayBuffer,
password: currentPassword,
}).promise;
} catch (e) {
console.error(e);
// This catch is specific to password errors in pdf.js
if (e.name === 'PasswordException') {
hideLoader();
showAlert('Error', `An unexpected error occurred: ${e.message}`);
showAlert(
'Incorrect Password',
'The current password you entered is incorrect.'
);
return;
}
throw e;
}
}
const numPages = pdf.numPages;
const pageImages = [];
for (let i = 1; i <= numPages; i++) {
document.getElementById('loader-text').textContent =
`Processing page ${i} of ${numPages}...`;
const page = await pdf.getPage(i);
const viewport = page.getViewport({ scale: 2.0 });
const canvas = document.createElement('canvas');
canvas.height = viewport.height;
canvas.width = viewport.width;
const context = canvas.getContext('2d');
await page.render({ canvasContext: context, viewport: viewport }).promise;
pageImages.push({
data: canvas.toDataURL('image/jpeg', 0.8),
width: viewport.width,
height: viewport.height,
});
}
document.getElementById('loader-text').textContent =
'Applying new permissions...';
const allowPrinting = (
document.getElementById('allow-printing') as HTMLInputElement
).checked;
const allowCopying = (
document.getElementById('allow-copying') as HTMLInputElement
).checked;
const allowModifying = (
document.getElementById('allow-modifying') as HTMLInputElement
).checked;
const allowAnnotating = (
document.getElementById('allow-annotating') as HTMLInputElement
).checked;
const allowFillingForms = (
document.getElementById('allow-filling-forms') as HTMLInputElement
).checked;
const allowContentAccessibility = (
document.getElementById('allow-content-accessibility') as HTMLInputElement
).checked;
const allowDocumentAssembly = (
document.getElementById('allow-document-assembly') as HTMLInputElement
).checked;
const doc = new PDFDocument({
size: [pageImages[0].width, pageImages[0].height],
pdfVersion: '1.7ext3', // Uses 256-bit AES encryption
// Apply the new, separate user and owner passwords
userPassword: newUserPassword,
ownerPassword: newOwnerPassword,
// Apply all seven permissions from the checkboxes
permissions: {
printing: allowPrinting ? 'highResolution' : false,
modifying: allowModifying,
copying: allowCopying,
annotating: allowAnnotating,
fillingForms: allowFillingForms,
contentAccessibility: allowContentAccessibility,
documentAssembly: allowDocumentAssembly,
},
});
const stream = doc.pipe(blobStream());
for (let i = 0; i < pageImages.length; i++) {
if (i > 0)
doc.addPage({ size: [pageImages[i].width, pageImages[i].height] });
doc.image(pageImages[i].data, 0, 0, {
width: pageImages[i].width,
height: pageImages[i].height,
});
}
doc.end();
stream.on('finish', function () {
const blob = stream.toBlob('application/pdf');
downloadFile(blob, `permissions-changed-${file.name}`);
hideLoader();
showAlert('Success', 'Permissions changed successfully!');
});
} catch (e) {
console.error(e);
hideLoader();
showAlert('Error', `An unexpected error occurred: ${e.message}`);
}
}

View File

@@ -1,6 +1,9 @@
import { showLoader, hideLoader, showAlert } from '../ui.js';
import { downloadFile, hexToRgb, readFileAsArrayBuffer } from '../utils/helpers.js';
import {
downloadFile,
hexToRgb,
readFileAsArrayBuffer,
} from '../utils/helpers.js';
import { state } from '../state.js';
import { PDFDocument as PDFLibDocument } from 'pdf-lib';
@@ -9,136 +12,168 @@ let isRenderingPreview = false;
let renderTimeout: any;
async function updateTextColorPreview() {
if (isRenderingPreview) return;
isRenderingPreview = true;
if (isRenderingPreview) return;
isRenderingPreview = true;
try {
const textColorCanvas = document.getElementById('text-color-canvas');
if (!textColorCanvas) return;
// @ts-expect-error TS(2304) FIXME: Cannot find name 'pdfjsLib'.
const pdf = await pdfjsLib.getDocument(await readFileAsArrayBuffer(state.files[0])).promise;
const page = await pdf.getPage(1); // Preview first page
const viewport = page.getViewport({ scale: 0.8 });
// @ts-expect-error TS(2339) FIXME: Property 'getContext' does not exist on type 'HTML... Remove this comment to see the full error message
const context = textColorCanvas.getContext('2d');
// @ts-expect-error TS(2339) FIXME: Property 'width' does not exist on type 'HTMLEleme... Remove this comment to see the full error message
textColorCanvas.width = viewport.width;
// @ts-expect-error TS(2339) FIXME: Property 'height' does not exist on type 'HTMLElem... Remove this comment to see the full error message
textColorCanvas.height = viewport.height;
await page.render({ canvasContext: context, viewport }).promise;
// @ts-expect-error TS(2339) FIXME: Property 'width' does not exist on type 'HTMLEleme... Remove this comment to see the full error message
const imageData = context.getImageData(0, 0, textColorCanvas.width, textColorCanvas.height);
const data = imageData.data;
// @ts-expect-error TS(2339) FIXME: Property 'value' does not exist on type 'HTMLEleme... Remove this comment to see the full error message
const colorHex = document.getElementById('text-color-input').value;
const { r, g, b } = hexToRgb(colorHex);
const darknessThreshold = 120;
for (let i = 0; i < data.length; i += 4) {
if (data[i] < darknessThreshold && data[i + 1] < darknessThreshold && data[i + 2] < darknessThreshold) {
data[i] = r * 255;
data[i + 1] = g * 255;
data[i + 2] = b * 255;
}
}
context.putImageData(imageData, 0, 0);
} catch (error) {
console.error('Error updating preview:', error);
} finally {
isRenderingPreview = false;
}
}
export async function setupTextColorTool() {
const originalCanvas = document.getElementById('original-canvas');
const colorInput = document.getElementById('text-color-input');
if (!originalCanvas || !colorInput) return;
// Debounce the preview update for performance
colorInput.addEventListener('input', () => {
clearTimeout(renderTimeout);
renderTimeout = setTimeout(updateTextColorPreview, 250);
});
try {
const textColorCanvas = document.getElementById('text-color-canvas');
if (!textColorCanvas) return;
// @ts-expect-error TS(2304) FIXME: Cannot find name 'pdfjsLib'.
const pdf = await pdfjsLib.getDocument(await readFileAsArrayBuffer(state.files[0])).promise;
const page = await pdf.getPage(1);
const pdf = await pdfjsLib.getDocument(
await readFileAsArrayBuffer(state.files[0])
).promise;
const page = await pdf.getPage(1); // Preview first page
const viewport = page.getViewport({ scale: 0.8 });
// @ts-expect-error TS(2339) FIXME: Property 'width' does not exist on type 'HTMLEleme... Remove this comment to see the full error message
originalCanvas.width = viewport.width;
// @ts-expect-error TS(2339) FIXME: Property 'height' does not exist on type 'HTMLElem... Remove this comment to see the full error message
originalCanvas.height = viewport.height;
// @ts-expect-error TS(2339) FIXME: Property 'getContext' does not exist on type 'HTML... Remove this comment to see the full error message
await page.render({ canvasContext: originalCanvas.getContext('2d'), viewport }).promise;
await updateTextColorPreview();
}
const context = textColorCanvas.getContext('2d');
export async function changeTextColor() {
if (!state.pdfDoc) {
showAlert('Error', 'PDF not loaded.');
return;
}
// @ts-expect-error TS(2339) FIXME: Property 'width' does not exist on type 'HTMLEleme... Remove this comment to see the full error message
textColorCanvas.width = viewport.width;
// @ts-expect-error TS(2339) FIXME: Property 'height' does not exist on type 'HTMLElem... Remove this comment to see the full error message
textColorCanvas.height = viewport.height;
await page.render({ canvasContext: context, viewport }).promise;
// @ts-expect-error TS(2339) FIXME: Property 'width' does not exist on type 'HTMLEleme... Remove this comment to see the full error message
const imageData = context.getImageData(
0,
0,
textColorCanvas.width,
textColorCanvas.height
);
const data = imageData.data;
// @ts-expect-error TS(2339) FIXME: Property 'value' does not exist on type 'HTMLEleme... Remove this comment to see the full error message
const colorHex = document.getElementById('text-color-input').value;
const { r, g, b } = hexToRgb(colorHex);
const darknessThreshold = 120;
showLoader('Changing text color...');
try {
const newPdfDoc = await PDFLibDocument.create();
// @ts-expect-error TS(2304) FIXME: Cannot find name 'pdfjsLib'.
const pdf = await pdfjsLib.getDocument(await readFileAsArrayBuffer(state.files[0])).promise;
for (let i = 1; i <= pdf.numPages; i++) {
showLoader(`Processing page ${i} of ${pdf.numPages}...`);
const page = await pdf.getPage(i);
const viewport = page.getViewport({ scale: 2.0 }); // High resolution for quality
const canvas = document.createElement('canvas');
canvas.width = viewport.width;
canvas.height = viewport.height;
const context = canvas.getContext('2d');
await page.render({ canvasContext: context, viewport }).promise;
const imageData = context.getImageData(0, 0, canvas.width, canvas.height);
const data = imageData.data;
for (let j = 0; j < data.length; j += 4) {
if (data[j] < darknessThreshold && data[j + 1] < darknessThreshold && data[j + 2] < darknessThreshold) {
data[j] = r * 255;
data[j + 1] = g * 255;
data[j + 2] = b * 255;
}
}
context.putImageData(imageData, 0, 0);
const pngImageBytes = await new Promise(resolve => canvas.toBlob(blob => {
const reader = new FileReader();
// @ts-expect-error TS(2769) FIXME: No overload matches this call.
reader.onload = () => resolve(new Uint8Array(reader.result));
reader.readAsArrayBuffer(blob);
}, 'image/png'));
const pngImage = await newPdfDoc.embedPng(pngImageBytes as ArrayBuffer);
const newPage = newPdfDoc.addPage([viewport.width, viewport.height]);
newPage.drawImage(pngImage, { x: 0, y: 0, width: viewport.width, height: viewport.height });
}
const newPdfBytes = await newPdfDoc.save();
downloadFile(new Blob([new Uint8Array(newPdfBytes)], { type: 'application/pdf' }), 'text-color-changed.pdf');
} catch (e) {
console.error(e);
showAlert('Error', 'Could not change text color.');
} finally {
hideLoader();
for (let i = 0; i < data.length; i += 4) {
if (
data[i] < darknessThreshold &&
data[i + 1] < darknessThreshold &&
data[i + 2] < darknessThreshold
) {
data[i] = r * 255;
data[i + 1] = g * 255;
data[i + 2] = b * 255;
}
}
}
context.putImageData(imageData, 0, 0);
} catch (error) {
console.error('Error updating preview:', error);
} finally {
isRenderingPreview = false;
}
}
export async function setupTextColorTool() {
const originalCanvas = document.getElementById('original-canvas');
const colorInput = document.getElementById('text-color-input');
if (!originalCanvas || !colorInput) return;
// Debounce the preview update for performance
colorInput.addEventListener('input', () => {
clearTimeout(renderTimeout);
renderTimeout = setTimeout(updateTextColorPreview, 250);
});
// @ts-expect-error TS(2304) FIXME: Cannot find name 'pdfjsLib'.
const pdf = await pdfjsLib.getDocument(
await readFileAsArrayBuffer(state.files[0])
).promise;
const page = await pdf.getPage(1);
const viewport = page.getViewport({ scale: 0.8 });
// @ts-expect-error TS(2339) FIXME: Property 'width' does not exist on type 'HTMLEleme... Remove this comment to see the full error message
originalCanvas.width = viewport.width;
// @ts-expect-error TS(2339) FIXME: Property 'height' does not exist on type 'HTMLElem... Remove this comment to see the full error message
originalCanvas.height = viewport.height;
// @ts-expect-error TS(2339) FIXME: Property 'getContext' does not exist on type 'HTML... Remove this comment to see the full error message
await page.render({
canvasContext: originalCanvas.getContext('2d'),
viewport,
}).promise;
await updateTextColorPreview();
}
export async function changeTextColor() {
if (!state.pdfDoc) {
showAlert('Error', 'PDF not loaded.');
return;
}
// @ts-expect-error TS(2339) FIXME: Property 'value' does not exist on type 'HTMLEleme... Remove this comment to see the full error message
const colorHex = document.getElementById('text-color-input').value;
const { r, g, b } = hexToRgb(colorHex);
const darknessThreshold = 120;
showLoader('Changing text color...');
try {
const newPdfDoc = await PDFLibDocument.create();
// @ts-expect-error TS(2304) FIXME: Cannot find name 'pdfjsLib'.
const pdf = await pdfjsLib.getDocument(
await readFileAsArrayBuffer(state.files[0])
).promise;
for (let i = 1; i <= pdf.numPages; i++) {
showLoader(`Processing page ${i} of ${pdf.numPages}...`);
const page = await pdf.getPage(i);
const viewport = page.getViewport({ scale: 2.0 }); // High resolution for quality
const canvas = document.createElement('canvas');
canvas.width = viewport.width;
canvas.height = viewport.height;
const context = canvas.getContext('2d');
await page.render({ canvasContext: context, viewport }).promise;
const imageData = context.getImageData(0, 0, canvas.width, canvas.height);
const data = imageData.data;
for (let j = 0; j < data.length; j += 4) {
if (
data[j] < darknessThreshold &&
data[j + 1] < darknessThreshold &&
data[j + 2] < darknessThreshold
) {
data[j] = r * 255;
data[j + 1] = g * 255;
data[j + 2] = b * 255;
}
}
context.putImageData(imageData, 0, 0);
const pngImageBytes = await new Promise((resolve) =>
canvas.toBlob((blob) => {
const reader = new FileReader();
// @ts-expect-error TS(2769) FIXME: No overload matches this call.
reader.onload = () => resolve(new Uint8Array(reader.result));
reader.readAsArrayBuffer(blob);
}, 'image/png')
);
const pngImage = await newPdfDoc.embedPng(pngImageBytes as ArrayBuffer);
const newPage = newPdfDoc.addPage([viewport.width, viewport.height]);
newPage.drawImage(pngImage, {
x: 0,
y: 0,
width: viewport.width,
height: viewport.height,
});
}
const newPdfBytes = await newPdfDoc.save();
downloadFile(
new Blob([new Uint8Array(newPdfBytes)], { type: 'application/pdf' }),
'text-color-changed.pdf'
);
} catch (e) {
console.error(e);
showAlert('Error', 'Could not change text color.');
} finally {
hideLoader();
}
}

View File

@@ -1,4 +1,3 @@
import { showLoader, hideLoader, showAlert } from '../ui.js';
import { downloadFile, hexToRgb } from '../utils/helpers.js';
import { state } from '../state.js';
@@ -6,72 +5,74 @@ import { state } from '../state.js';
import { PDFDocument as PDFLibDocument, rgb } from 'pdf-lib';
export async function combineToSinglePage() {
// @ts-expect-error TS(2339) FIXME: Property 'value' does not exist on type 'HTMLEleme... Remove this comment to see the full error message
const spacing = parseInt(document.getElementById('page-spacing').value) || 0;
// @ts-expect-error TS(2339) FIXME: Property 'value' does not exist on type 'HTMLEleme... Remove this comment to see the full error message
const backgroundColorHex = document.getElementById('background-color').value;
// @ts-expect-error TS(2339) FIXME: Property 'checked' does not exist on type 'HTMLEle... Remove this comment to see the full error message
const addSeparator = document.getElementById('add-separator').checked;
const backgroundColor = hexToRgb(backgroundColorHex);
// @ts-expect-error TS(2339) FIXME: Property 'value' does not exist on type 'HTMLEleme... Remove this comment to see the full error message
const spacing = parseInt(document.getElementById('page-spacing').value) || 0;
// @ts-expect-error TS(2339) FIXME: Property 'value' does not exist on type 'HTMLEleme... Remove this comment to see the full error message
const backgroundColorHex = document.getElementById('background-color').value;
// @ts-expect-error TS(2339) FIXME: Property 'checked' does not exist on type 'HTMLEle... Remove this comment to see the full error message
const addSeparator = document.getElementById('add-separator').checked;
const backgroundColor = hexToRgb(backgroundColorHex);
showLoader('Combining pages...');
try {
const sourceDoc = state.pdfDoc;
const newDoc = await PDFLibDocument.create();
const sourcePages = sourceDoc.getPages();
showLoader('Combining pages...');
try {
const sourceDoc = state.pdfDoc;
const newDoc = await PDFLibDocument.create();
const sourcePages = sourceDoc.getPages();
let maxWidth = 0;
let totalHeight = 0;
sourcePages.forEach((page: any) => {
const { width, height } = page.getSize();
if (width > maxWidth) maxWidth = width;
totalHeight += height;
});
totalHeight += Math.max(0, sourcePages.length - 1) * spacing;
let maxWidth = 0;
let totalHeight = 0;
sourcePages.forEach((page: any) => {
const { width, height } = page.getSize();
if (width > maxWidth) maxWidth = width;
totalHeight += height;
});
totalHeight += Math.max(0, sourcePages.length - 1) * spacing;
const newPage = newDoc.addPage([maxWidth, totalHeight]);
const newPage = newDoc.addPage([maxWidth, totalHeight]);
if (backgroundColorHex.toUpperCase() !== '#FFFFFF') {
newPage.drawRectangle({
x: 0,
y: 0,
width: maxWidth,
height: totalHeight,
color: rgb(backgroundColor.r, backgroundColor.g, backgroundColor.b),
});
}
let currentY = totalHeight;
for (let i = 0; i < sourcePages.length; i++) {
const sourcePage = sourcePages[i];
const { width, height } = sourcePage.getSize();
const embeddedPage = await newDoc.embedPage(sourcePage);
currentY -= height;
const x = (maxWidth - width) / 2;
newPage.drawPage(embeddedPage, { x, y: currentY, width, height });
if (addSeparator && i < sourcePages.length - 1) {
const lineY = currentY - (spacing / 2);
newPage.drawLine({
start: { x: 0, y: lineY },
end: { x: maxWidth, y: lineY },
thickness: 0.5,
color: rgb(0.8, 0.8, 0.8),
});
}
currentY -= spacing;
}
const newPdfBytes = await newDoc.save();
downloadFile(new Blob([new Uint8Array(newPdfBytes)], { type: 'application/pdf' }), 'combined-page.pdf');
} catch (e) {
console.error(e);
showAlert('Error', 'An error occurred while combining pages.');
} finally {
hideLoader();
if (backgroundColorHex.toUpperCase() !== '#FFFFFF') {
newPage.drawRectangle({
x: 0,
y: 0,
width: maxWidth,
height: totalHeight,
color: rgb(backgroundColor.r, backgroundColor.g, backgroundColor.b),
});
}
}
let currentY = totalHeight;
for (let i = 0; i < sourcePages.length; i++) {
const sourcePage = sourcePages[i];
const { width, height } = sourcePage.getSize();
const embeddedPage = await newDoc.embedPage(sourcePage);
currentY -= height;
const x = (maxWidth - width) / 2;
newPage.drawPage(embeddedPage, { x, y: currentY, width, height });
if (addSeparator && i < sourcePages.length - 1) {
const lineY = currentY - spacing / 2;
newPage.drawLine({
start: { x: 0, y: lineY },
end: { x: maxWidth, y: lineY },
thickness: 0.5,
color: rgb(0.8, 0.8, 0.8),
});
}
currentY -= spacing;
}
const newPdfBytes = await newDoc.save();
downloadFile(
new Blob([new Uint8Array(newPdfBytes)], { type: 'application/pdf' }),
'combined-page.pdf'
);
} catch (e) {
console.error(e);
showAlert('Error', 'An error occurred while combining pages.');
} finally {
hideLoader();
}
}

View File

@@ -1,13 +1,13 @@
import { showLoader, hideLoader, showAlert } from '../ui.js';
import { readFileAsArrayBuffer } from '../utils/helpers.js';
import { icons, createIcons } from "lucide";
import { icons, createIcons } from 'lucide';
const state = {
pdfDoc1: null,
pdfDoc2: null,
currentPage: 1,
viewMode: 'overlay',
isSyncScroll: true,
pdfDoc1: null,
pdfDoc2: null,
currentPage: 1,
viewMode: 'overlay',
isSyncScroll: true,
};
/**
@@ -17,110 +17,137 @@ const state = {
* @param {HTMLCanvasElement} canvas - The canvas element to draw on.
* @param {HTMLElement} container - The container to fit the canvas into.
*/
async function renderPage(pdfDoc: any, pageNum: any, canvas: any, container: any) {
const page = await pdfDoc.getPage(pageNum);
async function renderPage(
pdfDoc: any,
pageNum: any,
canvas: any,
container: any
) {
const page = await pdfDoc.getPage(pageNum);
// Calculate scale to fit the container width.
const containerWidth = container.clientWidth - 2; // Subtract border width
const viewport = page.getViewport({ scale: 1.0 });
const scale = containerWidth / viewport.width;
const scaledViewport = page.getViewport({ scale: scale });
// Calculate scale to fit the container width.
const containerWidth = container.clientWidth - 2; // Subtract border width
const viewport = page.getViewport({ scale: 1.0 });
const scale = containerWidth / viewport.width;
const scaledViewport = page.getViewport({ scale: scale });
canvas.width = scaledViewport.width;
canvas.height = scaledViewport.height;
canvas.width = scaledViewport.width;
canvas.height = scaledViewport.height;
await page.render({
canvasContext: canvas.getContext('2d'),
viewport: scaledViewport,
}).promise;
await page.render({
canvasContext: canvas.getContext('2d'),
viewport: scaledViewport,
}).promise;
}
async function renderBothPages() {
if (!state.pdfDoc1 || !state.pdfDoc2) return;
if (!state.pdfDoc1 || !state.pdfDoc2) return;
showLoader(`Loading page ${state.currentPage}...`);
showLoader(`Loading page ${state.currentPage}...`);
const canvas1 = document.getElementById('canvas-compare-1');
const canvas2 = document.getElementById('canvas-compare-2');
const panel1 = document.getElementById('panel-1');
const panel2 = document.getElementById('panel-2');
const wrapper = document.getElementById('compare-viewer-wrapper');
const canvas1 = document.getElementById('canvas-compare-1');
const canvas2 = document.getElementById('canvas-compare-2');
const panel1 = document.getElementById('panel-1');
const panel2 = document.getElementById('panel-2');
const wrapper = document.getElementById('compare-viewer-wrapper');
// Determine the correct container based on the view mode
const container1 = state.viewMode === 'overlay' ? wrapper : panel1;
const container2 = state.viewMode === 'overlay' ? wrapper : panel2;
// Determine the correct container based on the view mode
const container1 = state.viewMode === 'overlay' ? wrapper : panel1;
const container2 = state.viewMode === 'overlay' ? wrapper : panel2;
await Promise.all([
renderPage(state.pdfDoc1, Math.min(state.currentPage, state.pdfDoc1.numPages), canvas1, container1),
renderPage(state.pdfDoc2, Math.min(state.currentPage, state.pdfDoc2.numPages), canvas2, container2)
]);
await Promise.all([
renderPage(
state.pdfDoc1,
Math.min(state.currentPage, state.pdfDoc1.numPages),
canvas1,
container1
),
renderPage(
state.pdfDoc2,
Math.min(state.currentPage, state.pdfDoc2.numPages),
canvas2,
container2
),
]);
updateNavControls();
hideLoader();
updateNavControls();
hideLoader();
}
function updateNavControls() {
const maxPages = Math.max(state.pdfDoc1?.numPages || 0, state.pdfDoc2?.numPages || 0);
// @ts-expect-error TS(2322) FIXME: Type 'number' is not assignable to type 'string'.
document.getElementById('current-page-display-compare').textContent = state.currentPage;
// @ts-expect-error TS(2322) FIXME: Type 'number' is not assignable to type 'string'.
document.getElementById('total-pages-display-compare').textContent = maxPages;
// @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-compare').disabled = state.currentPage <= 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-compare').disabled = state.currentPage >= maxPages;
const maxPages = Math.max(
state.pdfDoc1?.numPages || 0,
state.pdfDoc2?.numPages || 0
);
// @ts-expect-error TS(2322) FIXME: Type 'number' is not assignable to type 'string'.
document.getElementById('current-page-display-compare').textContent =
state.currentPage;
// @ts-expect-error TS(2322) FIXME: Type 'number' is not assignable to type 'string'.
document.getElementById('total-pages-display-compare').textContent = maxPages;
// @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-compare').disabled =
state.currentPage <= 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-compare').disabled =
state.currentPage >= maxPages;
}
async function setupFileInput(inputId: any, docKey: any, displayId: any) {
const fileInput = document.getElementById(inputId);
const dropZone = document.getElementById(`drop-zone-${inputId.slice(-1)}`);
const fileInput = document.getElementById(inputId);
const dropZone = document.getElementById(`drop-zone-${inputId.slice(-1)}`);
const handleFile = async (file: any) => {
if (!file || file.type !== 'application/pdf') return showAlert('Invalid File', 'Please select a valid PDF file.');
const handleFile = async (file: any) => {
if (!file || file.type !== 'application/pdf')
return showAlert('Invalid File', 'Please select a valid PDF file.');
const displayDiv = document.getElementById(displayId);
displayDiv.textContent = '';
const displayDiv = document.getElementById(displayId);
displayDiv.textContent = '';
// 2. Create the icon element
const icon = document.createElement('i');
icon.setAttribute('data-lucide', 'check-circle');
icon.className = 'w-10 h-10 mb-3 text-green-500';
// 2. Create the icon element
const icon = document.createElement('i');
icon.setAttribute('data-lucide', 'check-circle');
icon.className = 'w-10 h-10 mb-3 text-green-500';
// 3. Create the paragraph element for the file name
const p = document.createElement('p');
p.className = 'text-sm text-gray-300 truncate';
// 3. Create the paragraph element for the file name
const p = document.createElement('p');
p.className = 'text-sm text-gray-300 truncate';
// 4. Set the file name safely using textContent
p.textContent = file.name;
// 4. Set the file name safely using textContent
p.textContent = file.name;
// 5. Append the safe elements to the container
displayDiv.append(icon, p);
createIcons({ icons });
// 5. Append the safe elements to the container
displayDiv.append(icon, p);
createIcons({ icons });
try {
showLoader(`Loading ${file.name}...`);
const pdfBytes = await readFileAsArrayBuffer(file);
// @ts-expect-error TS(2304) FIXME: Cannot find name 'pdfjsLib'.
state[docKey] = await pdfjsLib.getDocument(pdfBytes).promise;
try {
showLoader(`Loading ${file.name}...`);
const pdfBytes = await readFileAsArrayBuffer(file);
// @ts-expect-error TS(2304) FIXME: Cannot find name 'pdfjsLib'.
state[docKey] = await pdfjsLib.getDocument(pdfBytes).promise;
if (state.pdfDoc1 && state.pdfDoc2) {
document.getElementById('compare-viewer').classList.remove('hidden');
state.currentPage = 1;
await renderBothPages();
}
} catch (e) {
showAlert('Error', 'Could not load PDF. It may be corrupt or password-protected.');
console.error(e);
} finally {
hideLoader();
}
};
if (state.pdfDoc1 && state.pdfDoc2) {
document.getElementById('compare-viewer').classList.remove('hidden');
state.currentPage = 1;
await renderBothPages();
}
} catch (e) {
showAlert(
'Error',
'Could not load PDF. It may be corrupt or password-protected.'
);
console.error(e);
} finally {
hideLoader();
}
};
// @ts-expect-error TS(2339) FIXME: Property 'files' does not exist on type 'EventTarg... Remove this comment to see the full error message
fileInput.addEventListener('change', (e) => handleFile(e.target.files[0]));
dropZone.addEventListener('dragover', (e) => e.preventDefault());
dropZone.addEventListener('drop', (e) => { e.preventDefault(); handleFile(e.dataTransfer.files[0]); });
// @ts-expect-error TS(2339) FIXME: Property 'files' does not exist on type 'EventTarg... Remove this comment to see the full error message
fileInput.addEventListener('change', (e) => handleFile(e.target.files[0]));
dropZone.addEventListener('dragover', (e) => e.preventDefault());
dropZone.addEventListener('drop', (e) => {
e.preventDefault();
handleFile(e.dataTransfer.files[0]);
});
}
/**
@@ -128,81 +155,92 @@ async function setupFileInput(inputId: any, docKey: any, displayId: any) {
* @param {'overlay' | 'side-by-side'} mode
*/
function setViewMode(mode: any) {
state.viewMode = mode;
const wrapper = document.getElementById('compare-viewer-wrapper');
const overlayControls = document.getElementById('overlay-controls');
const sideControls = document.getElementById('side-by-side-controls');
const btnOverlay = document.getElementById('view-mode-overlay');
const btnSide = document.getElementById('view-mode-side');
const canvas2 = document.getElementById('canvas-compare-2');
const opacitySlider = document.getElementById('opacity-slider');
state.viewMode = mode;
const wrapper = document.getElementById('compare-viewer-wrapper');
const overlayControls = document.getElementById('overlay-controls');
const sideControls = document.getElementById('side-by-side-controls');
const btnOverlay = document.getElementById('view-mode-overlay');
const btnSide = document.getElementById('view-mode-side');
const canvas2 = document.getElementById('canvas-compare-2');
const opacitySlider = document.getElementById('opacity-slider');
if (mode === 'overlay') {
wrapper.className = 'compare-viewer-wrapper overlay-mode';
overlayControls.classList.remove('hidden');
sideControls.classList.add('hidden');
btnOverlay.classList.add('bg-indigo-600');
btnSide.classList.remove('bg-indigo-600');
// @ts-expect-error TS(2339) FIXME: Property 'value' does not exist on type 'HTMLEleme... Remove this comment to see the full error message
canvas2.style.opacity = opacitySlider.value;
} else {
wrapper.className = 'compare-viewer-wrapper side-by-side-mode';
overlayControls.classList.add('hidden');
sideControls.classList.remove('hidden');
btnOverlay.classList.remove('bg-indigo-600');
btnSide.classList.add('bg-indigo-600');
// CHANGE: When switching to side-by-side, reset the canvas opacity to 1.
canvas2.style.opacity = '1';
}
renderBothPages();
if (mode === 'overlay') {
wrapper.className = 'compare-viewer-wrapper overlay-mode';
overlayControls.classList.remove('hidden');
sideControls.classList.add('hidden');
btnOverlay.classList.add('bg-indigo-600');
btnSide.classList.remove('bg-indigo-600');
// @ts-expect-error TS(2339) FIXME: Property 'value' does not exist on type 'HTMLEleme... Remove this comment to see the full error message
canvas2.style.opacity = opacitySlider.value;
} else {
wrapper.className = 'compare-viewer-wrapper side-by-side-mode';
overlayControls.classList.add('hidden');
sideControls.classList.remove('hidden');
btnOverlay.classList.remove('bg-indigo-600');
btnSide.classList.add('bg-indigo-600');
// CHANGE: When switching to side-by-side, reset the canvas opacity to 1.
canvas2.style.opacity = '1';
}
renderBothPages();
}
export function setupCompareTool() {
setupFileInput('file-input-1', 'pdfDoc1', 'file-display-1');
setupFileInput('file-input-2', 'pdfDoc2', 'file-display-2');
setupFileInput('file-input-1', 'pdfDoc1', 'file-display-1');
setupFileInput('file-input-2', 'pdfDoc2', 'file-display-2');
document.getElementById('prev-page-compare').addEventListener('click', () => {
if (state.currentPage > 1) { state.currentPage--; renderBothPages(); }
});
document.getElementById('next-page-compare').addEventListener('click', () => {
const maxPages = Math.max(state.pdfDoc1.numPages, state.pdfDoc2.numPages);
if (state.currentPage < maxPages) { state.currentPage++; renderBothPages(); }
});
document.getElementById('prev-page-compare').addEventListener('click', () => {
if (state.currentPage > 1) {
state.currentPage--;
renderBothPages();
}
});
document.getElementById('next-page-compare').addEventListener('click', () => {
const maxPages = Math.max(state.pdfDoc1.numPages, state.pdfDoc2.numPages);
if (state.currentPage < maxPages) {
state.currentPage++;
renderBothPages();
}
});
document.getElementById('view-mode-overlay').addEventListener('click', () => setViewMode('overlay'));
document.getElementById('view-mode-side').addEventListener('click', () => setViewMode('side-by-side'));
document
.getElementById('view-mode-overlay')
.addEventListener('click', () => setViewMode('overlay'));
document
.getElementById('view-mode-side')
.addEventListener('click', () => setViewMode('side-by-side'));
const canvas2 = document.getElementById('canvas-compare-2');
document.getElementById('flicker-btn').addEventListener('click', () => {
canvas2.style.transition = 'opacity 150ms ease-in-out';
canvas2.style.opacity = (canvas2.style.opacity === '0') ? '1' : '0';
});
document.getElementById('opacity-slider').addEventListener('input', (e) => {
canvas2.style.transition = '';
// @ts-expect-error TS(2339) FIXME: Property 'value' does not exist on type 'EventTarg... Remove this comment to see the full error message
canvas2.style.opacity = e.target.value;
});
const canvas2 = document.getElementById('canvas-compare-2');
document.getElementById('flicker-btn').addEventListener('click', () => {
canvas2.style.transition = 'opacity 150ms ease-in-out';
canvas2.style.opacity = canvas2.style.opacity === '0' ? '1' : '0';
});
document.getElementById('opacity-slider').addEventListener('input', (e) => {
canvas2.style.transition = '';
// @ts-expect-error TS(2339) FIXME: Property 'value' does not exist on type 'EventTarg... Remove this comment to see the full error message
canvas2.style.opacity = e.target.value;
});
const panel1 = document.getElementById('panel-1');
const panel2 = document.getElementById('panel-2');
const syncToggle = document.getElementById('sync-scroll-toggle');
// @ts-expect-error TS(2339) FIXME: Property 'checked' does not exist on type 'HTMLEle... Remove this comment to see the full error message
syncToggle.addEventListener('change', () => { state.isSyncScroll = syncToggle.checked; });
const panel1 = document.getElementById('panel-1');
const panel2 = document.getElementById('panel-2');
const syncToggle = document.getElementById('sync-scroll-toggle');
// @ts-expect-error TS(2339) FIXME: Property 'checked' does not exist on type 'HTMLEle... Remove this comment to see the full error message
syncToggle.addEventListener('change', () => {
state.isSyncScroll = syncToggle.checked;
});
let scrollingPanel: any = null;
panel1.addEventListener('scroll', () => {
if (state.isSyncScroll && scrollingPanel !== panel2) {
scrollingPanel = panel1;
panel2.scrollTop = panel1.scrollTop;
setTimeout(() => scrollingPanel = null, 100);
}
});
panel2.addEventListener('scroll', () => {
if (state.isSyncScroll && scrollingPanel !== panel1) {
scrollingPanel = panel2;
panel1.scrollTop = panel2.scrollTop;
setTimeout(() => scrollingPanel = null, 100);
}
});
}
let scrollingPanel: any = null;
panel1.addEventListener('scroll', () => {
if (state.isSyncScroll && scrollingPanel !== panel2) {
scrollingPanel = panel1;
panel2.scrollTop = panel1.scrollTop;
setTimeout(() => (scrollingPanel = null), 100);
}
});
panel2.addEventListener('scroll', () => {
if (state.isSyncScroll && scrollingPanel !== panel1) {
scrollingPanel = panel2;
panel1.scrollTop = panel2.scrollTop;
setTimeout(() => (scrollingPanel = null), 100);
}
});
}

View File

@@ -1,279 +1,352 @@
import { showLoader, hideLoader, showAlert } from '../ui.js';
import { downloadFile, readFileAsArrayBuffer, formatBytes } from '../utils/helpers.js';
import {
downloadFile,
readFileAsArrayBuffer,
formatBytes,
} from '../utils/helpers.js';
import { state } from '../state.js';
import * as pdfjsLib from 'pdfjs-dist';
import { PDFDocument, PDFName, PDFDict, PDFStream, PDFNumber } from 'pdf-lib';
function dataUrlToBytes(dataUrl: any) {
const base64 = dataUrl.split(',')[1];
const binaryString = atob(base64);
const len = binaryString.length;
const bytes = new Uint8Array(len);
for (let i = 0; i < len; i++) {
bytes[i] = binaryString.charCodeAt(i);
}
return bytes;
const base64 = dataUrl.split(',')[1];
const binaryString = atob(base64);
const len = binaryString.length;
const bytes = new Uint8Array(len);
for (let i = 0; i < len; i++) {
bytes[i] = binaryString.charCodeAt(i);
}
return bytes;
}
async function performSmartCompression(arrayBuffer: any, settings: any) {
const pdfDoc = await PDFDocument.load(arrayBuffer, { ignoreEncryption: true });
const pages = pdfDoc.getPages();
const pdfDoc = await PDFDocument.load(arrayBuffer, {
ignoreEncryption: true,
});
const pages = pdfDoc.getPages();
if (settings.removeMetadata) {
try {
pdfDoc.setTitle('');
pdfDoc.setAuthor('');
pdfDoc.setSubject('');
pdfDoc.setKeywords([]);
pdfDoc.setCreator('');
pdfDoc.setProducer('');
} catch (e) {
console.warn('Could not remove metadata:', e);
}
if (settings.removeMetadata) {
try {
pdfDoc.setTitle('');
pdfDoc.setAuthor('');
pdfDoc.setSubject('');
pdfDoc.setKeywords([]);
pdfDoc.setCreator('');
pdfDoc.setProducer('');
} catch (e) {
console.warn('Could not remove metadata:', e);
}
}
for (let i = 0; i < pages.length; i++) {
const page = pages[i];
const resources = page.node.Resources();
if (!resources) continue;
for (let i = 0; i < pages.length; i++) {
const page = pages[i];
const resources = page.node.Resources();
if (!resources) continue;
const xobjects = resources.lookup(PDFName.of('XObject'));
if (!(xobjects instanceof PDFDict)) continue;
const xobjects = resources.lookup(PDFName.of('XObject'));
if (!(xobjects instanceof PDFDict)) continue;
for (const [key, value] of xobjects.entries()) {
const stream = pdfDoc.context.lookup(value);
if (!(stream instanceof PDFStream) || stream.dict.get(PDFName.of('Subtype')) !== PDFName.of('Image')) continue;
for (const [key, value] of xobjects.entries()) {
const stream = pdfDoc.context.lookup(value);
if (
!(stream instanceof PDFStream) ||
stream.dict.get(PDFName.of('Subtype')) !== PDFName.of('Image')
)
continue;
try {
const imageBytes = stream.getContents();
if (imageBytes.length < settings.skipSize) continue;
try {
const imageBytes = stream.getContents();
if (imageBytes.length < settings.skipSize) continue;
const width = stream.dict.get(PDFName.of('Width')) instanceof PDFNumber
? (stream.dict.get(PDFName.of('Width')) as PDFNumber).asNumber()
: 0;
const height = stream.dict.get(PDFName.of('Height')) instanceof PDFNumber
? (stream.dict.get(PDFName.of('Height')) as PDFNumber).asNumber()
: 0;
const bitsPerComponent = stream.dict.get(PDFName.of('BitsPerComponent')) instanceof PDFNumber
? (stream.dict.get(PDFName.of('BitsPerComponent')) as PDFNumber).asNumber()
: 8;
const width =
stream.dict.get(PDFName.of('Width')) instanceof PDFNumber
? (stream.dict.get(PDFName.of('Width')) as PDFNumber).asNumber()
: 0;
const height =
stream.dict.get(PDFName.of('Height')) instanceof PDFNumber
? (stream.dict.get(PDFName.of('Height')) as PDFNumber).asNumber()
: 0;
const bitsPerComponent =
stream.dict.get(PDFName.of('BitsPerComponent')) instanceof PDFNumber
? (
stream.dict.get(PDFName.of('BitsPerComponent')) as PDFNumber
).asNumber()
: 8;
if (width > 0 && height > 0) {
let newWidth = width;
let newHeight = height;
if (width > 0 && height > 0) {
let newWidth = width;
let newHeight = height;
const scaleFactor = settings.scaleFactor || 1.0;
newWidth = Math.floor(width * scaleFactor);
newHeight = Math.floor(height * scaleFactor);
const scaleFactor = settings.scaleFactor || 1.0;
newWidth = Math.floor(width * scaleFactor);
newHeight = Math.floor(height * scaleFactor);
if (newWidth > settings.maxWidth || newHeight > settings.maxHeight) {
const aspectRatio = newWidth / newHeight;
if (newWidth > newHeight) {
newWidth = Math.min(newWidth, settings.maxWidth);
newHeight = newWidth / aspectRatio;
} else {
newHeight = Math.min(newHeight, settings.maxHeight);
newWidth = newHeight * aspectRatio;
}
}
const minDim = settings.minDimension || 50;
if (newWidth < minDim || newHeight < minDim) continue;
const canvas = document.createElement('canvas');
const ctx = canvas.getContext('2d');
canvas.width = Math.floor(newWidth);
canvas.height = Math.floor(newHeight);
const img = new Image();
const imageUrl = URL.createObjectURL(new Blob([new Uint8Array(imageBytes)]));
await new Promise((resolve, reject) => {
img.onload = resolve;
img.onerror = reject;
img.src = imageUrl;
});
ctx.imageSmoothingEnabled = settings.smoothing !== false;
ctx.imageSmoothingQuality = settings.smoothingQuality || 'medium';
if (settings.grayscale) {
ctx.filter = 'grayscale(100%)';
} else if (settings.contrast) {
ctx.filter = `contrast(${settings.contrast}) brightness(${settings.brightness || 1})`;
}
ctx.drawImage(img, 0, 0, canvas.width, canvas.height);
let bestBytes = null;
let bestSize = imageBytes.length;
const jpegDataUrl = canvas.toDataURL('image/jpeg', settings.quality);
const jpegBytes = dataUrlToBytes(jpegDataUrl);
if (jpegBytes.length < bestSize) {
bestBytes = jpegBytes;
bestSize = jpegBytes.length;
}
if (settings.tryWebP) {
try {
const webpDataUrl = canvas.toDataURL('image/webp', settings.quality);
const webpBytes = dataUrlToBytes(webpDataUrl);
if (webpBytes.length < bestSize) {
bestBytes = webpBytes;
bestSize = webpBytes.length;
}
} catch (e) { /* WebP not supported */ }
}
if (bestBytes && bestSize < imageBytes.length * settings.threshold) {
(stream as any).contents = bestBytes;
stream.dict.set(PDFName.of('Length'), PDFNumber.of(bestSize));
stream.dict.set(PDFName.of('Width'), PDFNumber.of(canvas.width));
stream.dict.set(PDFName.of('Height'), PDFNumber.of(canvas.height));
stream.dict.set(PDFName.of('Filter'), PDFName.of('DCTDecode'));
stream.dict.delete(PDFName.of('DecodeParms'));
stream.dict.set(PDFName.of('BitsPerComponent'), PDFNumber.of(8));
if (settings.grayscale) {
stream.dict.set(PDFName.of('ColorSpace'), PDFName.of('DeviceGray'));
}
}
URL.revokeObjectURL(imageUrl);
}
} catch (error) {
console.warn('Skipping an uncompressible image in smart mode:', error);
if (newWidth > settings.maxWidth || newHeight > settings.maxHeight) {
const aspectRatio = newWidth / newHeight;
if (newWidth > newHeight) {
newWidth = Math.min(newWidth, settings.maxWidth);
newHeight = newWidth / aspectRatio;
} else {
newHeight = Math.min(newHeight, settings.maxHeight);
newWidth = newHeight * aspectRatio;
}
}
const minDim = settings.minDimension || 50;
if (newWidth < minDim || newHeight < minDim) continue;
const canvas = document.createElement('canvas');
const ctx = canvas.getContext('2d');
canvas.width = Math.floor(newWidth);
canvas.height = Math.floor(newHeight);
const img = new Image();
const imageUrl = URL.createObjectURL(
new Blob([new Uint8Array(imageBytes)])
);
await new Promise((resolve, reject) => {
img.onload = resolve;
img.onerror = reject;
img.src = imageUrl;
});
ctx.imageSmoothingEnabled = settings.smoothing !== false;
ctx.imageSmoothingQuality = settings.smoothingQuality || 'medium';
if (settings.grayscale) {
ctx.filter = 'grayscale(100%)';
} else if (settings.contrast) {
ctx.filter = `contrast(${settings.contrast}) brightness(${settings.brightness || 1})`;
}
ctx.drawImage(img, 0, 0, canvas.width, canvas.height);
let bestBytes = null;
let bestSize = imageBytes.length;
const jpegDataUrl = canvas.toDataURL('image/jpeg', settings.quality);
const jpegBytes = dataUrlToBytes(jpegDataUrl);
if (jpegBytes.length < bestSize) {
bestBytes = jpegBytes;
bestSize = jpegBytes.length;
}
if (settings.tryWebP) {
try {
const webpDataUrl = canvas.toDataURL(
'image/webp',
settings.quality
);
const webpBytes = dataUrlToBytes(webpDataUrl);
if (webpBytes.length < bestSize) {
bestBytes = webpBytes;
bestSize = webpBytes.length;
}
} catch (e) {
/* WebP not supported */
}
}
if (bestBytes && bestSize < imageBytes.length * settings.threshold) {
(stream as any).contents = bestBytes;
stream.dict.set(PDFName.of('Length'), PDFNumber.of(bestSize));
stream.dict.set(PDFName.of('Width'), PDFNumber.of(canvas.width));
stream.dict.set(PDFName.of('Height'), PDFNumber.of(canvas.height));
stream.dict.set(PDFName.of('Filter'), PDFName.of('DCTDecode'));
stream.dict.delete(PDFName.of('DecodeParms'));
stream.dict.set(PDFName.of('BitsPerComponent'), PDFNumber.of(8));
if (settings.grayscale) {
stream.dict.set(
PDFName.of('ColorSpace'),
PDFName.of('DeviceGray')
);
}
}
URL.revokeObjectURL(imageUrl);
}
} catch (error) {
console.warn('Skipping an uncompressible image in smart mode:', error);
}
}
}
const saveOptions = {
useObjectStreams: settings.useObjectStreams !== false,
addDefaultPage: false,
objectsPerTick: settings.objectsPerTick || 50
};
const saveOptions = {
useObjectStreams: settings.useObjectStreams !== false,
addDefaultPage: false,
objectsPerTick: settings.objectsPerTick || 50,
};
return await pdfDoc.save(saveOptions);
return await pdfDoc.save(saveOptions);
}
async function performLegacyCompression(arrayBuffer: any, settings: any) {
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 pdfJsDoc = await pdfjsLib.getDocument({ data: arrayBuffer }).promise;
const newPdfDoc = await PDFDocument.create();
const pdfJsDoc = await pdfjsLib.getDocument({ data: arrayBuffer }).promise;
const newPdfDoc = await PDFDocument.create();
for (let i = 1; i <= pdfJsDoc.numPages; i++) {
const page = await pdfJsDoc.getPage(i);
const viewport = page.getViewport({ scale: settings.scale });
const canvas = document.createElement('canvas');
const context = canvas.getContext('2d');
canvas.height = viewport.height;
canvas.width = viewport.width;
for (let i = 1; i <= pdfJsDoc.numPages; i++) {
const page = await pdfJsDoc.getPage(i);
const viewport = page.getViewport({ scale: settings.scale });
const canvas = document.createElement('canvas');
const context = canvas.getContext('2d');
canvas.height = viewport.height;
canvas.width = viewport.width;
await page.render({ canvasContext: context, viewport, canvas: canvas }).promise;
await page.render({ canvasContext: context, viewport, canvas: canvas })
.promise;
const jpegBlob = await new Promise(resolve => canvas.toBlob(resolve, 'image/jpeg', settings.quality));
// @ts-expect-error TS(2339) FIXME: Property 'arrayBuffer' does not exist on type 'unk... Remove this comment to see the full error message
const jpegBytes = await jpegBlob.arrayBuffer();
const jpegImage = await newPdfDoc.embedJpg(jpegBytes);
const newPage = newPdfDoc.addPage([viewport.width, viewport.height]);
newPage.drawImage(jpegImage, { x: 0, y: 0, width: viewport.width, height: viewport.height });
}
return await newPdfDoc.save();
const jpegBlob = await new Promise((resolve) =>
canvas.toBlob(resolve, 'image/jpeg', settings.quality)
);
// @ts-expect-error TS(2339) FIXME: Property 'arrayBuffer' does not exist on type 'unk... Remove this comment to see the full error message
const jpegBytes = await jpegBlob.arrayBuffer();
const jpegImage = await newPdfDoc.embedJpg(jpegBytes);
const newPage = newPdfDoc.addPage([viewport.width, viewport.height]);
newPage.drawImage(jpegImage, {
x: 0,
y: 0,
width: viewport.width,
height: viewport.height,
});
}
return await newPdfDoc.save();
}
export async function compress() {
// @ts-expect-error TS(2339) FIXME: Property 'value' does not exist on type 'HTMLEleme... Remove this comment to see the full error message
const level = document.getElementById('compression-level').value;
// @ts-expect-error TS(2339) FIXME: Property 'value' does not exist on type 'HTMLEleme... Remove this comment to see the full error message
const algorithm = document.getElementById('compression-algorithm').value;
// @ts-expect-error TS(2339) FIXME: Property 'value' does not exist on type 'HTMLEleme... Remove this comment to see the full error message
const level = document.getElementById('compression-level').value;
// @ts-expect-error TS(2339) FIXME: Property 'value' does not exist on type 'HTMLEleme... Remove this comment to see the full error message
const algorithm = document.getElementById('compression-algorithm').value;
const settings = {
'balanced': {
smart: { quality: 0.5, threshold: 0.95, maxWidth: 1800, maxHeight: 1800, skipSize: 3000 },
legacy: { scale: 1.5, quality: 0.6 }
},
'high-quality': {
smart: { quality: 0.70, threshold: 0.98, maxWidth: 2500, maxHeight: 2500, skipSize: 5000 },
legacy: { scale: 2.0, quality: 0.9 }
},
'small-size': {
smart: { quality: 0.3, threshold: 0.95, maxWidth: 1200, maxHeight: 1200, skipSize: 2000 },
legacy: { scale: 1.2, quality: 0.4 }
},
'extreme': {
smart: { quality: 0.1, threshold: 0.95, maxWidth: 1000, maxHeight: 1000, skipSize: 1000 },
legacy: { scale: 1.0, quality: 0.2 }
}
};
const settings = {
balanced: {
smart: {
quality: 0.5,
threshold: 0.95,
maxWidth: 1800,
maxHeight: 1800,
skipSize: 3000,
},
legacy: { scale: 1.5, quality: 0.6 },
},
'high-quality': {
smart: {
quality: 0.7,
threshold: 0.98,
maxWidth: 2500,
maxHeight: 2500,
skipSize: 5000,
},
legacy: { scale: 2.0, quality: 0.9 },
},
'small-size': {
smart: {
quality: 0.3,
threshold: 0.95,
maxWidth: 1200,
maxHeight: 1200,
skipSize: 2000,
},
legacy: { scale: 1.2, quality: 0.4 },
},
extreme: {
smart: {
quality: 0.1,
threshold: 0.95,
maxWidth: 1000,
maxHeight: 1000,
skipSize: 1000,
},
legacy: { scale: 1.0, quality: 0.2 },
},
};
const smartSettings = { ...settings[level].smart, removeMetadata: true };
const legacySettings = settings[level].legacy;
const smartSettings = { ...settings[level].smart, removeMetadata: true };
const legacySettings = settings[level].legacy;
try {
const originalFile = state.files[0];
const arrayBuffer = await readFileAsArrayBuffer(originalFile);
try {
const originalFile = state.files[0];
const arrayBuffer = await readFileAsArrayBuffer(originalFile);
let resultBytes;
let usedMethod;
let resultBytes;
let usedMethod;
if (algorithm === 'vector') {
showLoader('Running Vector (Smart) compression...');
resultBytes = await performSmartCompression(arrayBuffer, smartSettings);
usedMethod = 'Vector';
} else if (algorithm === 'photon') {
showLoader('Running Photon (Rasterize) compression...');
resultBytes = await performLegacyCompression(arrayBuffer, legacySettings);
usedMethod = 'Photon';
} else {
showLoader('Running Automatic (Vector first)...');
const vectorResultBytes = await performSmartCompression(arrayBuffer, smartSettings);
if (algorithm === 'vector') {
showLoader('Running Vector (Smart) compression...');
resultBytes = await performSmartCompression(arrayBuffer, smartSettings);
usedMethod = 'Vector';
} else if (algorithm === 'photon') {
showLoader('Running Photon (Rasterize) compression...');
resultBytes = await performLegacyCompression(arrayBuffer, legacySettings);
usedMethod = 'Photon';
} else {
showLoader('Running Automatic (Vector first)...');
const vectorResultBytes = await performSmartCompression(
arrayBuffer,
smartSettings
);
if (vectorResultBytes.length < originalFile.size) {
resultBytes = vectorResultBytes;
usedMethod = 'Vector (Automatic)';
} else {
// @ts-expect-error TS(2554) FIXME: Expected 2 arguments, but got 3.
showAlert('Vector failed to reduce size. Trying Photon...', 'info', 3000);
showLoader('Running Automatic (Photon fallback)...');
resultBytes = await performLegacyCompression(arrayBuffer, legacySettings);
usedMethod = 'Photon (Automatic)';
}
}
const originalSize = formatBytes(originalFile.size);
const compressedSize = formatBytes(resultBytes.length);
const savings = originalFile.size - resultBytes.length;
const savingsPercent = savings > 0 ? ((savings / originalFile.size) * 100).toFixed(1) : 0;
if (savings > 0) {
showAlert(
'Compression Complete',
`Method: **${usedMethod}**. ` +
`File size reduced from ${originalSize} to ${compressedSize} (Saved ${savingsPercent}%).`
);
} else {
showAlert(
'Compression Finished',
`Method: **${usedMethod}**. ` +
`Could not reduce file size. Original: ${originalSize}, New: ${compressedSize}.`,
// @ts-expect-error TS(2554) FIXME: Expected 2 arguments, but got 3.
'warning'
);
}
downloadFile(new Blob([resultBytes], { type: 'application/pdf' }), 'compressed-final.pdf');
} catch (e) {
console.error(e);
if (vectorResultBytes.length < originalFile.size) {
resultBytes = vectorResultBytes;
usedMethod = 'Vector (Automatic)';
} else {
// @ts-expect-error TS(2554) FIXME: Expected 2 arguments, but got 3.
showAlert('Error', `An error occurred during compression. Error: ${e.message}`, 'error');
} finally {
hideLoader();
showAlert(
'Vector failed to reduce size. Trying Photon...',
'info',
3000
);
showLoader('Running Automatic (Photon fallback)...');
resultBytes = await performLegacyCompression(
arrayBuffer,
legacySettings
);
usedMethod = 'Photon (Automatic)';
}
}
const originalSize = formatBytes(originalFile.size);
const compressedSize = formatBytes(resultBytes.length);
const savings = originalFile.size - resultBytes.length;
const savingsPercent =
savings > 0 ? ((savings / originalFile.size) * 100).toFixed(1) : 0;
if (savings > 0) {
showAlert(
'Compression Complete',
`Method: **${usedMethod}**. ` +
`File size reduced from ${originalSize} to ${compressedSize} (Saved ${savingsPercent}%).`
);
} else {
showAlert(
'Compression Finished',
`Method: **${usedMethod}**. ` +
`Could not reduce file size. Original: ${originalSize}, New: ${compressedSize}.`,
// @ts-expect-error TS(2554) FIXME: Expected 2 arguments, but got 3.
'warning'
);
}
downloadFile(
new Blob([resultBytes], { type: 'application/pdf' }),
'compressed-final.pdf'
);
} catch (e) {
console.error(e);
// @ts-expect-error TS(2554) FIXME: Expected 2 arguments, but got 3.
showAlert(
'Error',
`An error occurred during compression. Error: ${e.message}`,
'error'
);
} finally {
hideLoader();
}
}

View File

@@ -1,36 +1,35 @@
import { showLoader, hideLoader, showAlert } from '../ui.js';
import { downloadFile, readFileAsArrayBuffer } from '../utils/helpers.js';
import { state } from '../state.js';
import Cropper from "cropperjs";
import Cropper from 'cropperjs';
import * as pdfjsLib from 'pdfjs-dist';
import { PDFDocument as PDFLibDocument } from 'pdf-lib';
// --- Global State for the Cropper Tool ---
const cropperState = {
pdfDoc: null,
currentPageNum: 1,
cropper: null,
originalPdfBytes: null,
cropperImageElement: null,
pageCrops: {},
pdfDoc: null,
currentPageNum: 1,
cropper: null,
originalPdfBytes: null,
cropperImageElement: null,
pageCrops: {},
};
/**
* Saves the current crop data to the state object.
*/
function saveCurrentCrop() {
if (cropperState.cropper) {
const currentCrop = cropperState.cropper.getData(true);
const imageData = cropperState.cropper.getImageData();
const cropPercentages = {
x: currentCrop.x / imageData.naturalWidth,
y: currentCrop.y / imageData.naturalHeight,
width: currentCrop.width / imageData.naturalWidth,
height: currentCrop.height / imageData.naturalHeight,
};
cropperState.pageCrops[cropperState.currentPageNum] = cropPercentages;
}
if (cropperState.cropper) {
const currentCrop = cropperState.cropper.getData(true);
const imageData = cropperState.cropper.getImageData();
const cropPercentages = {
x: currentCrop.x / imageData.naturalWidth,
y: currentCrop.y / imageData.naturalHeight,
width: currentCrop.width / imageData.naturalWidth,
height: currentCrop.height / imageData.naturalHeight,
};
cropperState.pageCrops[cropperState.currentPageNum] = cropPercentages;
}
}
/**
@@ -38,60 +37,60 @@ function saveCurrentCrop() {
* @param {number} num The page number to render.
*/
async function displayPageAsImage(num: any) {
showLoader(`Rendering Page ${num}...`);
showLoader(`Rendering Page ${num}...`);
try {
const page = await cropperState.pdfDoc.getPage(num);
const viewport = page.getViewport({ scale: 2.5 });
try {
const page = await cropperState.pdfDoc.getPage(num);
const viewport = page.getViewport({ scale: 2.5 });
const tempCanvas = document.createElement('canvas');
const tempCtx = tempCanvas.getContext('2d');
tempCanvas.width = viewport.width;
tempCanvas.height = viewport.height;
await page.render({ canvasContext: tempCtx, viewport: viewport }).promise;
const tempCanvas = document.createElement('canvas');
const tempCtx = tempCanvas.getContext('2d');
tempCanvas.width = viewport.width;
tempCanvas.height = viewport.height;
await page.render({ canvasContext: tempCtx, viewport: viewport }).promise;
if (cropperState.cropper) {
cropperState.cropper.destroy();
}
const image = document.createElement('img');
image.src = tempCanvas.toDataURL('image/png');
document.getElementById('cropper-container').innerHTML = '';
document.getElementById('cropper-container').appendChild(image);
image.onload = () => {
cropperState.cropper = new Cropper(image, {
viewMode: 1,
background: false,
autoCropArea: 0.8,
responsive: true,
rotatable: false,
zoomable: false,
});
// Restore saved crop data for this page
const savedCrop = cropperState.pageCrops[num];
if (savedCrop) {
const imageData = cropperState.cropper.getImageData();
const cropData = {
x: savedCrop.x * imageData.naturalWidth,
y: savedCrop.y * imageData.naturalHeight,
width: savedCrop.width * imageData.naturalWidth,
height: savedCrop.height * imageData.naturalHeight,
};
cropperState.cropper.setData(cropData);
}
updatePageInfo();
enableControls();
hideLoader();
showAlert('Ready', 'Please select an area to crop.');
};
} catch (error) {
console.error("Error rendering page:", error);
showAlert('Error', 'Failed to render page.');
hideLoader();
if (cropperState.cropper) {
cropperState.cropper.destroy();
}
const image = document.createElement('img');
image.src = tempCanvas.toDataURL('image/png');
document.getElementById('cropper-container').innerHTML = '';
document.getElementById('cropper-container').appendChild(image);
image.onload = () => {
cropperState.cropper = new Cropper(image, {
viewMode: 1,
background: false,
autoCropArea: 0.8,
responsive: true,
rotatable: false,
zoomable: false,
});
// Restore saved crop data for this page
const savedCrop = cropperState.pageCrops[num];
if (savedCrop) {
const imageData = cropperState.cropper.getImageData();
const cropData = {
x: savedCrop.x * imageData.naturalWidth,
y: savedCrop.y * imageData.naturalHeight,
width: savedCrop.width * imageData.naturalWidth,
height: savedCrop.height * imageData.naturalHeight,
};
cropperState.cropper.setData(cropData);
}
updatePageInfo();
enableControls();
hideLoader();
showAlert('Ready', 'Please select an area to crop.');
};
} catch (error) {
console.error('Error rendering page:', error);
showAlert('Error', 'Failed to render page.');
hideLoader();
}
}
/**
@@ -99,211 +98,253 @@ async function displayPageAsImage(num: any) {
* @param {number} offset -1 for previous, 1 for next.
*/
async function changePage(offset: any) {
// Save the current page's crop before changing
saveCurrentCrop();
// Save the current page's crop before changing
saveCurrentCrop();
const newPageNum = cropperState.currentPageNum + offset;
if (newPageNum > 0 && newPageNum <= cropperState.pdfDoc.numPages) {
cropperState.currentPageNum = newPageNum;
await displayPageAsImage(cropperState.currentPageNum);
}
const newPageNum = cropperState.currentPageNum + offset;
if (newPageNum > 0 && newPageNum <= cropperState.pdfDoc.numPages) {
cropperState.currentPageNum = newPageNum;
await displayPageAsImage(cropperState.currentPageNum);
}
}
function updatePageInfo() {
document.getElementById('page-info').textContent = `Page ${cropperState.currentPageNum} of ${cropperState.pdfDoc.numPages}`;
document.getElementById('page-info').textContent =
`Page ${cropperState.currentPageNum} of ${cropperState.pdfDoc.numPages}`;
}
function enableControls() {
// @ts-expect-error TS(2339) FIXME: Property 'disabled' does not exist on type 'HTMLEl... Remove this comment to see the full error message
document.getElementById('prev-page').disabled = cropperState.currentPageNum <= 1;
// @ts-expect-error TS(2339) FIXME: Property 'disabled' does not exist on type 'HTMLEl... Remove this comment to see the full error message
document.getElementById('next-page').disabled = cropperState.currentPageNum >= cropperState.pdfDoc.numPages;
// @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('crop-button').disabled = false;
// @ts-expect-error TS(2339) FIXME: Property 'disabled' does not exist on type 'HTMLEl... Remove this comment to see the full error message
document.getElementById('prev-page').disabled =
cropperState.currentPageNum <= 1;
// @ts-expect-error TS(2339) FIXME: Property 'disabled' does not exist on type 'HTMLEl... Remove this comment to see the full error message
document.getElementById('next-page').disabled =
cropperState.currentPageNum >= cropperState.pdfDoc.numPages;
// @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('crop-button').disabled = false;
}
/**
* Performs a non-destructive crop by updating the page's crop box.
*/
async function performMetadataCrop(pdfToModify: any, cropData: any) {
for (const pageNum in cropData) {
// @ts-expect-error TS(2362) FIXME: The left-hand side of an arithmetic operation must... Remove this comment to see the full error message
const page = pdfToModify.getPages()[pageNum - 1];
const { width: pageWidth, height: pageHeight } = page.getSize();
const rotation = page.getRotation().angle;
const crop = cropData[pageNum];
for (const pageNum in cropData) {
// @ts-expect-error TS(2362) FIXME: The left-hand side of an arithmetic operation must... Remove this comment to see the full error message
const page = pdfToModify.getPages()[pageNum - 1];
const { width: pageWidth, height: pageHeight } = page.getSize();
const rotation = page.getRotation().angle;
const crop = cropData[pageNum];
const visualPdfWidth = pageWidth * crop.width;
const visualPdfHeight = pageHeight * crop.height;
const visualPdfX = pageWidth * crop.x;
const visualPdfY = pageHeight * crop.y;
const visualPdfWidth = pageWidth * crop.width;
const visualPdfHeight = pageHeight * crop.height;
const visualPdfX = pageWidth * crop.x;
const visualPdfY = pageHeight * crop.y;
let finalX, finalY, finalWidth, finalHeight;
switch (rotation) {
case 90:
finalX = visualPdfY;
finalY = pageWidth - visualPdfX - visualPdfWidth;
finalWidth = visualPdfHeight;
finalHeight = visualPdfWidth;
break;
case 180:
finalX = pageWidth - visualPdfX - visualPdfWidth;
finalY = pageHeight - visualPdfY - visualPdfHeight;
finalWidth = visualPdfWidth;
finalHeight = visualPdfHeight;
break;
case 270:
finalX = pageHeight - visualPdfY - visualPdfHeight;
finalY = visualPdfX;
finalWidth = visualPdfHeight;
finalHeight = visualPdfWidth;
break;
default:
finalX = visualPdfX;
finalY = pageHeight - visualPdfY - visualPdfHeight;
finalWidth = visualPdfWidth;
finalHeight = visualPdfHeight;
break;
}
page.setCropBox(finalX, finalY, finalWidth, finalHeight);
let finalX, finalY, finalWidth, finalHeight;
switch (rotation) {
case 90:
finalX = visualPdfY;
finalY = pageWidth - visualPdfX - visualPdfWidth;
finalWidth = visualPdfHeight;
finalHeight = visualPdfWidth;
break;
case 180:
finalX = pageWidth - visualPdfX - visualPdfWidth;
finalY = pageHeight - visualPdfY - visualPdfHeight;
finalWidth = visualPdfWidth;
finalHeight = visualPdfHeight;
break;
case 270:
finalX = pageHeight - visualPdfY - visualPdfHeight;
finalY = visualPdfX;
finalWidth = visualPdfHeight;
finalHeight = visualPdfWidth;
break;
default:
finalX = visualPdfX;
finalY = pageHeight - visualPdfY - visualPdfHeight;
finalWidth = visualPdfWidth;
finalHeight = visualPdfHeight;
break;
}
page.setCropBox(finalX, finalY, finalWidth, finalHeight);
}
}
/**
* Performs a destructive crop by flattening the selected area to an image.
*/
async function performFlatteningCrop(cropData: any) {
const newPdfDoc = await PDFLibDocument.create();
// Load the original PDF with pdf-lib to copy un-cropped pages from
const sourcePdfDocForCopying = await PDFLibDocument.load(cropperState.originalPdfBytes);
const totalPages = cropperState.pdfDoc.numPages;
const newPdfDoc = await PDFLibDocument.create();
for (let i = 0; i < totalPages; i++) {
const pageNum = i + 1;
showLoader(`Processing page ${pageNum} of ${totalPages}...`);
// Load the original PDF with pdf-lib to copy un-cropped pages from
const sourcePdfDocForCopying = await PDFLibDocument.load(
cropperState.originalPdfBytes
);
const totalPages = cropperState.pdfDoc.numPages;
if (cropData[pageNum]) {
const page = await cropperState.pdfDoc.getPage(pageNum);
const viewport = page.getViewport({ scale: 2.5 });
for (let i = 0; i < totalPages; i++) {
const pageNum = i + 1;
showLoader(`Processing page ${pageNum} of ${totalPages}...`);
const tempCanvas = document.createElement('canvas');
const tempCtx = tempCanvas.getContext('2d');
tempCanvas.width = viewport.width;
tempCanvas.height = viewport.height;
await page.render({ canvasContext: tempCtx, viewport: viewport }).promise;
if (cropData[pageNum]) {
const page = await cropperState.pdfDoc.getPage(pageNum);
const viewport = page.getViewport({ scale: 2.5 });
const finalCanvas = document.createElement('canvas');
const finalCtx = finalCanvas.getContext('2d');
const crop = cropData[pageNum];
const finalWidth = tempCanvas.width * crop.width;
const finalHeight = tempCanvas.height * crop.height;
finalCanvas.width = finalWidth;
finalCanvas.height = finalHeight;
const tempCanvas = document.createElement('canvas');
const tempCtx = tempCanvas.getContext('2d');
tempCanvas.width = viewport.width;
tempCanvas.height = viewport.height;
await page.render({ canvasContext: tempCtx, viewport: viewport }).promise;
finalCtx.drawImage(
tempCanvas,
tempCanvas.width * crop.x, tempCanvas.height * crop.y,
finalWidth, finalHeight,
0, 0, finalWidth, finalHeight
);
const finalCanvas = document.createElement('canvas');
const finalCtx = finalCanvas.getContext('2d');
const crop = cropData[pageNum];
const finalWidth = tempCanvas.width * crop.width;
const finalHeight = tempCanvas.height * crop.height;
finalCanvas.width = finalWidth;
finalCanvas.height = finalHeight;
const pngBytes = await new Promise(res => finalCanvas.toBlob(blob => blob.arrayBuffer().then(res), 'image/png'));
const embeddedImage = await newPdfDoc.embedPng(pngBytes as ArrayBuffer);
const newPage = newPdfDoc.addPage([finalWidth, finalHeight]);
newPage.drawImage(embeddedImage, { x: 0, y: 0, width: finalWidth, height: finalHeight });
} else {
const [copiedPage] = await newPdfDoc.copyPages(sourcePdfDocForCopying, [i]);
newPdfDoc.addPage(copiedPage);
}
finalCtx.drawImage(
tempCanvas,
tempCanvas.width * crop.x,
tempCanvas.height * crop.y,
finalWidth,
finalHeight,
0,
0,
finalWidth,
finalHeight
);
const pngBytes = await new Promise((res) =>
finalCanvas.toBlob((blob) => blob.arrayBuffer().then(res), 'image/png')
);
const embeddedImage = await newPdfDoc.embedPng(pngBytes as ArrayBuffer);
const newPage = newPdfDoc.addPage([finalWidth, finalHeight]);
newPage.drawImage(embeddedImage, {
x: 0,
y: 0,
width: finalWidth,
height: finalHeight,
});
} else {
const [copiedPage] = await newPdfDoc.copyPages(sourcePdfDocForCopying, [
i,
]);
newPdfDoc.addPage(copiedPage);
}
return newPdfDoc;
}
return newPdfDoc;
}
export async function setupCropperTool() {
if (state.files.length === 0) return;
if (state.files.length === 0) return;
// Clear pageCrops on new file upload
cropperState.pageCrops = {};
// Clear pageCrops on new file upload
cropperState.pageCrops = {};
const arrayBuffer = await readFileAsArrayBuffer(state.files[0]);
cropperState.originalPdfBytes = arrayBuffer;
const arrayBufferForPdfJs = (arrayBuffer as ArrayBuffer).slice(0);
pdfjsLib.GlobalWorkerOptions.workerSrc = new URL(
'pdfjs-dist/build/pdf.worker.min.mjs',
import.meta.url
).toString();
const arrayBuffer = await readFileAsArrayBuffer(state.files[0]);
cropperState.originalPdfBytes = arrayBuffer;
const arrayBufferForPdfJs = (arrayBuffer as ArrayBuffer).slice(0);
pdfjsLib.GlobalWorkerOptions.workerSrc = new URL(
'pdfjs-dist/build/pdf.worker.min.mjs',
import.meta.url
).toString();
try {
const loadingTask = pdfjsLib.getDocument({ data: arrayBufferForPdfJs });
try {
const loadingTask = pdfjsLib.getDocument({ data: arrayBufferForPdfJs });
cropperState.pdfDoc = await loadingTask.promise;
cropperState.currentPageNum = 1;
cropperState.pdfDoc = await loadingTask.promise;
cropperState.currentPageNum = 1;
await displayPageAsImage(cropperState.currentPageNum);
await displayPageAsImage(cropperState.currentPageNum);
document.getElementById('prev-page').addEventListener('click', () => changePage(-1));
document.getElementById('next-page').addEventListener('click', () => changePage(1));
document
.getElementById('prev-page')
.addEventListener('click', () => changePage(-1));
document
.getElementById('next-page')
.addEventListener('click', () => changePage(1));
document.getElementById('crop-button').addEventListener('click', async () => {
// Get the last known crop from the active page before processing
saveCurrentCrop();
document
.getElementById('crop-button')
.addEventListener('click', async () => {
// Get the last known crop from the active page before processing
saveCurrentCrop();
// @ts-expect-error TS(2339) FIXME: Property 'checked' does not exist on type 'HTMLEle... Remove this comment to see the full error message
const isDestructive = document.getElementById('destructive-crop-toggle').checked;
// @ts-expect-error TS(2339) FIXME: Property 'checked' does not exist on type 'HTMLEle... Remove this comment to see the full error message
const isApplyToAll = document.getElementById('apply-to-all-toggle').checked;
// @ts-expect-error TS(2339) FIXME: Property 'checked' does not exist on type 'HTMLEle... Remove this comment to see the full error message
const isDestructive = document.getElementById(
'destructive-crop-toggle'
).checked;
// @ts-expect-error TS(2339) FIXME: Property 'checked' does not exist on type 'HTMLEle... Remove this comment to see the full error message
const isApplyToAll = document.getElementById(
'apply-to-all-toggle'
).checked;
let finalCropData = {};
if (isApplyToAll) {
const currentCrop = cropperState.pageCrops[cropperState.currentPageNum];
if (!currentCrop) {
showAlert('No Crop Area', 'Please select an area to crop first.');
return;
}
// Apply the active page's crop to all pages
for (let i = 1; i <= cropperState.pdfDoc.numPages; i++) {
finalCropData[i] = currentCrop;
}
} else {
// If not applying to all, only process pages with saved crops
finalCropData = Object.keys(cropperState.pageCrops).reduce((obj, key) => {
obj[key] = cropperState.pageCrops[key];
return obj;
}, {});
}
let finalCropData = {};
if (isApplyToAll) {
const currentCrop =
cropperState.pageCrops[cropperState.currentPageNum];
if (!currentCrop) {
showAlert('No Crop Area', 'Please select an area to crop first.');
return;
}
// Apply the active page's crop to all pages
for (let i = 1; i <= cropperState.pdfDoc.numPages; i++) {
finalCropData[i] = currentCrop;
}
} else {
// If not applying to all, only process pages with saved crops
finalCropData = Object.keys(cropperState.pageCrops).reduce(
(obj, key) => {
obj[key] = cropperState.pageCrops[key];
return obj;
},
{}
);
}
if (Object.keys(finalCropData).length === 0) {
showAlert('No Crop Area', 'Please select an area on at least one page to crop.');
return;
}
if (Object.keys(finalCropData).length === 0) {
showAlert(
'No Crop Area',
'Please select an area on at least one page to crop.'
);
return;
}
showLoader('Applying crop...');
showLoader('Applying crop...');
try {
let finalPdfBytes;
if (isDestructive) {
const newPdfDoc = await performFlatteningCrop(finalCropData);
finalPdfBytes = await newPdfDoc.save();
} else {
const pdfToModify = await PDFLibDocument.load(cropperState.originalPdfBytes);
await performMetadataCrop(pdfToModify, finalCropData);
finalPdfBytes = await pdfToModify.save();
}
try {
let finalPdfBytes;
if (isDestructive) {
const newPdfDoc = await performFlatteningCrop(finalCropData);
finalPdfBytes = await newPdfDoc.save();
} else {
const pdfToModify = await PDFLibDocument.load(
cropperState.originalPdfBytes
);
await performMetadataCrop(pdfToModify, finalCropData);
finalPdfBytes = await pdfToModify.save();
}
const fileName = isDestructive ? 'flattened_crop.pdf' : 'standard_crop.pdf';
downloadFile(new Blob([finalPdfBytes], { type: 'application/pdf' }), fileName);
showAlert('Success', 'Crop complete! Your download has started.');
} catch (e) {
console.error(e);
showAlert('Error', 'An error occurred during cropping.');
} finally {
hideLoader();
}
});
} catch (error) {
console.error("Error setting up cropper tool:", error);
showAlert('Error', 'Failed to load PDF for cropping.');
}
const fileName = isDestructive
? 'flattened_crop.pdf'
: 'standard_crop.pdf';
downloadFile(
new Blob([finalPdfBytes], { type: 'application/pdf' }),
fileName
);
showAlert('Success', 'Crop complete! Your download has started.');
} catch (e) {
console.error(e);
showAlert('Error', 'An error occurred during cropping.');
} finally {
hideLoader();
}
});
} catch (error) {
console.error('Error setting up cropper tool:', error);
showAlert('Error', 'Failed to load PDF for cropping.');
}
}

View File

@@ -3,63 +3,78 @@ import { downloadFile, readFileAsArrayBuffer } from '../utils/helpers.js';
import { state } from '../state.js';
import PDFDocument from 'pdfkit/js/pdfkit.standalone';
import blobStream from 'blob-stream';
import * as pdfjsLib from "pdfjs-dist";
import * as pdfjsLib from 'pdfjs-dist';
export async function decrypt() {
const file = state.files[0];
// @ts-expect-error TS(2339) FIXME: Property 'value' does not exist on type 'HTMLEleme... Remove this comment to see the full error message
const password = document.getElementById('password-input').value;
if (!password.trim()) {
showAlert('Input Required', 'Please enter the PDF password.');
return;
const file = state.files[0];
// @ts-expect-error TS(2339) FIXME: Property 'value' does not exist on type 'HTMLEleme... Remove this comment to see the full error message
const password = document.getElementById('password-input').value;
if (!password.trim()) {
showAlert('Input Required', 'Please enter the PDF password.');
return;
}
try {
showLoader('Preparing to process...');
const pdfData = await readFileAsArrayBuffer(file);
// @ts-expect-error TS(2304) FIXME: Cannot find name 'pdfjsLib'.
const pdf = await pdfjsLib.getDocument({
data: pdfData,
password: password,
}).promise;
const numPages = pdf.numPages;
const pageImages = [];
for (let i = 1; i <= numPages; i++) {
document.getElementById('loader-text').textContent =
`Processing page ${i} of ${numPages}...`;
const page = await pdf.getPage(i);
const viewport = page.getViewport({ scale: 2.0 });
const canvas = document.createElement('canvas');
const context = canvas.getContext('2d');
canvas.height = viewport.height;
canvas.width = viewport.width;
await page.render({
canvasContext: context,
viewport: viewport,
canvas: canvas,
}).promise;
pageImages.push({
data: canvas.toDataURL('image/jpeg', 0.8),
width: viewport.width,
height: viewport.height,
});
}
try {
showLoader('Preparing to process...');
const pdfData = await readFileAsArrayBuffer(file);
// @ts-expect-error TS(2304) FIXME: Cannot find name 'pdfjsLib'.
const pdf = await pdfjsLib.getDocument({ data: pdfData, password: password }).promise;
const numPages = pdf.numPages;
const pageImages = [];
for (let i = 1; i <= numPages; i++) {
document.getElementById('loader-text').textContent = `Processing page ${i} of ${numPages}...`;
const page = await pdf.getPage(i);
const viewport = page.getViewport({ scale: 2.0 });
const canvas = document.createElement('canvas');
const context = canvas.getContext('2d');
canvas.height = viewport.height;
canvas.width = viewport.width;
await page.render({ canvasContext: context, viewport: viewport, canvas: canvas }).promise;
pageImages.push({
data: canvas.toDataURL('image/jpeg', 0.8),
width: viewport.width,
height: viewport.height
});
}
document.getElementById('loader-text').textContent = 'Building unlocked PDF...';
const doc = new PDFDocument({ size: [pageImages[0].width, pageImages[0].height] });
const stream = doc.pipe(blobStream());
for (let i = 0; i < pageImages.length; i++) {
if (i > 0) doc.addPage({ size: [pageImages[i].width, pageImages[i].height] });
doc.image(pageImages[i].data, 0, 0, { width: pageImages[i].width, height: pageImages[i].height });
}
doc.end();
stream.on('finish', function () {
const blob = stream.toBlob('application/pdf');
downloadFile(blob, `unlocked-${file.name}`);
hideLoader();
showAlert('Success', 'Decryption complete! Your download has started.');
});
} catch (error) {
console.error("Error during PDF decryption:", error);
hideLoader();
if (error.name === 'PasswordException') {
showAlert('Incorrect Password', 'The password you entered is incorrect.');
} else {
showAlert('Error', 'An error occurred. The PDF might be corrupted.');
}
document.getElementById('loader-text').textContent =
'Building unlocked PDF...';
const doc = new PDFDocument({
size: [pageImages[0].width, pageImages[0].height],
});
const stream = doc.pipe(blobStream());
for (let i = 0; i < pageImages.length; i++) {
if (i > 0)
doc.addPage({ size: [pageImages[i].width, pageImages[i].height] });
doc.image(pageImages[i].data, 0, 0, {
width: pageImages[i].width,
height: pageImages[i].height,
});
}
}
doc.end();
stream.on('finish', function () {
const blob = stream.toBlob('application/pdf');
downloadFile(blob, `unlocked-${file.name}`);
hideLoader();
showAlert('Success', 'Decryption complete! Your download has started.');
});
} catch (error) {
console.error('Error during PDF decryption:', error);
hideLoader();
if (error.name === 'PasswordException') {
showAlert('Incorrect Password', 'The password you entered is incorrect.');
} else {
showAlert('Error', 'An error occurred. The PDF might be corrupted.');
}
}
}

View File

@@ -5,53 +5,66 @@ import { state } from '../state.js';
import { PDFDocument as PDFLibDocument } from 'pdf-lib';
export async function deletePages() {
// @ts-expect-error TS(2339) FIXME: Property 'value' does not exist on type 'HTMLEleme... Remove this comment to see the full error message
const pageInput = document.getElementById('pages-to-delete').value;
if (!pageInput) {
showAlert('Invalid Input', 'Please enter page numbers to delete.');
return;
// @ts-expect-error TS(2339) FIXME: Property 'value' does not exist on type 'HTMLEleme... Remove this comment to see the full error message
const pageInput = document.getElementById('pages-to-delete').value;
if (!pageInput) {
showAlert('Invalid Input', 'Please enter page numbers to delete.');
return;
}
showLoader('Deleting pages...');
try {
const totalPages = state.pdfDoc.getPageCount();
const indicesToDelete = new Set();
const ranges = pageInput.split(',');
for (const range of ranges) {
const trimmedRange = range.trim();
if (trimmedRange.includes('-')) {
const [start, end] = trimmedRange.split('-').map(Number);
if (
isNaN(start) ||
isNaN(end) ||
start < 1 ||
end > totalPages ||
start > end
)
continue;
for (let i = start; i <= end; i++) indicesToDelete.add(i - 1);
} else {
const pageNum = Number(trimmedRange);
if (isNaN(pageNum) || pageNum < 1 || pageNum > totalPages) continue;
indicesToDelete.add(pageNum - 1);
}
}
showLoader('Deleting pages...');
try {
const totalPages = state.pdfDoc.getPageCount();
const indicesToDelete = new Set();
const ranges = pageInput.split(',');
for (const range of ranges) {
const trimmedRange = range.trim();
if (trimmedRange.includes('-')) {
const [start, end] = trimmedRange.split('-').map(Number);
if (isNaN(start) || isNaN(end) || start < 1 || end > totalPages || start > end) continue;
for (let i = start; i <= end; i++) indicesToDelete.add(i - 1);
} else {
const pageNum = Number(trimmedRange);
if (isNaN(pageNum) || pageNum < 1 || pageNum > totalPages) continue;
indicesToDelete.add(pageNum - 1);
}
}
if (indicesToDelete.size === 0) {
showAlert('Invalid Input', 'No valid pages selected for deletion.');
hideLoader();
return;
}
if (indicesToDelete.size >= totalPages) {
showAlert('Invalid Input', 'You cannot delete all pages.');
hideLoader();
return;
}
const indicesToKeep = Array.from({ length: totalPages }, (_, i) => i).filter(index => !indicesToDelete.has(index));
const newPdf = await PDFLibDocument.create();
const copiedPages = await newPdf.copyPages(state.pdfDoc, indicesToKeep);
copiedPages.forEach((page: any) => newPdf.addPage(page));
const newPdfBytes = await newPdf.save();
downloadFile(new Blob([new Uint8Array(newPdfBytes)], { type: 'application/pdf' }), 'deleted-pages.pdf');
} catch (e) {
console.error(e);
showAlert('Error', 'Could not delete pages.');
} finally {
hideLoader();
if (indicesToDelete.size === 0) {
showAlert('Invalid Input', 'No valid pages selected for deletion.');
hideLoader();
return;
}
}
if (indicesToDelete.size >= totalPages) {
showAlert('Invalid Input', 'You cannot delete all pages.');
hideLoader();
return;
}
const indicesToKeep = Array.from(
{ length: totalPages },
(_, i) => i
).filter((index) => !indicesToDelete.has(index));
const newPdf = await PDFLibDocument.create();
const copiedPages = await newPdf.copyPages(state.pdfDoc, indicesToKeep);
copiedPages.forEach((page: any) => newPdf.addPage(page));
const newPdfBytes = await newPdf.save();
downloadFile(
new Blob([new Uint8Array(newPdfBytes)], { type: 'application/pdf' }),
'deleted-pages.pdf'
);
} catch (e) {
console.error(e);
showAlert('Error', 'Could not delete pages.');
} finally {
hideLoader();
}
}

View File

@@ -1,39 +1,39 @@
import { showLoader, hideLoader, showAlert } from '../ui.js';
import { downloadFile } from '../utils/helpers.js';
import { state } from '../state.js';
import Sortable from 'sortablejs'
import {icons, createIcons} from "lucide";
import Sortable from 'sortablejs';
import { icons, createIcons } from 'lucide';
import { PDFDocument as PDFLibDocument } from 'pdf-lib';
const duplicateOrganizeState = {
sortableInstances: {}
sortableInstances: {},
};
function initializePageGridSortable() {
const grid = document.getElementById('page-grid');
if (!grid) return;
const grid = document.getElementById('page-grid');
if (!grid) return;
// @ts-expect-error TS(2339) FIXME: Property 'pageGrid' does not exist on type '{}'.
if (duplicateOrganizeState.sortableInstances.pageGrid) {
// @ts-expect-error TS(2339) FIXME: Property 'pageGrid' does not exist on type '{}'.
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, {
animation: 150,
ghostClass: 'sortable-ghost',
chosenClass: 'sortable-chosen',
dragClass: 'sortable-drag',
filter: '.duplicate-btn, .delete-btn',
preventOnFilter: true,
onStart: function(evt: any) {
evt.item.style.opacity = '0.5';
},
onEnd: function(evt: any) {
evt.item.style.opacity = '1';
}
});
// @ts-expect-error TS(2339) FIXME: Property 'pageGrid' does not exist on type '{}'.
duplicateOrganizeState.sortableInstances.pageGrid = Sortable.create(grid, {
animation: 150,
ghostClass: 'sortable-ghost',
chosenClass: 'sortable-chosen',
dragClass: 'sortable-drag',
filter: '.duplicate-btn, .delete-btn',
preventOnFilter: true,
onStart: function (evt: any) {
evt.item.style.opacity = '0.5';
},
onEnd: function (evt: any) {
evt.item.style.opacity = '1';
},
});
}
/**
@@ -41,124 +41,139 @@ function initializePageGridSortable() {
* @param {HTMLElement} element The thumbnail element to attach listeners to.
*/
function attachEventListeners(element: any) {
// Re-number all visible page labels
const renumberPages = () => {
const grid = document.getElementById('page-grid');
const pages = grid.querySelectorAll('.page-number');
pages.forEach((label, index) => {
// @ts-expect-error TS(2322) FIXME: Type 'number' is not assignable to type 'string'.
label.textContent = index + 1;
});
};
// Re-number all visible page labels
const renumberPages = () => {
const grid = document.getElementById('page-grid');
const pages = grid.querySelectorAll('.page-number');
pages.forEach((label, index) => {
// @ts-expect-error TS(2322) FIXME: Type 'number' is not assignable to type 'string'.
label.textContent = index + 1;
});
};
// Duplicate button listener
element.querySelector('.duplicate-btn').addEventListener('click', (e: any) => {
e.stopPropagation();
const clone = element.cloneNode(true);
element.after(clone);
attachEventListeners(clone);
renumberPages();
initializePageGridSortable();
// Duplicate button listener
element
.querySelector('.duplicate-btn')
.addEventListener('click', (e: any) => {
e.stopPropagation();
const clone = element.cloneNode(true);
element.after(clone);
attachEventListeners(clone);
renumberPages();
initializePageGridSortable();
});
element.querySelector('.delete-btn').addEventListener('click', (e: any) => {
e.stopPropagation();
if (document.getElementById('page-grid').children.length > 1) {
element.remove();
renumberPages();
initializePageGridSortable();
} else {
showAlert('Cannot Delete', 'You cannot delete the last page of the document.');
}
});
element.querySelector('.delete-btn').addEventListener('click', (e: any) => {
e.stopPropagation();
if (document.getElementById('page-grid').children.length > 1) {
element.remove();
renumberPages();
initializePageGridSortable();
} else {
showAlert(
'Cannot Delete',
'You cannot delete the last page of the document.'
);
}
});
}
export async function renderDuplicateOrganizeThumbnails() {
const grid = document.getElementById('page-grid');
if (!grid) return;
const grid = document.getElementById('page-grid');
if (!grid) return;
showLoader('Rendering page previews...');
const pdfData = await state.pdfDoc.save();
// @ts-expect-error TS(2304) FIXME: Cannot find name 'pdfjsLib'.
const pdfjsDoc = await pdfjsLib.getDocument({ data: pdfData }).promise;
showLoader('Rendering page previews...');
const pdfData = await state.pdfDoc.save();
// @ts-expect-error TS(2304) FIXME: Cannot find name 'pdfjsLib'.
const pdfjsDoc = await pdfjsLib.getDocument({ data: pdfData }).promise;
grid.textContent = '';
grid.textContent = '';
for (let i = 1; i <= pdfjsDoc.numPages; i++) {
const page = await pdfjsDoc.getPage(i);
const viewport = page.getViewport({ scale: 0.5 });
const canvas = document.createElement('canvas');
canvas.height = viewport.height;
canvas.width = viewport.width;
await page.render({ canvasContext: canvas.getContext('2d'), viewport }).promise;
for (let i = 1; i <= pdfjsDoc.numPages; i++) {
const page = await pdfjsDoc.getPage(i);
const viewport = page.getViewport({ scale: 0.5 });
const canvas = document.createElement('canvas');
canvas.height = viewport.height;
canvas.width = viewport.width;
await page.render({ canvasContext: canvas.getContext('2d'), viewport })
.promise;
const wrapper = document.createElement('div');
wrapper.className = 'page-thumbnail relative cursor-move flex flex-col items-center gap-2';
// @ts-expect-error TS(2322) FIXME: Type 'number' is not assignable to type 'string'.
wrapper.dataset.originalPageIndex = i - 1;
const wrapper = document.createElement('div');
wrapper.className =
'page-thumbnail relative cursor-move flex flex-col items-center gap-2';
// @ts-expect-error TS(2322) FIXME: Type 'number' is not assignable to type 'string'.
wrapper.dataset.originalPageIndex = i - 1;
const imgContainer = document.createElement('div');
imgContainer.className = 'w-full h-36 bg-gray-900 rounded-lg flex items-center justify-center overflow-hidden border-2 border-gray-600';
const imgContainer = document.createElement('div');
imgContainer.className =
'w-full h-36 bg-gray-900 rounded-lg flex items-center justify-center overflow-hidden border-2 border-gray-600';
const img = document.createElement('img');
img.src = canvas.toDataURL();
img.className = 'max-w-full max-h-full object-contain';
imgContainer.appendChild(img);
const img = document.createElement('img');
img.src = canvas.toDataURL();
img.className = 'max-w-full max-h-full object-contain';
imgContainer.appendChild(img);
const pageNumberSpan = document.createElement('span');
pageNumberSpan.className = 'page-number absolute top-1 left-1 bg-gray-900 bg-opacity-75 text-white text-xs rounded-full px-2 py-1';
pageNumberSpan.textContent = i.toString();
const pageNumberSpan = document.createElement('span');
pageNumberSpan.className =
'page-number absolute top-1 left-1 bg-gray-900 bg-opacity-75 text-white text-xs rounded-full px-2 py-1';
pageNumberSpan.textContent = i.toString();
const controlsDiv = document.createElement('div');
controlsDiv.className = 'flex items-center justify-center gap-4';
const controlsDiv = document.createElement('div');
controlsDiv.className = 'flex items-center justify-center gap-4';
const duplicateBtn = document.createElement('button');
duplicateBtn.className = 'duplicate-btn bg-green-600 hover:bg-green-700 text-white rounded-full w-8 h-8 flex items-center justify-center';
duplicateBtn.title = 'Duplicate Page';
const duplicateIcon = document.createElement('i');
duplicateIcon.setAttribute('data-lucide', 'copy-plus');
duplicateIcon.className = 'w-5 h-5';
duplicateBtn.appendChild(duplicateIcon);
const duplicateBtn = document.createElement('button');
duplicateBtn.className =
'duplicate-btn bg-green-600 hover:bg-green-700 text-white rounded-full w-8 h-8 flex items-center justify-center';
duplicateBtn.title = 'Duplicate Page';
const duplicateIcon = document.createElement('i');
duplicateIcon.setAttribute('data-lucide', 'copy-plus');
duplicateIcon.className = 'w-5 h-5';
duplicateBtn.appendChild(duplicateIcon);
const deleteBtn = document.createElement('button');
deleteBtn.className = 'delete-btn bg-red-600 hover:bg-red-700 text-white rounded-full w-8 h-8 flex items-center justify-center';
deleteBtn.title = 'Delete Page';
const deleteIcon = document.createElement('i');
deleteIcon.setAttribute('data-lucide', 'x-circle');
deleteIcon.className = 'w-5 h-5';
deleteBtn.appendChild(deleteIcon);
const deleteBtn = document.createElement('button');
deleteBtn.className =
'delete-btn bg-red-600 hover:bg-red-700 text-white rounded-full w-8 h-8 flex items-center justify-center';
deleteBtn.title = 'Delete Page';
const deleteIcon = document.createElement('i');
deleteIcon.setAttribute('data-lucide', 'x-circle');
deleteIcon.className = 'w-5 h-5';
deleteBtn.appendChild(deleteIcon);
controlsDiv.append(duplicateBtn, deleteBtn);
wrapper.append(imgContainer, pageNumberSpan, controlsDiv);
grid.appendChild(wrapper);
attachEventListeners(wrapper);
}
controlsDiv.append(duplicateBtn, deleteBtn);
wrapper.append(imgContainer, pageNumberSpan, controlsDiv);
grid.appendChild(wrapper);
attachEventListeners(wrapper);
}
initializePageGridSortable();
createIcons({icons});
hideLoader();
initializePageGridSortable();
createIcons({ icons });
hideLoader();
}
export async function processAndSave() {
showLoader('Building new PDF...');
try {
const grid = document.getElementById('page-grid');
const finalPageElements = grid.querySelectorAll('.page-thumbnail');
showLoader('Building new PDF...');
try {
const grid = document.getElementById('page-grid');
const finalPageElements = grid.querySelectorAll('.page-thumbnail');
// @ts-expect-error TS(2339) FIXME: Property 'dataset' does not exist on type 'Element... Remove this comment to see the full error message
const finalIndices = Array.from(finalPageElements).map(el => parseInt(el.dataset.originalPageIndex));
// @ts-expect-error TS(2339) FIXME: Property 'dataset' does not exist on type 'Element... Remove this comment to see the full error message
const finalIndices = Array.from(finalPageElements).map((el) =>
parseInt(el.dataset.originalPageIndex)
);
const newPdfDoc = await PDFLibDocument.create();
const copiedPages = await newPdfDoc.copyPages(state.pdfDoc, finalIndices);
copiedPages.forEach((page: any) => newPdfDoc.addPage(page));
const newPdfDoc = await PDFLibDocument.create();
const copiedPages = await newPdfDoc.copyPages(state.pdfDoc, finalIndices);
copiedPages.forEach((page: any) => newPdfDoc.addPage(page));
const newPdfBytes = await newPdfDoc.save();
downloadFile(new Blob([new Uint8Array(newPdfBytes)], { type: 'application/pdf' }), 'organized.pdf');
} catch (e) {
console.error(e);
showAlert('Error', 'Failed to save the new PDF.');
} finally {
hideLoader();
}
}
const newPdfBytes = await newPdfDoc.save();
downloadFile(
new Blob([new Uint8Array(newPdfBytes)], { type: 'application/pdf' }),
'organized.pdf'
);
} catch (e) {
console.error(e);
showAlert('Error', 'Failed to save the new PDF.');
} finally {
hideLoader();
}
}

View File

@@ -1,73 +1,93 @@
import { showLoader, hideLoader, showAlert } from '../ui.js';
import { downloadFile } from '../utils/helpers.js';
import { state } from '../state.js';
import { PDFName, PDFString } from "pdf-lib"
import { PDFName, PDFString } from 'pdf-lib';
export async function editMetadata() {
showLoader('Updating metadata...');
try {
// @ts-expect-error TS(2339) FIXME: Property 'value' does not exist on type 'HTMLEleme... Remove this comment to see the full error message
state.pdfDoc.setTitle(document.getElementById('meta-title').value);
// @ts-expect-error TS(2339) FIXME: Property 'value' does not exist on type 'HTMLEleme... Remove this comment to see the full error message
state.pdfDoc.setAuthor(document.getElementById('meta-author').value);
// @ts-expect-error TS(2339) FIXME: Property 'value' does not exist on type 'HTMLEleme... Remove this comment to see the full error message
state.pdfDoc.setSubject(document.getElementById('meta-subject').value);
// @ts-expect-error TS(2339) FIXME: Property 'value' does not exist on type 'HTMLEleme... Remove this comment to see the full error message
state.pdfDoc.setCreator(document.getElementById('meta-creator').value);
// @ts-expect-error TS(2339) FIXME: Property 'value' does not exist on type 'HTMLEleme... Remove this comment to see the full error message
state.pdfDoc.setProducer(document.getElementById('meta-producer').value);
// @ts-expect-error TS(2339) FIXME: Property 'value' does not exist on type 'HTMLEleme... Remove this comment to see the full error message
const keywords = document.getElementById('meta-keywords').value;
state.pdfDoc.setKeywords(keywords.split(',').map((k: any) => k.trim()).filter(Boolean));
showLoader('Updating metadata...');
try {
// @ts-expect-error TS(2339) FIXME: Property 'value' does not exist on type 'HTMLEleme... Remove this comment to see the full error message
state.pdfDoc.setTitle(document.getElementById('meta-title').value);
// @ts-expect-error TS(2339) FIXME: Property 'value' does not exist on type 'HTMLEleme... Remove this comment to see the full error message
state.pdfDoc.setAuthor(document.getElementById('meta-author').value);
// @ts-expect-error TS(2339) FIXME: Property 'value' does not exist on type 'HTMLEleme... Remove this comment to see the full error message
state.pdfDoc.setSubject(document.getElementById('meta-subject').value);
// @ts-expect-error TS(2339) FIXME: Property 'value' does not exist on type 'HTMLEleme... Remove this comment to see the full error message
state.pdfDoc.setCreator(document.getElementById('meta-creator').value);
// @ts-expect-error TS(2339) FIXME: Property 'value' does not exist on type 'HTMLEleme... Remove this comment to see the full error message
state.pdfDoc.setProducer(document.getElementById('meta-producer').value);
// @ts-expect-error TS(2339) FIXME: Property 'value' does not exist on type 'HTMLEleme... Remove this comment to see the full error message
const creationDate = document.getElementById('meta-creation-date').value;
if (creationDate) {
state.pdfDoc.setCreationDate(new Date(creationDate));
}
// @ts-expect-error TS(2339) FIXME: Property 'value' does not exist on type 'HTMLEleme... Remove this comment to see the full error message
const keywords = document.getElementById('meta-keywords').value;
state.pdfDoc.setKeywords(
keywords
.split(',')
.map((k: any) => k.trim())
.filter(Boolean)
);
// @ts-expect-error TS(2339) FIXME: Property 'value' does not exist on type 'HTMLEleme... Remove this comment to see the full error message
const modDate = document.getElementById('meta-mod-date').value;
if (modDate) {
state.pdfDoc.setModificationDate(new Date(modDate));
} else {
state.pdfDoc.setModificationDate(new Date());
}
const infoDict = state.pdfDoc.getInfoDict();
const standardKeys = new Set(['Title', 'Author', 'Subject', 'Keywords', 'Creator', 'Producer', 'CreationDate', 'ModDate']);
const allKeys = infoDict.keys().map((key: any) => key.asString().substring(1)); // Clean keys
allKeys.forEach((key: any) => {
if (!standardKeys.has(key)) {
infoDict.delete(PDFName.of(key));
}
});
const customKeys = document.querySelectorAll('.custom-meta-key');
const customValues = document.querySelectorAll('.custom-meta-value');
customKeys.forEach((keyInput, index) => {
// @ts-expect-error TS(2339) FIXME: Property 'value' does not exist on type 'Element'.
const key = keyInput.value.trim();
// @ts-expect-error TS(2339) FIXME: Property 'value' does not exist on type 'Element'.
const value = customValues[index].value.trim();
if (key && value) {
// Now we add the fields to a clean slate
infoDict.set(PDFName.of(key), PDFString.of(value));
}
});
const newPdfBytes = await state.pdfDoc.save();
downloadFile(new Blob([newPdfBytes], { type: 'application/pdf' }), 'metadata-edited.pdf');
} catch (e) {
console.error(e);
showAlert('Error', 'Could not update metadata. Please check that date formats are correct.');
} finally {
hideLoader();
// @ts-expect-error TS(2339) FIXME: Property 'value' does not exist on type 'HTMLEleme... Remove this comment to see the full error message
const creationDate = document.getElementById('meta-creation-date').value;
if (creationDate) {
state.pdfDoc.setCreationDate(new Date(creationDate));
}
}
// @ts-expect-error TS(2339) FIXME: Property 'value' does not exist on type 'HTMLEleme... Remove this comment to see the full error message
const modDate = document.getElementById('meta-mod-date').value;
if (modDate) {
state.pdfDoc.setModificationDate(new Date(modDate));
} else {
state.pdfDoc.setModificationDate(new Date());
}
const infoDict = state.pdfDoc.getInfoDict();
const standardKeys = new Set([
'Title',
'Author',
'Subject',
'Keywords',
'Creator',
'Producer',
'CreationDate',
'ModDate',
]);
const allKeys = infoDict
.keys()
.map((key: any) => key.asString().substring(1)); // Clean keys
allKeys.forEach((key: any) => {
if (!standardKeys.has(key)) {
infoDict.delete(PDFName.of(key));
}
});
const customKeys = document.querySelectorAll('.custom-meta-key');
const customValues = document.querySelectorAll('.custom-meta-value');
customKeys.forEach((keyInput, index) => {
// @ts-expect-error TS(2339) FIXME: Property 'value' does not exist on type 'Element'.
const key = keyInput.value.trim();
// @ts-expect-error TS(2339) FIXME: Property 'value' does not exist on type 'Element'.
const value = customValues[index].value.trim();
if (key && value) {
// Now we add the fields to a clean slate
infoDict.set(PDFName.of(key), PDFString.of(value));
}
});
const newPdfBytes = await state.pdfDoc.save();
downloadFile(
new Blob([newPdfBytes], { type: 'application/pdf' }),
'metadata-edited.pdf'
);
} catch (e) {
console.error(e);
showAlert(
'Error',
'Could not update metadata. Please check that date formats are correct.'
);
} finally {
hideLoader();
}
}

View File

@@ -3,71 +3,84 @@ import { downloadFile, readFileAsArrayBuffer } from '../utils/helpers.js';
import { state } from '../state.js';
import PDFDocument from 'pdfkit/js/pdfkit.standalone';
import blobStream from 'blob-stream';
import * as pdfjsLib from "pdfjs-dist";
import * as pdfjsLib from 'pdfjs-dist';
export async function encrypt() {
const file = state.files[0];
const password = (document.getElementById('password-input') as HTMLInputElement).value;
if (!password.trim()) {
showAlert('Input Required', 'Please enter a password.');
return;
const file = state.files[0];
const password = (
document.getElementById('password-input') as HTMLInputElement
).value;
if (!password.trim()) {
showAlert('Input Required', 'Please enter a password.');
return;
}
try {
showLoader('Preparing to process...');
const pdfData = await readFileAsArrayBuffer(file);
const pdf = await pdfjsLib.getDocument({ data: pdfData as ArrayBuffer })
.promise;
const numPages = pdf.numPages;
const pageImages = [];
for (let i = 1; i <= numPages; i++) {
document.getElementById('loader-text').textContent =
`Processing page ${i} of ${numPages}...`;
const page = await pdf.getPage(i);
const viewport = page.getViewport({ scale: 2.0 });
const canvas = document.createElement('canvas');
const context = canvas.getContext('2d');
canvas.height = viewport.height;
canvas.width = viewport.width;
await page.render({
canvasContext: context,
viewport: viewport,
canvas: canvas,
}).promise;
pageImages.push({
data: canvas.toDataURL('image/jpeg', 0.8),
width: viewport.width,
height: viewport.height,
});
}
try {
showLoader('Preparing to process...');
const pdfData = await readFileAsArrayBuffer(file);
const pdf = await pdfjsLib.getDocument({ data: pdfData as ArrayBuffer }).promise;
const numPages = pdf.numPages;
const pageImages = [];
for (let i = 1; i <= numPages; i++) {
document.getElementById('loader-text').textContent = `Processing page ${i} of ${numPages}...`;
const page = await pdf.getPage(i);
const viewport = page.getViewport({ scale: 2.0 });
const canvas = document.createElement('canvas');
const context = canvas.getContext('2d');
canvas.height = viewport.height;
canvas.width = viewport.width;
await page.render({ canvasContext: context, viewport: viewport, canvas: canvas }).promise;
pageImages.push({
data: canvas.toDataURL('image/jpeg', 0.8),
width: viewport.width,
height: viewport.height
});
}
document.getElementById('loader-text').textContent = 'Encrypting and building PDF...';
const doc = new PDFDocument({
size: [pageImages[0].width, pageImages[0].height],
pdfVersion: '1.7ext3', // Use 256-bit AES encryption
userPassword: password,
ownerPassword: password,
permissions: {
printing: 'highResolution',
modifying: false,
copying: false,
annotating: false,
fillingForms: false,
contentAccessibility: true,
documentAssembly: false
}
});
const stream = doc.pipe(blobStream());
for (let i = 0; i < pageImages.length; i++) {
if (i > 0) doc.addPage({ size: [pageImages[i].width, pageImages[i].height] });
doc.image(pageImages[i].data, 0, 0, { width: pageImages[i].width, height: pageImages[i].height });
}
doc.end();
stream.on('finish', function () {
const blob = stream.toBlob('application/pdf');
downloadFile(blob, `encrypted-${file.name}`);
hideLoader();
showAlert('Success', 'Encryption complete! Your download has started.');
});
} catch (error) {
console.error("Error during PDF encryption:", error);
hideLoader();
showAlert('Error', 'An error occurred. The PDF might be corrupted.');
document.getElementById('loader-text').textContent =
'Encrypting and building PDF...';
const doc = new PDFDocument({
size: [pageImages[0].width, pageImages[0].height],
pdfVersion: '1.7ext3', // Use 256-bit AES encryption
userPassword: password,
ownerPassword: password,
permissions: {
printing: 'highResolution',
modifying: false,
copying: false,
annotating: false,
fillingForms: false,
contentAccessibility: true,
documentAssembly: false,
},
});
const stream = doc.pipe(blobStream());
for (let i = 0; i < pageImages.length; i++) {
if (i > 0)
doc.addPage({ size: [pageImages[i].width, pageImages[i].height] });
doc.image(pageImages[i].data, 0, 0, {
width: pageImages[i].width,
height: pageImages[i].height,
});
}
}
doc.end();
stream.on('finish', function () {
const blob = stream.toBlob('application/pdf');
downloadFile(blob, `encrypted-${file.name}`);
hideLoader();
showAlert('Success', 'Encryption complete! Your download has started.');
});
} catch (error) {
console.error('Error during PDF encryption:', error);
hideLoader();
showAlert('Error', 'An error occurred. The PDF might be corrupted.');
}
}

View File

@@ -6,56 +6,65 @@ import JSZip from 'jszip';
import { PDFDocument as PDFLibDocument } from 'pdf-lib';
export async function extractPages() {
// @ts-expect-error TS(2339) FIXME: Property 'value' does not exist on type 'HTMLEleme... Remove this comment to see the full error message
const pageInput = document.getElementById('pages-to-extract').value;
if (!pageInput.trim()) {
showAlert('Invalid Input', 'Please enter page numbers to extract.');
return;
// @ts-expect-error TS(2339) FIXME: Property 'value' does not exist on type 'HTMLEleme... Remove this comment to see the full error message
const pageInput = document.getElementById('pages-to-extract').value;
if (!pageInput.trim()) {
showAlert('Invalid Input', 'Please enter page numbers to extract.');
return;
}
showLoader('Extracting pages...');
try {
const totalPages = state.pdfDoc.getPageCount();
const indicesToExtract = new Set();
const ranges = pageInput.split(',');
for (const range of ranges) {
const trimmedRange = range.trim();
if (trimmedRange.includes('-')) {
const [start, end] = trimmedRange.split('-').map(Number);
if (
isNaN(start) ||
isNaN(end) ||
start < 1 ||
end > totalPages ||
start > end
)
continue;
for (let i = start; i <= end; i++) indicesToExtract.add(i - 1);
} else {
const pageNum = Number(trimmedRange);
if (isNaN(pageNum) || pageNum < 1 || pageNum > totalPages) continue;
indicesToExtract.add(pageNum - 1);
}
}
showLoader('Extracting pages...');
try {
const totalPages = state.pdfDoc.getPageCount();
const indicesToExtract = new Set();
const ranges = pageInput.split(',');
for (const range of ranges) {
const trimmedRange = range.trim();
if (trimmedRange.includes('-')) {
const [start, end] = trimmedRange.split('-').map(Number);
if (isNaN(start) || isNaN(end) || start < 1 || end > totalPages || start > end) continue;
for (let i = start; i <= end; i++) indicesToExtract.add(i - 1);
} else {
const pageNum = Number(trimmedRange);
if (isNaN(pageNum) || pageNum < 1 || pageNum > totalPages) continue;
indicesToExtract.add(pageNum - 1);
}
}
if (indicesToExtract.size === 0) {
showAlert('Invalid Input', 'No valid pages selected for extraction.');
hideLoader();
return;
}
const zip = new JSZip();
// @ts-expect-error TS(2362) FIXME: The left-hand side of an arithmetic operation must... Remove this comment to see the full error message
const sortedIndices = Array.from(indicesToExtract).sort((a, b) => a - b);
for (const index of sortedIndices) {
const newPdf = await PDFLibDocument.create();
const [copiedPage] = await newPdf.copyPages(state.pdfDoc, [index as number]);
newPdf.addPage(copiedPage);
const newPdfBytes = await newPdf.save();
// @ts-expect-error TS(2365) FIXME: Operator '+' cannot be applied to types 'unknown' ... Remove this comment to see the full error message
zip.file(`page-${index + 1}.pdf`, newPdfBytes);
}
const zipBlob = await zip.generateAsync({ type: 'blob' });
downloadFile(zipBlob, 'extracted-pages.zip');
} catch (e) {
console.error(e);
showAlert('Error', 'Could not extract pages.');
} finally {
hideLoader();
if (indicesToExtract.size === 0) {
showAlert('Invalid Input', 'No valid pages selected for extraction.');
hideLoader();
return;
}
}
const zip = new JSZip();
// @ts-expect-error TS(2362) FIXME: The left-hand side of an arithmetic operation must... Remove this comment to see the full error message
const sortedIndices = Array.from(indicesToExtract).sort((a, b) => a - b);
for (const index of sortedIndices) {
const newPdf = await PDFLibDocument.create();
const [copiedPage] = await newPdf.copyPages(state.pdfDoc, [
index as number,
]);
newPdf.addPage(copiedPage);
const newPdfBytes = await newPdf.save();
// @ts-expect-error TS(2365) FIXME: Operator '+' cannot be applied to types 'unknown' ... Remove this comment to see the full error message
zip.file(`page-${index + 1}.pdf`, newPdfBytes);
}
const zipBlob = await zip.generateAsync({ type: 'blob' });
downloadFile(zipBlob, 'extracted-pages.zip');
} catch (e) {
console.error(e);
showAlert('Error', 'Could not extract pages.');
} finally {
hideLoader();
}
}

View File

@@ -5,84 +5,108 @@ import { state } from '../state.js';
import { PDFDocument as PDFLibDocument, rgb, PageSizes } from 'pdf-lib';
export function setupFixDimensionsUI() {
const targetSizeSelect = document.getElementById('target-size');
const customSizeWrapper = document.getElementById('custom-size-wrapper');
if (targetSizeSelect && customSizeWrapper) {
targetSizeSelect.addEventListener('change', () => {
// @ts-expect-error TS(2339) FIXME: Property 'value' does not exist on type 'HTMLEleme... Remove this comment to see the full error message
customSizeWrapper.classList.toggle('hidden', targetSizeSelect.value !== 'Custom');
});
}
const targetSizeSelect = document.getElementById('target-size');
const customSizeWrapper = document.getElementById('custom-size-wrapper');
if (targetSizeSelect && customSizeWrapper) {
targetSizeSelect.addEventListener('change', () => {
// @ts-expect-error TS(2339) FIXME: Property 'value' does not exist on type 'HTMLEleme... Remove this comment to see the full error message
customSizeWrapper.classList.toggle(
'hidden',
targetSizeSelect.value !== 'Custom'
);
});
}
}
export async function fixDimensions() {
// @ts-expect-error TS(2339) FIXME: Property 'value' does not exist on type 'HTMLEleme... Remove this comment to see the full error message
const targetSizeKey = document.getElementById('target-size').value;
// @ts-expect-error TS(2339) FIXME: Property 'value' does not exist on type 'HTMLEleme... Remove this comment to see the full error message
const orientation = document.getElementById('orientation').value;
// @ts-expect-error TS(2339) FIXME: Property 'value' does not exist on type 'Element'.
const scalingMode = document.querySelector('input[name="scaling-mode"]:checked').value;
// @ts-expect-error TS(2339) FIXME: Property 'value' does not exist on type 'HTMLEleme... Remove this comment to see the full error message
const backgroundColor = hexToRgb(document.getElementById('background-color').value);
showLoader('Standardizing pages...');
try {
let targetWidth, targetHeight;
// @ts-expect-error TS(2339) FIXME: Property 'value' does not exist on type 'HTMLEleme... Remove this comment to see the full error message
const targetSizeKey = document.getElementById('target-size').value;
// @ts-expect-error TS(2339) FIXME: Property 'value' does not exist on type 'HTMLEleme... Remove this comment to see the full error message
const orientation = document.getElementById('orientation').value;
// @ts-expect-error TS(2339) FIXME: Property 'value' does not exist on type 'Element'.
const scalingMode = document.querySelector(
'input[name="scaling-mode"]:checked'
).value;
// @ts-expect-error TS(2339) FIXME: Property 'value' does not exist on type 'HTMLEleme... Remove this comment to see the full error message
const backgroundColor = hexToRgb(
document.getElementById('background-color').value
);
if (targetSizeKey === 'Custom') {
// @ts-expect-error TS(2339) FIXME: Property 'value' does not exist on type 'HTMLEleme... Remove this comment to see the full error message
const width = parseFloat(document.getElementById('custom-width').value);
// @ts-expect-error TS(2339) FIXME: Property 'value' does not exist on type 'HTMLEleme... Remove this comment to see the full error message
const height = parseFloat(document.getElementById('custom-height').value);
// @ts-expect-error TS(2339) FIXME: Property 'value' does not exist on type 'HTMLEleme... Remove this comment to see the full error message
const units = document.getElementById('custom-units').value;
if (units === 'in') {
targetWidth = width * 72;
targetHeight = height * 72;
} else { // mm
targetWidth = width * (72 / 25.4);
targetHeight = height * (72 / 25.4);
}
} else {
[targetWidth, targetHeight] = PageSizes[targetSizeKey];
}
showLoader('Standardizing pages...');
try {
let targetWidth, targetHeight;
if (orientation === 'landscape' && targetWidth < targetHeight) {
[targetWidth, targetHeight] = [targetHeight, targetWidth];
} else if (orientation === 'portrait' && targetWidth > targetHeight) {
[targetWidth, targetHeight] = [targetHeight, targetWidth];
}
const sourceDoc = state.pdfDoc;
const newDoc = await PDFLibDocument.create();
for (const sourcePage of sourceDoc.getPages()) {
const { width: sourceWidth, height: sourceHeight } = sourcePage.getSize();
const embeddedPage = await newDoc.embedPage(sourcePage);
const newPage = newDoc.addPage([targetWidth, targetHeight]);
newPage.drawRectangle({ x: 0, y: 0, width: targetWidth, height: targetHeight, color: rgb(backgroundColor.r, backgroundColor.g, backgroundColor.b) });
const scaleX = targetWidth / sourceWidth;
const scaleY = targetHeight / sourceHeight;
const scale = scalingMode === 'fit' ? Math.min(scaleX, scaleY) : Math.max(scaleX, scaleY);
const scaledWidth = sourceWidth * scale;
const scaledHeight = sourceHeight * scale;
const x = (targetWidth - scaledWidth) / 2;
const y = (targetHeight - scaledHeight) / 2;
newPage.drawPage(embeddedPage, { x, y, width: scaledWidth, height: scaledHeight });
}
const newPdfBytes = await newDoc.save();
downloadFile(new Blob([new Uint8Array(newPdfBytes)], { type: 'application/pdf' }), 'standardized.pdf');
} catch (e) {
console.error(e);
showAlert('Error', 'An error occurred while standardizing pages.');
} finally {
hideLoader();
if (targetSizeKey === 'Custom') {
// @ts-expect-error TS(2339) FIXME: Property 'value' does not exist on type 'HTMLEleme... Remove this comment to see the full error message
const width = parseFloat(document.getElementById('custom-width').value);
// @ts-expect-error TS(2339) FIXME: Property 'value' does not exist on type 'HTMLEleme... Remove this comment to see the full error message
const height = parseFloat(document.getElementById('custom-height').value);
// @ts-expect-error TS(2339) FIXME: Property 'value' does not exist on type 'HTMLEleme... Remove this comment to see the full error message
const units = document.getElementById('custom-units').value;
if (units === 'in') {
targetWidth = width * 72;
targetHeight = height * 72;
} else {
// mm
targetWidth = width * (72 / 25.4);
targetHeight = height * (72 / 25.4);
}
} else {
[targetWidth, targetHeight] = PageSizes[targetSizeKey];
}
}
if (orientation === 'landscape' && targetWidth < targetHeight) {
[targetWidth, targetHeight] = [targetHeight, targetWidth];
} else if (orientation === 'portrait' && targetWidth > targetHeight) {
[targetWidth, targetHeight] = [targetHeight, targetWidth];
}
const sourceDoc = state.pdfDoc;
const newDoc = await PDFLibDocument.create();
for (const sourcePage of sourceDoc.getPages()) {
const { width: sourceWidth, height: sourceHeight } = sourcePage.getSize();
const embeddedPage = await newDoc.embedPage(sourcePage);
const newPage = newDoc.addPage([targetWidth, targetHeight]);
newPage.drawRectangle({
x: 0,
y: 0,
width: targetWidth,
height: targetHeight,
color: rgb(backgroundColor.r, backgroundColor.g, backgroundColor.b),
});
const scaleX = targetWidth / sourceWidth;
const scaleY = targetHeight / sourceHeight;
const scale =
scalingMode === 'fit'
? Math.min(scaleX, scaleY)
: Math.max(scaleX, scaleY);
const scaledWidth = sourceWidth * scale;
const scaledHeight = sourceHeight * scale;
const x = (targetWidth - scaledWidth) / 2;
const y = (targetHeight - scaledHeight) / 2;
newPage.drawPage(embeddedPage, {
x,
y,
width: scaledWidth,
height: scaledHeight,
});
}
const newPdfBytes = await newDoc.save();
downloadFile(
new Blob([new Uint8Array(newPdfBytes)], { type: 'application/pdf' }),
'standardized.pdf'
);
} catch (e) {
console.error(e);
showAlert('Error', 'An error occurred while standardizing pages.');
} finally {
hideLoader();
}
}

View File

@@ -3,25 +3,31 @@ import { downloadFile } from '../utils/helpers.js';
import { state } from '../state.js';
export async function flatten() {
if (!state.pdfDoc) {
showAlert('Error', 'PDF not loaded.');
return;
}
showLoader('Flattening PDF...');
try {
const form = state.pdfDoc.getForm();
form.flatten();
if (!state.pdfDoc) {
showAlert('Error', 'PDF not loaded.');
return;
}
showLoader('Flattening PDF...');
try {
const form = state.pdfDoc.getForm();
form.flatten();
const flattenedBytes = await state.pdfDoc.save();
downloadFile(new Blob([flattenedBytes], { type: 'application/pdf' }), 'flattened.pdf');
} catch (e) {
console.error(e);
if (e.message.includes('getForm')) {
showAlert('No Form Found', 'This PDF does not contain any form fields to flatten.');
} else {
showAlert('Error', 'Could not flatten the PDF.');
}
} finally {
hideLoader();
const flattenedBytes = await state.pdfDoc.save();
downloadFile(
new Blob([flattenedBytes], { type: 'application/pdf' }),
'flattened.pdf'
);
} catch (e) {
console.error(e);
if (e.message.includes('getForm')) {
showAlert(
'No Form Found',
'This PDF does not contain any form fields to flatten.'
);
} else {
showAlert('Error', 'Could not flatten the PDF.');
}
}
} finally {
hideLoader();
}
}

View File

@@ -2,21 +2,21 @@ import { showLoader, hideLoader, showAlert } from '../ui.js';
import { downloadFile } from '../utils/helpers.js';
import { state } from '../state.js';
import {
PDFDocument as PDFLibDocument,
PDFTextField,
PDFCheckBox,
PDFRadioGroup,
PDFDropdown,
PDFButton,
PDFSignature,
PDFOptionList
PDFDocument as PDFLibDocument,
PDFTextField,
PDFCheckBox,
PDFRadioGroup,
PDFDropdown,
PDFButton,
PDFSignature,
PDFOptionList,
} from 'pdf-lib';
import * as pdfjsLib from 'pdfjs-dist';
pdfjsLib.GlobalWorkerOptions.workerSrc = new URL(
'pdfjs-dist/build/pdf.worker.min.mjs',
import.meta.url
'pdfjs-dist/build/pdf.worker.min.mjs',
import.meta.url
).toString();
let pdfJsDoc: any = null;
@@ -24,351 +24,380 @@ let currentPageNum = 1;
let pdfRendering = false;
let renderTimeout: any = null;
const formState = {
scale: 2,
fields: [],
scale: 2,
fields: [],
};
let fieldValues: Record<string, any> = {};
async function renderPage() {
if (pdfRendering || !pdfJsDoc) return;
if (pdfRendering || !pdfJsDoc) return;
pdfRendering = true;
showLoader(`Rendering page ${currentPageNum}...`);
pdfRendering = true;
showLoader(`Rendering page ${currentPageNum}...`);
const page = await pdfJsDoc.getPage(currentPageNum);
const viewport = page.getViewport({ scale: 1.0 });
const page = await pdfJsDoc.getPage(currentPageNum);
const viewport = page.getViewport({ scale: 1.0 });
const canvas = document.getElementById('pdf-canvas') as HTMLCanvasElement;
const context = canvas.getContext('2d');
if (!context) {
console.error('Could not get canvas context');
pdfRendering = false;
hideLoader();
return;
}
canvas.height = viewport.height;
canvas.width = viewport.width;
canvas.style.transformOrigin = 'top left';
canvas.style.transform = `scale(${formState.scale})`;
const tempPdfDoc = await PDFLibDocument.load(await state.pdfDoc.save(), { ignoreEncryption: true });
const form = tempPdfDoc.getForm();
Object.keys(fieldValues).forEach(fieldName => {
try {
const field = form.getField(fieldName);
if (!field) return;
if (field instanceof PDFTextField) {
field.setText(fieldValues[fieldName]);
} else if (field instanceof PDFCheckBox) {
if (fieldValues[fieldName] === 'on') {
field.check();
} else {
field.uncheck();
}
} else if (field instanceof PDFRadioGroup) {
field.select(fieldValues[fieldName]);
} else if (field instanceof PDFDropdown) {
field.select(fieldValues[fieldName]);
} else if (field instanceof PDFOptionList) {
if (Array.isArray(fieldValues[fieldName])) {
(fieldValues[fieldName] as any[]).forEach(option => field.select(option));
}
}
} catch (e) {
console.error(`Error applying value to field "${fieldName}":`, e);
}
});
const tempPdfBytes = await tempPdfDoc.save();
const tempPdfJsDoc = await pdfjsLib.getDocument({ data: tempPdfBytes }).promise;
const tempPage = await tempPdfJsDoc.getPage(currentPageNum);
await tempPage.render({
canvasContext: context,
viewport: viewport
} as any).promise;
const currentPageDisplay = document.getElementById('current-page-display');
const totalPagesDisplay = document.getElementById('total-pages-display');
const prevPageBtn = document.getElementById('prev-page') as HTMLButtonElement;
const nextPageBtn = document.getElementById('next-page') as HTMLButtonElement;
if (currentPageDisplay) currentPageDisplay.textContent = String(currentPageNum);
if (totalPagesDisplay) totalPagesDisplay.textContent = String(pdfJsDoc.numPages);
if (prevPageBtn) prevPageBtn.disabled = currentPageNum <= 1;
if (nextPageBtn) nextPageBtn.disabled = currentPageNum >= pdfJsDoc.numPages;
const canvas = document.getElementById('pdf-canvas') as HTMLCanvasElement;
const context = canvas.getContext('2d');
if (!context) {
console.error('Could not get canvas context');
pdfRendering = false;
hideLoader();
return;
}
canvas.height = viewport.height;
canvas.width = viewport.width;
canvas.style.transformOrigin = 'top left';
canvas.style.transform = `scale(${formState.scale})`;
const tempPdfDoc = await PDFLibDocument.load(await state.pdfDoc.save(), {
ignoreEncryption: true,
});
const form = tempPdfDoc.getForm();
Object.keys(fieldValues).forEach((fieldName) => {
try {
const field = form.getField(fieldName);
if (!field) return;
if (field instanceof PDFTextField) {
field.setText(fieldValues[fieldName]);
} else if (field instanceof PDFCheckBox) {
if (fieldValues[fieldName] === 'on') {
field.check();
} else {
field.uncheck();
}
} else if (field instanceof PDFRadioGroup) {
field.select(fieldValues[fieldName]);
} else if (field instanceof PDFDropdown) {
field.select(fieldValues[fieldName]);
} else if (field instanceof PDFOptionList) {
if (Array.isArray(fieldValues[fieldName])) {
(fieldValues[fieldName] as any[]).forEach((option) =>
field.select(option)
);
}
}
} catch (e) {
console.error(`Error applying value to field "${fieldName}":`, e);
}
});
const tempPdfBytes = await tempPdfDoc.save();
const tempPdfJsDoc = await pdfjsLib.getDocument({ data: tempPdfBytes })
.promise;
const tempPage = await tempPdfJsDoc.getPage(currentPageNum);
await tempPage.render({
canvasContext: context,
viewport: viewport,
} as any).promise;
const currentPageDisplay = document.getElementById('current-page-display');
const totalPagesDisplay = document.getElementById('total-pages-display');
const prevPageBtn = document.getElementById('prev-page') as HTMLButtonElement;
const nextPageBtn = document.getElementById('next-page') as HTMLButtonElement;
if (currentPageDisplay)
currentPageDisplay.textContent = String(currentPageNum);
if (totalPagesDisplay)
totalPagesDisplay.textContent = String(pdfJsDoc.numPages);
if (prevPageBtn) prevPageBtn.disabled = currentPageNum <= 1;
if (nextPageBtn) nextPageBtn.disabled = currentPageNum >= pdfJsDoc.numPages;
pdfRendering = false;
hideLoader();
}
async function changePage(offset: number) {
const newPageNum = currentPageNum + offset;
if (newPageNum > 0 && newPageNum <= pdfJsDoc.numPages) {
currentPageNum = newPageNum;
await renderPage();
}
const newPageNum = currentPageNum + offset;
if (newPageNum > 0 && newPageNum <= pdfJsDoc.numPages) {
currentPageNum = newPageNum;
await renderPage();
}
}
async function setZoom(factor: number) {
formState.scale = factor;
await renderPage();
formState.scale = factor;
await renderPage();
}
function handleFormChange(event: Event) {
const input = event.target as HTMLInputElement | HTMLSelectElement;
const name = input.name;
let value: any;
const input = event.target as HTMLInputElement | HTMLSelectElement;
const name = input.name;
let value: any;
if (input instanceof HTMLInputElement && input.type === 'checkbox') {
value = input.checked ? 'on' : 'off';
} else if (input instanceof HTMLSelectElement && input.multiple) {
value = Array.from(input.options)
.filter(option => option.selected)
.map(option => option.value);
} else {
value = input.value;
}
if (input instanceof HTMLInputElement && input.type === 'checkbox') {
value = input.checked ? 'on' : 'off';
} else if (input instanceof HTMLSelectElement && input.multiple) {
value = Array.from(input.options)
.filter((option) => option.selected)
.map((option) => option.value);
} else {
value = input.value;
}
fieldValues[name] = value;
fieldValues[name] = value;
clearTimeout(renderTimeout);
renderTimeout = setTimeout(() => {
renderPage();
}, 350);
clearTimeout(renderTimeout);
renderTimeout = setTimeout(() => {
renderPage();
}, 350);
}
function createFormFieldHtml(field: any): HTMLElement {
const name = field.getName();
const isRequired = field.isRequired();
const labelText = name.replace(/[_-]/g, ' ');
const name = field.getName();
const isRequired = field.isRequired();
const labelText = name.replace(/[_-]/g, ' ');
const wrapper = document.createElement('div');
wrapper.className = 'form-field-group p-4 bg-gray-800 rounded-lg border border-gray-700';
const wrapper = document.createElement('div');
wrapper.className =
'form-field-group p-4 bg-gray-800 rounded-lg border border-gray-700';
const label = document.createElement('label');
label.htmlFor = `field-${name}`;
label.className = 'block text-sm font-medium text-gray-300 capitalize mb-1';
label.textContent = labelText;
const label = document.createElement('label');
label.htmlFor = `field-${name}`;
label.className = 'block text-sm font-medium text-gray-300 capitalize mb-1';
label.textContent = labelText;
if (isRequired) {
const requiredSpan = document.createElement('span');
requiredSpan.className = 'text-red-500';
requiredSpan.textContent = ' *';
label.appendChild(requiredSpan);
if (isRequired) {
const requiredSpan = document.createElement('span');
requiredSpan.className = 'text-red-500';
requiredSpan.textContent = ' *';
label.appendChild(requiredSpan);
}
wrapper.appendChild(label);
let inputElement: HTMLElement | DocumentFragment;
if (field instanceof PDFTextField) {
fieldValues[name] = field.getText() || '';
const input = document.createElement('input');
input.type = 'text';
input.id = `field-${name}`;
input.name = name;
input.value = fieldValues[name];
input.className =
'w-full bg-gray-700 border border-gray-600 text-white rounded-lg p-2.5';
inputElement = input;
} else if (field instanceof PDFCheckBox) {
fieldValues[name] = field.isChecked() ? 'on' : 'off';
const input = document.createElement('input');
input.type = 'checkbox';
input.id = `field-${name}`;
input.name = name;
input.className =
'w-4 h-4 rounded text-indigo-600 bg-gray-700 border-gray-600 focus:ring-indigo-500';
input.checked = field.isChecked();
inputElement = input;
} else if (field instanceof PDFRadioGroup) {
fieldValues[name] = field.getSelected();
const options = field.getOptions();
const fragment = document.createDocumentFragment();
options.forEach((opt: string) => {
const optionLabel = document.createElement('label');
optionLabel.className = 'flex items-center gap-2';
const radio = document.createElement('input');
radio.type = 'radio';
radio.name = name;
radio.value = opt;
radio.className =
'w-4 h-4 rounded text-indigo-600 bg-gray-700 border-gray-600 focus:ring-indigo-500';
if (opt === field.getSelected()) radio.checked = true;
const span = document.createElement('span');
span.className = 'text-gray-300 text-sm';
span.textContent = opt;
optionLabel.append(radio, span);
fragment.appendChild(optionLabel);
});
inputElement = fragment;
} else if (field instanceof PDFDropdown || field instanceof PDFOptionList) {
const selectedValues = field.getSelected();
fieldValues[name] = selectedValues;
const options = field.getOptions();
const select = document.createElement('select');
select.id = `field-${name}`;
select.name = name;
select.className =
'w-full bg-gray-700 border border-gray-600 text-white rounded-lg p-2.5';
if (field instanceof PDFOptionList) {
select.multiple = true;
select.size = Math.min(10, options.length);
select.classList.add('h-auto');
}
wrapper.appendChild(label);
let inputElement: HTMLElement | DocumentFragment;
if (field instanceof PDFTextField) {
fieldValues[name] = field.getText() || '';
const input = document.createElement('input');
input.type = 'text';
input.id = `field-${name}`;
input.name = name;
input.value = fieldValues[name];
input.className = 'w-full bg-gray-700 border border-gray-600 text-white rounded-lg p-2.5';
inputElement = input;
} else if (field instanceof PDFCheckBox) {
fieldValues[name] = field.isChecked() ? 'on' : 'off';
const input = document.createElement('input');
input.type = 'checkbox';
input.id = `field-${name}`;
input.name = name;
input.className = 'w-4 h-4 rounded text-indigo-600 bg-gray-700 border-gray-600 focus:ring-indigo-500';
input.checked = field.isChecked();
inputElement = input;
} else if (field instanceof PDFRadioGroup) {
fieldValues[name] = field.getSelected();
const options = field.getOptions();
const fragment = document.createDocumentFragment();
options.forEach((opt: string) => {
const optionLabel = document.createElement('label');
optionLabel.className = 'flex items-center gap-2';
const radio = document.createElement('input');
radio.type = 'radio';
radio.name = name;
radio.value = opt;
radio.className = 'w-4 h-4 rounded text-indigo-600 bg-gray-700 border-gray-600 focus:ring-indigo-500';
if (opt === field.getSelected()) radio.checked = true;
const span = document.createElement('span');
span.className = 'text-gray-300 text-sm';
span.textContent = opt;
optionLabel.append(radio, span);
fragment.appendChild(optionLabel);
});
inputElement = fragment;
} else if (field instanceof PDFDropdown || field instanceof PDFOptionList) {
const selectedValues = field.getSelected();
fieldValues[name] = selectedValues;
const options = field.getOptions();
const select = document.createElement('select');
select.id = `field-${name}`;
select.name = name;
select.className = 'w-full bg-gray-700 border border-gray-600 text-white rounded-lg p-2.5';
if (field instanceof PDFOptionList) {
select.multiple = true;
select.size = Math.min(10, options.length);
select.classList.add('h-auto');
}
options.forEach((opt: string) => {
const option = document.createElement('option');
option.value = opt;
option.textContent = opt;
if (selectedValues.includes(opt)) option.selected = true;
select.appendChild(option);
});
inputElement = select;
options.forEach((opt: string) => {
const option = document.createElement('option');
option.value = opt;
option.textContent = opt;
if (selectedValues.includes(opt)) option.selected = true;
select.appendChild(option);
});
inputElement = select;
} else {
const unsupportedDiv = document.createElement('div');
unsupportedDiv.className =
'p-4 bg-gray-800 rounded-lg border border-gray-700';
const p = document.createElement('p');
p.className = 'text-sm text-gray-400';
if (field instanceof PDFSignature) {
p.textContent = 'Signature field: Not supported for direct editing.';
} else if (field instanceof PDFButton) {
p.textContent = `Button: ${labelText}`;
} else {
const unsupportedDiv = document.createElement('div');
unsupportedDiv.className = 'p-4 bg-gray-800 rounded-lg border border-gray-700';
const p = document.createElement('p');
p.className = 'text-sm text-gray-400';
if (field instanceof PDFSignature) {
p.textContent = 'Signature field: Not supported for direct editing.';
} else if (field instanceof PDFButton) {
p.textContent = `Button: ${labelText}`;
} else {
p.textContent = `Unsupported field type: ${field.constructor.name}`;
}
unsupportedDiv.appendChild(p);
return unsupportedDiv;
p.textContent = `Unsupported field type: ${field.constructor.name}`;
}
unsupportedDiv.appendChild(p);
return unsupportedDiv;
}
wrapper.appendChild(inputElement);
return wrapper;
wrapper.appendChild(inputElement);
return wrapper;
}
export async function setupFormFiller() {
if (!state.pdfDoc) return;
if (!state.pdfDoc) return;
showLoader('Analyzing form fields...');
const formContainer = document.getElementById('form-fields-container');
const processBtn = document.getElementById('process-btn');
showLoader('Analyzing form fields...');
const formContainer = document.getElementById('form-fields-container');
const processBtn = document.getElementById('process-btn');
if (!formContainer || !processBtn) {
console.error('Required DOM elements not found');
hideLoader();
return;
}
if (!formContainer || !processBtn) {
console.error('Required DOM elements not found');
hideLoader();
return;
}
try {
const form = state.pdfDoc.getForm();
const fields = form.getFields();
formState.fields = fields;
try {
const form = state.pdfDoc.getForm();
const fields = form.getFields();
formState.fields = fields;
formContainer.textContent = '';
formContainer.textContent = '';
if (fields.length === 0) {
formContainer.innerHTML = '<p class="text-center text-gray-400">This PDF contains no form fields.</p>';
processBtn.classList.add('hidden');
} else {
fields.forEach((field: any) => {
try {
const fieldElement = createFormFieldHtml(field);
formContainer.appendChild(fieldElement);
} catch (e: any) {
console.error(`Error processing field "${field.getName()}":`, e);
const errorDiv = document.createElement('div');
errorDiv.className = 'p-4 bg-gray-800 rounded-lg border border-gray-700';
// Sanitize error message display
const p1 = document.createElement('p');
p1.className = 'text-sm text-gray-500';
p1.textContent = `Unsupported field: ${field.getName()}`;
const p2 = document.createElement('p');
p2.className = 'text-xs text-gray-500';
p2.textContent = e.message;
errorDiv.append(p1, p2);
formContainer.appendChild(errorDiv);
}
});
processBtn.classList.remove('hidden');
formContainer.addEventListener('change', handleFormChange);
formContainer.addEventListener('input', handleFormChange);
if (fields.length === 0) {
formContainer.innerHTML =
'<p class="text-center text-gray-400">This PDF contains no form fields.</p>';
processBtn.classList.add('hidden');
} else {
fields.forEach((field: any) => {
try {
const fieldElement = createFormFieldHtml(field);
formContainer.appendChild(fieldElement);
} catch (e: any) {
console.error(`Error processing field "${field.getName()}":`, e);
const errorDiv = document.createElement('div');
errorDiv.className =
'p-4 bg-gray-800 rounded-lg border border-gray-700';
// Sanitize error message display
const p1 = document.createElement('p');
p1.className = 'text-sm text-gray-500';
p1.textContent = `Unsupported field: ${field.getName()}`;
const p2 = document.createElement('p');
p2.className = 'text-xs text-gray-500';
p2.textContent = e.message;
errorDiv.append(p1, p2);
formContainer.appendChild(errorDiv);
}
});
const pdfBytes = await state.pdfDoc.save();
pdfJsDoc = await pdfjsLib.getDocument({ data: pdfBytes }).promise;
currentPageNum = 1;
await renderPage();
const zoomInBtn = document.getElementById('zoom-in-btn');
const zoomOutBtn = document.getElementById('zoom-out-btn');
const prevPageBtn = document.getElementById('prev-page');
const nextPageBtn = document.getElementById('next-page');
if (zoomInBtn) zoomInBtn.addEventListener('click', () => setZoom(formState.scale + 0.25));
if (zoomOutBtn) zoomOutBtn.addEventListener('click', () => setZoom(Math.max(1, formState.scale - 0.25)));
if (prevPageBtn) prevPageBtn.addEventListener('click', () => changePage(-1));
if (nextPageBtn) nextPageBtn.addEventListener('click', () => changePage(1));
hideLoader();
const formFillerOptions = document.getElementById('form-filler-options');
if (formFillerOptions) formFillerOptions.classList.remove('hidden');
} catch (e) {
console.error("Critical error setting up form filler:", e);
showAlert('Error', 'Failed to read PDF form data. The file may be corrupt or not a valid form.');
hideLoader();
processBtn.classList.remove('hidden');
formContainer.addEventListener('change', handleFormChange);
formContainer.addEventListener('input', handleFormChange);
}
const pdfBytes = await state.pdfDoc.save();
pdfJsDoc = await pdfjsLib.getDocument({ data: pdfBytes }).promise;
currentPageNum = 1;
await renderPage();
const zoomInBtn = document.getElementById('zoom-in-btn');
const zoomOutBtn = document.getElementById('zoom-out-btn');
const prevPageBtn = document.getElementById('prev-page');
const nextPageBtn = document.getElementById('next-page');
if (zoomInBtn)
zoomInBtn.addEventListener('click', () =>
setZoom(formState.scale + 0.25)
);
if (zoomOutBtn)
zoomOutBtn.addEventListener('click', () =>
setZoom(Math.max(1, formState.scale - 0.25))
);
if (prevPageBtn)
prevPageBtn.addEventListener('click', () => changePage(-1));
if (nextPageBtn) nextPageBtn.addEventListener('click', () => changePage(1));
hideLoader();
const formFillerOptions = document.getElementById('form-filler-options');
if (formFillerOptions) formFillerOptions.classList.remove('hidden');
} catch (e) {
console.error('Critical error setting up form filler:', e);
showAlert(
'Error',
'Failed to read PDF form data. The file may be corrupt or not a valid form.'
);
hideLoader();
}
}
export async function processAndDownloadForm() {
showLoader('Applying form data...');
try {
const form = state.pdfDoc.getForm();
showLoader('Applying form data...');
try {
const form = state.pdfDoc.getForm();
Object.keys(fieldValues).forEach(fieldName => {
try {
const field = form.getField(fieldName);
const value = fieldValues[fieldName];
Object.keys(fieldValues).forEach((fieldName) => {
try {
const field = form.getField(fieldName);
const value = fieldValues[fieldName];
if (field instanceof PDFTextField) {
field.setText(value);
} else if (field instanceof PDFCheckBox) {
if (value === 'on') {
field.check();
} else {
field.uncheck();
}
} else if (field instanceof PDFRadioGroup) {
field.select(value);
} else if (field instanceof PDFDropdown) {
field.select(value);
} else if (field instanceof PDFOptionList) {
if (Array.isArray(value)) {
value.forEach(option => field.select(option));
}
}
} catch (e) {
console.error(`Error processing field "${fieldName}" during download:`, e);
}
});
if (field instanceof PDFTextField) {
field.setText(value);
} else if (field instanceof PDFCheckBox) {
if (value === 'on') {
field.check();
} else {
field.uncheck();
}
} else if (field instanceof PDFRadioGroup) {
field.select(value);
} else if (field instanceof PDFDropdown) {
field.select(value);
} else if (field instanceof PDFOptionList) {
if (Array.isArray(value)) {
value.forEach((option) => field.select(option));
}
}
} catch (e) {
console.error(
`Error processing field "${fieldName}" during download:`,
e
);
}
});
const newPdfBytes = await state.pdfDoc.save();
downloadFile(new Blob([newPdfBytes], { type: 'application/pdf' }), 'filled-form.pdf');
const newPdfBytes = await state.pdfDoc.save();
downloadFile(
new Blob([newPdfBytes], { type: 'application/pdf' }),
'filled-form.pdf'
);
showAlert('Success', 'Form has been filled and downloaded.');
} catch (e) {
console.error(e);
showAlert('Error', 'Failed to save the filled form.');
} finally {
hideLoader();
}
}
showAlert('Success', 'Form has been filled and downloaded.');
} catch (e) {
console.error(e);
showAlert('Error', 'Failed to save the filled form.');
} finally {
hideLoader();
}
}

View File

@@ -1,4 +1,3 @@
import { showLoader, hideLoader, showAlert } from '../ui.js';
import { downloadFile } from '../utils/helpers.js';
import { state } from '../state.js';
@@ -6,31 +5,44 @@ import heic2any from 'heic2any';
import { PDFDocument as PDFLibDocument } from 'pdf-lib';
export async function heicToPdf() {
if (state.files.length === 0) {
showAlert('No Files', 'Please select at least one HEIC file.');
return;
if (state.files.length === 0) {
showAlert('No Files', 'Please select at least one HEIC file.');
return;
}
showLoader('Converting HEIC to PDF...');
try {
const pdfDoc = await PDFLibDocument.create();
for (const file of state.files) {
const conversionResult = await heic2any({
blob: file,
toType: 'image/png',
});
const pngBlob = Array.isArray(conversionResult)
? conversionResult[0]
: conversionResult;
const pngBytes = await pngBlob.arrayBuffer();
const pngImage = await pdfDoc.embedPng(pngBytes);
const page = pdfDoc.addPage([pngImage.width, pngImage.height]);
page.drawImage(pngImage, {
x: 0,
y: 0,
width: pngImage.width,
height: pngImage.height,
});
}
showLoader('Converting HEIC to PDF...');
try {
const pdfDoc = await PDFLibDocument.create();
for (const file of state.files) {
const conversionResult = await heic2any({
blob: file,
toType: "image/png",
});
const pngBlob = Array.isArray(conversionResult) ? conversionResult[0] : conversionResult;
const pngBytes = await pngBlob.arrayBuffer();
const pngImage = await pdfDoc.embedPng(pngBytes);
const page = pdfDoc.addPage([pngImage.width, pngImage.height]);
page.drawImage(pngImage, { x: 0, y: 0, width: pngImage.width, height: pngImage.height });
}
const pdfBytes = await pdfDoc.save();
downloadFile(new Blob([new Uint8Array(pdfBytes)], { type: 'application/pdf' }), 'from_heic.pdf');
} catch (e) {
console.error(e);
showAlert('Error', 'Failed to convert HEIC to PDF. One of the files may be invalid or unsupported.');
} finally {
hideLoader();
}
}
const pdfBytes = await pdfDoc.save();
downloadFile(
new Blob([new Uint8Array(pdfBytes)], { type: 'application/pdf' }),
'from_heic.pdf'
);
} catch (e) {
console.error(e);
showAlert(
'Error',
'Failed to convert HEIC to PDF. One of the files may be invalid or unsupported.'
);
} finally {
hideLoader();
}
}

View File

@@ -25,9 +25,12 @@ function sanitizeImageAsJpeg(imageBytes: any) {
ctx.drawImage(img, 0, 0);
canvas.toBlob(
async (jpegBlob) => {
if (!jpegBlob) return reject(new Error('Canvas to JPEG conversion failed.'));
if (!jpegBlob)
return reject(new Error('Canvas to JPEG conversion failed.'));
resolve(new Uint8Array(await jpegBlob.arrayBuffer()));
}, 'image/jpeg', 0.9
},
'image/jpeg',
0.9
);
URL.revokeObjectURL(imageUrl);
};
@@ -56,12 +59,11 @@ function sanitizeImageAsPng(imageBytes: any) {
canvas.height = img.naturalHeight;
const ctx = canvas.getContext('2d');
ctx.drawImage(img, 0, 0);
canvas.toBlob(
async (pngBlob) => {
if (!pngBlob) return reject(new Error('Canvas to PNG conversion failed.'));
resolve(new Uint8Array(await pngBlob.arrayBuffer()));
}, 'image/png'
);
canvas.toBlob(async (pngBlob) => {
if (!pngBlob)
return reject(new Error('Canvas to PNG conversion failed.'));
resolve(new Uint8Array(await pngBlob.arrayBuffer()));
}, 'image/png');
URL.revokeObjectURL(imageUrl);
};
img.onerror = () => {
@@ -72,62 +74,77 @@ function sanitizeImageAsPng(imageBytes: any) {
});
}
export async function imageToPdf() {
if (state.files.length === 0) {
showAlert('No Files', 'Please select at least one image file.');
return;
}
showLoader('Converting images to PDF...');
try {
const pdfDoc = await PDFLibDocument.create();
const imageList = document.getElementById('image-list');
const sortedFiles = Array.from(imageList.children)
// @ts-expect-error TS(2339) FIXME: Property 'dataset' does not exist on type 'Element... Remove this comment to see the full error message
.map(li => state.files.find(f => f.name === li.dataset.fileName))
.filter(Boolean);
if (state.files.length === 0) {
showAlert('No Files', 'Please select at least one image file.');
return;
}
showLoader('Converting images to PDF...');
try {
const pdfDoc = await PDFLibDocument.create();
const imageList = document.getElementById('image-list');
const sortedFiles = Array.from(imageList.children)
// @ts-expect-error TS(2339) FIXME: Property 'dataset' does not exist on type 'Element... Remove this comment to see the full error message
.map((li) => state.files.find((f) => f.name === li.dataset.fileName))
.filter(Boolean);
for (const file of sortedFiles) {
const fileBuffer = await readFileAsArrayBuffer(file);
let image;
for (const file of sortedFiles) {
const fileBuffer = await readFileAsArrayBuffer(file);
let image;
if (file.type === 'image/jpeg') {
try {
image = await pdfDoc.embedJpg(fileBuffer as Uint8Array);
} catch (e) {
console.warn(`Direct JPG embedding failed for ${file.name}, sanitizing to JPG...`);
const sanitizedBytes = await sanitizeImageAsJpeg(fileBuffer);
image = await pdfDoc.embedJpg(sanitizedBytes as Uint8Array);
}
} else if (file.type === 'image/png') {
try {
image = await pdfDoc.embedPng(fileBuffer as Uint8Array);
} catch (e) {
console.warn(`Direct PNG embedding failed for ${file.name}, sanitizing to PNG...`);
const sanitizedBytes = await sanitizeImageAsPng(fileBuffer);
image = await pdfDoc.embedPng(sanitizedBytes as Uint8Array);
}
} else {
// For WebP and other types, convert to PNG to preserve transparency
console.warn(`Unsupported type "${file.type}" for ${file.name}, converting to PNG...`);
const sanitizedBytes = await sanitizeImageAsPng(fileBuffer);
image = await pdfDoc.embedPng(sanitizedBytes as Uint8Array);
}
const page = pdfDoc.addPage([image.width, image.height]);
page.drawImage(image, { x: 0, y: 0, width: image.width, height: image.height });
if (file.type === 'image/jpeg') {
try {
image = await pdfDoc.embedJpg(fileBuffer as Uint8Array);
} catch (e) {
console.warn(
`Direct JPG embedding failed for ${file.name}, sanitizing to JPG...`
);
const sanitizedBytes = await sanitizeImageAsJpeg(fileBuffer);
image = await pdfDoc.embedJpg(sanitizedBytes as Uint8Array);
}
if (pdfDoc.getPageCount() === 0) {
throw new Error("No valid images could be processed. Please check your files.");
} else if (file.type === 'image/png') {
try {
image = await pdfDoc.embedPng(fileBuffer as Uint8Array);
} catch (e) {
console.warn(
`Direct PNG embedding failed for ${file.name}, sanitizing to PNG...`
);
const sanitizedBytes = await sanitizeImageAsPng(fileBuffer);
image = await pdfDoc.embedPng(sanitizedBytes as Uint8Array);
}
} else {
// For WebP and other types, convert to PNG to preserve transparency
console.warn(
`Unsupported type "${file.type}" for ${file.name}, converting to PNG...`
);
const sanitizedBytes = await sanitizeImageAsPng(fileBuffer);
image = await pdfDoc.embedPng(sanitizedBytes as Uint8Array);
}
const pdfBytes = await pdfDoc.save();
downloadFile(new Blob([new Uint8Array(pdfBytes)], { type: 'application/pdf' }), 'from-images.pdf');
} catch (e) {
console.error(e);
showAlert('Error', e.message || 'Failed to create PDF from images.');
} finally {
hideLoader();
const page = pdfDoc.addPage([image.width, image.height]);
page.drawImage(image, {
x: 0,
y: 0,
width: image.width,
height: image.height,
});
}
if (pdfDoc.getPageCount() === 0) {
throw new Error(
'No valid images could be processed. Please check your files.'
);
}
const pdfBytes = await pdfDoc.save();
downloadFile(
new Blob([new Uint8Array(pdfBytes)], { type: 'application/pdf' }),
'from-images.pdf'
);
} catch (e) {
console.error(e);
showAlert('Error', e.message || 'Failed to create PDF from images.');
} finally {
hideLoader();
}
}

View File

@@ -49,68 +49,83 @@ import { setupCompareTool } from './compare-pdfs.js';
import { setupOcrTool } from './ocr-pdf.js';
import { wordToPdf } from './word-to-pdf.js';
import { applyAndSaveSignatures, setupSignTool } from './sign-pdf.js';
import { removeAnnotations, setupRemoveAnnotationsTool } from './remove-annotations.js';
import {
removeAnnotations,
setupRemoveAnnotationsTool,
} from './remove-annotations.js';
import { setupCropperTool } from './cropper.js';
import { processAndDownloadForm, setupFormFiller } from './form-filler.js';
import { posterize, setupPosterizeTool } from './posterize.js';
import { removeBlankPages, setupRemoveBlankPagesTool } from './remove-blank-pages.js';
import {
removeBlankPages,
setupRemoveBlankPagesTool,
} from './remove-blank-pages.js';
import { alternateMerge, setupAlternateMergeTool } from './alternate-merge.js';
export const toolLogic = {
merge: { process: merge, setup: setupMergeTool },
split: { process: split, setup: setupSplitTool },
encrypt,
decrypt,
organize,
rotate,
'add-page-numbers': addPageNumbers,
'pdf-to-jpg': pdfToJpg,
'jpg-to-pdf': jpgToPdf,
'scan-to-pdf': scanToPdf,
compress,
'pdf-to-greyscale': pdfToGreyscale,
'pdf-to-zip': pdfToZip,
'edit-metadata': editMetadata,
'remove-metadata': removeMetadata,
flatten,
'pdf-to-png': pdfToPng,
'png-to-pdf': pngToPdf,
'pdf-to-webp': pdfToWebp,
'webp-to-pdf': webpToPdf,
'delete-pages': deletePages,
'add-blank-page': addBlankPage,
'extract-pages': extractPages,
'add-watermark': { process: addWatermark, setup: setupWatermarkUI },
'add-header-footer': addHeaderFooter,
'image-to-pdf': imageToPdf,
'change-permissions': changePermissions,
'pdf-to-markdown': pdfToMarkdown,
'txt-to-pdf': txtToPdf,
'invert-colors': invertColors,
'reverse-pages': reversePages,
'md-to-pdf': mdToPdf,
'svg-to-pdf': svgToPdf,
'bmp-to-pdf': bmpToPdf,
'heic-to-pdf': heicToPdf,
'tiff-to-pdf': tiffToPdf,
'pdf-to-bmp': pdfToBmp,
'pdf-to-tiff': pdfToTiff,
'split-in-half': splitInHalf,
'page-dimensions': analyzeAndDisplayDimensions,
'n-up': { process: nUpTool, setup: setupNUpUI },
'duplicate-organize': { process: processAndSave },
'combine-single-page': combineToSinglePage,
'fix-dimensions': { process: fixDimensions, setup: setupFixDimensionsUI },
'change-background-color': changeBackgroundColor,
'change-text-color': { process: changeTextColor, setup: setupTextColorTool },
'compare-pdfs': { setup: setupCompareTool },
'ocr-pdf': { setup: setupOcrTool },
'word-to-pdf': wordToPdf,
'sign-pdf': { process: applyAndSaveSignatures, setup: setupSignTool },
'remove-annotations': { process: removeAnnotations, setup: setupRemoveAnnotationsTool },
'cropper': { setup: setupCropperTool },
'form-filler': { process: processAndDownloadForm, setup: setupFormFiller},
'posterize': { process: posterize, setup: setupPosterizeTool },
'remove-blank-pages': { process: removeBlankPages, setup: setupRemoveBlankPagesTool },
'alternate-merge': { process: alternateMerge, setup: setupAlternateMergeTool },
};
merge: { process: merge, setup: setupMergeTool },
split: { process: split, setup: setupSplitTool },
encrypt,
decrypt,
organize,
rotate,
'add-page-numbers': addPageNumbers,
'pdf-to-jpg': pdfToJpg,
'jpg-to-pdf': jpgToPdf,
'scan-to-pdf': scanToPdf,
compress,
'pdf-to-greyscale': pdfToGreyscale,
'pdf-to-zip': pdfToZip,
'edit-metadata': editMetadata,
'remove-metadata': removeMetadata,
flatten,
'pdf-to-png': pdfToPng,
'png-to-pdf': pngToPdf,
'pdf-to-webp': pdfToWebp,
'webp-to-pdf': webpToPdf,
'delete-pages': deletePages,
'add-blank-page': addBlankPage,
'extract-pages': extractPages,
'add-watermark': { process: addWatermark, setup: setupWatermarkUI },
'add-header-footer': addHeaderFooter,
'image-to-pdf': imageToPdf,
'change-permissions': changePermissions,
'pdf-to-markdown': pdfToMarkdown,
'txt-to-pdf': txtToPdf,
'invert-colors': invertColors,
'reverse-pages': reversePages,
'md-to-pdf': mdToPdf,
'svg-to-pdf': svgToPdf,
'bmp-to-pdf': bmpToPdf,
'heic-to-pdf': heicToPdf,
'tiff-to-pdf': tiffToPdf,
'pdf-to-bmp': pdfToBmp,
'pdf-to-tiff': pdfToTiff,
'split-in-half': splitInHalf,
'page-dimensions': analyzeAndDisplayDimensions,
'n-up': { process: nUpTool, setup: setupNUpUI },
'duplicate-organize': { process: processAndSave },
'combine-single-page': combineToSinglePage,
'fix-dimensions': { process: fixDimensions, setup: setupFixDimensionsUI },
'change-background-color': changeBackgroundColor,
'change-text-color': { process: changeTextColor, setup: setupTextColorTool },
'compare-pdfs': { setup: setupCompareTool },
'ocr-pdf': { setup: setupOcrTool },
'word-to-pdf': wordToPdf,
'sign-pdf': { process: applyAndSaveSignatures, setup: setupSignTool },
'remove-annotations': {
process: removeAnnotations,
setup: setupRemoveAnnotationsTool,
},
cropper: { setup: setupCropperTool },
'form-filler': { process: processAndDownloadForm, setup: setupFormFiller },
posterize: { process: posterize, setup: setupPosterizeTool },
'remove-blank-pages': {
process: removeBlankPages,
setup: setupRemoveBlankPagesTool,
},
'alternate-merge': {
process: alternateMerge,
setup: setupAlternateMergeTool,
},
};

View File

@@ -4,49 +4,62 @@ import { state } from '../state.js';
import { PDFDocument as PDFLibDocument } from 'pdf-lib';
export async function invertColors() {
if (!state.pdfDoc) { showAlert('Error', 'PDF not loaded.'); return; }
showLoader('Inverting PDF colors...');
try {
const newPdfDoc = await PDFLibDocument.create();
const pdfBytes = await state.pdfDoc.save();
// @ts-expect-error TS(2304) FIXME: Cannot find name 'pdfjsLib'.
const pdfjsDoc = await pdfjsLib.getDocument({ data: pdfBytes }).promise;
if (!state.pdfDoc) {
showAlert('Error', 'PDF not loaded.');
return;
}
showLoader('Inverting PDF colors...');
try {
const newPdfDoc = await PDFLibDocument.create();
const pdfBytes = await state.pdfDoc.save();
// @ts-expect-error TS(2304) FIXME: Cannot find name 'pdfjsLib'.
const pdfjsDoc = await pdfjsLib.getDocument({ data: pdfBytes }).promise;
for (let i = 1; i <= pdfjsDoc.numPages; i++) {
const page = await pdfjsDoc.getPage(i);
const viewport = page.getViewport({ scale: 1.5 });
const canvas = document.createElement('canvas');
canvas.width = viewport.width;
canvas.height = viewport.height;
const ctx = canvas.getContext('2d');
await page.render({ canvasContext: ctx, viewport }).promise;
for (let i = 1; i <= pdfjsDoc.numPages; i++) {
const page = await pdfjsDoc.getPage(i);
const viewport = page.getViewport({ scale: 1.5 });
const canvas = document.createElement('canvas');
canvas.width = viewport.width;
canvas.height = viewport.height;
const ctx = canvas.getContext('2d');
await page.render({ canvasContext: ctx, viewport }).promise;
const imageData = ctx.getImageData(0, 0, canvas.width, canvas.height);
const data = imageData.data;
for (let j = 0; j < data.length; j += 4) {
data[j] = 255 - data[j]; // red
data[j + 1] = 255 - data[j + 1]; // green
data[j + 2] = 255 - data[j + 2]; // blue
}
ctx.putImageData(imageData, 0, 0);
const imageData = ctx.getImageData(0, 0, canvas.width, canvas.height);
const data = imageData.data;
for (let j = 0; j < data.length; j += 4) {
data[j] = 255 - data[j]; // red
data[j + 1] = 255 - data[j + 1]; // green
data[j + 2] = 255 - data[j + 2]; // blue
}
ctx.putImageData(imageData, 0, 0);
const pngImageBytes = await new Promise(resolve => canvas.toBlob(blob => {
const reader = new FileReader();
// @ts-expect-error TS(2769) FIXME: No overload matches this call.
reader.onload = () => resolve(new Uint8Array(reader.result));
reader.readAsArrayBuffer(blob);
}, 'image/png'));
const image = await newPdfDoc.embedPng(pngImageBytes as ArrayBuffer);
const newPage = newPdfDoc.addPage([image.width, image.height]);
newPage.drawImage(image, { x: 0, y: 0, width: image.width, height: image.height });
}
const newPdfBytes = await newPdfDoc.save();
downloadFile(new Blob([new Uint8Array(newPdfBytes)], { type: 'application/pdf' }), 'inverted.pdf');
} catch (e) {
console.error(e);
showAlert('Error', 'Could not invert PDF colors.');
} finally {
hideLoader();
const pngImageBytes = await new Promise((resolve) =>
canvas.toBlob((blob) => {
const reader = new FileReader();
// @ts-expect-error TS(2769) FIXME: No overload matches this call.
reader.onload = () => resolve(new Uint8Array(reader.result));
reader.readAsArrayBuffer(blob);
}, 'image/png')
);
const image = await newPdfDoc.embedPng(pngImageBytes as ArrayBuffer);
const newPage = newPdfDoc.addPage([image.width, image.height]);
newPage.drawImage(image, {
x: 0,
y: 0,
width: image.width,
height: image.height,
});
}
}
const newPdfBytes = await newPdfDoc.save();
downloadFile(
new Blob([new Uint8Array(newPdfBytes)], { type: 'application/pdf' }),
'inverted.pdf'
);
} catch (e) {
console.error(e);
showAlert('Error', 'Could not invert PDF colors.');
} finally {
hideLoader();
}
}

View File

@@ -5,7 +5,7 @@ import { state } from '../state.js';
import { PDFDocument as PDFLibDocument } from 'pdf-lib';
/**
* Takes any image byte array and uses the browser's canvas to convert it
* Takes any image byte array and uses the browser's canvas to convert it
* into a standard, web-friendly (baseline, sRGB) JPEG byte array.
* @param {Uint8Array} imageBytes The raw bytes of the image file.
* @returns {Promise<Uint8Array>} A promise that resolves with the sanitized JPEG bytes.
@@ -39,7 +39,11 @@ function sanitizeImageAsJpeg(imageBytes: any) {
img.onerror = () => {
URL.revokeObjectURL(imageUrl);
reject(new Error('The provided file could not be loaded as an image. It may be corrupted.'));
reject(
new Error(
'The provided file could not be loaded as an image. It may be corrupted.'
)
);
};
img.src = imageUrl;
@@ -63,13 +67,20 @@ export async function jpgToPdf() {
jpgImage = await pdfDoc.embedJpg(originalBytes as Uint8Array);
} catch (e) {
// @ts-expect-error TS(2554) FIXME: Expected 2 arguments, but got 1.
showAlert(`Direct JPG embedding failed for ${file.name}, attempting to sanitize...`);
showAlert(
`Direct JPG embedding failed for ${file.name}, attempting to sanitize...`
);
try {
const sanitizedBytes = await sanitizeImageAsJpeg(originalBytes);
jpgImage = await pdfDoc.embedJpg(sanitizedBytes as Uint8Array);
} catch (fallbackError) {
console.error(`Failed to process ${file.name} after sanitization:`, fallbackError);
throw new Error(`Could not process "${file.name}". The file may be corrupted.`);
console.error(
`Failed to process ${file.name} after sanitization:`,
fallbackError
);
throw new Error(
`Could not process "${file.name}". The file may be corrupted.`
);
}
}
@@ -83,7 +94,10 @@ export async function jpgToPdf() {
}
const pdfBytes = await pdfDoc.save();
downloadFile(new Blob([new Uint8Array(pdfBytes)], { type: 'application/pdf' }), 'from_jpgs.pdf');
downloadFile(
new Blob([new Uint8Array(pdfBytes)], { type: 'application/pdf' }),
'from_jpgs.pdf'
);
} catch (e) {
console.error(e);
showAlert('Conversion Error', e.message);

View File

@@ -3,35 +3,41 @@ import { showLoader, hideLoader, showAlert } from '../ui.js';
import { downloadFile } from '../utils/helpers.js';
import html2canvas from 'html2canvas';
export async function mdToPdf() {
// @ts-expect-error TS(2339) FIXME: Property 'jspdf' does not exist on type 'Window & ... Remove this comment to see the full error message
if (typeof window.jspdf === 'undefined' || typeof window.html2canvas === 'undefined') {
showAlert('Libraries Not Ready', 'PDF generation libraries are loading. Please try again.');
return;
}
// @ts-expect-error TS(2339) FIXME: Property 'jspdf' does not exist on type 'Window & ... Remove this comment to see the full error message
if (
typeof window.jspdf === 'undefined' ||
typeof window.html2canvas === 'undefined'
) {
showAlert(
'Libraries Not Ready',
'PDF generation libraries are loading. Please try again.'
);
return;
}
// @ts-expect-error TS(2339) FIXME: Property 'value' does not exist on type 'HTMLEleme... Remove this comment to see the full error message
const markdownContent = document.getElementById('md-input').value.trim();
if (!markdownContent) {
showAlert('Input Required', 'Please enter some Markdown text.');
return;
}
showLoader('Generating High-Quality PDF...');
try {
// @ts-expect-error TS(2304) FIXME: Cannot find name 'marked'.
const htmlContent = marked.parse(markdownContent);
// @ts-expect-error TS(2339) FIXME: Property 'value' does not exist on type 'HTMLEleme... Remove this comment to see the full error message
const markdownContent = document.getElementById('md-input').value.trim();
if (!markdownContent) {
showAlert('Input Required', 'Please enter some Markdown text.');
return;
}
showLoader('Generating High-Quality PDF...');
const pageFormat = document.getElementById('page-format').value;
// @ts-expect-error TS(2339) FIXME: Property 'value' does not exist on type 'HTMLEleme... Remove this comment to see the full error message
const orientation = document.getElementById('orientation').value;
// @ts-expect-error TS(2339) FIXME: Property 'value' does not exist on type 'HTMLEleme... Remove this comment to see the full error message
const marginSize = document.getElementById('margin-size').value;
try {
// @ts-expect-error TS(2304) FIXME: Cannot find name 'marked'.
const htmlContent = marked.parse(markdownContent);
// @ts-expect-error TS(2339) FIXME: Property 'value' does not exist on type 'HTMLEleme... Remove this comment to see the full error message
const pageFormat = document.getElementById('page-format').value;
// @ts-expect-error TS(2339) FIXME: Property 'value' does not exist on type 'HTMLEleme... Remove this comment to see the full error message
const orientation = document.getElementById('orientation').value;
// @ts-expect-error TS(2339) FIXME: Property 'value' does not exist on type 'HTMLEleme... Remove this comment to see the full error message
const marginSize = document.getElementById('margin-size').value;
const tempContainer = document.createElement('div');
tempContainer.style.cssText = 'position: absolute; top: -9999px; left: -9999px; width: 800px; padding: 40px; background: white; color: black;';
const styleSheet = document.createElement('style');
styleSheet.textContent = `
const tempContainer = document.createElement('div');
tempContainer.style.cssText =
'position: absolute; top: -9999px; left: -9999px; width: 800px; padding: 40px; background: white; color: black;';
const styleSheet = document.createElement('style');
styleSheet.textContent = `
body { font-family: Helvetica, Arial, sans-serif; line-height: 1.6; font-size: 12px; }
h1, h2, h3 { margin: 20px 0 10px 0; font-weight: 600; border-bottom: 1px solid #eaecef; padding-bottom: .3em; }
h1 { font-size: 2em; } h2 { font-size: 1.5em; }
@@ -42,44 +48,48 @@ export async function mdToPdf() {
table { width: 100%; border-collapse: collapse; } th, td { padding: 6px 13px; border: 1px solid #dfe2e5; }
img { max-width: 100%; }
`;
tempContainer.appendChild(styleSheet);
tempContainer.innerHTML += htmlContent;
document.body.appendChild(tempContainer);
tempContainer.appendChild(styleSheet);
tempContainer.innerHTML += htmlContent;
document.body.appendChild(tempContainer);
const canvas = await html2canvas(tempContainer, { scale: 2, useCORS: true });
document.body.removeChild(tempContainer);
const canvas = await html2canvas(tempContainer, {
scale: 2,
useCORS: true,
});
document.body.removeChild(tempContainer);
// @ts-expect-error TS(2339) FIXME: Property 'jspdf' does not exist on type 'Window & ... Remove this comment to see the full error message
const { jsPDF } = window.jspdf;
const pdf = new jsPDF({ orientation, unit: 'mm', format: pageFormat });
const pageFormats = { 'a4': [210, 297], 'letter': [216, 279] };
const format = pageFormats[pageFormat];
const [pageWidth, pageHeight] = orientation === 'landscape' ? [format[1], format[0]] : format;
const margins = { 'narrow': 10, 'normal': 20, 'wide': 30 };
const margin = margins[marginSize];
const contentWidth = pageWidth - (margin * 2);
const contentHeight = pageHeight - (margin * 2);
const imgData = canvas.toDataURL('image/png');
const imgHeight = (canvas.height * contentWidth) / canvas.width;
// @ts-expect-error TS(2339) FIXME: Property 'jspdf' does not exist on type 'Window & ... Remove this comment to see the full error message
const { jsPDF } = window.jspdf;
const pdf = new jsPDF({ orientation, unit: 'mm', format: pageFormat });
const pageFormats = { a4: [210, 297], letter: [216, 279] };
const format = pageFormats[pageFormat];
const [pageWidth, pageHeight] =
orientation === 'landscape' ? [format[1], format[0]] : format;
const margins = { narrow: 10, normal: 20, wide: 30 };
const margin = margins[marginSize];
const contentWidth = pageWidth - margin * 2;
const contentHeight = pageHeight - margin * 2;
const imgData = canvas.toDataURL('image/png');
const imgHeight = (canvas.height * contentWidth) / canvas.width;
let heightLeft = imgHeight;
let position = margin;
pdf.addImage(imgData, 'PNG', margin, position, contentWidth, imgHeight);
heightLeft -= contentHeight;
let heightLeft = imgHeight;
let position = margin;
pdf.addImage(imgData, 'PNG', margin, position, contentWidth, imgHeight);
heightLeft -= contentHeight;
while (heightLeft > 0) {
position = position - pageHeight;
pdf.addPage();
pdf.addImage(imgData, 'PNG', margin, position, contentWidth, imgHeight);
heightLeft -= contentHeight;
}
const pdfBlob = pdf.output('blob');
downloadFile(pdfBlob, 'markdown-document.pdf');
} catch (error) {
console.error('MD to PDF conversion error:', error);
showAlert('Conversion Error', 'Failed to generate PDF.');
} finally {
hideLoader();
while (heightLeft > 0) {
position = position - pageHeight;
pdf.addPage();
pdf.addImage(imgData, 'PNG', margin, position, contentWidth, imgHeight);
heightLeft -= contentHeight;
}
}
const pdfBlob = pdf.output('blob');
downloadFile(pdfBlob, 'markdown-document.pdf');
} catch (error) {
console.error('MD to PDF conversion error:', error);
showAlert('Conversion Error', 'Failed to generate PDF.');
} finally {
hideLoader();
}
}

View File

@@ -7,390 +7,419 @@ import * as pdfjsLib from 'pdfjs-dist';
import Sortable from 'sortablejs';
pdfjsLib.GlobalWorkerOptions.workerSrc = new URL(
'pdfjs-dist/build/pdf.worker.min.mjs',
import.meta.url
'pdfjs-dist/build/pdf.worker.min.mjs',
import.meta.url
).toString();
const mergeState = {
pdfDocs: {},
activeMode: 'file',
sortableInstances: {},
isRendering: false,
cachedThumbnails: null,
lastFileHash: null
pdfDocs: {},
activeMode: 'file',
sortableInstances: {},
isRendering: false,
cachedThumbnails: null,
lastFileHash: null,
};
function parsePageRanges(rangeString: any, totalPages: any) {
const indices = new Set();
if (!rangeString.trim()) return [];
const indices = new Set();
if (!rangeString.trim()) return [];
const ranges = rangeString.split(',');
for (const range of ranges) {
const trimmedRange = range.trim();
if (trimmedRange.includes('-')) {
const [start, end] = trimmedRange.split('-').map(Number);
if (isNaN(start) || isNaN(end) || start < 1 || end > totalPages || start > end) continue;
for (let i = start; i <= end; i++) {
indices.add(i - 1);
}
} else {
const pageNum = Number(trimmedRange);
if (isNaN(pageNum) || pageNum < 1 || pageNum > totalPages) continue;
indices.add(pageNum - 1);
}
const ranges = rangeString.split(',');
for (const range of ranges) {
const trimmedRange = range.trim();
if (trimmedRange.includes('-')) {
const [start, end] = trimmedRange.split('-').map(Number);
if (
isNaN(start) ||
isNaN(end) ||
start < 1 ||
end > totalPages ||
start > end
)
continue;
for (let i = start; i <= end; i++) {
indices.add(i - 1);
}
} else {
const pageNum = Number(trimmedRange);
if (isNaN(pageNum) || pageNum < 1 || pageNum > totalPages) continue;
indices.add(pageNum - 1);
}
// @ts-expect-error TS(2362) FIXME: The left-hand side of an arithmetic operation must... Remove this comment to see the full error message
return Array.from(indices).sort((a, b) => a - b);
}
// @ts-expect-error TS(2362) FIXME: The left-hand side of an arithmetic operation must... Remove this comment to see the full error message
return Array.from(indices).sort((a, b) => a - b);
}
function initializeFileListSortable() {
const fileList = document.getElementById('file-list');
if (!fileList) return;
const fileList = document.getElementById('file-list');
if (!fileList) return;
// @ts-expect-error TS(2339) FIXME: Property 'fileList' does not exist on type '{}'.
if (mergeState.sortableInstances.fileList) {
// @ts-expect-error TS(2339) FIXME: Property 'fileList' does not exist on type '{}'.
if (mergeState.sortableInstances.fileList) {
// @ts-expect-error TS(2339) FIXME: Property 'fileList' does not exist on type '{}'.
mergeState.sortableInstances.fileList.destroy();
}
mergeState.sortableInstances.fileList.destroy();
}
// @ts-expect-error TS(2339) FIXME: Property 'fileList' does not exist on type '{}'.
mergeState.sortableInstances.fileList = Sortable.create(fileList, {
handle: '.drag-handle',
animation: 150,
ghostClass: 'sortable-ghost',
chosenClass: 'sortable-chosen',
dragClass: 'sortable-drag',
onStart: function (evt: any) {
evt.item.style.opacity = '0.5';
},
onEnd: function (evt: any) {
evt.item.style.opacity = '1';
}
});
// @ts-expect-error TS(2339) FIXME: Property 'fileList' does not exist on type '{}'.
mergeState.sortableInstances.fileList = Sortable.create(fileList, {
handle: '.drag-handle',
animation: 150,
ghostClass: 'sortable-ghost',
chosenClass: 'sortable-chosen',
dragClass: 'sortable-drag',
onStart: function (evt: any) {
evt.item.style.opacity = '0.5';
},
onEnd: function (evt: any) {
evt.item.style.opacity = '1';
},
});
}
function initializePageThumbnailsSortable() {
const container = document.getElementById('page-merge-preview');
if (!container) return;
const container = document.getElementById('page-merge-preview');
if (!container) return;
// @ts-expect-error TS(2339) FIXME: Property 'pageThumbnails' does not exist on type '... Remove this comment to see the full error message
if (mergeState.sortableInstances.pageThumbnails) {
// @ts-expect-error TS(2339) FIXME: Property 'pageThumbnails' does not exist on type '... Remove this comment to see the full error message
if (mergeState.sortableInstances.pageThumbnails) {
// @ts-expect-error TS(2339) FIXME: Property 'pageThumbnails' does not exist on type '... Remove this comment to see the full error message
mergeState.sortableInstances.pageThumbnails.destroy();
}
mergeState.sortableInstances.pageThumbnails.destroy();
}
// @ts-expect-error TS(2339) FIXME: Property 'pageThumbnails' does not exist on type '... Remove this comment to see the full error message
mergeState.sortableInstances.pageThumbnails = Sortable.create(container, {
animation: 150,
ghostClass: 'sortable-ghost',
chosenClass: 'sortable-chosen',
dragClass: 'sortable-drag',
onStart: function (evt: any) {
evt.item.style.opacity = '0.5';
},
onEnd: function (evt: any) {
evt.item.style.opacity = '1';
}
});
// @ts-expect-error TS(2339) FIXME: Property 'pageThumbnails' does not exist on type '... Remove this comment to see the full error message
mergeState.sortableInstances.pageThumbnails = Sortable.create(container, {
animation: 150,
ghostClass: 'sortable-ghost',
chosenClass: 'sortable-chosen',
dragClass: 'sortable-drag',
onStart: function (evt: any) {
evt.item.style.opacity = '0.5';
},
onEnd: function (evt: any) {
evt.item.style.opacity = '1';
},
});
}
function generateFileHash() {
return (state.files as File[]).map(f => `${f.name}-${f.size}-${f.lastModified}`).join('|');
return (state.files as File[])
.map((f) => `${f.name}-${f.size}-${f.lastModified}`)
.join('|');
}
async function renderPageMergeThumbnails() {
const container = document.getElementById('page-merge-preview');
if (!container) return;
const container = document.getElementById('page-merge-preview');
if (!container) return;
const currentFileHash = generateFileHash();
const filesChanged = currentFileHash !== mergeState.lastFileHash;
const currentFileHash = generateFileHash();
const filesChanged = currentFileHash !== mergeState.lastFileHash;
if (!filesChanged && mergeState.cachedThumbnails !== null) {
// Simple check to see if it's already rendered to avoid flicker.
if (container.firstChild) {
initializePageThumbnailsSortable();
return;
}
if (!filesChanged && mergeState.cachedThumbnails !== null) {
// Simple check to see if it's already rendered to avoid flicker.
if (container.firstChild) {
initializePageThumbnailsSortable();
return;
}
}
if (mergeState.isRendering) {
return;
}
mergeState.isRendering = true;
container.textContent = '';
let currentPageNumber = 0;
let totalPages = state.files.reduce((sum, file) => {
const pdfDoc = mergeState.pdfDocs[file.name];
return sum + (pdfDoc ? pdfDoc.getPageCount() : 0);
}, 0);
try {
const thumbnailsHTML = [];
for (const file of state.files) {
const pdfDoc = mergeState.pdfDocs[file.name];
if (!pdfDoc) continue;
const pdfData = await pdfDoc.save();
const pdfjsDoc = await pdfjsLib.getDocument({ data: pdfData }).promise;
for (let i = 1; i <= pdfjsDoc.numPages; i++) {
currentPageNumber++;
showLoader(
`Rendering page previews: ${currentPageNumber}/${totalPages}`
);
const page = await pdfjsDoc.getPage(i);
const viewport = page.getViewport({ scale: 0.3 });
const canvas = document.createElement('canvas');
canvas.height = viewport.height;
canvas.width = viewport.width;
const context = canvas.getContext('2d')!;
await page.render({
canvasContext: context,
canvas: canvas,
viewport,
}).promise;
const wrapper = document.createElement('div');
wrapper.className =
'page-thumbnail relative cursor-move flex flex-col items-center gap-1 p-2 border-2 border-gray-600 hover:border-indigo-500 rounded-lg bg-gray-700 transition-colors';
wrapper.dataset.fileName = file.name;
wrapper.dataset.pageIndex = (i - 1).toString();
const imgContainer = document.createElement('div');
imgContainer.className = 'relative';
const img = document.createElement('img');
img.src = canvas.toDataURL();
img.className = 'rounded-md shadow-md max-w-full h-auto';
const pageNumDiv = document.createElement('div');
pageNumDiv.className =
'absolute top-1 left-1 bg-indigo-600 text-white text-xs px-2 py-1 rounded-md font-semibold shadow-lg';
pageNumDiv.textContent = i.toString();
imgContainer.append(img, pageNumDiv);
const fileNamePara = document.createElement('p');
fileNamePara.className =
'text-xs text-gray-400 truncate w-full text-center';
const fullTitle = `${file.name} (page ${i})`;
fileNamePara.title = fullTitle;
fileNamePara.textContent = `${file.name.substring(0, 10)}... (p${i})`;
wrapper.append(imgContainer, fileNamePara);
container.appendChild(wrapper);
}
pdfjsDoc.destroy();
}
if (mergeState.isRendering) {
return;
}
mergeState.cachedThumbnails = true;
mergeState.lastFileHash = currentFileHash;
mergeState.isRendering = true;
container.textContent = '';
let currentPageNumber = 0;
let totalPages = state.files.reduce((sum, file) => {
const pdfDoc = mergeState.pdfDocs[file.name];
return sum + (pdfDoc ? pdfDoc.getPageCount() : 0);
}, 0);
try {
const thumbnailsHTML = [];
for (const file of state.files) {
const pdfDoc = mergeState.pdfDocs[file.name];
if (!pdfDoc) continue;
const pdfData = await pdfDoc.save();
const pdfjsDoc = await pdfjsLib.getDocument({ data: pdfData }).promise;
for (let i = 1; i <= pdfjsDoc.numPages; i++) {
currentPageNumber++;
showLoader(`Rendering page previews: ${currentPageNumber}/${totalPages}`);
const page = await pdfjsDoc.getPage(i);
const viewport = page.getViewport({ scale: 0.3 });
const canvas = document.createElement('canvas');
canvas.height = viewport.height;
canvas.width = viewport.width;
const context = canvas.getContext('2d')!;
await page.render({
canvasContext: context,
canvas: canvas,
viewport
}).promise;
const wrapper = document.createElement('div');
wrapper.className = 'page-thumbnail relative cursor-move flex flex-col items-center gap-1 p-2 border-2 border-gray-600 hover:border-indigo-500 rounded-lg bg-gray-700 transition-colors';
wrapper.dataset.fileName = file.name;
wrapper.dataset.pageIndex = (i - 1).toString();
const imgContainer = document.createElement('div');
imgContainer.className = 'relative';
const img = document.createElement('img');
img.src = canvas.toDataURL();
img.className = 'rounded-md shadow-md max-w-full h-auto';
const pageNumDiv = document.createElement('div');
pageNumDiv.className = 'absolute top-1 left-1 bg-indigo-600 text-white text-xs px-2 py-1 rounded-md font-semibold shadow-lg';
pageNumDiv.textContent = i.toString();
imgContainer.append(img, pageNumDiv);
const fileNamePara = document.createElement('p');
fileNamePara.className = 'text-xs text-gray-400 truncate w-full text-center';
const fullTitle = `${file.name} (page ${i})`;
fileNamePara.title = fullTitle;
fileNamePara.textContent = `${file.name.substring(0, 10)}... (p${i})`;
wrapper.append(imgContainer, fileNamePara);
container.appendChild(wrapper);
}
pdfjsDoc.destroy();
}
mergeState.cachedThumbnails = true;
mergeState.lastFileHash = currentFileHash;
initializePageThumbnailsSortable();
} catch (error) {
console.error('Error rendering page thumbnails:', error);
showAlert('Error', 'Failed to render page thumbnails');
} finally {
hideLoader();
mergeState.isRendering = false;
}
initializePageThumbnailsSortable();
} catch (error) {
console.error('Error rendering page thumbnails:', error);
showAlert('Error', 'Failed to render page thumbnails');
} finally {
hideLoader();
mergeState.isRendering = false;
}
}
export async function merge() {
showLoader('Merging PDFs...');
try {
const newPdfDoc = await PDFLibDocument.create();
showLoader('Merging PDFs...');
try {
const newPdfDoc = await PDFLibDocument.create();
if (mergeState.activeMode === 'file') {
const fileList = document.getElementById('file-list');
const sortedFiles = Array.from(fileList.children).map(li => {
// @ts-expect-error TS(2339) FIXME: Property 'dataset' does not exist on type 'Element... Remove this comment to see the full error message
return state.files.find(f => f.name === li.dataset.fileName);
}).filter(Boolean);
if (mergeState.activeMode === 'file') {
const fileList = document.getElementById('file-list');
const sortedFiles = Array.from(fileList.children)
.map((li) => {
// @ts-expect-error TS(2339) FIXME: Property 'dataset' does not exist on type 'Element... Remove this comment to see the full error message
return state.files.find((f) => f.name === li.dataset.fileName);
})
.filter(Boolean);
for (const file of sortedFiles) {
const safeFileName = file.name.replace(/[^a-zA-Z0-9]/g, '_');
const rangeInput = document.getElementById(`range-${safeFileName}`);
if (!rangeInput) continue;
for (const file of sortedFiles) {
const safeFileName = file.name.replace(/[^a-zA-Z0-9]/g, '_');
const rangeInput = document.getElementById(`range-${safeFileName}`);
if (!rangeInput) continue;
// @ts-expect-error TS(2339) FIXME: Property 'value' does not exist on type 'HTMLEleme... Remove this comment to see the full error message
const rangeInputValue = rangeInput.value;
const sourcePdf = mergeState.pdfDocs[file.name];
if (!sourcePdf) continue;
// @ts-expect-error TS(2339) FIXME: Property 'value' does not exist on type 'HTMLEleme... Remove this comment to see the full error message
const rangeInputValue = rangeInput.value;
const sourcePdf = mergeState.pdfDocs[file.name];
if (!sourcePdf) continue;
const totalPages = sourcePdf.getPageCount();
const pageIndices = parsePageRanges(rangeInputValue, totalPages);
const totalPages = sourcePdf.getPageCount();
const pageIndices = parsePageRanges(rangeInputValue, totalPages);
const indicesToCopy = pageIndices.length > 0 ? pageIndices : sourcePdf.getPageIndices();
const copiedPages = await newPdfDoc.copyPages(sourcePdf, indicesToCopy);
copiedPages.forEach((page: any) => newPdfDoc.addPage(page));
}
const indicesToCopy =
pageIndices.length > 0 ? pageIndices : sourcePdf.getPageIndices();
const copiedPages = await newPdfDoc.copyPages(sourcePdf, indicesToCopy);
copiedPages.forEach((page: any) => newPdfDoc.addPage(page));
}
} else {
const pageContainer = document.getElementById('page-merge-preview');
const pageElements = Array.from(pageContainer.children);
} else {
const pageContainer = document.getElementById('page-merge-preview');
const pageElements = Array.from(pageContainer.children);
for (const el of pageElements) {
// @ts-expect-error TS(2339) FIXME: Property 'dataset' does not exist on type 'Element... Remove this comment to see the full error message
const fileName = el.dataset.fileName;
// @ts-expect-error TS(2339) FIXME: Property 'dataset' does not exist on type 'Element... Remove this comment to see the full error message
const pageIndex = parseInt(el.dataset.pageIndex, 10);
for (const el of pageElements) {
// @ts-expect-error TS(2339) FIXME: Property 'dataset' does not exist on type 'Element... Remove this comment to see the full error message
const fileName = el.dataset.fileName;
// @ts-expect-error TS(2339) FIXME: Property 'dataset' does not exist on type 'Element... Remove this comment to see the full error message
const pageIndex = parseInt(el.dataset.pageIndex, 10);
const sourcePdf = mergeState.pdfDocs[fileName];
if (sourcePdf && !isNaN(pageIndex)) {
const [copiedPage] = await newPdfDoc.copyPages(sourcePdf, [pageIndex]);
newPdfDoc.addPage(copiedPage);
}
}
const sourcePdf = mergeState.pdfDocs[fileName];
if (sourcePdf && !isNaN(pageIndex)) {
const [copiedPage] = await newPdfDoc.copyPages(sourcePdf, [
pageIndex,
]);
newPdfDoc.addPage(copiedPage);
}
const mergedPdfBytes = await newPdfDoc.save();
downloadFile(new Blob([new Uint8Array(mergedPdfBytes)], { type: 'application/pdf' }), 'merged.pdf');
showAlert('Success', 'PDFs merged successfully!');
} catch (e) {
console.error('Merge error:', e);
showAlert('Error', 'Failed to merge PDFs. Please check that all files are valid and not password-protected.');
} finally {
hideLoader();
}
}
const mergedPdfBytes = await newPdfDoc.save();
downloadFile(
new Blob([new Uint8Array(mergedPdfBytes)], { type: 'application/pdf' }),
'merged.pdf'
);
showAlert('Success', 'PDFs merged successfully!');
} catch (e) {
console.error('Merge error:', e);
showAlert(
'Error',
'Failed to merge PDFs. Please check that all files are valid and not password-protected.'
);
} finally {
hideLoader();
}
}
export async function setupMergeTool() {
document.getElementById('merge-options').classList.remove('hidden');
// @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('process-btn').disabled = false;
document.getElementById('merge-options').classList.remove('hidden');
// @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('process-btn').disabled = false;
const wasInPageMode = mergeState.activeMode === 'page';
const wasInPageMode = mergeState.activeMode === 'page';
showLoader('Loading PDF documents...');
try {
for (const file of state.files) {
if (!mergeState.pdfDocs[file.name]) {
const pdfBytes = await readFileAsArrayBuffer(file);
mergeState.pdfDocs[file.name] = await PDFLibDocument.load(pdfBytes as ArrayBuffer, {
ignoreEncryption: true
});
}
}
} catch (error) {
console.error('Error loading PDFs:', error);
showAlert('Error', 'Failed to load one or more PDF files');
return;
} finally {
hideLoader();
showLoader('Loading PDF documents...');
try {
for (const file of state.files) {
if (!mergeState.pdfDocs[file.name]) {
const pdfBytes = await readFileAsArrayBuffer(file);
mergeState.pdfDocs[file.name] = await PDFLibDocument.load(
pdfBytes as ArrayBuffer,
{
ignoreEncryption: true,
}
);
}
}
} catch (error) {
console.error('Error loading PDFs:', error);
showAlert('Error', 'Failed to load one or more PDF files');
return;
} finally {
hideLoader();
}
const fileModeBtn = document.getElementById('file-mode-btn');
const pageModeBtn = document.getElementById('page-mode-btn');
const filePanel = document.getElementById('file-mode-panel');
const pagePanel = document.getElementById('page-mode-panel');
const fileList = document.getElementById('file-list');
const fileModeBtn = document.getElementById('file-mode-btn');
const pageModeBtn = document.getElementById('page-mode-btn');
const filePanel = document.getElementById('file-mode-panel');
const pagePanel = document.getElementById('page-mode-panel');
const fileList = document.getElementById('file-list');
fileList.textContent = ''; // Clear list safely
(state.files as File[]).forEach(f => {
const doc = mergeState.pdfDocs[f.name];
const pageCount = doc ? doc.getPageCount() : 'N/A';
const safeFileName = f.name.replace(/[^a-zA-Z0-9]/g, '_');
fileList.textContent = ''; // Clear list safely
(state.files as File[]).forEach((f) => {
const doc = mergeState.pdfDocs[f.name];
const pageCount = doc ? doc.getPageCount() : 'N/A';
const safeFileName = f.name.replace(/[^a-zA-Z0-9]/g, '_');
const li = document.createElement('li');
li.className = 'bg-gray-700 p-3 rounded-lg border border-gray-600 hover:border-indigo-500 transition-colors';
li.dataset.fileName = f.name;
const li = document.createElement('li');
li.className =
'bg-gray-700 p-3 rounded-lg border border-gray-600 hover:border-indigo-500 transition-colors';
li.dataset.fileName = f.name;
const mainDiv = document.createElement('div');
mainDiv.className = 'flex items-center justify-between';
const mainDiv = document.createElement('div');
mainDiv.className = 'flex items-center justify-between';
const nameSpan = document.createElement('span');
nameSpan.className = 'truncate font-medium text-white flex-1 mr-2';
nameSpan.title = f.name;
nameSpan.textContent = f.name;
const nameSpan = document.createElement('span');
nameSpan.className = 'truncate font-medium text-white flex-1 mr-2';
nameSpan.title = f.name;
nameSpan.textContent = f.name;
const dragHandle = document.createElement('div');
dragHandle.className = 'drag-handle cursor-move text-gray-400 hover:text-white p-1 rounded transition-colors';
dragHandle.innerHTML = `<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><circle cx="9" cy="5" r="1"/><circle cx="15" cy="5" r="1"/><circle cx="9" cy="12" r="1"/><circle cx="15" cy="12" r="1"/><circle cx="9" cy="19" r="1"/><circle cx="15" cy="19" r="1"/></svg>`; // Safe: static content
const dragHandle = document.createElement('div');
dragHandle.className =
'drag-handle cursor-move text-gray-400 hover:text-white p-1 rounded transition-colors';
dragHandle.innerHTML = `<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><circle cx="9" cy="5" r="1"/><circle cx="15" cy="5" r="1"/><circle cx="9" cy="12" r="1"/><circle cx="15" cy="12" r="1"/><circle cx="9" cy="19" r="1"/><circle cx="15" cy="19" r="1"/></svg>`; // Safe: static content
mainDiv.append(nameSpan, dragHandle);
mainDiv.append(nameSpan, dragHandle);
const rangeDiv = document.createElement('div');
rangeDiv.className = 'mt-2';
const rangeDiv = document.createElement('div');
rangeDiv.className = 'mt-2';
const label = document.createElement('label');
label.htmlFor = `range-${safeFileName}`;
label.className = 'text-xs text-gray-400';
label.textContent = `Pages (e.g., 1-3, 5) - Total: ${pageCount}`;
const label = document.createElement('label');
label.htmlFor = `range-${safeFileName}`;
label.className = 'text-xs text-gray-400';
label.textContent = `Pages (e.g., 1-3, 5) - Total: ${pageCount}`;
const input = document.createElement('input');
input.type = 'text';
input.id = `range-${safeFileName}`;
input.className = 'w-full bg-gray-800 border border-gray-600 text-white rounded-md p-2 text-sm mt-1 focus:border-indigo-500 focus:ring-1 focus:ring-indigo-500 transition-colors';
input.placeholder = 'Leave blank for all pages';
const input = document.createElement('input');
input.type = 'text';
input.id = `range-${safeFileName}`;
input.className =
'w-full bg-gray-800 border border-gray-600 text-white rounded-md p-2 text-sm mt-1 focus:border-indigo-500 focus:ring-1 focus:ring-indigo-500 transition-colors';
input.placeholder = 'Leave blank for all pages';
rangeDiv.append(label, input);
li.append(mainDiv, rangeDiv);
fileList.appendChild(li);
});
rangeDiv.append(label, input);
li.append(mainDiv, rangeDiv);
fileList.appendChild(li);
});
initializeFileListSortable();
initializeFileListSortable();
const newFileModeBtn = fileModeBtn.cloneNode(true);
const newPageModeBtn = pageModeBtn.cloneNode(true);
fileModeBtn.replaceWith(newFileModeBtn);
pageModeBtn.replaceWith(newPageModeBtn);
const newFileModeBtn = fileModeBtn.cloneNode(true);
const newPageModeBtn = pageModeBtn.cloneNode(true);
fileModeBtn.replaceWith(newFileModeBtn);
pageModeBtn.replaceWith(newPageModeBtn);
newFileModeBtn.addEventListener('click', () => {
if (mergeState.activeMode === 'file') return;
newFileModeBtn.addEventListener('click', () => {
if (mergeState.activeMode === 'file') return;
mergeState.activeMode = 'file';
filePanel.classList.remove('hidden');
pagePanel.classList.add('hidden');
mergeState.activeMode = 'file';
filePanel.classList.remove('hidden');
pagePanel.classList.add('hidden');
// @ts-expect-error TS(2339) FIXME: Property 'classList' does not exist on type 'Node'... Remove this comment to see the full error message
newFileModeBtn.classList.add('bg-indigo-600', 'text-white');
// @ts-expect-error TS(2339) FIXME: Property 'classList' does not exist on type 'Node'... Remove this comment to see the full error message
newFileModeBtn.classList.remove('bg-gray-700', 'text-gray-300');
// @ts-expect-error TS(2339) FIXME: Property 'classList' does not exist on type 'Node'... Remove this comment to see the full error message
newPageModeBtn.classList.remove('bg-indigo-600', 'text-white');
// @ts-expect-error TS(2339) FIXME: Property 'classList' does not exist on type 'Node'... Remove this comment to see the full error message
newPageModeBtn.classList.add('bg-gray-700', 'text-gray-300');
});
// @ts-expect-error TS(2339) FIXME: Property 'classList' does not exist on type 'Node'... Remove this comment to see the full error message
newFileModeBtn.classList.add('bg-indigo-600', 'text-white');
// @ts-expect-error TS(2339) FIXME: Property 'classList' does not exist on type 'Node'... Remove this comment to see the full error message
newFileModeBtn.classList.remove('bg-gray-700', 'text-gray-300');
// @ts-expect-error TS(2339) FIXME: Property 'classList' does not exist on type 'Node'... Remove this comment to see the full error message
newPageModeBtn.classList.remove('bg-indigo-600', 'text-white');
// @ts-expect-error TS(2339) FIXME: Property 'classList' does not exist on type 'Node'... Remove this comment to see the full error message
newPageModeBtn.classList.add('bg-gray-700', 'text-gray-300');
});
newPageModeBtn.addEventListener('click', async () => {
if (mergeState.activeMode === 'page') return;
newPageModeBtn.addEventListener('click', async () => {
if (mergeState.activeMode === 'page') return;
mergeState.activeMode = 'page';
filePanel.classList.add('hidden');
pagePanel.classList.remove('hidden');
mergeState.activeMode = 'page';
filePanel.classList.add('hidden');
pagePanel.classList.remove('hidden');
// @ts-expect-error TS(2339) FIXME: Property 'classList' does not exist on type 'Node'... Remove this comment to see the full error message
newPageModeBtn.classList.add('bg-indigo-600', 'text-white');
// @ts-expect-error TS(2339) FIXME: Property 'classList' does not exist on type 'Node'... Remove this comment to see the full error message
newPageModeBtn.classList.remove('bg-gray-700', 'text-gray-300');
// @ts-expect-error TS(2339) FIXME: Property 'classList' does not exist on type 'Node'... Remove this comment to see the full error message
newFileModeBtn.classList.remove('bg-indigo-600', 'text-white');
// @ts-expect-error TS(2339) FIXME: Property 'classList' does not exist on type 'Node'... Remove this comment to see the full error message
newFileModeBtn.classList.add('bg-gray-700', 'text-gray-300');
// @ts-expect-error TS(2339) FIXME: Property 'classList' does not exist on type 'Node'... Remove this comment to see the full error message
newPageModeBtn.classList.add('bg-indigo-600', 'text-white');
// @ts-expect-error TS(2339) FIXME: Property 'classList' does not exist on type 'Node'... Remove this comment to see the full error message
newPageModeBtn.classList.remove('bg-gray-700', 'text-gray-300');
// @ts-expect-error TS(2339) FIXME: Property 'classList' does not exist on type 'Node'... Remove this comment to see the full error message
newFileModeBtn.classList.remove('bg-indigo-600', 'text-white');
// @ts-expect-error TS(2339) FIXME: Property 'classList' does not exist on type 'Node'... Remove this comment to see the full error message
newFileModeBtn.classList.add('bg-gray-700', 'text-gray-300');
await renderPageMergeThumbnails();
});
await renderPageMergeThumbnails();
});
if (wasInPageMode) {
mergeState.activeMode = 'page';
filePanel.classList.add('hidden');
pagePanel.classList.remove('hidden');
if (wasInPageMode) {
mergeState.activeMode = 'page';
filePanel.classList.add('hidden');
pagePanel.classList.remove('hidden');
// @ts-expect-error TS(2339) FIXME: Property 'classList' does not exist on type 'Node'... Remove this comment to see the full error message
newPageModeBtn.classList.add('bg-indigo-600', 'text-white');
// @ts-expect-error TS(2339) FIXME: Property 'classList' does not exist on type 'Node'... Remove this comment to see the full error message
newPageModeBtn.classList.remove('bg-gray-700', 'text-gray-300');
// @ts-expect-error TS(2339) FIXME: Property 'classList' does not exist on type 'Node'... Remove this comment to see the full error message
newFileModeBtn.classList.remove('bg-indigo-600', 'text-white');
// @ts-expect-error TS(2339) FIXME: Property 'classList' does not exist on type 'Node'... Remove this comment to see the full error message
newFileModeBtn.classList.add('bg-gray-700', 'text-gray-300');
// @ts-expect-error TS(2339) FIXME: Property 'classList' does not exist on type 'Node'... Remove this comment to see the full error message
newPageModeBtn.classList.add('bg-indigo-600', 'text-white');
// @ts-expect-error TS(2339) FIXME: Property 'classList' does not exist on type 'Node'... Remove this comment to see the full error message
newPageModeBtn.classList.remove('bg-gray-700', 'text-gray-300');
// @ts-expect-error TS(2339) FIXME: Property 'classList' does not exist on type 'Node'... Remove this comment to see the full error message
newFileModeBtn.classList.remove('bg-indigo-600', 'text-white');
// @ts-expect-error TS(2339) FIXME: Property 'classList' does not exist on type 'Node'... Remove this comment to see the full error message
newFileModeBtn.classList.add('bg-gray-700', 'text-gray-300');
await renderPageMergeThumbnails();
} else {
// @ts-expect-error TS(2339) FIXME: Property 'classList' does not exist on type 'Node'... Remove this comment to see the full error message
newFileModeBtn.classList.add('bg-indigo-600', 'text-white');
// @ts-expect-error TS(2339) FIXME: Property 'classList' does not exist on type 'Node'... Remove this comment to see the full error message
newPageModeBtn.classList.add('bg-gray-700', 'text-gray-300');
}
}
await renderPageMergeThumbnails();
} else {
// @ts-expect-error TS(2339) FIXME: Property 'classList' does not exist on type 'Node'... Remove this comment to see the full error message
newFileModeBtn.classList.add('bg-indigo-600', 'text-white');
// @ts-expect-error TS(2339) FIXME: Property 'classList' does not exist on type 'Node'... Remove this comment to see the full error message
newPageModeBtn.classList.add('bg-gray-700', 'text-gray-300');
}
}

View File

@@ -1,4 +1,3 @@
import { showLoader, hideLoader, showAlert } from '../ui.js';
import { downloadFile, hexToRgb } from '../utils/helpers.js';
import { state } from '../state.js';
@@ -6,106 +5,124 @@ import { state } from '../state.js';
import { PDFDocument as PDFLibDocument, rgb, PageSizes } from 'pdf-lib';
export function setupNUpUI() {
const addBorderCheckbox = document.getElementById('add-border');
const borderColorWrapper = document.getElementById('border-color-wrapper');
if (addBorderCheckbox && borderColorWrapper) {
addBorderCheckbox.addEventListener('change', () => {
// @ts-expect-error TS(2339) FIXME: Property 'checked' does not exist on type 'HTMLEle... Remove this comment to see the full error message
borderColorWrapper.classList.toggle('hidden', !addBorderCheckbox.checked);
});
}
const addBorderCheckbox = document.getElementById('add-border');
const borderColorWrapper = document.getElementById('border-color-wrapper');
if (addBorderCheckbox && borderColorWrapper) {
addBorderCheckbox.addEventListener('change', () => {
// @ts-expect-error TS(2339) FIXME: Property 'checked' does not exist on type 'HTMLEle... Remove this comment to see the full error message
borderColorWrapper.classList.toggle('hidden', !addBorderCheckbox.checked);
});
}
}
export async function nUpTool() {
// 1. Gather all options from the UI
// @ts-expect-error TS(2339) FIXME: Property 'value' does not exist on type 'HTMLEleme... Remove this comment to see the full error message
const n = parseInt(document.getElementById('pages-per-sheet').value);
// @ts-expect-error TS(2339) FIXME: Property 'value' does not exist on type 'HTMLEleme... Remove this comment to see the full error message
const pageSizeKey = document.getElementById('output-page-size').value;
// @ts-expect-error TS(2339) FIXME: Property 'value' does not exist on type 'HTMLEleme... Remove this comment to see the full error message
let orientation = document.getElementById('output-orientation').value;
// @ts-expect-error TS(2339) FIXME: Property 'checked' does not exist on type 'HTMLEle... Remove this comment to see the full error message
const useMargins = document.getElementById('add-margins').checked;
// @ts-expect-error TS(2339) FIXME: Property 'checked' does not exist on type 'HTMLEle... Remove this comment to see the full error message
const addBorder = document.getElementById('add-border').checked;
// @ts-expect-error TS(2339) FIXME: Property 'value' does not exist on type 'HTMLEleme... Remove this comment to see the full error message
const borderColor = hexToRgb(document.getElementById('border-color').value);
showLoader('Creating N-Up PDF...');
try {
const sourceDoc = state.pdfDoc;
const newDoc = await PDFLibDocument.create();
const sourcePages = sourceDoc.getPages();
// 1. Gather all options from the UI
// @ts-expect-error TS(2339) FIXME: Property 'value' does not exist on type 'HTMLEleme... Remove this comment to see the full error message
const n = parseInt(document.getElementById('pages-per-sheet').value);
// @ts-expect-error TS(2339) FIXME: Property 'value' does not exist on type 'HTMLEleme... Remove this comment to see the full error message
const pageSizeKey = document.getElementById('output-page-size').value;
// @ts-expect-error TS(2339) FIXME: Property 'value' does not exist on type 'HTMLEleme... Remove this comment to see the full error message
let orientation = document.getElementById('output-orientation').value;
// @ts-expect-error TS(2339) FIXME: Property 'checked' does not exist on type 'HTMLEle... Remove this comment to see the full error message
const useMargins = document.getElementById('add-margins').checked;
// @ts-expect-error TS(2339) FIXME: Property 'checked' does not exist on type 'HTMLEle... Remove this comment to see the full error message
const addBorder = document.getElementById('add-border').checked;
// @ts-expect-error TS(2339) FIXME: Property 'value' does not exist on type 'HTMLEleme... Remove this comment to see the full error message
const borderColor = hexToRgb(document.getElementById('border-color').value);
const gridDims = { 2: [2, 1], 4: [2, 2], 9: [3, 3], 16: [4, 4] }[n];
let [pageWidth, pageHeight] = PageSizes[pageSizeKey];
showLoader('Creating N-Up PDF...');
try {
const sourceDoc = state.pdfDoc;
const newDoc = await PDFLibDocument.create();
const sourcePages = sourceDoc.getPages();
if (orientation === 'auto') {
const firstPage = sourcePages[0];
const isSourceLandscape = firstPage.getWidth() > firstPage.getHeight();
// If source is landscape and grid is wider than tall (like 2x1), output landscape.
orientation = (isSourceLandscape && gridDims[0] > gridDims[1]) ? 'landscape' : 'portrait';
}
if (orientation === 'landscape' && pageWidth < pageHeight) {
[pageWidth, pageHeight] = [pageHeight, pageWidth];
}
const gridDims = { 2: [2, 1], 4: [2, 2], 9: [3, 3], 16: [4, 4] }[n];
const margin = useMargins ? 36 : 0;
const gutter = useMargins ? 10 : 0;
const usableWidth = pageWidth - (margin * 2);
const usableHeight = pageHeight - (margin * 2);
let [pageWidth, pageHeight] = PageSizes[pageSizeKey];
// Loop through the source pages in chunks of 'n'
for (let i = 0; i < sourcePages.length; i += n) {
showLoader(`Processing sheet ${Math.floor(i / n) + 1}...`);
const chunk = sourcePages.slice(i, i + n);
const outputPage = newDoc.addPage([pageWidth, pageHeight]);
// Calculate dimensions of each cell in the grid
const cellWidth = (usableWidth - (gutter * (gridDims[0] - 1))) / gridDims[0];
const cellHeight = (usableHeight - (gutter * (gridDims[1] - 1))) / gridDims[1];
for (let j = 0; j < chunk.length; j++) {
const sourcePage = chunk[j];
const embeddedPage = await newDoc.embedPage(sourcePage);
// Calculate scaled dimensions to fit the cell, preserving aspect ratio
const scale = Math.min(cellWidth / embeddedPage.width, cellHeight / embeddedPage.height);
const scaledWidth = embeddedPage.width * scale;
const scaledHeight = embeddedPage.height * scale;
// Calculate position (x, y) for this cell
const row = Math.floor(j / gridDims[0]);
const col = j % gridDims[0];
const cellX = margin + col * (cellWidth + gutter);
const cellY = pageHeight - margin - (row + 1) * cellHeight - row * gutter;
// Center the page within its cell
const x = cellX + (cellWidth - scaledWidth) / 2;
const y = cellY + (cellHeight - scaledHeight) / 2;
outputPage.drawPage(embeddedPage, { x, y, width: scaledWidth, height: scaledHeight });
if (addBorder) {
outputPage.drawRectangle({
x, y,
width: scaledWidth,
height: scaledHeight,
borderColor: rgb(borderColor.r, borderColor.g, borderColor.b),
borderWidth: 1,
});
}
}
}
const newPdfBytes = await newDoc.save();
downloadFile(new Blob([new Uint8Array(newPdfBytes)], { type: 'application/pdf' }), `n-up_${n}.pdf`);
} catch (e) {
console.error(e);
showAlert('Error', 'An error occurred while creating the N-Up PDF.');
} finally {
hideLoader();
if (orientation === 'auto') {
const firstPage = sourcePages[0];
const isSourceLandscape = firstPage.getWidth() > firstPage.getHeight();
// If source is landscape and grid is wider than tall (like 2x1), output landscape.
orientation =
isSourceLandscape && gridDims[0] > gridDims[1]
? 'landscape'
: 'portrait';
}
}
if (orientation === 'landscape' && pageWidth < pageHeight) {
[pageWidth, pageHeight] = [pageHeight, pageWidth];
}
const margin = useMargins ? 36 : 0;
const gutter = useMargins ? 10 : 0;
const usableWidth = pageWidth - margin * 2;
const usableHeight = pageHeight - margin * 2;
// Loop through the source pages in chunks of 'n'
for (let i = 0; i < sourcePages.length; i += n) {
showLoader(`Processing sheet ${Math.floor(i / n) + 1}...`);
const chunk = sourcePages.slice(i, i + n);
const outputPage = newDoc.addPage([pageWidth, pageHeight]);
// Calculate dimensions of each cell in the grid
const cellWidth =
(usableWidth - gutter * (gridDims[0] - 1)) / gridDims[0];
const cellHeight =
(usableHeight - gutter * (gridDims[1] - 1)) / gridDims[1];
for (let j = 0; j < chunk.length; j++) {
const sourcePage = chunk[j];
const embeddedPage = await newDoc.embedPage(sourcePage);
// Calculate scaled dimensions to fit the cell, preserving aspect ratio
const scale = Math.min(
cellWidth / embeddedPage.width,
cellHeight / embeddedPage.height
);
const scaledWidth = embeddedPage.width * scale;
const scaledHeight = embeddedPage.height * scale;
// Calculate position (x, y) for this cell
const row = Math.floor(j / gridDims[0]);
const col = j % gridDims[0];
const cellX = margin + col * (cellWidth + gutter);
const cellY =
pageHeight - margin - (row + 1) * cellHeight - row * gutter;
// Center the page within its cell
const x = cellX + (cellWidth - scaledWidth) / 2;
const y = cellY + (cellHeight - scaledHeight) / 2;
outputPage.drawPage(embeddedPage, {
x,
y,
width: scaledWidth,
height: scaledHeight,
});
if (addBorder) {
outputPage.drawRectangle({
x,
y,
width: scaledWidth,
height: scaledHeight,
borderColor: rgb(borderColor.r, borderColor.g, borderColor.b),
borderWidth: 1,
});
}
}
}
const newPdfBytes = await newDoc.save();
downloadFile(
new Blob([new Uint8Array(newPdfBytes)], { type: 'application/pdf' }),
`n-up_${n}.pdf`
);
} catch (e) {
console.error(e);
showAlert('Error', 'An error occurred while creating the N-Up PDF.');
} finally {
hideLoader();
}
}

View File

@@ -4,275 +4,307 @@ import { downloadFile, readFileAsArrayBuffer } from '../utils/helpers.js';
import { state } from '../state.js';
import Tesseract from 'tesseract.js';
import { PDFDocument as PDFLibDocument, StandardFonts, rgb } from 'pdf-lib';
import { icons, createIcons } from "lucide";
import { icons, createIcons } from 'lucide';
let searchablePdfBytes: any = null;
function sanitizeTextForWinAnsi(text: string): string {
// Remove invisible Unicode control characters (like Left-to-Right Mark U+200E)
return text.replace(/[\u0000-\u001F\u007F-\u009F\u200E\u200F\u202A-\u202E\uFEFF]/g, '')
.replace(/[^\u0020-\u007E\u00A0-\u00FF]/g, '');
// Remove invisible Unicode control characters (like Left-to-Right Mark U+200E)
return text
.replace(/[\u0000-\u001F\u007F-\u009F\u200E\u200F\u202A-\u202E\uFEFF]/g, '')
.replace(/[^\u0020-\u007E\u00A0-\u00FF]/g, '');
}
function parseHOCR(hocrText: string) {
const parser = new DOMParser();
const doc = parser.parseFromString(hocrText, 'text/html');
const words = [];
// Find all word elements in hOCR
const wordElements = doc.querySelectorAll('.ocrx_word');
wordElements.forEach((wordEl) => {
const titleAttr = wordEl.getAttribute('title');
const text = wordEl.textContent?.trim() || '';
if (!titleAttr || !text) return;
// Parse bbox coordinates from title attribute
// Format: "bbox x0 y0 x1 y1; x_wconf confidence"
const bboxMatch = titleAttr.match(/bbox (\d+) (\d+) (\d+) (\d+)/);
const confMatch = titleAttr.match(/x_wconf (\d+)/);
if (bboxMatch) {
words.push({
text: text,
bbox: {
x0: parseInt(bboxMatch[1]),
y0: parseInt(bboxMatch[2]),
x1: parseInt(bboxMatch[3]),
y1: parseInt(bboxMatch[4])
},
confidence: confMatch ? parseInt(confMatch[1]) : 0
});
}
});
return words;
const parser = new DOMParser();
const doc = parser.parseFromString(hocrText, 'text/html');
const words = [];
// Find all word elements in hOCR
const wordElements = doc.querySelectorAll('.ocrx_word');
wordElements.forEach((wordEl) => {
const titleAttr = wordEl.getAttribute('title');
const text = wordEl.textContent?.trim() || '';
if (!titleAttr || !text) return;
// Parse bbox coordinates from title attribute
// Format: "bbox x0 y0 x1 y1; x_wconf confidence"
const bboxMatch = titleAttr.match(/bbox (\d+) (\d+) (\d+) (\d+)/);
const confMatch = titleAttr.match(/x_wconf (\d+)/);
if (bboxMatch) {
words.push({
text: text,
bbox: {
x0: parseInt(bboxMatch[1]),
y0: parseInt(bboxMatch[2]),
x1: parseInt(bboxMatch[3]),
y1: parseInt(bboxMatch[4]),
},
confidence: confMatch ? parseInt(confMatch[1]) : 0,
});
}
});
return words;
}
function binarizeCanvas(ctx: any) {
const imageData = ctx.getImageData(0, 0, ctx.canvas.width, ctx.canvas.height);
const data = imageData.data;
for (let i = 0; i < data.length; i += 4) {
// A simple luminance-based threshold for determining black or white
const brightness = 0.299 * data[i] + 0.587 * data[i + 1] + 0.114 * data[i + 2];
const color = brightness > 128 ? 255 : 0; // If brighter than mid-gray, make it white, otherwise black
data[i] = data[i + 1] = data[i + 2] = color;
}
ctx.putImageData(imageData, 0, 0);
const imageData = ctx.getImageData(0, 0, ctx.canvas.width, ctx.canvas.height);
const data = imageData.data;
for (let i = 0; i < data.length; i += 4) {
// A simple luminance-based threshold for determining black or white
const brightness =
0.299 * data[i] + 0.587 * data[i + 1] + 0.114 * data[i + 2];
const color = brightness > 128 ? 255 : 0; // If brighter than mid-gray, make it white, otherwise black
data[i] = data[i + 1] = data[i + 2] = color;
}
ctx.putImageData(imageData, 0, 0);
}
function updateProgress(status: any, progress: any) {
const progressBar = document.getElementById('progress-bar');
const progressStatus = document.getElementById('progress-status');
const progressLog = document.getElementById('progress-log');
const progressBar = document.getElementById('progress-bar');
const progressStatus = document.getElementById('progress-status');
const progressLog = document.getElementById('progress-log');
if (!progressBar || !progressStatus || !progressLog) return;
if (!progressBar || !progressStatus || !progressLog) return;
progressStatus.textContent = status;
// Tesseract's progress can sometimes exceed 1, so we cap it at 100%.
progressBar.style.width = `${Math.min(100, progress * 100)}%`;
progressStatus.textContent = status;
// Tesseract's progress can sometimes exceed 1, so we cap it at 100%.
progressBar.style.width = `${Math.min(100, progress * 100)}%`;
const logMessage = `Status: ${status}`;
progressLog.textContent += logMessage + '\n';
progressLog.scrollTop = progressLog.scrollHeight;
const logMessage = `Status: ${status}`;
progressLog.textContent += logMessage + '\n';
progressLog.scrollTop = progressLog.scrollHeight;
}
async function runOCR() {
// @ts-expect-error TS(2339) FIXME: Property 'value' does not exist on type 'Element'.
const selectedLangs = Array.from(document.querySelectorAll('.lang-checkbox:checked')).map(cb => cb.value);
// @ts-expect-error TS(2339) FIXME: Property 'value' does not exist on type 'HTMLEleme... Remove this comment to see the full error message
const scale = parseFloat(document.getElementById('ocr-resolution').value);
// @ts-expect-error TS(2339) FIXME: Property 'checked' does not exist on type 'HTMLEle... Remove this comment to see the full error message
const binarize = document.getElementById('ocr-binarize').checked;
// @ts-expect-error TS(2339) FIXME: Property 'value' does not exist on type 'HTMLEleme... Remove this comment to see the full error message
const whitelist = document.getElementById('ocr-whitelist').value;
// @ts-expect-error TS(2339) FIXME: Property 'value' does not exist on type 'Element'.
const selectedLangs = Array.from(
document.querySelectorAll('.lang-checkbox:checked')
).map((cb) => cb.value);
// @ts-expect-error TS(2339) FIXME: Property 'value' does not exist on type 'HTMLEleme... Remove this comment to see the full error message
const scale = parseFloat(document.getElementById('ocr-resolution').value);
// @ts-expect-error TS(2339) FIXME: Property 'checked' does not exist on type 'HTMLEle... Remove this comment to see the full error message
const binarize = document.getElementById('ocr-binarize').checked;
// @ts-expect-error TS(2339) FIXME: Property 'value' does not exist on type 'HTMLEleme... Remove this comment to see the full error message
const whitelist = document.getElementById('ocr-whitelist').value;
if (selectedLangs.length === 0) {
showAlert('No Languages Selected', 'Please select at least one language for OCR.');
return;
if (selectedLangs.length === 0) {
showAlert(
'No Languages Selected',
'Please select at least one language for OCR.'
);
return;
}
const langString = selectedLangs.join('+');
document.getElementById('ocr-options').classList.add('hidden');
document.getElementById('ocr-progress').classList.remove('hidden');
try {
const worker = await Tesseract.createWorker(langString, 1, {
logger: (m: any) => updateProgress(m.status, m.progress || 0),
});
// Enable hOCR output
await worker.setParameters({
tessjs_create_hocr: '1',
});
if (whitelist.trim()) {
await worker.setParameters({
tessedit_char_whitelist: whitelist.trim(),
});
}
const langString = selectedLangs.join('+');
document.getElementById('ocr-options').classList.add('hidden');
document.getElementById('ocr-progress').classList.remove('hidden');
// @ts-expect-error TS(2304) FIXME: Cannot find name 'pdfjsLib'.
const pdf = await pdfjsLib.getDocument(
await readFileAsArrayBuffer(state.files[0])
).promise;
const newPdfDoc = await PDFLibDocument.create();
const font = await newPdfDoc.embedFont(StandardFonts.Helvetica);
let fullText = '';
try {
const worker = await Tesseract.createWorker(langString, 1, {
logger: (m: any) => updateProgress(m.status, m.progress || 0)
});
for (let i = 1; i <= pdf.numPages; i++) {
updateProgress(
`Processing page ${i} of ${pdf.numPages}`,
(i - 1) / pdf.numPages
);
const page = await pdf.getPage(i);
const viewport = page.getViewport({ scale });
const canvas = document.createElement('canvas');
canvas.width = viewport.width;
canvas.height = viewport.height;
const context = canvas.getContext('2d');
await page.render({ canvasContext: context, viewport }).promise;
// Enable hOCR output
await worker.setParameters({
tessjs_create_hocr: '1',
});
if (binarize) {
binarizeCanvas(context);
}
if (whitelist.trim()) {
await worker.setParameters({
tessedit_char_whitelist: whitelist.trim(),
const result = await worker.recognize(canvas);
const data = result.data;
const newPage = newPdfDoc.addPage([viewport.width, viewport.height]);
const pngImageBytes = await new Promise((resolve) =>
canvas.toBlob((blob) => {
const reader = new FileReader();
// @ts-expect-error TS(2769) FIXME: No overload matches this call.
reader.onload = () => resolve(new Uint8Array(reader.result));
reader.readAsArrayBuffer(blob);
}, 'image/png')
);
const pngImage = await newPdfDoc.embedPng(pngImageBytes as ArrayBuffer);
newPage.drawImage(pngImage, {
x: 0,
y: 0,
width: viewport.width,
height: viewport.height,
});
// Parse hOCR to get word-level data
if (data.hocr) {
const words = parseHOCR(data.hocr);
words.forEach((word: any) => {
const { x0, y0, x1, y1 } = word.bbox;
// Sanitize the text to remove characters WinAnsi cannot encode
const text = sanitizeTextForWinAnsi(word.text);
// Skip words that become empty after sanitization
if (!text.trim()) return;
const bboxWidth = x1 - x0;
const bboxHeight = y1 - y0;
let fontSize = bboxHeight * 0.9;
let textWidth = font.widthOfTextAtSize(text, fontSize);
while (textWidth > bboxWidth && fontSize > 1) {
fontSize -= 0.5;
textWidth = font.widthOfTextAtSize(text, fontSize);
}
try {
newPage.drawText(text, {
x: x0,
y: viewport.height - y1 + (bboxHeight - fontSize) / 2,
font,
size: fontSize,
color: rgb(0, 0, 0),
opacity: 0,
});
}
} catch (error) {
// If drawing fails despite sanitization, log and skip this word
console.warn(`Could not draw text "${text}":`, error);
}
});
}
// @ts-expect-error TS(2304) FIXME: Cannot find name 'pdfjsLib'.
const pdf = await pdfjsLib.getDocument(await readFileAsArrayBuffer(state.files[0])).promise;
const newPdfDoc = await PDFLibDocument.create();
const font = await newPdfDoc.embedFont(StandardFonts.Helvetica);
let fullText = '';
fullText += data.text + '\n\n';
}
for (let i = 1; i <= pdf.numPages; i++) {
updateProgress(`Processing page ${i} of ${pdf.numPages}`, (i - 1) / pdf.numPages);
const page = await pdf.getPage(i);
const viewport = page.getViewport({ scale });
const canvas = document.createElement('canvas');
canvas.width = viewport.width;
canvas.height = viewport.height;
const context = canvas.getContext('2d');
await page.render({ canvasContext: context, viewport }).promise;
await worker.terminate();
if (binarize) {
binarizeCanvas(context);
}
searchablePdfBytes = await newPdfDoc.save();
document.getElementById('ocr-progress').classList.add('hidden');
document.getElementById('ocr-results').classList.remove('hidden');
const result = await worker.recognize(canvas);
const data = result.data;
const newPage = newPdfDoc.addPage([viewport.width, viewport.height]);
const pngImageBytes = await new Promise(resolve => canvas.toBlob(blob => {
const reader = new FileReader();
// @ts-expect-error TS(2769) FIXME: No overload matches this call.
reader.onload = () => resolve(new Uint8Array(reader.result));
reader.readAsArrayBuffer(blob);
}, 'image/png'));
const pngImage = await newPdfDoc.embedPng(pngImageBytes as ArrayBuffer);
newPage.drawImage(pngImage, { x: 0, y: 0, width: viewport.width, height: viewport.height });
createIcons({ icons });
// @ts-expect-error TS(2339) FIXME: Property 'value' does not exist on type 'HTMLEleme... Remove this comment to see the full error message
document.getElementById('ocr-text-output').value = fullText.trim();
// Parse hOCR to get word-level data
if (data.hocr) {
const words = parseHOCR(data.hocr);
words.forEach((word: any) => {
const { x0, y0, x1, y1 } = word.bbox;
// Sanitize the text to remove characters WinAnsi cannot encode
const text = sanitizeTextForWinAnsi(word.text);
document
.getElementById('download-searchable-pdf')
.addEventListener('click', () => {
downloadFile(
new Blob([searchablePdfBytes], { type: 'application/pdf' }),
'searchable.pdf'
);
});
// Skip words that become empty after sanitization
if (!text.trim()) return;
// CHANGE: The copy button logic is updated to be safer.
document.getElementById('copy-text-btn').addEventListener('click', (e) => {
const button = e.currentTarget as HTMLButtonElement;
// @ts-expect-error TS(2339) FIXME: Property 'value' does not exist on type 'HTMLEleme...
const textToCopy = document.getElementById('ocr-text-output').value;
const bboxWidth = x1 - x0;
const bboxHeight = y1 - y0;
navigator.clipboard.writeText(textToCopy).then(() => {
button.textContent = ''; // Clear the button safely
const icon = document.createElement('i');
icon.setAttribute('data-lucide', 'check');
icon.className = 'w-4 h-4 text-green-400';
button.appendChild(icon);
createIcons({ icons });
let fontSize = bboxHeight * 0.9;
let textWidth = font.widthOfTextAtSize(text, fontSize);
while (textWidth > bboxWidth && fontSize > 1) {
fontSize -= 0.5;
textWidth = font.widthOfTextAtSize(text, fontSize);
}
setTimeout(() => {
const currentButton = document.getElementById('copy-text-btn');
if (currentButton) {
currentButton.textContent = ''; // Clear the button safely
const resetIcon = document.createElement('i');
resetIcon.setAttribute('data-lucide', 'clipboard-copy');
resetIcon.className = 'w-4 h-4 text-gray-300';
currentButton.appendChild(resetIcon);
createIcons({ icons });
}
}, 2000);
});
});
try {
newPage.drawText(text, {
x: x0,
y: viewport.height - y1 + (bboxHeight - fontSize) / 2,
font,
size: fontSize,
color: rgb(0, 0, 0),
opacity: 0,
});
} catch (error) {
// If drawing fails despite sanitization, log and skip this word
console.warn(`Could not draw text "${text}":`, error);
}
});
}
fullText += data.text + '\n\n';
}
await worker.terminate();
searchablePdfBytes = await newPdfDoc.save();
document.getElementById('ocr-progress').classList.add('hidden');
document.getElementById('ocr-results').classList.remove('hidden');
createIcons({icons});
document
.getElementById('download-txt-btn')
.addEventListener('click', () => {
// @ts-expect-error TS(2339) FIXME: Property 'value' does not exist on type 'HTMLEleme... Remove this comment to see the full error message
document.getElementById('ocr-text-output').value = fullText.trim();
document.getElementById('download-searchable-pdf').addEventListener('click', () => {
downloadFile(new Blob([searchablePdfBytes], { type: 'application/pdf' }), 'searchable.pdf');
});
// CHANGE: The copy button logic is updated to be safer.
document.getElementById('copy-text-btn').addEventListener('click', (e) => {
const button = e.currentTarget as HTMLButtonElement;
// @ts-expect-error TS(2339) FIXME: Property 'value' does not exist on type 'HTMLEleme...
const textToCopy = document.getElementById('ocr-text-output').value;
navigator.clipboard.writeText(textToCopy).then(() => {
button.textContent = ''; // Clear the button safely
const icon = document.createElement('i');
icon.setAttribute('data-lucide', 'check');
icon.className = 'w-4 h-4 text-green-400';
button.appendChild(icon);
createIcons({ icons });
setTimeout(() => {
const currentButton = document.getElementById('copy-text-btn');
if (currentButton) {
currentButton.textContent = ''; // Clear the button safely
const resetIcon = document.createElement('i');
resetIcon.setAttribute('data-lucide', 'clipboard-copy');
resetIcon.className = 'w-4 h-4 text-gray-300';
currentButton.appendChild(resetIcon);
createIcons({ icons });
}
}, 2000);
});
});
document.getElementById('download-txt-btn').addEventListener('click', () => {
// @ts-expect-error TS(2339) FIXME: Property 'value' does not exist on type 'HTMLEleme... Remove this comment to see the full error message
const textToSave = document.getElementById('ocr-text-output').value;
const blob = new Blob([textToSave], { type: 'text/plain' });
downloadFile(blob, 'ocr-text.txt');
});
} catch (e) {
console.error(e);
showAlert('OCR Error', 'An error occurred during the OCR process. The worker may have failed to load. Please try again.');
document.getElementById('ocr-options').classList.remove('hidden');
document.getElementById('ocr-progress').classList.add('hidden');
}
const textToSave = document.getElementById('ocr-text-output').value;
const blob = new Blob([textToSave], { type: 'text/plain' });
downloadFile(blob, 'ocr-text.txt');
});
} catch (e) {
console.error(e);
showAlert(
'OCR Error',
'An error occurred during the OCR process. The worker may have failed to load. Please try again.'
);
document.getElementById('ocr-options').classList.remove('hidden');
document.getElementById('ocr-progress').classList.add('hidden');
}
}
/**
* Sets up the UI and event listeners for the OCR tool.
*/
export function setupOcrTool() {
const langSearch = document.getElementById('lang-search');
const langList = document.getElementById('lang-list');
const selectedLangsDisplay = document.getElementById('selected-langs-display');
const processBtn = document.getElementById('process-btn');
const langSearch = document.getElementById('lang-search');
const langList = document.getElementById('lang-list');
const selectedLangsDisplay = document.getElementById(
'selected-langs-display'
);
const processBtn = document.getElementById('process-btn');
langSearch.addEventListener('input', () => {
// @ts-expect-error TS(2339) FIXME: Property 'value' does not exist on type 'HTMLEleme... Remove this comment to see the full error message
const searchTerm = langSearch.value.toLowerCase();
langList.querySelectorAll('label').forEach(label => {
label.style.display = label.textContent.toLowerCase().includes(searchTerm) ? '' : 'none';
});
langSearch.addEventListener('input', () => {
// @ts-expect-error TS(2339) FIXME: Property 'value' does not exist on type 'HTMLEleme... Remove this comment to see the full error message
const searchTerm = langSearch.value.toLowerCase();
langList.querySelectorAll('label').forEach((label) => {
label.style.display = label.textContent.toLowerCase().includes(searchTerm)
? ''
: 'none';
});
});
// Update the display of selected languages
langList.addEventListener('change', () => {
const selected = Array.from(langList.querySelectorAll('.lang-checkbox:checked'))
// @ts-expect-error TS(2339) FIXME: Property 'value' does not exist on type 'Element'.
.map(cb => tesseractLanguages[cb.value]);
selectedLangsDisplay.textContent = selected.length > 0 ? selected.join(', ') : 'None';
// @ts-expect-error TS(2339) FIXME: Property 'disabled' does not exist on type 'HTMLEl... Remove this comment to see the full error message
processBtn.disabled = selected.length === 0;
});
// Update the display of selected languages
langList.addEventListener('change', () => {
const selected = Array.from(
langList.querySelectorAll('.lang-checkbox:checked')
)
// @ts-expect-error TS(2339) FIXME: Property 'value' does not exist on type 'Element'.
.map((cb) => tesseractLanguages[cb.value]);
selectedLangsDisplay.textContent =
selected.length > 0 ? selected.join(', ') : 'None';
// @ts-expect-error TS(2339) FIXME: Property 'disabled' does not exist on type 'HTMLEl... Remove this comment to see the full error message
processBtn.disabled = selected.length === 0;
});
// Attach the main OCR function to the process button
processBtn.addEventListener('click', runOCR);
}
// Attach the main OCR function to the process button
processBtn.addEventListener('click', runOCR);
}

View File

@@ -1,4 +1,3 @@
import { showLoader, hideLoader, showAlert } from '../ui.js';
import { downloadFile } from '../utils/helpers.js';
import { state } from '../state.js';
@@ -6,22 +5,27 @@ import { state } from '../state.js';
import { PDFDocument as PDFLibDocument } from 'pdf-lib';
export async function organize() {
showLoader('Saving changes...');
try {
const newPdf = await PDFLibDocument.create();
const pageContainer = document.getElementById('page-organizer');
// @ts-expect-error TS(2339) FIXME: Property 'dataset' does not exist on type 'Element... Remove this comment to see the full error message
const pageIndices = Array.from(pageContainer.children).map(child => parseInt(child.dataset.pageIndex));
showLoader('Saving changes...');
try {
const newPdf = await PDFLibDocument.create();
const pageContainer = document.getElementById('page-organizer');
// @ts-expect-error TS(2339) FIXME: Property 'dataset' does not exist on type 'Element... Remove this comment to see the full error message
const pageIndices = Array.from(pageContainer.children).map((child) =>
parseInt(child.dataset.pageIndex)
);
const copiedPages = await newPdf.copyPages(state.pdfDoc, pageIndices);
copiedPages.forEach((page: any) => newPdf.addPage(page));
const copiedPages = await newPdf.copyPages(state.pdfDoc, pageIndices);
copiedPages.forEach((page: any) => newPdf.addPage(page));
const newPdfBytes = await newPdf.save();
downloadFile(new Blob([new Uint8Array(newPdfBytes)], { type: 'application/pdf' }), 'organized.pdf');
} catch (e) {
console.error(e);
showAlert('Error', 'Could not save the changes.');
} finally {
hideLoader();
}
}
const newPdfBytes = await newPdf.save();
downloadFile(
new Blob([new Uint8Array(newPdfBytes)], { type: 'application/pdf' }),
'organized.pdf'
);
} catch (e) {
console.error(e);
showAlert('Error', 'Could not save the changes.');
} finally {
hideLoader();
}
}

View File

@@ -8,37 +8,37 @@ let analyzedPagesData: any = []; // Store raw data to avoid re-analyzing
* @param {string} unit The unit to display dimensions in ('pt', 'in', 'mm', 'px').
*/
function renderTable(unit: any) {
const tableBody = document.getElementById('dimensions-table-body');
if (!tableBody) return;
tableBody.textContent = ''; // Clear the table body safely
const tableBody = document.getElementById('dimensions-table-body');
if (!tableBody) return;
analyzedPagesData.forEach(pageData => {
const width = convertPoints(pageData.width, unit);
const height = convertPoints(pageData.height, unit);
const row = document.createElement('tr');
tableBody.textContent = ''; // Clear the table body safely
// Create and append each cell safely using textContent
const pageNumCell = document.createElement('td');
pageNumCell.className = 'px-4 py-3 text-white';
pageNumCell.textContent = pageData.pageNum;
const dimensionsCell = document.createElement('td');
dimensionsCell.className = 'px-4 py-3 text-gray-300';
dimensionsCell.textContent = `${width} x ${height} ${unit}`;
const sizeCell = document.createElement('td');
sizeCell.className = 'px-4 py-3 text-gray-300';
sizeCell.textContent = pageData.standardSize;
analyzedPagesData.forEach((pageData) => {
const width = convertPoints(pageData.width, unit);
const height = convertPoints(pageData.height, unit);
const orientationCell = document.createElement('td');
orientationCell.className = 'px-4 py-3 text-gray-300';
orientationCell.textContent = pageData.orientation;
const row = document.createElement('tr');
row.append(pageNumCell, dimensionsCell, sizeCell, orientationCell);
tableBody.appendChild(row);
});
// Create and append each cell safely using textContent
const pageNumCell = document.createElement('td');
pageNumCell.className = 'px-4 py-3 text-white';
pageNumCell.textContent = pageData.pageNum;
const dimensionsCell = document.createElement('td');
dimensionsCell.className = 'px-4 py-3 text-gray-300';
dimensionsCell.textContent = `${width} x ${height} ${unit}`;
const sizeCell = document.createElement('td');
sizeCell.className = 'px-4 py-3 text-gray-300';
sizeCell.textContent = pageData.standardSize;
const orientationCell = document.createElement('td');
orientationCell.className = 'px-4 py-3 text-gray-300';
orientationCell.textContent = pageData.orientation;
row.append(pageNumCell, dimensionsCell, sizeCell, orientationCell);
tableBody.appendChild(row);
});
}
/**
@@ -46,35 +46,35 @@ function renderTable(unit: any) {
* This is called once after the file is loaded.
*/
export function analyzeAndDisplayDimensions() {
if (!state.pdfDoc) return;
analyzedPagesData = []; // Reset stored data
const pages = state.pdfDoc.getPages();
pages.forEach((page: any, index: any) => {
const { width, height } = page.getSize();
analyzedPagesData.push({
pageNum: index + 1,
width, // Store raw width in points
height, // Store raw height in points
orientation: width > height ? 'Landscape' : 'Portrait',
standardSize: getStandardPageName(width, height),
});
});
if (!state.pdfDoc) return;
const resultsContainer = document.getElementById('dimensions-results');
const unitsSelect = document.getElementById('units-select');
// Initial render with default unit (points)
// @ts-expect-error TS(2339) FIXME: Property 'value' does not exist on type 'HTMLEleme... Remove this comment to see the full error message
renderTable(unitsSelect.value);
// Show the results table
resultsContainer.classList.remove('hidden');
analyzedPagesData = []; // Reset stored data
const pages = state.pdfDoc.getPages();
// Add event listener to handle unit changes
unitsSelect.addEventListener('change', (e) => {
// @ts-expect-error TS(2339) FIXME: Property 'value' does not exist on type 'EventTarg... Remove this comment to see the full error message
renderTable(e.target.value);
pages.forEach((page: any, index: any) => {
const { width, height } = page.getSize();
analyzedPagesData.push({
pageNum: index + 1,
width, // Store raw width in points
height, // Store raw height in points
orientation: width > height ? 'Landscape' : 'Portrait',
standardSize: getStandardPageName(width, height),
});
}
});
const resultsContainer = document.getElementById('dimensions-results');
const unitsSelect = document.getElementById('units-select');
// Initial render with default unit (points)
// @ts-expect-error TS(2339) FIXME: Property 'value' does not exist on type 'HTMLEleme... Remove this comment to see the full error message
renderTable(unitsSelect.value);
// Show the results table
resultsContainer.classList.remove('hidden');
// Add event listener to handle unit changes
unitsSelect.addEventListener('change', (e) => {
// @ts-expect-error TS(2339) FIXME: Property 'value' does not exist on type 'EventTarg... Remove this comment to see the full error message
renderTable(e.target.value);
});
}

View File

@@ -10,84 +10,87 @@ import JSZip from 'jszip';
* @returns {ArrayBuffer} The complete BMP file as an ArrayBuffer.
*/
function encodeBMP(imageData: any) {
const { width, height, data } = imageData;
const stride = Math.floor((24 * width + 31) / 32) * 4; // Row size must be a multiple of 4 bytes
const fileSize = stride * height + 54; // 54 byte header
const buffer = new ArrayBuffer(fileSize);
const view = new DataView(buffer);
const { width, height, data } = imageData;
const stride = Math.floor((24 * width + 31) / 32) * 4; // Row size must be a multiple of 4 bytes
const fileSize = stride * height + 54; // 54 byte header
const buffer = new ArrayBuffer(fileSize);
const view = new DataView(buffer);
// BMP File Header (14 bytes)
view.setUint16(0, 0x4D42, true); // 'BM'
view.setUint32(2, fileSize, true);
view.setUint32(10, 54, true); // Offset to pixel data
// BMP File Header (14 bytes)
view.setUint16(0, 0x4d42, true); // 'BM'
view.setUint32(2, fileSize, true);
view.setUint32(10, 54, true); // Offset to pixel data
// DIB Header (BITMAPINFOHEADER) (40 bytes)
view.setUint32(14, 40, true); // DIB header size
view.setUint32(18, width, true);
view.setUint32(22, -height, true); // Negative height for top-down scanline order
view.setUint16(26, 1, true); // Color planes
view.setUint16(28, 24, true); // Bits per pixel
view.setUint32(30, 0, true); // No compression
view.setUint32(34, stride * height, true); // Image size
view.setUint32(38, 2835, true); // Horizontal resolution (72 DPI)
view.setUint32(42, 2835, true); // Vertical resolution (72 DPI)
// DIB Header (BITMAPINFOHEADER) (40 bytes)
view.setUint32(14, 40, true); // DIB header size
view.setUint32(18, width, true);
view.setUint32(22, -height, true); // Negative height for top-down scanline order
view.setUint16(26, 1, true); // Color planes
view.setUint16(28, 24, true); // Bits per pixel
view.setUint32(30, 0, true); // No compression
view.setUint32(34, stride * height, true); // Image size
view.setUint32(38, 2835, true); // Horizontal resolution (72 DPI)
view.setUint32(42, 2835, true); // Vertical resolution (72 DPI)
// Pixel Data
let offset = 54;
for (let y = 0; y < height; y++) {
for (let x = 0; x < width; x++) {
const i = (y * width + x) * 4;
// BMP is BGR, not RGB
view.setUint8(offset++, data[i + 2]); // Blue
view.setUint8(offset++, data[i + 1]); // Green
view.setUint8(offset++, data[i]); // Red
}
// Add padding to make the row a multiple of 4 bytes
for (let p = 0; p < (stride - width * 3); p++) {
view.setUint8(offset++, 0);
}
// Pixel Data
let offset = 54;
for (let y = 0; y < height; y++) {
for (let x = 0; x < width; x++) {
const i = (y * width + x) * 4;
// BMP is BGR, not RGB
view.setUint8(offset++, data[i + 2]); // Blue
view.setUint8(offset++, data[i + 1]); // Green
view.setUint8(offset++, data[i]); // Red
}
return buffer;
// Add padding to make the row a multiple of 4 bytes
for (let p = 0; p < stride - width * 3; p++) {
view.setUint8(offset++, 0);
}
}
return buffer;
}
export async function pdfToBmp() {
showLoader('Converting PDF to BMP images...');
try {
// @ts-expect-error TS(2304) FIXME: Cannot find name 'pdfjsLib'.
const pdf = await pdfjsLib.getDocument(await readFileAsArrayBuffer(state.files[0])).promise;
const zip = new JSZip();
showLoader('Converting PDF to BMP images...');
try {
// @ts-expect-error TS(2304) FIXME: Cannot find name 'pdfjsLib'.
const pdf = await pdfjsLib.getDocument(
await readFileAsArrayBuffer(state.files[0])
).promise;
const zip = new JSZip();
for (let i = 1; i <= pdf.numPages; i++) {
showLoader(`Processing page ${i} of ${pdf.numPages}...`);
const page = await pdf.getPage(i);
const viewport = page.getViewport({ scale: 1.5 });
const canvas = document.createElement('canvas');
const context = canvas.getContext('2d');
canvas.height = viewport.height;
canvas.width = viewport.width;
for (let i = 1; i <= pdf.numPages; i++) {
showLoader(`Processing page ${i} of ${pdf.numPages}...`);
const page = await pdf.getPage(i);
const viewport = page.getViewport({ scale: 1.5 });
const canvas = document.createElement('canvas');
const context = canvas.getContext('2d');
canvas.height = viewport.height;
canvas.width = viewport.width;
// Render the PDF page directly to the canvas
await page.render({ canvasContext: context, viewport: viewport }).promise;
// Render the PDF page directly to the canvas
await page.render({ canvasContext: context, viewport: viewport }).promise;
// Get the raw pixel data from this canvas
const imageData = context.getImageData(0, 0, canvas.width, canvas.height);
// Use our new self-contained function to create the BMP file
const bmpBuffer = encodeBMP(imageData);
// Add the generated BMP file to the zip archive
zip.file(`page_${i}.bmp`, bmpBuffer);
}
// Get the raw pixel data from this canvas
const imageData = context.getImageData(0, 0, canvas.width, canvas.height);
showLoader('Compressing files into a ZIP...');
const zipBlob = await zip.generateAsync({ type: "blob" });
downloadFile(zipBlob, 'converted_bmp_images.zip');
// Use our new self-contained function to create the BMP file
const bmpBuffer = encodeBMP(imageData);
} catch (e) {
console.error(e);
showAlert('Error', 'Failed to convert PDF to BMP. The file might be corrupted.');
} finally {
hideLoader();
// Add the generated BMP file to the zip archive
zip.file(`page_${i}.bmp`, bmpBuffer);
}
}
showLoader('Compressing files into a ZIP...');
const zipBlob = await zip.generateAsync({ type: 'blob' });
downloadFile(zipBlob, 'converted_bmp_images.zip');
} catch (e) {
console.error(e);
showAlert(
'Error',
'Failed to convert PDF to BMP. The file might be corrupted.'
);
} finally {
hideLoader();
}
}

View File

@@ -5,54 +5,64 @@ import { state } from '../state.js';
import { PDFDocument as PDFLibDocument } from 'pdf-lib';
export async function pdfToGreyscale() {
if (!state.pdfDoc) {
showAlert('Error', 'PDF not loaded.');
return;
if (!state.pdfDoc) {
showAlert('Error', 'PDF not loaded.');
return;
}
showLoader('Converting to greyscale...');
try {
const newPdfDoc = await PDFLibDocument.create();
const pdfBytes = await state.pdfDoc.save();
// @ts-expect-error TS(2304) FIXME: Cannot find name 'pdfjsLib'.
const pdfjsDoc = await pdfjsLib.getDocument({ data: pdfBytes }).promise;
for (let i = 1; i <= pdfjsDoc.numPages; i++) {
const page = await pdfjsDoc.getPage(i);
const viewport = page.getViewport({ scale: 1.5 });
const canvas = document.createElement('canvas');
canvas.width = viewport.width;
canvas.height = viewport.height;
const ctx = canvas.getContext('2d');
await page.render({ canvasContext: ctx, viewport }).promise;
const imageData = ctx.getImageData(0, 0, canvas.width, canvas.height);
const data = imageData.data;
for (let j = 0; j < data.length; j += 4) {
const avg = (data[j] + data[j + 1] + data[j + 2]) / 3;
data[j] = avg; // red
data[j + 1] = avg; // green
data[j + 2] = avg; // blue
}
ctx.putImageData(imageData, 0, 0);
const imageBytes = await new Promise((resolve) =>
canvas.toBlob((blob) => {
const reader = new FileReader();
// @ts-expect-error TS(2769) FIXME: No overload matches this call.
reader.onload = () => resolve(new Uint8Array(reader.result));
reader.readAsArrayBuffer(blob);
}, 'image/png')
);
const image = await newPdfDoc.embedPng(imageBytes as Uint8Array);
const newPage = newPdfDoc.addPage([image.width, image.height]);
newPage.drawImage(image, {
x: 0,
y: 0,
width: image.width,
height: image.height,
});
}
showLoader('Converting to greyscale...');
try {
const newPdfDoc = await PDFLibDocument.create();
const pdfBytes = await state.pdfDoc.save();
// @ts-expect-error TS(2304) FIXME: Cannot find name 'pdfjsLib'.
const pdfjsDoc = await pdfjsLib.getDocument({ data: pdfBytes }).promise;
for (let i = 1; i <= pdfjsDoc.numPages; i++) {
const page = await pdfjsDoc.getPage(i);
const viewport = page.getViewport({ scale: 1.5 });
const canvas = document.createElement('canvas');
canvas.width = viewport.width;
canvas.height = viewport.height;
const ctx = canvas.getContext('2d');
await page.render({ canvasContext: ctx, viewport }).promise;
const imageData = ctx.getImageData(0, 0, canvas.width, canvas.height);
const data = imageData.data;
for (let j = 0; j < data.length; j += 4) {
const avg = (data[j] + data[j + 1] + data[j + 2]) / 3;
data[j] = avg; // red
data[j + 1] = avg; // green
data[j + 2] = avg; // blue
}
ctx.putImageData(imageData, 0, 0);
const imageBytes = await new Promise(resolve => canvas.toBlob(blob => {
const reader = new FileReader();
// @ts-expect-error TS(2769) FIXME: No overload matches this call.
reader.onload = () => resolve(new Uint8Array(reader.result));
reader.readAsArrayBuffer(blob);
}, 'image/png'));
const image = await newPdfDoc.embedPng(imageBytes as Uint8Array);
const newPage = newPdfDoc.addPage([image.width, image.height]);
newPage.drawImage(image, { x: 0, y: 0, width: image.width, height: image.height });
}
const newPdfBytes = await newPdfDoc.save();
downloadFile(new Blob([new Uint8Array(newPdfBytes)], { type: 'application/pdf' }), 'greyscale.pdf');
} catch (e) {
console.error(e);
showAlert('Error', 'Could not convert to greyscale.');
} finally {
hideLoader();
}
}
const newPdfBytes = await newPdfDoc.save();
downloadFile(
new Blob([new Uint8Array(newPdfBytes)], { type: 'application/pdf' }),
'greyscale.pdf'
);
} catch (e) {
console.error(e);
showAlert('Error', 'Could not convert to greyscale.');
} finally {
hideLoader();
}
}

View File

@@ -4,32 +4,39 @@ import { state } from '../state.js';
import JSZip from 'jszip';
export async function pdfToJpg() {
showLoader('Converting to JPG...');
try {
// @ts-expect-error TS(2304) FIXME: Cannot find name 'pdfjsLib'.
const pdf = await pdfjsLib.getDocument(await readFileAsArrayBuffer(state.files[0])).promise;
const zip = new JSZip();
showLoader('Converting to JPG...');
try {
// @ts-expect-error TS(2304) FIXME: Cannot find name 'pdfjsLib'.
const pdf = await pdfjsLib.getDocument(
await readFileAsArrayBuffer(state.files[0])
).promise;
const zip = new JSZip();
for (let i = 1; i <= pdf.numPages; i++) {
const page = await pdf.getPage(i);
const viewport = page.getViewport({ scale: 2.0 });
const canvas = document.createElement('canvas');
const context = canvas.getContext('2d');
canvas.height = viewport.height;
canvas.width = viewport.width;
for (let i = 1; i <= pdf.numPages; i++) {
const page = await pdf.getPage(i);
const viewport = page.getViewport({ scale: 2.0 });
const canvas = document.createElement('canvas');
const context = canvas.getContext('2d');
canvas.height = viewport.height;
canvas.width = viewport.width;
await page.render({ canvasContext: context, viewport: viewport }).promise;
await page.render({ canvasContext: context, viewport: viewport }).promise;
const blob = await new Promise(resolve => canvas.toBlob(resolve, 'image/jpeg', 0.9));
zip.file(`page_${i}.jpg`, blob as Blob);
}
const zipBlob = await zip.generateAsync({ type: "blob" });
downloadFile(zipBlob, 'converted_images.zip');
} catch (e) {
console.error(e);
showAlert('Error', 'Failed to convert PDF to JPG. The file might be corrupted.');
} finally {
hideLoader();
const blob = await new Promise((resolve) =>
canvas.toBlob(resolve, 'image/jpeg', 0.9)
);
zip.file(`page_${i}.jpg`, blob as Blob);
}
}
const zipBlob = await zip.generateAsync({ type: 'blob' });
downloadFile(zipBlob, 'converted_images.zip');
} catch (e) {
console.error(e);
showAlert(
'Error',
'Failed to convert PDF to JPG. The file might be corrupted.'
);
} finally {
hideLoader();
}
}

View File

@@ -1,31 +1,33 @@
import { showLoader, hideLoader, showAlert } from '../ui.js';
import { downloadFile, readFileAsArrayBuffer } from '../utils/helpers.js';
import { state } from '../state.js';
export async function pdfToMarkdown() {
showLoader('Converting to Markdown...');
try {
const file = state.files[0];
const arrayBuffer = await readFileAsArrayBuffer(file);
// @ts-expect-error TS(2304) FIXME: Cannot find name 'pdfjsLib'.
const pdf = await pdfjsLib.getDocument({ data: arrayBuffer }).promise;
let markdown = '';
showLoader('Converting to Markdown...');
try {
const file = state.files[0];
const arrayBuffer = await readFileAsArrayBuffer(file);
// @ts-expect-error TS(2304) FIXME: Cannot find name 'pdfjsLib'.
const pdf = await pdfjsLib.getDocument({ data: arrayBuffer }).promise;
let markdown = '';
for (let i = 1; i <= pdf.numPages; i++) {
const page = await pdf.getPage(i);
const content = await page.getTextContent();
// This is a simple text extraction. For more advanced formatting, more complex logic is needed.
const text = content.items.map((item: any) => item.str).join(' ');
markdown += text + '\n\n'; // Add double newline for paragraph breaks between pages
}
const blob = new Blob([markdown], { type: 'text/markdown' });
downloadFile(blob, file.name.replace(/\.pdf$/i, '.md'));
} catch (e) {
console.error(e);
showAlert('Conversion Error', 'Failed to convert PDF. It may be image-based or corrupted.');
} finally {
hideLoader();
for (let i = 1; i <= pdf.numPages; i++) {
const page = await pdf.getPage(i);
const content = await page.getTextContent();
// This is a simple text extraction. For more advanced formatting, more complex logic is needed.
const text = content.items.map((item: any) => item.str).join(' ');
markdown += text + '\n\n'; // Add double newline for paragraph breaks between pages
}
}
const blob = new Blob([markdown], { type: 'text/markdown' });
downloadFile(blob, file.name.replace(/\.pdf$/i, '.md'));
} catch (e) {
console.error(e);
showAlert(
'Conversion Error',
'Failed to convert PDF. It may be image-based or corrupted.'
);
} finally {
hideLoader();
}
}

View File

@@ -1,33 +1,35 @@
import { showLoader, hideLoader, showAlert } from '../ui.js';
import { downloadFile, readFileAsArrayBuffer } from '../utils/helpers.js';
import { state } from '../state.js';
import JSZip from 'jszip';
export async function pdfToPng() {
showLoader('Converting to PNG...');
try {
// @ts-expect-error TS(2304) FIXME: Cannot find name 'pdfjsLib'.
const pdf = await pdfjsLib.getDocument(await readFileAsArrayBuffer(state.files[0])).promise;
const zip = new JSZip();
for (let i = 1; i <= pdf.numPages; i++) {
const page = await pdf.getPage(i);
const viewport = page.getViewport({ scale: 2.0 });
const canvas = document.createElement('canvas');
canvas.height = viewport.height;
canvas.width = viewport.width;
const context = canvas.getContext('2d');
await page.render({ canvasContext: context, viewport: viewport }).promise;
const blob = await new Promise(resolve => canvas.toBlob(resolve, 'image/png'));
zip.file(`page_${i}.png`, blob as Blob);
}
const zipBlob = await zip.generateAsync({ type: "blob" });
downloadFile(zipBlob, 'converted_pngs.zip');
} catch (e) {
console.error(e);
showAlert('Error', 'Failed to convert PDF to PNG.');
} finally {
hideLoader();
showLoader('Converting to PNG...');
try {
// @ts-expect-error TS(2304) FIXME: Cannot find name 'pdfjsLib'.
const pdf = await pdfjsLib.getDocument(
await readFileAsArrayBuffer(state.files[0])
).promise;
const zip = new JSZip();
for (let i = 1; i <= pdf.numPages; i++) {
const page = await pdf.getPage(i);
const viewport = page.getViewport({ scale: 2.0 });
const canvas = document.createElement('canvas');
canvas.height = viewport.height;
canvas.width = viewport.width;
const context = canvas.getContext('2d');
await page.render({ canvasContext: context, viewport: viewport }).promise;
const blob = await new Promise((resolve) =>
canvas.toBlob(resolve, 'image/png')
);
zip.file(`page_${i}.png`, blob as Blob);
}
}
const zipBlob = await zip.generateAsync({ type: 'blob' });
downloadFile(zipBlob, 'converted_pngs.zip');
} catch (e) {
console.error(e);
showAlert('Error', 'Failed to convert PDF to PNG.');
} finally {
hideLoader();
}
}

View File

@@ -3,36 +3,49 @@ import { downloadFile, readFileAsArrayBuffer } from '../utils/helpers.js';
import { state } from '../state.js';
import JSZip from 'jszip';
import UTIF from 'utif';
import * as pdfjsLib from "pdfjs-dist";
import * as pdfjsLib from 'pdfjs-dist';
export async function pdfToTiff() {
showLoader('Converting PDF to TIFF...');
try {
const pdf = await pdfjsLib.getDocument(await readFileAsArrayBuffer(state.files[0])).promise;
const zip = new JSZip();
showLoader('Converting PDF to TIFF...');
try {
const pdf = await pdfjsLib.getDocument(
await readFileAsArrayBuffer(state.files[0])
).promise;
const zip = new JSZip();
for (let i = 1; i <= pdf.numPages; i++) {
const page = await pdf.getPage(i);
const viewport = page.getViewport({ scale: 2.0 }); // Use 2x scale for high quality
const canvas = document.createElement('canvas');
const context = canvas.getContext('2d');
canvas.height = viewport.height;
canvas.width = viewport.width;
for (let i = 1; i <= pdf.numPages; i++) {
const page = await pdf.getPage(i);
const viewport = page.getViewport({ scale: 2.0 }); // Use 2x scale for high quality
const canvas = document.createElement('canvas');
const context = canvas.getContext('2d');
canvas.height = viewport.height;
canvas.width = viewport.width;
await page.render({ canvasContext: context, viewport: viewport, canvas: canvas }).promise;
const imageData = context.getImageData(0, 0, canvas.width, canvas.height);
const rgba = imageData.data;
const tiffBuffer = UTIF.encodeImage(new Uint8Array(rgba), canvas.width, canvas.height);
zip.file(`page_${i}.tiff`, tiffBuffer);
}
await page.render({
canvasContext: context,
viewport: viewport,
canvas: canvas,
}).promise;
const imageData = context.getImageData(0, 0, canvas.width, canvas.height);
const rgba = imageData.data;
const tiffBuffer = UTIF.encodeImage(
new Uint8Array(rgba),
canvas.width,
canvas.height
);
const zipBlob = await zip.generateAsync({ type: "blob" });
downloadFile(zipBlob, 'converted_tiff_images.zip');
} catch (e) {
console.error(e);
showAlert('Error', 'Failed to convert PDF to TIFF. The file might be corrupted.');
} finally {
hideLoader();
zip.file(`page_${i}.tiff`, tiffBuffer);
}
}
const zipBlob = await zip.generateAsync({ type: 'blob' });
downloadFile(zipBlob, 'converted_tiff_images.zip');
} catch (e) {
console.error(e);
showAlert(
'Error',
'Failed to convert PDF to TIFF. The file might be corrupted.'
);
} finally {
hideLoader();
}
}

View File

@@ -1,32 +1,35 @@
import { showLoader, hideLoader, showAlert } from '../ui.js';
import { downloadFile, readFileAsArrayBuffer } from '../utils/helpers.js';
import { state } from '../state.js';
import JSZip from 'jszip';
export async function pdfToWebp() {
showLoader('Converting to WebP...');
try {
// @ts-expect-error TS(2304) FIXME: Cannot find name 'pdfjsLib'.
const pdf = await pdfjsLib.getDocument(await readFileAsArrayBuffer(state.files[0])).promise;
const zip = new JSZip();
for (let i = 1; i <= pdf.numPages; i++) {
const page = await pdf.getPage(i);
const viewport = page.getViewport({ scale: 2.0 });
const canvas = document.createElement('canvas');
canvas.height = viewport.height;
canvas.width = viewport.width;
const context = canvas.getContext('2d');
await page.render({ canvasContext: context, viewport: viewport }).promise;
const blob = await new Promise(resolve => canvas.toBlob(resolve, 'image/webp', 0.9));
zip.file(`page_${i}.webp`, blob as Blob);
}
const zipBlob = await zip.generateAsync({ type: "blob" });
downloadFile(zipBlob, 'converted_webp.zip');
} catch (e) {
console.error(e);
showAlert('Error', 'Failed to convert PDF to WebP.');
} finally {
hideLoader();
showLoader('Converting to WebP...');
try {
// @ts-expect-error TS(2304) FIXME: Cannot find name 'pdfjsLib'.
const pdf = await pdfjsLib.getDocument(
await readFileAsArrayBuffer(state.files[0])
).promise;
const zip = new JSZip();
for (let i = 1; i <= pdf.numPages; i++) {
const page = await pdf.getPage(i);
const viewport = page.getViewport({ scale: 2.0 });
const canvas = document.createElement('canvas');
canvas.height = viewport.height;
canvas.width = viewport.width;
const context = canvas.getContext('2d');
await page.render({ canvasContext: context, viewport: viewport }).promise;
const blob = await new Promise((resolve) =>
canvas.toBlob(resolve, 'image/webp', 0.9)
);
zip.file(`page_${i}.webp`, blob as Blob);
}
}
const zipBlob = await zip.generateAsync({ type: 'blob' });
downloadFile(zipBlob, 'converted_webp.zip');
} catch (e) {
console.error(e);
showAlert('Error', 'Failed to convert PDF to WebP.');
} finally {
hideLoader();
}
}

View File

@@ -3,25 +3,24 @@ import { downloadFile, readFileAsArrayBuffer } from '../utils/helpers.js';
import { state } from '../state.js';
import JSZip from 'jszip';
export async function pdfToZip() {
if (state.files.length === 0) {
showAlert('No Files', 'Please select one or more PDF files.');
return;
if (state.files.length === 0) {
showAlert('No Files', 'Please select one or more PDF files.');
return;
}
showLoader('Creating ZIP file...');
try {
const zip = new JSZip();
for (const file of state.files) {
const fileBuffer = await readFileAsArrayBuffer(file);
zip.file(file.name, fileBuffer as ArrayBuffer);
}
showLoader('Creating ZIP file...');
try {
const zip = new JSZip();
for (const file of state.files) {
const fileBuffer = await readFileAsArrayBuffer(file);
zip.file(file.name, fileBuffer as ArrayBuffer);
}
const zipBlob = await zip.generateAsync({ type: 'blob' });
downloadFile(zipBlob, 'pdfs.zip');
} catch (e) {
console.error(e);
showAlert('Error', 'Failed to create ZIP file.');
} finally {
hideLoader();
}
}
const zipBlob = await zip.generateAsync({ type: 'blob' });
downloadFile(zipBlob, 'pdfs.zip');
} catch (e) {
console.error(e);
showAlert('Error', 'Failed to create ZIP file.');
} finally {
hideLoader();
}
}

View File

@@ -1,4 +1,3 @@
import { showLoader, hideLoader, showAlert } from '../ui.js';
import { downloadFile, readFileAsArrayBuffer } from '../utils/helpers.js';
import { state } from '../state.js';
@@ -6,30 +5,36 @@ import { state } from '../state.js';
import { PDFDocument as PDFLibDocument } from 'pdf-lib';
export async function pngToPdf() {
if (state.files.length === 0) {
showAlert('No Files', 'Please select at least one PNG file.');
return;
if (state.files.length === 0) {
showAlert('No Files', 'Please select at least one PNG file.');
return;
}
showLoader('Creating PDF from PNGs...');
try {
const pdfDoc = await PDFLibDocument.create();
for (const file of state.files) {
const pngBytes = await readFileAsArrayBuffer(file);
const pngImage = await pdfDoc.embedPng(pngBytes as ArrayBuffer);
const page = pdfDoc.addPage([pngImage.width, pngImage.height]);
page.drawImage(pngImage, {
x: 0,
y: 0,
width: pngImage.width,
height: pngImage.height,
});
}
showLoader('Creating PDF from PNGs...');
try {
const pdfDoc = await PDFLibDocument.create();
for (const file of state.files) {
const pngBytes = await readFileAsArrayBuffer(file);
const pngImage = await pdfDoc.embedPng(pngBytes as ArrayBuffer);
const page = pdfDoc.addPage([pngImage.width, pngImage.height]);
page.drawImage(pngImage, {
x: 0,
y: 0,
width: pngImage.width,
height: pngImage.height,
});
}
const pdfBytes = await pdfDoc.save();
downloadFile(new Blob([new Uint8Array(pdfBytes)], { type: 'application/pdf' }), 'from_pngs.pdf');
} catch (e) {
console.error(e);
showAlert('Error', 'Failed to create PDF from PNG images. Ensure all files are valid PNGs.');
} finally {
hideLoader();
}
}
const pdfBytes = await pdfDoc.save();
downloadFile(
new Blob([new Uint8Array(pdfBytes)], { type: 'application/pdf' }),
'from_pngs.pdf'
);
} catch (e) {
console.error(e);
showAlert(
'Error',
'Failed to create PDF from PNG images. Ensure all files are valid PNGs.'
);
} finally {
hideLoader();
}
}

View File

@@ -6,197 +6,282 @@ import * as pdfjsLib from 'pdfjs-dist';
import { createIcons, icons } from 'lucide';
const posterizeState = {
pdfJsDoc: null,
pageSnapshots: {},
currentPage: 1,
pdfJsDoc: null,
pageSnapshots: {},
currentPage: 1,
};
async function renderPosterizePreview(pageNum: number) {
if (!posterizeState.pdfJsDoc) return;
posterizeState.currentPage = pageNum;
showLoader(`Rendering preview for page ${pageNum}...`);
if (!posterizeState.pdfJsDoc) return;
const canvas = document.getElementById('posterize-preview-canvas') as HTMLCanvasElement;
const context = canvas.getContext('2d');
posterizeState.currentPage = pageNum;
showLoader(`Rendering preview for page ${pageNum}...`);
if (posterizeState.pageSnapshots[pageNum]) {
canvas.width = posterizeState.pageSnapshots[pageNum].width;
canvas.height = posterizeState.pageSnapshots[pageNum].height;
context.putImageData(posterizeState.pageSnapshots[pageNum], 0, 0);
} else {
const page = await posterizeState.pdfJsDoc.getPage(pageNum);
const viewport = page.getViewport({ scale: 1.5 });
canvas.width = viewport.width;
canvas.height = viewport.height;
await page.render({ canvasContext: context, viewport }).promise;
posterizeState.pageSnapshots[pageNum] = context.getImageData(0, 0, canvas.width, canvas.height);
}
updatePreviewNav();
drawGridOverlay();
hideLoader();
const canvas = document.getElementById(
'posterize-preview-canvas'
) as HTMLCanvasElement;
const context = canvas.getContext('2d');
if (posterizeState.pageSnapshots[pageNum]) {
canvas.width = posterizeState.pageSnapshots[pageNum].width;
canvas.height = posterizeState.pageSnapshots[pageNum].height;
context.putImageData(posterizeState.pageSnapshots[pageNum], 0, 0);
} else {
const page = await posterizeState.pdfJsDoc.getPage(pageNum);
const viewport = page.getViewport({ scale: 1.5 });
canvas.width = viewport.width;
canvas.height = viewport.height;
await page.render({ canvasContext: context, viewport }).promise;
posterizeState.pageSnapshots[pageNum] = context.getImageData(
0,
0,
canvas.width,
canvas.height
);
}
updatePreviewNav();
drawGridOverlay();
hideLoader();
}
function drawGridOverlay() {
if (!posterizeState.pageSnapshots[posterizeState.currentPage]) return;
if (!posterizeState.pageSnapshots[posterizeState.currentPage]) return;
const canvas = document.getElementById('posterize-preview-canvas') as HTMLCanvasElement;
const context = canvas.getContext('2d');
context.putImageData(posterizeState.pageSnapshots[posterizeState.currentPage], 0, 0);
const canvas = document.getElementById(
'posterize-preview-canvas'
) as HTMLCanvasElement;
const context = canvas.getContext('2d');
context.putImageData(
posterizeState.pageSnapshots[posterizeState.currentPage],
0,
0
);
const pageRangeInput = (document.getElementById('page-range') as HTMLInputElement).value;
const pagesToProcess = parsePageRanges(pageRangeInput, posterizeState.pdfJsDoc.numPages);
if (pagesToProcess.includes(posterizeState.currentPage - 1)) {
const rows = parseInt((document.getElementById('posterize-rows') as HTMLInputElement).value) || 1;
const cols = parseInt((document.getElementById('posterize-cols') as HTMLInputElement).value) || 1;
const pageRangeInput = (
document.getElementById('page-range') as HTMLInputElement
).value;
const pagesToProcess = parsePageRanges(
pageRangeInput,
posterizeState.pdfJsDoc.numPages
);
context.strokeStyle = 'rgba(239, 68, 68, 0.9)';
context.lineWidth = 2;
context.setLineDash([10, 5]);
if (pagesToProcess.includes(posterizeState.currentPage - 1)) {
const rows =
parseInt(
(document.getElementById('posterize-rows') as HTMLInputElement).value
) || 1;
const cols =
parseInt(
(document.getElementById('posterize-cols') as HTMLInputElement).value
) || 1;
const cellWidth = canvas.width / cols;
const cellHeight = canvas.height / rows;
context.strokeStyle = 'rgba(239, 68, 68, 0.9)';
context.lineWidth = 2;
context.setLineDash([10, 5]);
for (let i = 1; i < cols; i++) {
context.beginPath();
context.moveTo(i * cellWidth, 0);
context.lineTo(i * cellWidth, canvas.height);
context.stroke();
}
const cellWidth = canvas.width / cols;
const cellHeight = canvas.height / rows;
for (let i = 1; i < rows; i++) {
context.beginPath();
context.moveTo(0, i * cellHeight);
context.lineTo(canvas.width, i * cellHeight);
context.stroke();
}
context.setLineDash([]);
for (let i = 1; i < cols; i++) {
context.beginPath();
context.moveTo(i * cellWidth, 0);
context.lineTo(i * cellWidth, canvas.height);
context.stroke();
}
for (let i = 1; i < rows; i++) {
context.beginPath();
context.moveTo(0, i * cellHeight);
context.lineTo(canvas.width, i * cellHeight);
context.stroke();
}
context.setLineDash([]);
}
}
function updatePreviewNav() {
const currentPageSpan = document.getElementById('current-preview-page');
const prevBtn = document.getElementById('prev-preview-page') as HTMLButtonElement;
const nextBtn = document.getElementById('next-preview-page') as HTMLButtonElement;
const currentPageSpan = document.getElementById('current-preview-page');
const prevBtn = document.getElementById(
'prev-preview-page'
) as HTMLButtonElement;
const nextBtn = document.getElementById(
'next-preview-page'
) as HTMLButtonElement;
currentPageSpan.textContent = posterizeState.currentPage.toString();
prevBtn.disabled = posterizeState.currentPage <= 1;
nextBtn.disabled = posterizeState.currentPage >= posterizeState.pdfJsDoc.numPages;
currentPageSpan.textContent = posterizeState.currentPage.toString();
prevBtn.disabled = posterizeState.currentPage <= 1;
nextBtn.disabled =
posterizeState.currentPage >= posterizeState.pdfJsDoc.numPages;
}
export async function setupPosterizeTool() {
if (state.pdfDoc) {
document.getElementById('total-pages').textContent = state.pdfDoc.getPageCount().toString();
const pdfBytes = await state.pdfDoc.save();
posterizeState.pdfJsDoc = await pdfjsLib.getDocument({ data: pdfBytes }).promise;
posterizeState.pageSnapshots = {};
posterizeState.currentPage = 1;
document.getElementById('total-preview-pages').textContent = posterizeState.pdfJsDoc.numPages.toString();
await renderPosterizePreview(1);
if (state.pdfDoc) {
document.getElementById('total-pages').textContent = state.pdfDoc
.getPageCount()
.toString();
const pdfBytes = await state.pdfDoc.save();
posterizeState.pdfJsDoc = await pdfjsLib.getDocument({ data: pdfBytes })
.promise;
posterizeState.pageSnapshots = {};
posterizeState.currentPage = 1;
document.getElementById('prev-preview-page').onclick = () => renderPosterizePreview(posterizeState.currentPage - 1);
document.getElementById('next-preview-page').onclick = () => renderPosterizePreview(posterizeState.currentPage + 1);
document.getElementById('total-preview-pages').textContent =
posterizeState.pdfJsDoc.numPages.toString();
await renderPosterizePreview(1);
['posterize-rows', 'posterize-cols', 'page-range'].forEach(id => {
document.getElementById(id).addEventListener('input', drawGridOverlay);
});
createIcons({ icons });
}
document.getElementById('prev-preview-page').onclick = () =>
renderPosterizePreview(posterizeState.currentPage - 1);
document.getElementById('next-preview-page').onclick = () =>
renderPosterizePreview(posterizeState.currentPage + 1);
['posterize-rows', 'posterize-cols', 'page-range'].forEach((id) => {
document.getElementById(id).addEventListener('input', drawGridOverlay);
});
createIcons({ icons });
}
}
export async function posterize() {
showLoader('Posterizing PDF...');
try {
const rows = parseInt((document.getElementById('posterize-rows') as HTMLInputElement).value) || 1;
const cols = parseInt((document.getElementById('posterize-cols') as HTMLInputElement).value) || 1;
const pageSizeKey = (document.getElementById('output-page-size') as HTMLSelectElement).value;
let orientation = (document.getElementById('output-orientation') as HTMLSelectElement).value;
const scalingMode = (document.querySelector('input[name="scaling-mode"]:checked') as HTMLInputElement).value;
const overlap = parseFloat((document.getElementById('overlap') as HTMLInputElement).value) || 0;
const overlapUnits = (document.getElementById('overlap-units') as HTMLSelectElement).value;
const pageRangeInput = (document.getElementById('page-range') as HTMLInputElement).value;
showLoader('Posterizing PDF...');
try {
const rows =
parseInt(
(document.getElementById('posterize-rows') as HTMLInputElement).value
) || 1;
const cols =
parseInt(
(document.getElementById('posterize-cols') as HTMLInputElement).value
) || 1;
const pageSizeKey = (
document.getElementById('output-page-size') as HTMLSelectElement
).value;
let orientation = (
document.getElementById('output-orientation') as HTMLSelectElement
).value;
const scalingMode = (
document.querySelector(
'input[name="scaling-mode"]:checked'
) as HTMLInputElement
).value;
const overlap =
parseFloat(
(document.getElementById('overlap') as HTMLInputElement).value
) || 0;
const overlapUnits = (
document.getElementById('overlap-units') as HTMLSelectElement
).value;
const pageRangeInput = (
document.getElementById('page-range') as HTMLInputElement
).value;
let overlapInPoints = overlap;
if (overlapUnits === 'in') overlapInPoints = overlap * 72;
else if (overlapUnits === 'mm') overlapInPoints = overlap * (72 / 25.4);
let overlapInPoints = overlap;
if (overlapUnits === 'in') overlapInPoints = overlap * 72;
else if (overlapUnits === 'mm') overlapInPoints = overlap * (72 / 25.4);
const newDoc = await PDFDocument.create();
const totalPages = posterizeState.pdfJsDoc.numPages;
const pageIndicesToProcess = parsePageRanges(pageRangeInput, totalPages);
const newDoc = await PDFDocument.create();
const totalPages = posterizeState.pdfJsDoc.numPages;
const pageIndicesToProcess = parsePageRanges(pageRangeInput, totalPages);
if (pageIndicesToProcess.length === 0) {
throw new Error("Invalid page range specified.");
}
const tempCanvas = document.createElement('canvas');
const tempCtx = tempCanvas.getContext('2d');
for (const pageIndex of pageIndicesToProcess) {
const page = await posterizeState.pdfJsDoc.getPage(Number(pageIndex) + 1);
const viewport = page.getViewport({ scale: 2.0 });
tempCanvas.width = viewport.width;
tempCanvas.height = viewport.height;
await page.render({ canvasContext: tempCtx, viewport }).promise;
let [targetWidth, targetHeight] = PageSizes[pageSizeKey];
let currentOrientation = orientation;
if (currentOrientation === 'auto') {
currentOrientation = viewport.width > viewport.height ? 'landscape' : 'portrait';
}
if (currentOrientation === 'landscape' && targetWidth < targetHeight) {
[targetWidth, targetHeight] = [targetHeight, targetWidth];
} else if (currentOrientation === 'portrait' && targetWidth > targetHeight) {
[targetWidth, targetHeight] = [targetHeight, targetWidth];
}
const tileWidth = tempCanvas.width / cols;
const tileHeight = tempCanvas.height / rows;
for (let r = 0; r < rows; r++) {
for (let c = 0; c < cols; c++) {
const sx = c * tileWidth - (c > 0 ? overlapInPoints : 0);
const sy = r * tileHeight - (r > 0 ? overlapInPoints : 0);
const sWidth = tileWidth + (c > 0 ? overlapInPoints : 0) + (c < cols - 1 ? overlapInPoints : 0);
const sHeight = tileHeight + (r > 0 ? overlapInPoints : 0) + (r < rows - 1 ? overlapInPoints : 0);
const tileCanvas = document.createElement('canvas');
tileCanvas.width = sWidth;
tileCanvas.height = sHeight;
tileCanvas.getContext('2d').drawImage(tempCanvas, sx, sy, sWidth, sHeight, 0, 0, sWidth, sHeight);
const tileImage = await newDoc.embedPng(tileCanvas.toDataURL('image/png'));
const newPage = newDoc.addPage([targetWidth, targetHeight]);
const scaleX = newPage.getWidth() / sWidth;
const scaleY = newPage.getHeight() / sHeight;
const scale = scalingMode === 'fit' ? Math.min(scaleX, scaleY) : Math.max(scaleX, scaleY);
const scaledWidth = sWidth * scale;
const scaledHeight = sHeight * scale;
newPage.drawImage(tileImage, {
x: (newPage.getWidth() - scaledWidth) / 2,
y: (newPage.getHeight() - scaledHeight) / 2,
width: scaledWidth,
height: scaledHeight,
});
}
}
}
const newPdfBytes = await newDoc.save();
downloadFile(new Blob([new Uint8Array(newPdfBytes)], { type: 'application/pdf' }), 'posterized.pdf');
showAlert('Success', 'Your PDF has been posterized.');
} catch (e) {
console.error(e);
showAlert('Error', e.message || 'Could not posterize the PDF.');
} finally {
hideLoader();
if (pageIndicesToProcess.length === 0) {
throw new Error('Invalid page range specified.');
}
const tempCanvas = document.createElement('canvas');
const tempCtx = tempCanvas.getContext('2d');
for (const pageIndex of pageIndicesToProcess) {
const page = await posterizeState.pdfJsDoc.getPage(Number(pageIndex) + 1);
const viewport = page.getViewport({ scale: 2.0 });
tempCanvas.width = viewport.width;
tempCanvas.height = viewport.height;
await page.render({ canvasContext: tempCtx, viewport }).promise;
let [targetWidth, targetHeight] = PageSizes[pageSizeKey];
let currentOrientation = orientation;
if (currentOrientation === 'auto') {
currentOrientation =
viewport.width > viewport.height ? 'landscape' : 'portrait';
}
if (currentOrientation === 'landscape' && targetWidth < targetHeight) {
[targetWidth, targetHeight] = [targetHeight, targetWidth];
} else if (
currentOrientation === 'portrait' &&
targetWidth > targetHeight
) {
[targetWidth, targetHeight] = [targetHeight, targetWidth];
}
const tileWidth = tempCanvas.width / cols;
const tileHeight = tempCanvas.height / rows;
for (let r = 0; r < rows; r++) {
for (let c = 0; c < cols; c++) {
const sx = c * tileWidth - (c > 0 ? overlapInPoints : 0);
const sy = r * tileHeight - (r > 0 ? overlapInPoints : 0);
const sWidth =
tileWidth +
(c > 0 ? overlapInPoints : 0) +
(c < cols - 1 ? overlapInPoints : 0);
const sHeight =
tileHeight +
(r > 0 ? overlapInPoints : 0) +
(r < rows - 1 ? overlapInPoints : 0);
const tileCanvas = document.createElement('canvas');
tileCanvas.width = sWidth;
tileCanvas.height = sHeight;
tileCanvas
.getContext('2d')
.drawImage(
tempCanvas,
sx,
sy,
sWidth,
sHeight,
0,
0,
sWidth,
sHeight
);
const tileImage = await newDoc.embedPng(
tileCanvas.toDataURL('image/png')
);
const newPage = newDoc.addPage([targetWidth, targetHeight]);
const scaleX = newPage.getWidth() / sWidth;
const scaleY = newPage.getHeight() / sHeight;
const scale =
scalingMode === 'fit'
? Math.min(scaleX, scaleY)
: Math.max(scaleX, scaleY);
const scaledWidth = sWidth * scale;
const scaledHeight = sHeight * scale;
newPage.drawImage(tileImage, {
x: (newPage.getWidth() - scaledWidth) / 2,
y: (newPage.getHeight() - scaledHeight) / 2,
width: scaledWidth,
height: scaledHeight,
});
}
}
}
const newPdfBytes = await newDoc.save();
downloadFile(
new Blob([new Uint8Array(newPdfBytes)], { type: 'application/pdf' }),
'posterized.pdf'
);
showAlert('Success', 'Your PDF has been posterized.');
} catch (e) {
console.error(e);
showAlert('Error', e.message || 'Could not posterize the PDF.');
} finally {
hideLoader();
}
}

View File

@@ -6,36 +6,39 @@ import { state } from '../state.js';
const { rgb } = window.PDFLib;
export async function redact(redactions: any, canvasScale: any) {
showLoader('Applying redactions...');
try {
const pdfPages = state.pdfDoc.getPages();
const conversionScale = 1 / canvasScale;
showLoader('Applying redactions...');
try {
const pdfPages = state.pdfDoc.getPages();
const conversionScale = 1 / canvasScale;
redactions.forEach((r: any) => {
const page = pdfPages[r.pageIndex];
const { height: pageHeight } = page.getSize();
redactions.forEach((r: any) => {
const page = pdfPages[r.pageIndex];
const { height: pageHeight } = page.getSize();
// Convert canvas coordinates back to PDF coordinates
const pdfX = r.canvasX * conversionScale;
const pdfWidth = r.canvasWidth * conversionScale;
const pdfHeight = r.canvasHeight * conversionScale;
const pdfY = pageHeight - (r.canvasY * conversionScale) - pdfHeight;
// Convert canvas coordinates back to PDF coordinates
const pdfX = r.canvasX * conversionScale;
const pdfWidth = r.canvasWidth * conversionScale;
const pdfHeight = r.canvasHeight * conversionScale;
const pdfY = pageHeight - r.canvasY * conversionScale - pdfHeight;
page.drawRectangle({
x: pdfX,
y: pdfY,
width: pdfWidth,
height: pdfHeight,
color: rgb(0, 0, 0),
});
});
page.drawRectangle({
x: pdfX,
y: pdfY,
width: pdfWidth,
height: pdfHeight,
color: rgb(0, 0, 0),
});
});
const redactedBytes = await state.pdfDoc.save();
downloadFile(new Blob([redactedBytes], { type: 'application/pdf' }), 'redacted.pdf');
} catch (e) {
console.error(e);
showAlert('Error', 'Failed to apply redactions.');
} finally {
hideLoader();
}
}
const redactedBytes = await state.pdfDoc.save();
downloadFile(
new Blob([redactedBytes], { type: 'application/pdf' }),
'redacted.pdf'
);
} catch (e) {
console.error(e);
showAlert('Error', 'Failed to apply redactions.');
} finally {
hideLoader();
}
}

View File

@@ -4,101 +4,123 @@ import { state } from '../state.js';
import { PDFName } from 'pdf-lib';
export function setupRemoveAnnotationsTool() {
if (state.pdfDoc) {
document.getElementById('total-pages').textContent = state.pdfDoc.getPageCount();
}
if (state.pdfDoc) {
document.getElementById('total-pages').textContent =
state.pdfDoc.getPageCount();
}
const pageScopeRadios = document.querySelectorAll('input[name="page-scope"]');
const pageRangeWrapper = document.getElementById('page-range-wrapper');
pageScopeRadios.forEach(radio => {
radio.addEventListener('change', () => {
// @ts-expect-error TS(2339) FIXME: Property 'value' does not exist on type 'Element'.
pageRangeWrapper.classList.toggle('hidden', radio.value !== 'specific');
});
const pageScopeRadios = document.querySelectorAll('input[name="page-scope"]');
const pageRangeWrapper = document.getElementById('page-range-wrapper');
pageScopeRadios.forEach((radio) => {
radio.addEventListener('change', () => {
// @ts-expect-error TS(2339) FIXME: Property 'value' does not exist on type 'Element'.
pageRangeWrapper.classList.toggle('hidden', radio.value !== 'specific');
});
});
const selectAllCheckbox = document.getElementById('select-all-annotations');
const allAnnotCheckboxes = document.querySelectorAll('.annot-checkbox');
selectAllCheckbox.addEventListener('change', () => {
allAnnotCheckboxes.forEach(checkbox => {
// @ts-expect-error TS(2339) FIXME: Property 'checked' does not exist on type 'Element... Remove this comment to see the full error message
checkbox.checked = selectAllCheckbox.checked;
});
const selectAllCheckbox = document.getElementById('select-all-annotations');
const allAnnotCheckboxes = document.querySelectorAll('.annot-checkbox');
selectAllCheckbox.addEventListener('change', () => {
allAnnotCheckboxes.forEach((checkbox) => {
// @ts-expect-error TS(2339) FIXME: Property 'checked' does not exist on type 'Element... Remove this comment to see the full error message
checkbox.checked = selectAllCheckbox.checked;
});
});
}
export async function removeAnnotations() {
showLoader('Removing annotations...');
try {
const totalPages = state.pdfDoc.getPageCount();
let targetPageIndices = [];
showLoader('Removing annotations...');
try {
const totalPages = state.pdfDoc.getPageCount();
let targetPageIndices = [];
// @ts-expect-error TS(2339) FIXME: Property 'value' does not exist on type 'Element'.
const pageScope = document.querySelector('input[name="page-scope"]:checked').value;
if (pageScope === 'all') {
targetPageIndices = Array.from({ length: totalPages }, (_, i) => i);
// @ts-expect-error TS(2339) FIXME: Property 'value' does not exist on type 'Element'.
const pageScope = document.querySelector(
'input[name="page-scope"]:checked'
).value;
if (pageScope === 'all') {
targetPageIndices = Array.from({ length: totalPages }, (_, i) => i);
} else {
// @ts-expect-error TS(2339) FIXME: Property 'value' does not exist on type 'HTMLEleme... Remove this comment to see the full error message
const rangeInput = document.getElementById('page-range-input').value;
if (!rangeInput.trim()) throw new Error('Please enter a page range.');
const ranges = rangeInput.split(',');
for (const range of ranges) {
const trimmedRange = range.trim();
if (trimmedRange.includes('-')) {
const [start, end] = trimmedRange.split('-').map(Number);
if (
isNaN(start) ||
isNaN(end) ||
start < 1 ||
end > totalPages ||
start > end
)
continue;
for (let i = start; i <= end; i++) targetPageIndices.push(i - 1);
} else {
// @ts-expect-error TS(2339) FIXME: Property 'value' does not exist on type 'HTMLEleme... Remove this comment to see the full error message
const rangeInput = document.getElementById('page-range-input').value;
if (!rangeInput.trim()) throw new Error('Please enter a page range.');
const ranges = rangeInput.split(',');
for (const range of ranges) {
const trimmedRange = range.trim();
if (trimmedRange.includes('-')) {
const [start, end] = trimmedRange.split('-').map(Number);
if (isNaN(start) || isNaN(end) || start < 1 || end > totalPages || start > end) continue;
for (let i = start; i <= end; i++) targetPageIndices.push(i - 1);
} else {
const pageNum = Number(trimmedRange);
if (isNaN(pageNum) || pageNum < 1 || pageNum > totalPages) continue;
targetPageIndices.push(pageNum - 1);
}
}
targetPageIndices = [...new Set(targetPageIndices)];
const pageNum = Number(trimmedRange);
if (isNaN(pageNum) || pageNum < 1 || pageNum > totalPages) continue;
targetPageIndices.push(pageNum - 1);
}
if (targetPageIndices.length === 0) throw new Error('No valid pages were selected.');
const typesToRemove = new Set(
// @ts-expect-error TS(2339) FIXME: Property 'value' does not exist on type 'Element'.
Array.from(document.querySelectorAll('.annot-checkbox:checked')).map(cb => cb.value)
);
if (typesToRemove.size === 0) throw new Error('Please select at least one annotation type to remove.');
const pages = state.pdfDoc.getPages();
for (const pageIndex of targetPageIndices) {
const page = pages[pageIndex];
const annotRefs = page.node.Annots()?.asArray() || [];
const annotsToKeep = [];
for (const ref of annotRefs) {
const annot = state.pdfDoc.context.lookup(ref);
const subtype = annot.get(PDFName.of('Subtype'))?.toString().substring(1);
// If the subtype is NOT in the list to remove, add it to our new array
if (!subtype || !typesToRemove.has(subtype)) {
annotsToKeep.push(ref);
}
}
if (annotsToKeep.length > 0) {
const newAnnotsArray = state.pdfDoc.context.obj(annotsToKeep);
page.node.set(PDFName.of('Annots'), newAnnotsArray);
} else {
page.node.delete(PDFName.of('Annots'));
}
}
const newPdfBytes = await state.pdfDoc.save();
downloadFile(new Blob([newPdfBytes], { type: 'application/pdf' }), 'annotations-removed.pdf');
} catch (e) {
console.error(e);
showAlert('Error', e.message || 'Could not remove annotations. Please check your page range.');
} finally {
hideLoader();
}
targetPageIndices = [...new Set(targetPageIndices)];
}
if (targetPageIndices.length === 0)
throw new Error('No valid pages were selected.');
const typesToRemove = new Set(
// @ts-expect-error TS(2339) FIXME: Property 'value' does not exist on type 'Element'.
Array.from(document.querySelectorAll('.annot-checkbox:checked')).map(
(cb) => cb.value
)
);
if (typesToRemove.size === 0)
throw new Error('Please select at least one annotation type to remove.');
const pages = state.pdfDoc.getPages();
for (const pageIndex of targetPageIndices) {
const page = pages[pageIndex];
const annotRefs = page.node.Annots()?.asArray() || [];
const annotsToKeep = [];
for (const ref of annotRefs) {
const annot = state.pdfDoc.context.lookup(ref);
const subtype = annot
.get(PDFName.of('Subtype'))
?.toString()
.substring(1);
// If the subtype is NOT in the list to remove, add it to our new array
if (!subtype || !typesToRemove.has(subtype)) {
annotsToKeep.push(ref);
}
}
if (annotsToKeep.length > 0) {
const newAnnotsArray = state.pdfDoc.context.obj(annotsToKeep);
page.node.set(PDFName.of('Annots'), newAnnotsArray);
} else {
page.node.delete(PDFName.of('Annots'));
}
}
const newPdfBytes = await state.pdfDoc.save();
downloadFile(
new Blob([newPdfBytes], { type: 'application/pdf' }),
'annotations-removed.pdf'
);
} catch (e) {
console.error(e);
showAlert(
'Error',
e.message || 'Could not remove annotations. Please check your page range.'
);
} finally {
hideLoader();
}
}

View File

@@ -8,147 +8,167 @@ import { PDFPageProxy } from 'pdfjs-dist/types/src/display/api.js';
let analysisCache = [];
async function isPageBlank(page: PDFPageProxy, threshold: number) {
const canvas = document.createElement('canvas');
const context = canvas.getContext('2d');
const viewport = page.getViewport({ scale: 0.2 });
canvas.width = viewport.width;
canvas.height = viewport.height;
const canvas = document.createElement('canvas');
const context = canvas.getContext('2d');
await page.render({ canvasContext: context, viewport, canvas: canvas }).promise;
const viewport = page.getViewport({ scale: 0.2 });
canvas.width = viewport.width;
canvas.height = viewport.height;
const imageData = context.getImageData(0, 0, canvas.width, canvas.height);
const data = imageData.data;
const totalPixels = data.length / 4;
let nonWhitePixels = 0;
await page.render({ canvasContext: context, viewport, canvas: canvas })
.promise;
for (let i = 0; i < data.length; i += 4) {
if (data[i] < 245 || data[i+1] < 245 || data[i+2] < 245) {
nonWhitePixels++;
}
const imageData = context.getImageData(0, 0, canvas.width, canvas.height);
const data = imageData.data;
const totalPixels = data.length / 4;
let nonWhitePixels = 0;
for (let i = 0; i < data.length; i += 4) {
if (data[i] < 245 || data[i + 1] < 245 || data[i + 2] < 245) {
nonWhitePixels++;
}
}
const blankness = 1 - (nonWhitePixels / totalPixels);
return blankness >= (threshold / 100);
const blankness = 1 - nonWhitePixels / totalPixels;
return blankness >= threshold / 100;
}
async function analyzePages() {
if (!state.pdfDoc) return;
showLoader('Analyzing for blank pages...');
if (!state.pdfDoc) return;
showLoader('Analyzing for blank pages...');
const pdfBytes = await state.pdfDoc.save();
const pdf = await pdfjsLib.getDocument({ data: pdfBytes }).promise;
analysisCache = [];
const promises = [];
const pdfBytes = await state.pdfDoc.save();
const pdf = await pdfjsLib.getDocument({ data: pdfBytes }).promise;
for (let i = 1; i <= pdf.numPages; i++) {
promises.push(
pdf.getPage(i).then(page =>
isPageBlank(page, 0).then(isActuallyBlank => ({
pageNum: i,
isInitiallyBlank: isActuallyBlank,
pageRef: page,
}))
)
);
}
analysisCache = await Promise.all(promises);
hideLoader();
updateAnalysisUI();
analysisCache = [];
const promises = [];
for (let i = 1; i <= pdf.numPages; i++) {
promises.push(
pdf.getPage(i).then((page) =>
isPageBlank(page, 0).then((isActuallyBlank) => ({
pageNum: i,
isInitiallyBlank: isActuallyBlank,
pageRef: page,
}))
)
);
}
analysisCache = await Promise.all(promises);
hideLoader();
updateAnalysisUI();
}
async function updateAnalysisUI() {
const sensitivity = parseInt((document.getElementById('sensitivity-slider') as HTMLInputElement).value);
(document.getElementById('sensitivity-value') as HTMLSpanElement).textContent = sensitivity.toString();
const sensitivity = parseInt(
(document.getElementById('sensitivity-slider') as HTMLInputElement).value
);
(
document.getElementById('sensitivity-value') as HTMLSpanElement
).textContent = sensitivity.toString();
const previewContainer = document.getElementById('analysis-preview');
const analysisText = document.getElementById('analysis-text');
const thumbnailsContainer = document.getElementById('removed-pages-thumbnails');
thumbnailsContainer.innerHTML = '';
const pagesToRemove = [];
for (const pageData of analysisCache) {
const isConsideredBlank = await isPageBlank(pageData.pageRef, sensitivity);
if (isConsideredBlank) {
pagesToRemove.push(pageData.pageNum);
}
const previewContainer = document.getElementById('analysis-preview');
const analysisText = document.getElementById('analysis-text');
const thumbnailsContainer = document.getElementById(
'removed-pages-thumbnails'
);
thumbnailsContainer.innerHTML = '';
const pagesToRemove = [];
for (const pageData of analysisCache) {
const isConsideredBlank = await isPageBlank(pageData.pageRef, sensitivity);
if (isConsideredBlank) {
pagesToRemove.push(pageData.pageNum);
}
if (pagesToRemove.length > 0) {
analysisText.textContent = `Found ${pagesToRemove.length} blank page(s) to remove: ${pagesToRemove.join(', ')}`;
previewContainer.classList.remove('hidden');
}
for (const pageNum of pagesToRemove) {
const pageData = analysisCache[pageNum-1];
const viewport = pageData.pageRef.getViewport({ scale: 0.1 });
const thumbCanvas = document.createElement('canvas');
thumbCanvas.width = viewport.width;
thumbCanvas.height = viewport.height;
await pageData.pageRef.render({ canvasContext: thumbCanvas.getContext('2d'), viewport }).promise;
if (pagesToRemove.length > 0) {
analysisText.textContent = `Found ${pagesToRemove.length} blank page(s) to remove: ${pagesToRemove.join(', ')}`;
previewContainer.classList.remove('hidden');
const img = document.createElement('img');
img.src = thumbCanvas.toDataURL();
img.className = 'rounded border border-gray-600';
img.title = `Page ${pageNum}`;
thumbnailsContainer.appendChild(img);
}
for (const pageNum of pagesToRemove) {
const pageData = analysisCache[pageNum - 1];
const viewport = pageData.pageRef.getViewport({ scale: 0.1 });
const thumbCanvas = document.createElement('canvas');
thumbCanvas.width = viewport.width;
thumbCanvas.height = viewport.height;
await pageData.pageRef.render({
canvasContext: thumbCanvas.getContext('2d'),
viewport,
}).promise;
} else {
analysisText.textContent = 'No blank pages found at this sensitivity level.';
previewContainer.classList.remove('hidden');
const img = document.createElement('img');
img.src = thumbCanvas.toDataURL();
img.className = 'rounded border border-gray-600';
img.title = `Page ${pageNum}`;
thumbnailsContainer.appendChild(img);
}
} else {
analysisText.textContent =
'No blank pages found at this sensitivity level.';
previewContainer.classList.remove('hidden');
}
}
export async function setupRemoveBlankPagesTool() {
await analyzePages();
document.getElementById('sensitivity-slider').addEventListener('input', updateAnalysisUI);
await analyzePages();
document
.getElementById('sensitivity-slider')
.addEventListener('input', updateAnalysisUI);
}
export async function removeBlankPages() {
showLoader('Removing blank pages...');
try {
const sensitivity = parseInt((document.getElementById('sensitivity-slider') as HTMLInputElement).value);
const indicesToKeep = [];
showLoader('Removing blank pages...');
try {
const sensitivity = parseInt(
(document.getElementById('sensitivity-slider') as HTMLInputElement).value
);
const indicesToKeep = [];
for (const pageData of analysisCache) {
const isConsideredBlank = await isPageBlank(pageData.pageRef, sensitivity);
if (!isConsideredBlank) {
indicesToKeep.push(pageData.pageNum - 1);
}
}
if (indicesToKeep.length === 0) {
hideLoader();
showAlert(
'No Content Found',
'All pages were identified as blank at the current sensitivity setting. No new file was created. Try lowering the sensitivity if you believe this is an error.'
);
return;
}
if (indicesToKeep.length === state.pdfDoc.getPageCount()) {
hideLoader();
showAlert('No Pages Removed', 'No pages were identified as blank at the current sensitivity level.');
return;
}
const newPdf = await PDFDocument.create();
const copiedPages = await newPdf.copyPages(state.pdfDoc, indicesToKeep);
copiedPages.forEach(page => newPdf.addPage(page));
const newPdfBytes = await newPdf.save();
downloadFile(new Blob([new Uint8Array(newPdfBytes)], { type: 'application/pdf' }), 'non-blank.pdf');
} catch (e) {
console.error(e);
showAlert('Error', 'Could not remove blank pages.');
} finally {
hideLoader();
for (const pageData of analysisCache) {
const isConsideredBlank = await isPageBlank(
pageData.pageRef,
sensitivity
);
if (!isConsideredBlank) {
indicesToKeep.push(pageData.pageNum - 1);
}
}
}
if (indicesToKeep.length === 0) {
hideLoader();
showAlert(
'No Content Found',
'All pages were identified as blank at the current sensitivity setting. No new file was created. Try lowering the sensitivity if you believe this is an error.'
);
return;
}
if (indicesToKeep.length === state.pdfDoc.getPageCount()) {
hideLoader();
showAlert(
'No Pages Removed',
'No pages were identified as blank at the current sensitivity level.'
);
return;
}
const newPdf = await PDFDocument.create();
const copiedPages = await newPdf.copyPages(state.pdfDoc, indicesToKeep);
copiedPages.forEach((page) => newPdf.addPage(page));
const newPdfBytes = await newPdf.save();
downloadFile(
new Blob([new Uint8Array(newPdfBytes)], { type: 'application/pdf' }),
'non-blank.pdf'
);
} catch (e) {
console.error(e);
showAlert('Error', 'Could not remove blank pages.');
} finally {
hideLoader();
}
}

View File

@@ -1,32 +1,33 @@
import { showLoader, hideLoader, showAlert } from '../ui.js';
import { downloadFile } from '../utils/helpers.js';
import { state } from '../state.js';
export async function removeMetadata() {
showLoader('Removing all metadata...');
try {
const infoDict = state.pdfDoc.getInfoDict();
showLoader('Removing all metadata...');
try {
const infoDict = state.pdfDoc.getInfoDict();
const allKeys = infoDict.keys();
allKeys.forEach((key: any) => {
infoDict.delete(key);
});
const allKeys = infoDict.keys();
allKeys.forEach((key: any) => {
infoDict.delete(key);
});
state.pdfDoc.setTitle('');
state.pdfDoc.setAuthor('');
state.pdfDoc.setSubject('');
state.pdfDoc.setKeywords([]);
state.pdfDoc.setCreator('');
state.pdfDoc.setProducer('');
state.pdfDoc.setTitle('');
state.pdfDoc.setAuthor('');
state.pdfDoc.setSubject('');
state.pdfDoc.setKeywords([]);
state.pdfDoc.setCreator('');
state.pdfDoc.setProducer('');
const newPdfBytes = await state.pdfDoc.save();
downloadFile(new Blob([newPdfBytes], { type: 'application/pdf' }), 'metadata-removed.pdf');
} catch (e) {
console.error(e);
showAlert('Error', 'An error occurred while trying to remove metadata.');
} finally {
hideLoader();
}
}
const newPdfBytes = await state.pdfDoc.save();
downloadFile(
new Blob([newPdfBytes], { type: 'application/pdf' }),
'metadata-removed.pdf'
);
} catch (e) {
console.error(e);
showAlert('Error', 'An error occurred while trying to remove metadata.');
} finally {
hideLoader();
}
}

View File

@@ -5,22 +5,31 @@ import { state } from '../state.js';
import { PDFDocument as PDFLibDocument } from 'pdf-lib';
export async function reversePages() {
if (!state.pdfDoc) { showAlert('Error', 'PDF not loaded.'); return; }
showLoader('Reversing page order...');
try {
const newPdf = await PDFLibDocument.create();
const pageCount = state.pdfDoc.getPageCount();
const reversedIndices = Array.from({ length: pageCount }, (_, i) => pageCount - 1 - i);
if (!state.pdfDoc) {
showAlert('Error', 'PDF not loaded.');
return;
}
showLoader('Reversing page order...');
try {
const newPdf = await PDFLibDocument.create();
const pageCount = state.pdfDoc.getPageCount();
const reversedIndices = Array.from(
{ length: pageCount },
(_, i) => pageCount - 1 - i
);
const copiedPages = await newPdf.copyPages(state.pdfDoc, reversedIndices);
copiedPages.forEach((page: any) => newPdf.addPage(page));
const copiedPages = await newPdf.copyPages(state.pdfDoc, reversedIndices);
copiedPages.forEach((page: any) => newPdf.addPage(page));
const newPdfBytes = await newPdf.save();
downloadFile(new Blob([new Uint8Array(newPdfBytes)], { type: 'application/pdf' }), 'reversed.pdf');
} catch (e) {
console.error(e);
showAlert('Error', 'Could not reverse the PDF pages.');
} finally {
hideLoader();
}
}
const newPdfBytes = await newPdf.save();
downloadFile(
new Blob([new Uint8Array(newPdfBytes)], { type: 'application/pdf' }),
'reversed.pdf'
);
} catch (e) {
console.error(e);
showAlert('Error', 'Could not reverse the PDF pages.');
} finally {
hideLoader();
}
}

View File

@@ -5,26 +5,29 @@ import { state } from '../state.js';
import { degrees } from 'pdf-lib';
export async function rotate() {
showLoader('Applying rotations...');
try {
const pages = state.pdfDoc.getPages();
document.querySelectorAll('.page-rotator-item').forEach(item => {
// @ts-expect-error TS(2339) FIXME: Property 'dataset' does not exist on type 'Element... Remove this comment to see the full error message
const pageIndex = parseInt(item.dataset.pageIndex);
// @ts-expect-error TS(2339) FIXME: Property 'dataset' does not exist on type 'Element... Remove this comment to see the full error message
const rotation = parseInt(item.dataset.rotation || '0');
if (rotation !== 0) {
const currentRotation = pages[pageIndex].getRotation().angle;
pages[pageIndex].setRotation(degrees(currentRotation + rotation));
}
});
showLoader('Applying rotations...');
try {
const pages = state.pdfDoc.getPages();
document.querySelectorAll('.page-rotator-item').forEach((item) => {
// @ts-expect-error TS(2339) FIXME: Property 'dataset' does not exist on type 'Element... Remove this comment to see the full error message
const pageIndex = parseInt(item.dataset.pageIndex);
// @ts-expect-error TS(2339) FIXME: Property 'dataset' does not exist on type 'Element... Remove this comment to see the full error message
const rotation = parseInt(item.dataset.rotation || '0');
if (rotation !== 0) {
const currentRotation = pages[pageIndex].getRotation().angle;
pages[pageIndex].setRotation(degrees(currentRotation + rotation));
}
});
const rotatedPdfBytes = await state.pdfDoc.save();
downloadFile(new Blob([rotatedPdfBytes], { type: 'application/pdf' }), 'rotated.pdf');
} catch (e) {
console.error(e);
showAlert('Error', 'Could not apply rotations.');
} finally {
hideLoader();
}
}
const rotatedPdfBytes = await state.pdfDoc.save();
downloadFile(
new Blob([rotatedPdfBytes], { type: 'application/pdf' }),
'rotated.pdf'
);
} catch (e) {
console.error(e);
showAlert('Error', 'Could not apply rotations.');
} finally {
hideLoader();
}
}

View File

@@ -1,4 +1,4 @@
// This is essentially the same as image-to-pdf.
import { imageToPdf } from './image-to-pdf.js';
export const scanToPdf = imageToPdf;
export const scanToPdf = imageToPdf;

View File

@@ -4,413 +4,544 @@ import { state } from '../state.js';
import html2canvas from 'html2canvas';
const signState = {
pdf: null, canvas: null, context: null, pageRendering: false,
currentPageNum: 1, scale: 1.0,
pageSnapshot: null,
drawCanvas: null, drawContext: null, isDrawing: false,
savedSignatures: [], placedSignatures: [], activeSignature: null,
interactionMode: 'none',
draggedSigId: null,
dragOffsetX: 0, dragOffsetY: 0,
hoveredSigId: null,
resizeHandle: null,
pdf: null,
canvas: null,
context: null,
pageRendering: false,
currentPageNum: 1,
scale: 1.0,
pageSnapshot: null,
drawCanvas: null,
drawContext: null,
isDrawing: false,
savedSignatures: [],
placedSignatures: [],
activeSignature: null,
interactionMode: 'none',
draggedSigId: null,
dragOffsetX: 0,
dragOffsetY: 0,
hoveredSigId: null,
resizeHandle: null,
};
async function renderPage(num: any) {
signState.pageRendering = true;
const page = await signState.pdf.getPage(num);
const viewport = page.getViewport({ scale: signState.scale });
signState.canvas.height = viewport.height;
signState.canvas.width = viewport.width;
await page.render({ canvasContext: signState.context, viewport }).promise;
signState.pageRendering = true;
const page = await signState.pdf.getPage(num);
const viewport = page.getViewport({ scale: signState.scale });
signState.canvas.height = viewport.height;
signState.canvas.width = viewport.width;
await page.render({ canvasContext: signState.context, viewport }).promise;
signState.pageSnapshot = signState.context.getImageData(0, 0, signState.canvas.width, signState.canvas.height);
drawSignatures();
signState.pageSnapshot = signState.context.getImageData(
0,
0,
signState.canvas.width,
signState.canvas.height
);
signState.pageRendering = false;
document.getElementById('current-page-display-sign').textContent = num;
drawSignatures();
signState.pageRendering = false;
document.getElementById('current-page-display-sign').textContent = num;
}
function drawSignatures() {
if (!signState.pageSnapshot) return;
signState.context.putImageData(signState.pageSnapshot, 0, 0);
if (!signState.pageSnapshot) return;
signState.context.putImageData(signState.pageSnapshot, 0, 0);
signState.placedSignatures
.filter(sig => sig.pageIndex === signState.currentPageNum - 1)
.forEach(sig => {
signState.context.drawImage(sig.image, sig.x, sig.y, sig.width, sig.height);
if (signState.hoveredSigId === sig.id || signState.draggedSigId === sig.id) {
signState.context.strokeStyle = '#4f46e5';
signState.context.setLineDash([6, 3]);
signState.context.strokeRect(sig.x, sig.y, sig.width, sig.height);
signState.context.setLineDash([]);
signState.placedSignatures
.filter((sig) => sig.pageIndex === signState.currentPageNum - 1)
.forEach((sig) => {
signState.context.drawImage(
sig.image,
sig.x,
sig.y,
sig.width,
sig.height
);
drawResizeHandles(sig);
}
});
}
if (
signState.hoveredSigId === sig.id ||
signState.draggedSigId === sig.id
) {
signState.context.strokeStyle = '#4f46e5';
signState.context.setLineDash([6, 3]);
signState.context.strokeRect(sig.x, sig.y, sig.width, sig.height);
signState.context.setLineDash([]);
function drawResizeHandles(sig: any) {
const handleSize = 8;
const halfHandle = handleSize / 2;
const handles = getResizeHandles(sig);
signState.context.fillStyle = '#4f46e5';
Object.values(handles).forEach(handle => {
signState.context.fillRect(handle.x - halfHandle, handle.y - halfHandle, handleSize, handleSize);
drawResizeHandles(sig);
}
});
}
async function fitToWidth() {
const page = await signState.pdf.getPage(signState.currentPageNum);
const container = document.getElementById('canvas-container-sign');
signState.scale = container.clientWidth / page.getViewport({ scale: 1.0 }).width;
renderPage(signState.currentPageNum);
function drawResizeHandles(sig: any) {
const handleSize = 8;
const halfHandle = handleSize / 2;
const handles = getResizeHandles(sig);
signState.context.fillStyle = '#4f46e5';
Object.values(handles).forEach((handle) => {
signState.context.fillRect(
handle.x - halfHandle,
handle.y - halfHandle,
handleSize,
handleSize
);
});
}
async function fitToWidth() {
const page = await signState.pdf.getPage(signState.currentPageNum);
const container = document.getElementById('canvas-container-sign');
signState.scale =
container.clientWidth / page.getViewport({ scale: 1.0 }).width;
renderPage(signState.currentPageNum);
}
function setupDrawingCanvas() {
signState.drawCanvas = document.getElementById('signature-draw-canvas');
signState.drawContext = signState.drawCanvas.getContext('2d');
const rect = signState.drawCanvas.getBoundingClientRect();
const dpi = window.devicePixelRatio || 1;
signState.drawCanvas.width = rect.width * dpi;
signState.drawCanvas.height = rect.height * dpi;
signState.drawContext.scale(dpi, dpi);
signState.drawContext.lineWidth = 2;
signState.drawCanvas = document.getElementById('signature-draw-canvas');
signState.drawContext = signState.drawCanvas.getContext('2d');
const colorPicker = document.getElementById('signature-color');
// @ts-expect-error TS(2339) FIXME: Property 'value' does not exist on type 'HTMLEleme... Remove this comment to see the full error message
colorPicker.oninput = () => signState.drawContext.strokeStyle = colorPicker.value;
// @ts-expect-error TS(2339) FIXME: Property 'value' does not exist on type 'HTMLEleme... Remove this comment to see the full error message
signState.drawContext.strokeStyle = colorPicker.value;
const rect = signState.drawCanvas.getBoundingClientRect();
const dpi = window.devicePixelRatio || 1;
signState.drawCanvas.width = rect.width * dpi;
signState.drawCanvas.height = rect.height * dpi;
signState.drawContext.scale(dpi, dpi);
signState.drawContext.lineWidth = 2;
const start = (e: any) => {
signState.isDrawing = true;
const pos = getMousePos(signState.drawCanvas, e.touches ? e.touches[0] : e);
signState.drawContext.beginPath();
signState.drawContext.moveTo(pos.x, pos.y);
};
const draw = (e: any) => {
if (!signState.isDrawing) return;
e.preventDefault();
const pos = getMousePos(signState.drawCanvas, e.touches ? e.touches[0] : e);
signState.drawContext.lineTo(pos.x, pos.y);
signState.drawContext.stroke();
};
const stop = () => signState.isDrawing = false;
['mousedown', 'touchstart'].forEach(evt => signState.drawCanvas.addEventListener(evt, start, { passive: false }));
['mousemove', 'touchmove'].forEach(evt => signState.drawCanvas.addEventListener(evt, draw, { passive: false }));
['mouseup', 'mouseleave', 'touchend'].forEach(evt => signState.drawCanvas.addEventListener(evt, stop));
const colorPicker = document.getElementById('signature-color');
// @ts-expect-error TS(2339) FIXME: Property 'value' does not exist on type 'HTMLEleme... Remove this comment to see the full error message
colorPicker.oninput = () =>
(signState.drawContext.strokeStyle = colorPicker.value);
// @ts-expect-error TS(2339) FIXME: Property 'value' does not exist on type 'HTMLEleme... Remove this comment to see the full error message
signState.drawContext.strokeStyle = colorPicker.value;
const start = (e: any) => {
signState.isDrawing = true;
const pos = getMousePos(signState.drawCanvas, e.touches ? e.touches[0] : e);
signState.drawContext.beginPath();
signState.drawContext.moveTo(pos.x, pos.y);
};
const draw = (e: any) => {
if (!signState.isDrawing) return;
e.preventDefault();
const pos = getMousePos(signState.drawCanvas, e.touches ? e.touches[0] : e);
signState.drawContext.lineTo(pos.x, pos.y);
signState.drawContext.stroke();
};
const stop = () => (signState.isDrawing = false);
['mousedown', 'touchstart'].forEach((evt) =>
signState.drawCanvas.addEventListener(evt, start, { passive: false })
);
['mousemove', 'touchmove'].forEach((evt) =>
signState.drawCanvas.addEventListener(evt, draw, { passive: false })
);
['mouseup', 'mouseleave', 'touchend'].forEach((evt) =>
signState.drawCanvas.addEventListener(evt, stop)
);
}
function getMousePos(canvas: any, evt: any) {
const rect = canvas.getBoundingClientRect();
return {
x: evt.clientX - rect.left,
y: evt.clientY - rect.top
};
const rect = canvas.getBoundingClientRect();
return {
x: evt.clientX - rect.left,
y: evt.clientY - rect.top,
};
}
function addSignatureToSaved(imageDataUrl: any) {
const img = new Image();
img.src = imageDataUrl;
signState.savedSignatures.push(img);
renderSavedSignatures();
const img = new Image();
img.src = imageDataUrl;
signState.savedSignatures.push(img);
renderSavedSignatures();
}
function renderSavedSignatures() {
const container = document.getElementById('saved-signatures-container');
container.textContent = ''; //change
signState.savedSignatures.forEach((img, index) => {
const wrapper = document.createElement('div');
wrapper.className = 'saved-signature p-1 bg-white rounded-md cursor-pointer border-2 border-transparent hover:border-indigo-500 h-16';
img.className = 'h-full w-auto mx-auto';
wrapper.appendChild(img);
container.appendChild(wrapper);
const container = document.getElementById('saved-signatures-container');
container.textContent = ''; //change
signState.savedSignatures.forEach((img, index) => {
const wrapper = document.createElement('div');
wrapper.className =
'saved-signature p-1 bg-white rounded-md cursor-pointer border-2 border-transparent hover:border-indigo-500 h-16';
img.className = 'h-full w-auto mx-auto';
wrapper.appendChild(img);
container.appendChild(wrapper);
wrapper.onclick = () => {
signState.activeSignature = { image: img, index };
document.querySelectorAll('.saved-signature').forEach(el => el.classList.remove('selected'));
wrapper.classList.add('selected');
};
});
wrapper.onclick = () => {
signState.activeSignature = { image: img, index };
document
.querySelectorAll('.saved-signature')
.forEach((el) => el.classList.remove('selected'));
wrapper.classList.add('selected');
};
});
}
function getResizeHandles(sig: any) {
return {
'top-left': { x: sig.x, y: sig.y },
'top-middle': { x: sig.x + sig.width / 2, y: sig.y },
'top-right': { x: sig.x + sig.width, y: sig.y },
'middle-left': { x: sig.x, y: sig.y + sig.height / 2 },
'middle-right': { x: sig.x + sig.width, y: sig.y + sig.height / 2 },
'bottom-left': { x: sig.x, y: sig.y + sig.height },
'bottom-middle':{ x: sig.x + sig.width / 2, y: sig.y + sig.height },
'bottom-right': { x: sig.x + sig.width, y: sig.y + sig.height },
};
return {
'top-left': { x: sig.x, y: sig.y },
'top-middle': { x: sig.x + sig.width / 2, y: sig.y },
'top-right': { x: sig.x + sig.width, y: sig.y },
'middle-left': { x: sig.x, y: sig.y + sig.height / 2 },
'middle-right': { x: sig.x + sig.width, y: sig.y + sig.height / 2 },
'bottom-left': { x: sig.x, y: sig.y + sig.height },
'bottom-middle': { x: sig.x + sig.width / 2, y: sig.y + sig.height },
'bottom-right': { x: sig.x + sig.width, y: sig.y + sig.height },
};
}
function getHandleAtPos(pos: any, sig: any) {
const handles = getResizeHandles(sig);
const handleSize = 10;
for (const [name, handlePos] of Object.entries(handles)) {
if (Math.abs(pos.x - handlePos.x) < handleSize && Math.abs(pos.y - handlePos.y) < handleSize) {
return name;
}
const handles = getResizeHandles(sig);
const handleSize = 10;
for (const [name, handlePos] of Object.entries(handles)) {
if (
Math.abs(pos.x - handlePos.x) < handleSize &&
Math.abs(pos.y - handlePos.y) < handleSize
) {
return name;
}
return null;
}
return null;
}
function setupPlacementListeners() {
const canvas = signState.canvas;
const ghost = document.getElementById('signature-ghost');
const mouseMoveHandler = (e: any) => {
if (signState.interactionMode !== 'none') return;
const canvas = signState.canvas;
const ghost = document.getElementById('signature-ghost');
if (signState.activeSignature) {
ghost.style.backgroundImage = `url('${signState.activeSignature.image.src}')`;
ghost.style.width = '150px';
ghost.style.height = `${(signState.activeSignature.image.height / signState.activeSignature.image.width) * 150}px`;
ghost.style.left = `${e.clientX + 5}px`;
ghost.style.top = `${e.clientY + 5}px`;
ghost.classList.remove('hidden');
const mouseMoveHandler = (e: any) => {
if (signState.interactionMode !== 'none') return;
if (signState.activeSignature) {
ghost.style.backgroundImage = `url('${signState.activeSignature.image.src}')`;
ghost.style.width = '150px';
ghost.style.height = `${(signState.activeSignature.image.height / signState.activeSignature.image.width) * 150}px`;
ghost.style.left = `${e.clientX + 5}px`;
ghost.style.top = `${e.clientY + 5}px`;
ghost.classList.remove('hidden');
}
const pos = getMousePos(canvas, e);
let foundSigId: any = null;
let foundHandle = null;
signState.placedSignatures
.filter((s) => s.pageIndex === signState.currentPageNum - 1)
.reverse()
.forEach((sig) => {
if (foundSigId) return;
const handle = getHandleAtPos(pos, sig);
if (handle) {
foundSigId = sig.id;
foundHandle = handle;
} else if (
pos.x >= sig.x &&
pos.x <= sig.x + sig.width &&
pos.y >= sig.y &&
pos.y <= sig.y + sig.height
) {
foundSigId = sig.id;
}
const pos = getMousePos(canvas, e);
let foundSigId: any = null;
let foundHandle = null;
});
signState.placedSignatures.filter(s => s.pageIndex === signState.currentPageNum - 1).reverse().forEach(sig => {
if (foundSigId) return;
const handle = getHandleAtPos(pos, sig);
if (handle) {
foundSigId = sig.id;
foundHandle = handle;
} else if (pos.x >= sig.x && pos.x <= sig.x + sig.width && pos.y >= sig.y && pos.y <= sig.y + sig.height) {
foundSigId = sig.id;
}
});
canvas.className = '';
if (foundHandle) {
if (['top-left', 'bottom-right'].includes(foundHandle))
canvas.classList.add('resize-nwse');
else if (['top-right', 'bottom-left'].includes(foundHandle))
canvas.classList.add('resize-nesw');
else if (['top-middle', 'bottom-middle'].includes(foundHandle))
canvas.classList.add('resize-ns');
else if (['middle-left', 'middle-right'].includes(foundHandle))
canvas.classList.add('resize-ew');
} else if (foundSigId) {
canvas.classList.add('movable');
}
canvas.className = '';
if (foundHandle) {
if (['top-left', 'bottom-right'].includes(foundHandle)) canvas.classList.add('resize-nwse');
else if (['top-right', 'bottom-left'].includes(foundHandle)) canvas.classList.add('resize-nesw');
else if (['top-middle', 'bottom-middle'].includes(foundHandle)) canvas.classList.add('resize-ns');
else if (['middle-left', 'middle-right'].includes(foundHandle)) canvas.classList.add('resize-ew');
} else if (foundSigId) {
canvas.classList.add('movable');
if (signState.hoveredSigId !== foundSigId) {
signState.hoveredSigId = foundSigId;
drawSignatures();
}
};
canvas.addEventListener('mousemove', mouseMoveHandler);
document
.getElementById('canvas-container-sign')
.addEventListener('mouseleave', () => ghost.classList.add('hidden'));
const onDragStart = (e: any) => {
const pos = getMousePos(canvas, e.touches ? e.touches[0] : e);
let clickedOnSignature = false;
signState.placedSignatures
.filter((s) => s.pageIndex === signState.currentPageNum - 1)
.reverse()
.forEach((sig) => {
if (clickedOnSignature) return;
const handle = getHandleAtPos(pos, sig);
if (handle) {
signState.interactionMode = 'resize';
signState.resizeHandle = handle;
signState.draggedSigId = sig.id;
clickedOnSignature = true;
} else if (
pos.x >= sig.x &&
pos.x <= sig.x + sig.width &&
pos.y >= sig.y &&
pos.y <= sig.y + sig.height
) {
signState.interactionMode = 'drag';
signState.draggedSigId = sig.id;
signState.dragOffsetX = pos.x - sig.x;
signState.dragOffsetY = pos.y - sig.y;
clickedOnSignature = true;
}
});
if (signState.hoveredSigId !== foundSigId) {
signState.hoveredSigId = foundSigId;
drawSignatures();
}
};
if (clickedOnSignature) {
ghost.classList.add('hidden');
} else if (signState.activeSignature) {
const sigWidth = 150;
const sigHeight =
(signState.activeSignature.image.height /
signState.activeSignature.image.width) *
sigWidth;
signState.placedSignatures.push({
id: Date.now(),
image: signState.activeSignature.image,
x: pos.x - sigWidth / 2,
y: pos.y - sigHeight / 2,
width: sigWidth,
height: sigHeight,
pageIndex: signState.currentPageNum - 1,
aspectRatio: sigWidth / sigHeight,
});
drawSignatures();
}
};
canvas.addEventListener('mousemove', mouseMoveHandler);
document.getElementById('canvas-container-sign').addEventListener('mouseleave', () => ghost.classList.add('hidden'));
const onDragMove = (e: any) => {
if (signState.interactionMode === 'none') return;
e.preventDefault();
const pos = getMousePos(canvas, e.touches ? e.touches[0] : e);
const sig = signState.placedSignatures.find(
(s) => s.id === signState.draggedSigId
);
if (!sig) return;
const onDragStart = (e: any) => {
const pos = getMousePos(canvas, e.touches ? e.touches[0] : e);
let clickedOnSignature = false;
signState.placedSignatures.filter(s => s.pageIndex === signState.currentPageNum - 1).reverse().forEach(sig => {
if (clickedOnSignature) return;
const handle = getHandleAtPos(pos, sig);
if (handle) {
signState.interactionMode = 'resize';
signState.resizeHandle = handle;
signState.draggedSigId = sig.id;
clickedOnSignature = true;
} else if (pos.x >= sig.x && pos.x <= sig.x + sig.width && pos.y >= sig.y && pos.y <= sig.y + sig.height) {
signState.interactionMode = 'drag';
signState.draggedSigId = sig.id;
signState.dragOffsetX = pos.x - sig.x;
signState.dragOffsetY = pos.y - sig.y;
clickedOnSignature = true;
}
});
if (signState.interactionMode === 'drag') {
sig.x = pos.x - signState.dragOffsetX;
sig.y = pos.y - signState.dragOffsetY;
} else if (signState.interactionMode === 'resize') {
const originalRight = sig.x + sig.width;
const originalBottom = sig.y + sig.height;
if (clickedOnSignature) {
ghost.classList.add('hidden');
} else if (signState.activeSignature) {
const sigWidth = 150;
const sigHeight = (signState.activeSignature.image.height / signState.activeSignature.image.width) * sigWidth;
signState.placedSignatures.push({
id: Date.now(), image: signState.activeSignature.image,
x: pos.x - sigWidth / 2, y: pos.y - sigHeight / 2,
width: sigWidth, height: sigHeight, pageIndex: signState.currentPageNum - 1,
aspectRatio: sigWidth / sigHeight,
});
drawSignatures();
}
};
const onDragMove = (e: any) => {
if (signState.interactionMode === 'none') return;
e.preventDefault();
const pos = getMousePos(canvas, e.touches ? e.touches[0] : e);
const sig = signState.placedSignatures.find(s => s.id === signState.draggedSigId);
if (!sig) return;
if (signState.interactionMode === 'drag') {
sig.x = pos.x - signState.dragOffsetX;
sig.y = pos.y - signState.dragOffsetY;
} else if (signState.interactionMode === 'resize') {
const originalRight = sig.x + sig.width;
const originalBottom = sig.y + sig.height;
if (signState.resizeHandle.includes('right'))
sig.width = Math.max(20, pos.x - sig.x);
if (signState.resizeHandle.includes('bottom'))
sig.height = Math.max(20, pos.y - sig.y);
if (signState.resizeHandle.includes('left')) {
sig.width = Math.max(20, originalRight - pos.x);
sig.x = originalRight - sig.width;
}
if (signState.resizeHandle.includes('top')) {
sig.height = Math.max(20, originalBottom - pos.y);
sig.y = originalBottom - sig.height;
}
if (signState.resizeHandle.includes('right')) sig.width = Math.max(20, pos.x - sig.x);
if (signState.resizeHandle.includes('bottom')) sig.height = Math.max(20, pos.y - sig.y);
if (signState.resizeHandle.includes('left')) {
sig.width = Math.max(20, originalRight - pos.x);
sig.x = originalRight - sig.width;
}
if (signState.resizeHandle.includes('top')) {
sig.height = Math.max(20, originalBottom - pos.y);
sig.y = originalBottom - sig.height;
}
if (
signState.resizeHandle.includes('left') ||
signState.resizeHandle.includes('right')
) {
sig.height = sig.width / sig.aspectRatio;
} else if (
signState.resizeHandle.includes('top') ||
signState.resizeHandle.includes('bottom')
) {
sig.width = sig.height * sig.aspectRatio;
}
}
drawSignatures();
};
if (signState.resizeHandle.includes('left') || signState.resizeHandle.includes('right')) {
sig.height = sig.width / sig.aspectRatio;
} else if (signState.resizeHandle.includes('top') || signState.resizeHandle.includes('bottom')) {
sig.width = sig.height * sig.aspectRatio;
}
}
drawSignatures();
};
const onDragEnd = () => {
signState.interactionMode = 'none';
signState.draggedSigId = null;
drawSignatures();
};
const onDragEnd = () => {
signState.interactionMode = 'none';
signState.draggedSigId = null;
drawSignatures();
};
['mousedown', 'touchstart'].forEach(evt => canvas.addEventListener(evt, onDragStart, { passive: false }));
['mousemove', 'touchmove'].forEach(evt => canvas.addEventListener(evt, onDragMove, { passive: false }));
['mouseup', 'mouseleave', 'touchend'].forEach(evt => canvas.addEventListener(evt, onDragEnd));
['mousedown', 'touchstart'].forEach((evt) =>
canvas.addEventListener(evt, onDragStart, { passive: false })
);
['mousemove', 'touchmove'].forEach((evt) =>
canvas.addEventListener(evt, onDragMove, { passive: false })
);
['mouseup', 'mouseleave', 'touchend'].forEach((evt) =>
canvas.addEventListener(evt, onDragEnd)
);
}
export async function setupSignTool() {
document.getElementById('signature-editor').classList.remove('hidden');
signState.canvas = document.getElementById('canvas-sign');
signState.context = signState.canvas.getContext('2d');
const pdfData = await state.pdfDoc.save();
// @ts-expect-error TS(2304) FIXME: Cannot find name 'pdfjsLib'.
signState.pdf = await pdfjsLib.getDocument({ data: pdfData }).promise;
document.getElementById('total-pages-display-sign').textContent = signState.pdf.numPages;
await fitToWidth();
setupDrawingCanvas();
setupPlacementListeners();
document.getElementById('signature-editor').classList.remove('hidden');
document.getElementById('prev-page-sign').onclick = () => { if (signState.currentPageNum > 1) { signState.currentPageNum--; renderPage(signState.currentPageNum); } };
document.getElementById('next-page-sign').onclick = () => { if (signState.currentPageNum < signState.pdf.numPages) { signState.currentPageNum++; renderPage(signState.currentPageNum); } };
document.getElementById('zoom-in-btn').onclick = () => { signState.scale += 0.25; renderPage(signState.currentPageNum); };
document.getElementById('zoom-out-btn').onclick = () => { signState.scale = Math.max(0.25, signState.scale - 0.25); renderPage(signState.currentPageNum); };
document.getElementById('fit-width-btn').onclick = fitToWidth;
document.getElementById('undo-btn').onclick = () => { signState.placedSignatures.pop(); drawSignatures(); };
const tabs = ['draw', 'type', 'upload'];
const tabButtons = tabs.map(t => document.getElementById(`${t}-tab-btn`));
const tabPanels = tabs.map(t => document.getElementById(`${t}-panel`));
tabButtons.forEach((button, index) => {
button.onclick = () => {
tabPanels.forEach(panel => panel.classList.add('hidden'));
tabButtons.forEach(btn => {
btn.classList.remove('border-indigo-500', 'text-white');
btn.classList.add('border-transparent', 'text-gray-400');
});
tabPanels[index].classList.remove('hidden');
button.classList.add('border-indigo-500', 'text-white');
button.classList.remove('border-transparent', 'text-gray-400');
};
signState.canvas = document.getElementById('canvas-sign');
signState.context = signState.canvas.getContext('2d');
const pdfData = await state.pdfDoc.save();
// @ts-expect-error TS(2304) FIXME: Cannot find name 'pdfjsLib'.
signState.pdf = await pdfjsLib.getDocument({ data: pdfData }).promise;
document.getElementById('total-pages-display-sign').textContent =
signState.pdf.numPages;
await fitToWidth();
setupDrawingCanvas();
setupPlacementListeners();
document.getElementById('prev-page-sign').onclick = () => {
if (signState.currentPageNum > 1) {
signState.currentPageNum--;
renderPage(signState.currentPageNum);
}
};
document.getElementById('next-page-sign').onclick = () => {
if (signState.currentPageNum < signState.pdf.numPages) {
signState.currentPageNum++;
renderPage(signState.currentPageNum);
}
};
document.getElementById('zoom-in-btn').onclick = () => {
signState.scale += 0.25;
renderPage(signState.currentPageNum);
};
document.getElementById('zoom-out-btn').onclick = () => {
signState.scale = Math.max(0.25, signState.scale - 0.25);
renderPage(signState.currentPageNum);
};
document.getElementById('fit-width-btn').onclick = fitToWidth;
document.getElementById('undo-btn').onclick = () => {
signState.placedSignatures.pop();
drawSignatures();
};
const tabs = ['draw', 'type', 'upload'];
const tabButtons = tabs.map((t) => document.getElementById(`${t}-tab-btn`));
const tabPanels = tabs.map((t) => document.getElementById(`${t}-panel`));
tabButtons.forEach((button, index) => {
button.onclick = () => {
tabPanels.forEach((panel) => panel.classList.add('hidden'));
tabButtons.forEach((btn) => {
btn.classList.remove('border-indigo-500', 'text-white');
btn.classList.add('border-transparent', 'text-gray-400');
});
tabPanels[index].classList.remove('hidden');
button.classList.add('border-indigo-500', 'text-white');
button.classList.remove('border-transparent', 'text-gray-400');
};
});
document.getElementById('clear-draw-btn').onclick = () =>
signState.drawContext.clearRect(
0,
0,
signState.drawCanvas.width,
signState.drawCanvas.height
);
document.getElementById('save-draw-btn').onclick = () => {
addSignatureToSaved(signState.drawCanvas.toDataURL());
signState.drawContext.clearRect(
0,
0,
signState.drawCanvas.width,
signState.drawCanvas.height
);
};
const textInput = document.getElementById('signature-text-input');
const fontPreview = document.getElementById('font-preview');
const fontFamilySelect = document.getElementById('font-family-select');
const fontSizeSlider = document.getElementById('font-size-slider');
const fontSizeValue = document.getElementById('font-size-value');
const fontColorPicker = document.getElementById('font-color-picker');
const updateFontPreview = () => {
// @ts-expect-error TS(2339) FIXME: Property 'value' does not exist on type 'HTMLEleme... Remove this comment to see the full error message
fontPreview.textContent = textInput.value || 'Your Name';
// @ts-expect-error TS(2339) FIXME: Property 'value' does not exist on type 'HTMLEleme... Remove this comment to see the full error message
fontPreview.style.fontFamily = fontFamilySelect.value;
// @ts-expect-error TS(2339) FIXME: Property 'value' does not exist on type 'HTMLEleme... Remove this comment to see the full error message
fontPreview.style.fontSize = `${fontSizeSlider.value}px`;
// @ts-expect-error TS(2339) FIXME: Property 'value' does not exist on type 'HTMLEleme... Remove this comment to see the full error message
fontPreview.style.color = fontColorPicker.value;
// @ts-expect-error TS(2339) FIXME: Property 'value' does not exist on type 'HTMLEleme... Remove this comment to see the full error message
fontSizeValue.textContent = fontSizeSlider.value;
};
[textInput, fontFamilySelect, fontSizeSlider, fontColorPicker].forEach((el) =>
el.addEventListener('input', updateFontPreview)
);
updateFontPreview();
document.getElementById('save-type-btn').onclick = async () => {
// @ts-expect-error TS(2339) FIXME: Property 'value' does not exist on type 'HTMLEleme... Remove this comment to see the full error message
if (!textInput.value) return;
const canvas = await html2canvas(fontPreview, {
backgroundColor: null,
scale: 2,
});
addSignatureToSaved(canvas.toDataURL());
};
document.getElementById('clear-draw-btn').onclick = () => signState.drawContext.clearRect(0, 0, signState.drawCanvas.width, signState.drawCanvas.height);
document.getElementById('save-draw-btn').onclick = () => { addSignatureToSaved(signState.drawCanvas.toDataURL()); signState.drawContext.clearRect(0, 0, signState.drawCanvas.width, signState.drawCanvas.height); };
const textInput = document.getElementById('signature-text-input');
const fontPreview = document.getElementById('font-preview');
const fontFamilySelect = document.getElementById('font-family-select');
const fontSizeSlider = document.getElementById('font-size-slider');
const fontSizeValue = document.getElementById('font-size-value');
const fontColorPicker = document.getElementById('font-color-picker');
const updateFontPreview = () => {
// @ts-expect-error TS(2339) FIXME: Property 'value' does not exist on type 'HTMLEleme... Remove this comment to see the full error message
fontPreview.textContent = textInput.value || 'Your Name';
// @ts-expect-error TS(2339) FIXME: Property 'value' does not exist on type 'HTMLEleme... Remove this comment to see the full error message
fontPreview.style.fontFamily = fontFamilySelect.value;
// @ts-expect-error TS(2339) FIXME: Property 'value' does not exist on type 'HTMLEleme... Remove this comment to see the full error message
fontPreview.style.fontSize = `${fontSizeSlider.value}px`;
// @ts-expect-error TS(2339) FIXME: Property 'value' does not exist on type 'HTMLEleme... Remove this comment to see the full error message
fontPreview.style.color = fontColorPicker.value;
// @ts-expect-error TS(2339) FIXME: Property 'value' does not exist on type 'HTMLEleme... Remove this comment to see the full error message
fontSizeValue.textContent = fontSizeSlider.value;
};
[textInput, fontFamilySelect, fontSizeSlider, fontColorPicker].forEach(el => el.addEventListener('input', updateFontPreview));
updateFontPreview();
document.getElementById('save-type-btn').onclick = async () => {
// @ts-expect-error TS(2339) FIXME: Property 'value' does not exist on type 'HTMLEleme... Remove this comment to see the full error message
if (!textInput.value) return;
const canvas = await html2canvas(fontPreview, { backgroundColor: null, scale: 2 });
addSignatureToSaved(canvas.toDataURL());
};
document.getElementById('signature-upload-input').onchange = (e) => {
// @ts-expect-error TS(2339) FIXME: Property 'files' does not exist on type 'EventTarg... Remove this comment to see the full error message
const file = e.target.files[0];
if (!file) return;
const reader = new FileReader();
reader.onload = (event) => addSignatureToSaved(event.target.result);
reader.readAsDataURL(file);
};
document.getElementById('signature-upload-input').onchange = (e) => {
// @ts-expect-error TS(2339) FIXME: Property 'files' does not exist on type 'EventTarg... Remove this comment to see the full error message
const file = e.target.files[0];
if (!file) return;
const reader = new FileReader();
reader.onload = (event) => addSignatureToSaved(event.target.result);
reader.readAsDataURL(file);
};
}
export async function applyAndSaveSignatures() {
if (signState.placedSignatures.length === 0) {
showAlert('No Signatures Placed', 'Please place at least one signature.');
return;
}
showLoader('Applying signatures...');
try {
const pages = state.pdfDoc.getPages();
for (const sig of signState.placedSignatures) {
const page = pages[sig.pageIndex];
const originalPageSize = page.getSize();
const pngBytes = await fetch(sig.image.src).then(res => res.arrayBuffer());
const pngImage = await state.pdfDoc.embedPng(pngBytes);
const renderedPage = await signState.pdf.getPage(sig.pageIndex + 1);
const renderedViewport = renderedPage.getViewport({ scale: signState.scale });
const scaleRatio = originalPageSize.width / renderedViewport.width;
if (signState.placedSignatures.length === 0) {
showAlert('No Signatures Placed', 'Please place at least one signature.');
return;
}
showLoader('Applying signatures...');
try {
const pages = state.pdfDoc.getPages();
for (const sig of signState.placedSignatures) {
const page = pages[sig.pageIndex];
const originalPageSize = page.getSize();
const pngBytes = await fetch(sig.image.src).then((res) =>
res.arrayBuffer()
);
const pngImage = await state.pdfDoc.embedPng(pngBytes);
page.drawImage(pngImage, {
x: sig.x * scaleRatio,
y: originalPageSize.height - (sig.y * scaleRatio) - (sig.height * scaleRatio),
width: sig.width * scaleRatio,
height: sig.height * scaleRatio,
});
}
const newPdfBytes = await state.pdfDoc.save();
downloadFile(new Blob([newPdfBytes], { type: 'application/pdf' }), 'signed.pdf');
} catch (e) {
console.error(e);
showAlert('Error', 'Failed to apply signatures.');
} finally {
hideLoader();
const renderedPage = await signState.pdf.getPage(sig.pageIndex + 1);
const renderedViewport = renderedPage.getViewport({
scale: signState.scale,
});
const scaleRatio = originalPageSize.width / renderedViewport.width;
page.drawImage(pngImage, {
x: sig.x * scaleRatio,
y:
originalPageSize.height -
sig.y * scaleRatio -
sig.height * scaleRatio,
width: sig.width * scaleRatio,
height: sig.height * scaleRatio,
});
}
const newPdfBytes = await state.pdfDoc.save();
downloadFile(
new Blob([newPdfBytes], { type: 'application/pdf' }),
'signed.pdf'
);
} catch (e) {
console.error(e);
showAlert('Error', 'Failed to apply signatures.');
} finally {
hideLoader();
}
}

View File

@@ -5,49 +5,51 @@ import { state } from '../state.js';
import { PDFDocument as PDFLibDocument, rgb } from 'pdf-lib';
export async function splitInHalf() {
// @ts-expect-error TS(2339) FIXME: Property 'value' does not exist on type 'HTMLEleme... Remove this comment to see the full error message
const splitType = document.getElementById('split-type').value;
if (!state.pdfDoc) {
showAlert('Error', 'No PDF document is loaded.');
return;
// @ts-expect-error TS(2339) FIXME: Property 'value' does not exist on type 'HTMLEleme... Remove this comment to see the full error message
const splitType = document.getElementById('split-type').value;
if (!state.pdfDoc) {
showAlert('Error', 'No PDF document is loaded.');
return;
}
showLoader('Splitting PDF pages...');
try {
const newPdfDoc = await PDFLibDocument.create();
const pages = state.pdfDoc.getPages();
for (let i = 0; i < pages.length; i++) {
const originalPage = pages[i];
const { width, height } = originalPage.getSize();
const whiteColor = rgb(1, 1, 1); // For masking
showLoader(`Processing page ${i + 1} of ${pages.length}...`);
// Copy the page twice for all split types
const [page1] = await newPdfDoc.copyPages(state.pdfDoc, [i]);
const [page2] = await newPdfDoc.copyPages(state.pdfDoc, [i]);
switch (splitType) {
case 'vertical':
page1.setCropBox(0, 0, width / 2, height);
page2.setCropBox(width / 2, 0, width / 2, height);
break;
case 'horizontal':
page1.setCropBox(0, height / 2, width, height / 2); // Top half
page2.setCropBox(0, 0, width, height / 2); // Bottom half
break;
}
newPdfDoc.addPage(page1);
newPdfDoc.addPage(page2);
}
showLoader('Splitting PDF pages...');
try {
const newPdfDoc = await PDFLibDocument.create();
const pages = state.pdfDoc.getPages();
for (let i = 0; i < pages.length; i++) {
const originalPage = pages[i];
const { width, height } = originalPage.getSize();
const whiteColor = rgb(1, 1, 1); // For masking
showLoader(`Processing page ${i + 1} of ${pages.length}...`);
// Copy the page twice for all split types
const [page1] = await newPdfDoc.copyPages(state.pdfDoc, [i]);
const [page2] = await newPdfDoc.copyPages(state.pdfDoc, [i]);
switch (splitType) {
case 'vertical':
page1.setCropBox(0, 0, width / 2, height);
page2.setCropBox(width / 2, 0, width / 2, height);
break;
case 'horizontal':
page1.setCropBox(0, height / 2, width, height / 2); // Top half
page2.setCropBox(0, 0, width, height / 2); // Bottom half
break;
}
newPdfDoc.addPage(page1);
newPdfDoc.addPage(page2);
}
const newPdfBytes = await newPdfDoc.save();
downloadFile(new Blob([new Uint8Array(newPdfBytes)], { type: 'application/pdf' }), 'split-half.pdf');
} catch (e) {
console.error(e);
showAlert('Error', 'An error occurred while splitting the PDF.');
} finally {
hideLoader();
}
}
const newPdfBytes = await newPdfDoc.save();
downloadFile(
new Blob([new Uint8Array(newPdfBytes)], { type: 'application/pdf' }),
'split-half.pdf'
);
} catch (e) {
console.error(e);
showAlert('Error', 'An error occurred while splitting the PDF.');
} finally {
hideLoader();
}
}

View File

@@ -1,4 +1,3 @@
import { showLoader, hideLoader, showAlert } from '../ui.js';
import { downloadFile } from '../utils/helpers.js';
import { state } from '../state.js';
@@ -9,205 +8,232 @@ import { PDFDocument as PDFLibDocument } from 'pdf-lib';
// Track if visual selector has been rendered to avoid duplicates
let visualSelectorRendered = false;
async function renderVisualSelector() {
if (visualSelectorRendered) return;
if (visualSelectorRendered) return;
const container = document.getElementById('page-selector-grid');
if (!container) return;
const container = document.getElementById('page-selector-grid');
if (!container) return;
visualSelectorRendered = true;
visualSelectorRendered = true;
container.textContent = '';
showLoader('Rendering page previews...');
try {
const pdfData = await state.pdfDoc.save();
// @ts-expect-error TS(2304) FIXME: Cannot find name 'pdfjsLib'.
const pdf = await pdfjsLib.getDocument({ data: pdfData }).promise;
container.textContent = '';
for (let i = 1; i <= pdf.numPages; i++) {
const page = await pdf.getPage(i);
const viewport = page.getViewport({ scale: 0.4 });
const canvas = document.createElement('canvas');
canvas.height = viewport.height;
canvas.width = viewport.width;
await page.render({ canvasContext: canvas.getContext('2d'), viewport: viewport }).promise;
const wrapper = document.createElement('div');
wrapper.className = 'page-thumbnail-wrapper p-1 border-2 border-transparent rounded-lg cursor-pointer hover:border-indigo-500';
// @ts-expect-error TS(2322) FIXME: Type 'number' is not assignable to type 'string'.
wrapper.dataset.pageIndex = i - 1;
showLoader('Rendering page previews...');
try {
const pdfData = await state.pdfDoc.save();
// @ts-expect-error TS(2304) FIXME: Cannot find name 'pdfjsLib'.
const pdf = await pdfjsLib.getDocument({ data: pdfData }).promise;
const img = document.createElement('img');
img.src = canvas.toDataURL();
img.className = 'rounded-md w-full h-auto';
const p = document.createElement('p');
p.className = 'text-center text-xs mt-1 text-gray-300';
p.textContent = `Page ${i}`;
wrapper.append(img, p);
const handleSelection = (e: any) => {
e.preventDefault();
e.stopPropagation();
const isSelected = wrapper.classList.contains('selected');
if (isSelected) {
wrapper.classList.remove('selected', 'border-indigo-500');
wrapper.classList.add('border-transparent');
} else {
wrapper.classList.add('selected', 'border-indigo-500');
wrapper.classList.remove('border-transparent');
}
};
for (let i = 1; i <= pdf.numPages; i++) {
const page = await pdf.getPage(i);
const viewport = page.getViewport({ scale: 0.4 });
const canvas = document.createElement('canvas');
canvas.height = viewport.height;
canvas.width = viewport.width;
await page.render({
canvasContext: canvas.getContext('2d'),
viewport: viewport,
}).promise;
wrapper.addEventListener('click', handleSelection);
wrapper.addEventListener('touchend', handleSelection);
wrapper.addEventListener('touchstart', (e) => {
e.preventDefault();
});
container.appendChild(wrapper);
const wrapper = document.createElement('div');
wrapper.className =
'page-thumbnail-wrapper p-1 border-2 border-transparent rounded-lg cursor-pointer hover:border-indigo-500';
// @ts-expect-error TS(2322) FIXME: Type 'number' is not assignable to type 'string'.
wrapper.dataset.pageIndex = i - 1;
const img = document.createElement('img');
img.src = canvas.toDataURL();
img.className = 'rounded-md w-full h-auto';
const p = document.createElement('p');
p.className = 'text-center text-xs mt-1 text-gray-300';
p.textContent = `Page ${i}`;
wrapper.append(img, p);
const handleSelection = (e: any) => {
e.preventDefault();
e.stopPropagation();
const isSelected = wrapper.classList.contains('selected');
if (isSelected) {
wrapper.classList.remove('selected', 'border-indigo-500');
wrapper.classList.add('border-transparent');
} else {
wrapper.classList.add('selected', 'border-indigo-500');
wrapper.classList.remove('border-transparent');
}
} catch (error) {
console.error('Error rendering visual selector:', error);
showAlert('Error', 'Failed to render page previews.');
// 4. ADDED: Reset the flag on error so the user can try again.
visualSelectorRendered = false;
} finally {
hideLoader();
}
}
};
wrapper.addEventListener('click', handleSelection);
wrapper.addEventListener('touchend', handleSelection);
wrapper.addEventListener('touchstart', (e) => {
e.preventDefault();
});
container.appendChild(wrapper);
}
} catch (error) {
console.error('Error rendering visual selector:', error);
showAlert('Error', 'Failed to render page previews.');
// 4. ADDED: Reset the flag on error so the user can try again.
visualSelectorRendered = false;
} finally {
hideLoader();
}
}
export function setupSplitTool() {
const splitModeSelect = document.getElementById('split-mode');
const rangePanel = document.getElementById('range-panel');
const visualPanel = document.getElementById('visual-select-panel');
const evenOddPanel = document.getElementById('even-odd-panel');
const zipOptionWrapper = document.getElementById('zip-option-wrapper');
const allPagesPanel = document.getElementById('all-pages-panel');
const splitModeSelect = document.getElementById('split-mode');
const rangePanel = document.getElementById('range-panel');
const visualPanel = document.getElementById('visual-select-panel');
const evenOddPanel = document.getElementById('even-odd-panel');
const zipOptionWrapper = document.getElementById('zip-option-wrapper');
const allPagesPanel = document.getElementById('all-pages-panel');
if (!splitModeSelect) return;
if (!splitModeSelect) return;
splitModeSelect.addEventListener('change', (e) => {
// @ts-expect-error TS(2339) FIXME: Property 'value' does not exist on type 'EventTarg... Remove this comment to see the full error message
const mode = e.target.value;
if (mode !== 'visual') {
visualSelectorRendered = false;
const container = document.getElementById('page-selector-grid');
if (container) container.innerHTML = '';
}
rangePanel.classList.add('hidden');
visualPanel.classList.add('hidden');
evenOddPanel.classList.add('hidden');
allPagesPanel.classList.add('hidden');
zipOptionWrapper.classList.add('hidden');
if (mode === 'range') {
rangePanel.classList.remove('hidden');
zipOptionWrapper.classList.remove('hidden');
} else if (mode === 'visual') {
visualPanel.classList.remove('hidden');
zipOptionWrapper.classList.remove('hidden');
renderVisualSelector();
} else if (mode === 'even-odd') {
evenOddPanel.classList.remove('hidden');
} else if (mode === 'all') {
allPagesPanel.classList.remove('hidden');
}
});
splitModeSelect.addEventListener('change', (e) => {
// @ts-expect-error TS(2339) FIXME: Property 'value' does not exist on type 'EventTarg... Remove this comment to see the full error message
const mode = e.target.value;
if (mode !== 'visual') {
visualSelectorRendered = false;
const container = document.getElementById('page-selector-grid');
if (container) container.innerHTML = '';
}
rangePanel.classList.add('hidden');
visualPanel.classList.add('hidden');
evenOddPanel.classList.add('hidden');
allPagesPanel.classList.add('hidden');
zipOptionWrapper.classList.add('hidden');
if (mode === 'range') {
rangePanel.classList.remove('hidden');
zipOptionWrapper.classList.remove('hidden');
} else if (mode === 'visual') {
visualPanel.classList.remove('hidden');
zipOptionWrapper.classList.remove('hidden');
renderVisualSelector();
} else if (mode === 'even-odd') {
evenOddPanel.classList.remove('hidden');
} else if (mode === 'all') {
allPagesPanel.classList.remove('hidden');
}
});
}
export async function split() {
// @ts-expect-error TS(2339) FIXME: Property 'value' does not exist on type 'HTMLEleme... Remove this comment to see the full error message
const splitMode = document.getElementById('split-mode').value;
// @ts-expect-error TS(2339) FIXME: Property 'checked' does not exist on type 'HTMLEle... Remove this comment to see the full error message
const downloadAsZip = document.getElementById('download-as-zip')?.checked || false;
showLoader('Splitting PDF...');
// @ts-expect-error TS(2339) FIXME: Property 'value' does not exist on type 'HTMLEleme... Remove this comment to see the full error message
const splitMode = document.getElementById('split-mode').value;
// @ts-expect-error TS(2339) FIXME: Property 'checked' does not exist on type 'HTMLEle... Remove this comment to see the full error message
const downloadAsZip =
document.getElementById('download-as-zip')?.checked || false;
try {
const totalPages = state.pdfDoc.getPageCount();
let indicesToExtract: any = [];
switch (splitMode) {
case 'range':
// @ts-expect-error TS(2339) FIXME: Property 'value' does not exist on type 'HTMLEleme... Remove this comment to see the full error message
const pageRangeInput = document.getElementById('page-range').value;
if (!pageRangeInput) throw new Error('Please enter a page range.');
const ranges = pageRangeInput.split(',');
for (const range of ranges) {
const trimmedRange = range.trim();
if (trimmedRange.includes('-')) {
const [start, end] = trimmedRange.split('-').map(Number);
if (isNaN(start) || isNaN(end) || start < 1 || end > totalPages || start > end) continue;
for (let i = start; i <= end; i++) indicesToExtract.push(i - 1);
} else {
const pageNum = Number(trimmedRange);
if (isNaN(pageNum) || pageNum < 1 || pageNum > totalPages) continue;
indicesToExtract.push(pageNum - 1);
}
}
break;
case 'even-odd':
const choiceElement = document.querySelector('input[name="even-odd-choice"]:checked');
if (!choiceElement) throw new Error('Please select even or odd pages.');
// @ts-expect-error TS(2339) FIXME: Property 'value' does not exist on type 'Element'.
const choice = choiceElement.value;
for (let i = 0; i < totalPages; i++) {
if (choice === 'even' && (i + 1) % 2 === 0) indicesToExtract.push(i);
if (choice === 'odd' && (i + 1) % 2 !== 0) indicesToExtract.push(i);
}
break;
case 'all':
indicesToExtract = Array.from({ length: totalPages }, (_, i) => i);
break;
case 'visual':
indicesToExtract = Array.from(document.querySelectorAll('.page-thumbnail-wrapper.selected'))
// @ts-expect-error TS(2339) FIXME: Property 'dataset' does not exist on type 'Element... Remove this comment to see the full error message
.map(el => parseInt(el.dataset.pageIndex));
break;
showLoader('Splitting PDF...');
try {
const totalPages = state.pdfDoc.getPageCount();
let indicesToExtract: any = [];
switch (splitMode) {
case 'range':
// @ts-expect-error TS(2339) FIXME: Property 'value' does not exist on type 'HTMLEleme... Remove this comment to see the full error message
const pageRangeInput = document.getElementById('page-range').value;
if (!pageRangeInput) throw new Error('Please enter a page range.');
const ranges = pageRangeInput.split(',');
for (const range of ranges) {
const trimmedRange = range.trim();
if (trimmedRange.includes('-')) {
const [start, end] = trimmedRange.split('-').map(Number);
if (
isNaN(start) ||
isNaN(end) ||
start < 1 ||
end > totalPages ||
start > end
)
continue;
for (let i = start; i <= end; i++) indicesToExtract.push(i - 1);
} else {
const pageNum = Number(trimmedRange);
if (isNaN(pageNum) || pageNum < 1 || pageNum > totalPages) continue;
indicesToExtract.push(pageNum - 1);
}
}
const uniqueIndices = [...new Set(indicesToExtract)];
if (uniqueIndices.length === 0) {
throw new Error('No pages were selected for splitting.');
break;
case 'even-odd':
const choiceElement = document.querySelector(
'input[name="even-odd-choice"]:checked'
);
if (!choiceElement) throw new Error('Please select even or odd pages.');
// @ts-expect-error TS(2339) FIXME: Property 'value' does not exist on type 'Element'.
const choice = choiceElement.value;
for (let i = 0; i < totalPages; i++) {
if (choice === 'even' && (i + 1) % 2 === 0) indicesToExtract.push(i);
if (choice === 'odd' && (i + 1) % 2 !== 0) indicesToExtract.push(i);
}
if (splitMode === 'all' || (['range', 'visual'].includes(splitMode) && downloadAsZip)) {
showLoader('Creating ZIP file...');
const zip = new JSZip();
for (const index of uniqueIndices) {
const newPdf = await PDFLibDocument.create();
const [copiedPage] = await newPdf.copyPages(state.pdfDoc, [index as number]);
newPdf.addPage(copiedPage);
const pdfBytes = await newPdf.save();
// @ts-expect-error TS(2365) FIXME: Operator '+' cannot be applied to types 'unknown' ... Remove this comment to see the full error message
zip.file(`page-${index + 1}.pdf`, pdfBytes);
}
const zipBlob = await zip.generateAsync({ type: 'blob' });
downloadFile(zipBlob, 'split-pages.zip');
} else {
const newPdf = await PDFLibDocument.create();
const copiedPages = await newPdf.copyPages(state.pdfDoc, uniqueIndices as number[]);
copiedPages.forEach((page: any) => newPdf.addPage(page));
const pdfBytes = await newPdf.save();
downloadFile(new Blob([new Uint8Array(pdfBytes)], { type: 'application/pdf' }), 'split-document.pdf');
}
if (splitMode === 'visual') {
visualSelectorRendered = false;
}
} catch (e) {
console.error(e);
showAlert('Error', e.message || 'Failed to split PDF. Please check your selection.');
} finally {
hideLoader();
break;
case 'all':
indicesToExtract = Array.from({ length: totalPages }, (_, i) => i);
break;
case 'visual':
indicesToExtract = Array.from(
document.querySelectorAll('.page-thumbnail-wrapper.selected')
)
// @ts-expect-error TS(2339) FIXME: Property 'dataset' does not exist on type 'Element... Remove this comment to see the full error message
.map((el) => parseInt(el.dataset.pageIndex));
break;
}
const uniqueIndices = [...new Set(indicesToExtract)];
if (uniqueIndices.length === 0) {
throw new Error('No pages were selected for splitting.');
}
if (
splitMode === 'all' ||
(['range', 'visual'].includes(splitMode) && downloadAsZip)
) {
showLoader('Creating ZIP file...');
const zip = new JSZip();
for (const index of uniqueIndices) {
const newPdf = await PDFLibDocument.create();
const [copiedPage] = await newPdf.copyPages(state.pdfDoc, [
index as number,
]);
newPdf.addPage(copiedPage);
const pdfBytes = await newPdf.save();
// @ts-expect-error TS(2365) FIXME: Operator '+' cannot be applied to types 'unknown' ... Remove this comment to see the full error message
zip.file(`page-${index + 1}.pdf`, pdfBytes);
}
const zipBlob = await zip.generateAsync({ type: 'blob' });
downloadFile(zipBlob, 'split-pages.zip');
} else {
const newPdf = await PDFLibDocument.create();
const copiedPages = await newPdf.copyPages(
state.pdfDoc,
uniqueIndices as number[]
);
copiedPages.forEach((page: any) => newPdf.addPage(page));
const pdfBytes = await newPdf.save();
downloadFile(
new Blob([new Uint8Array(pdfBytes)], { type: 'application/pdf' }),
'split-document.pdf'
);
}
if (splitMode === 'visual') {
visualSelectorRendered = false;
}
} catch (e) {
console.error(e);
showAlert(
'Error',
e.message || 'Failed to split PDF. Please check your selection.'
);
} finally {
hideLoader();
}
}

View File

@@ -5,51 +5,64 @@ import { state } from '../state.js';
import { PDFDocument as PDFLibDocument } from 'pdf-lib';
async function convertImageToPngBytes(file: any) {
return new Promise((resolve, reject) => {
const img = new Image();
const reader = new FileReader();
return new Promise((resolve, reject) => {
const img = new Image();
const reader = new FileReader();
reader.onload = (e) => {
img.onload = async () => {
const canvas = document.createElement('canvas');
canvas.width = img.width;
canvas.height = img.height;
const ctx = canvas.getContext('2d');
ctx.drawImage(img, 0, 0);
const pngBlob = await new Promise(res => canvas.toBlob(res, 'image/png'));
// @ts-expect-error TS(2339) FIXME: Property 'arrayBuffer' does not exist on type 'unk... Remove this comment to see the full error message
const pngBytes = await pngBlob.arrayBuffer();
resolve(pngBytes);
};
img.onerror = () => reject(new Error('Failed to load image.'));
// @ts-expect-error TS(2322) FIXME: Type 'string | ArrayBuffer' is not assignable to t... Remove this comment to see the full error message
img.src = e.target.result;
};
reader.onerror = () => reject(new Error('Failed to read file.'));
reader.readAsDataURL(file);
});
reader.onload = (e) => {
img.onload = async () => {
const canvas = document.createElement('canvas');
canvas.width = img.width;
canvas.height = img.height;
const ctx = canvas.getContext('2d');
ctx.drawImage(img, 0, 0);
const pngBlob = await new Promise((res) =>
canvas.toBlob(res, 'image/png')
);
// @ts-expect-error TS(2339) FIXME: Property 'arrayBuffer' does not exist on type 'unk... Remove this comment to see the full error message
const pngBytes = await pngBlob.arrayBuffer();
resolve(pngBytes);
};
img.onerror = () => reject(new Error('Failed to load image.'));
// @ts-expect-error TS(2322) FIXME: Type 'string | ArrayBuffer' is not assignable to t... Remove this comment to see the full error message
img.src = e.target.result;
};
reader.onerror = () => reject(new Error('Failed to read file.'));
reader.readAsDataURL(file);
});
}
export async function svgToPdf() {
if (state.files.length === 0) {
showAlert('No Files', 'Please select at least one SVG file.');
return;
if (state.files.length === 0) {
showAlert('No Files', 'Please select at least one SVG file.');
return;
}
showLoader('Converting SVG to PDF...');
try {
const pdfDoc = await PDFLibDocument.create();
for (const file of state.files) {
const pngBytes = await convertImageToPngBytes(file);
const pngImage = await pdfDoc.embedPng(pngBytes as ArrayBuffer);
const page = pdfDoc.addPage([pngImage.width, pngImage.height]);
page.drawImage(pngImage, {
x: 0,
y: 0,
width: pngImage.width,
height: pngImage.height,
});
}
showLoader('Converting SVG to PDF...');
try {
const pdfDoc = await PDFLibDocument.create();
for (const file of state.files) {
const pngBytes = await convertImageToPngBytes(file);
const pngImage = await pdfDoc.embedPng(pngBytes as ArrayBuffer);
const page = pdfDoc.addPage([pngImage.width, pngImage.height]);
page.drawImage(pngImage, { x: 0, y: 0, width: pngImage.width, height: pngImage.height });
}
const pdfBytes = await pdfDoc.save();
downloadFile(new Blob([new Uint8Array(pdfBytes)], { type: 'application/pdf' }), 'from_svgs.pdf');
} catch (e) {
console.error(e);
showAlert('Error', 'Failed to convert SVG to PDF. One of the files may be invalid.');
} finally {
hideLoader();
}
}
const pdfBytes = await pdfDoc.save();
downloadFile(
new Blob([new Uint8Array(pdfBytes)], { type: 'application/pdf' }),
'from_svgs.pdf'
);
} catch (e) {
console.error(e);
showAlert(
'Error',
'Failed to convert SVG to PDF. One of the files may be invalid.'
);
} finally {
hideLoader();
}
}

View File

@@ -5,78 +5,91 @@ import { PDFDocument as PDFLibDocument } from 'pdf-lib';
import { decode } from 'tiff';
export async function tiffToPdf() {
if (state.files.length === 0) {
showAlert('No Files', 'Please select at least one TIFF file.');
return;
}
showLoader('Converting TIFF to PDF...');
try {
const pdfDoc = await PDFLibDocument.create();
for (const file of state.files) {
const tiffBytes = await readFileAsArrayBuffer(file);
const ifds = decode(tiffBytes as any);
for (const ifd of ifds) {
const canvas = document.createElement('canvas');
canvas.width = ifd.width;
canvas.height = ifd.height;
const ctx = canvas.getContext('2d');
if (!ctx) {
throw new Error('Failed to get canvas context');
}
const imageData = ctx.createImageData(ifd.width, ifd.height);
const pixels = imageData.data;
// Calculate samples per pixel from data length
const totalPixels = ifd.width * ifd.height;
const samplesPerPixel = ifd.data.length / totalPixels;
// Convert TIFF data to RGBA
for (let i = 0; i < totalPixels; i++) {
const dstIndex = i * 4;
if (samplesPerPixel === 1) {
// Grayscale
const gray = ifd.data[i];
pixels[dstIndex] = gray;
pixels[dstIndex + 1] = gray;
pixels[dstIndex + 2] = gray;
pixels[dstIndex + 3] = 255;
} else if (samplesPerPixel === 3) {
// RGB
const srcIndex = i * 3;
pixels[dstIndex] = ifd.data[srcIndex];
pixels[dstIndex + 1] = ifd.data[srcIndex + 1];
pixels[dstIndex + 2] = ifd.data[srcIndex + 2];
pixels[dstIndex + 3] = 255;
} else if (samplesPerPixel === 4) {
// RGBA
const srcIndex = i * 4;
pixels[dstIndex] = ifd.data[srcIndex];
pixels[dstIndex + 1] = ifd.data[srcIndex + 1];
pixels[dstIndex + 2] = ifd.data[srcIndex + 2];
pixels[dstIndex + 3] = ifd.data[srcIndex + 3];
}
}
ctx.putImageData(imageData, 0, 0);
const pngBlob = await new Promise<Blob>((res) => canvas.toBlob(res!, 'image/png'));
const pngBytes = await pngBlob.arrayBuffer();
const pngImage = await pdfDoc.embedPng(pngBytes);
const page = pdfDoc.addPage([pngImage.width, pngImage.height]);
page.drawImage(pngImage, { x: 0, y: 0, width: pngImage.width, height: pngImage.height });
}
if (state.files.length === 0) {
showAlert('No Files', 'Please select at least one TIFF file.');
return;
}
showLoader('Converting TIFF to PDF...');
try {
const pdfDoc = await PDFLibDocument.create();
for (const file of state.files) {
const tiffBytes = await readFileAsArrayBuffer(file);
const ifds = decode(tiffBytes as any);
for (const ifd of ifds) {
const canvas = document.createElement('canvas');
canvas.width = ifd.width;
canvas.height = ifd.height;
const ctx = canvas.getContext('2d');
if (!ctx) {
throw new Error('Failed to get canvas context');
}
const pdfBytes = await pdfDoc.save();
downloadFile(new Blob([new Uint8Array(pdfBytes)], { type: 'application/pdf' }), 'from_tiff.pdf');
} catch (e) {
console.error(e);
showAlert('Error', 'Failed to convert TIFF to PDF. One of the files may be invalid or corrupted.');
} finally {
hideLoader();
const imageData = ctx.createImageData(ifd.width, ifd.height);
const pixels = imageData.data;
// Calculate samples per pixel from data length
const totalPixels = ifd.width * ifd.height;
const samplesPerPixel = ifd.data.length / totalPixels;
// Convert TIFF data to RGBA
for (let i = 0; i < totalPixels; i++) {
const dstIndex = i * 4;
if (samplesPerPixel === 1) {
// Grayscale
const gray = ifd.data[i];
pixels[dstIndex] = gray;
pixels[dstIndex + 1] = gray;
pixels[dstIndex + 2] = gray;
pixels[dstIndex + 3] = 255;
} else if (samplesPerPixel === 3) {
// RGB
const srcIndex = i * 3;
pixels[dstIndex] = ifd.data[srcIndex];
pixels[dstIndex + 1] = ifd.data[srcIndex + 1];
pixels[dstIndex + 2] = ifd.data[srcIndex + 2];
pixels[dstIndex + 3] = 255;
} else if (samplesPerPixel === 4) {
// RGBA
const srcIndex = i * 4;
pixels[dstIndex] = ifd.data[srcIndex];
pixels[dstIndex + 1] = ifd.data[srcIndex + 1];
pixels[dstIndex + 2] = ifd.data[srcIndex + 2];
pixels[dstIndex + 3] = ifd.data[srcIndex + 3];
}
}
ctx.putImageData(imageData, 0, 0);
const pngBlob = await new Promise<Blob>((res) =>
canvas.toBlob(res!, 'image/png')
);
const pngBytes = await pngBlob.arrayBuffer();
const pngImage = await pdfDoc.embedPng(pngBytes);
const page = pdfDoc.addPage([pngImage.width, pngImage.height]);
page.drawImage(pngImage, {
x: 0,
y: 0,
width: pngImage.width,
height: pngImage.height,
});
}
}
}
const pdfBytes = await pdfDoc.save();
downloadFile(
new Blob([new Uint8Array(pdfBytes)], { type: 'application/pdf' }),
'from_tiff.pdf'
);
} catch (e) {
console.error(e);
showAlert(
'Error',
'Failed to convert TIFF to PDF. One of the files may be invalid or corrupted.'
);
} finally {
hideLoader();
}
}

View File

@@ -1,74 +1,95 @@
import { showLoader, hideLoader, showAlert } from '../ui.js';
import { downloadFile, hexToRgb } from '../utils/helpers.js';
import { PDFDocument as PDFLibDocument, rgb, StandardFonts, PageSizes } from 'pdf-lib';
import {
PDFDocument as PDFLibDocument,
rgb,
StandardFonts,
PageSizes,
} from 'pdf-lib';
export async function txtToPdf() {
showLoader('Creating PDF...');
try {
// @ts-expect-error TS(2339) FIXME: Property 'value' does not exist on type 'HTMLEleme... Remove this comment to see the full error message
const text = document.getElementById('text-input').value;
if (!text.trim()) {
showAlert('Input Required', 'Please enter some text to convert.');
hideLoader();
return;
}
// @ts-expect-error TS(2339) FIXME: Property 'value' does not exist on type 'HTMLEleme... Remove this comment to see the full error message
const fontFamilyKey = document.getElementById('font-family').value;
// @ts-expect-error TS(2339) FIXME: Property 'value' does not exist on type 'HTMLEleme... Remove this comment to see the full error message
const fontSize = parseInt(document.getElementById('font-size').value) || 12;
// @ts-expect-error TS(2339) FIXME: Property 'value' does not exist on type 'HTMLEleme... Remove this comment to see the full error message
const pageSizeKey = document.getElementById('page-size').value;
// @ts-expect-error TS(2339) FIXME: Property 'value' does not exist on type 'HTMLEleme... Remove this comment to see the full error message
const colorHex = document.getElementById('text-color').value;
const textColor = hexToRgb(colorHex);
const pdfDoc = await PDFLibDocument.create();
const font = await pdfDoc.embedFont(StandardFonts[fontFamilyKey]);
const pageSize = PageSizes[pageSizeKey];
const margin = 72; // 1 inch
let page = pdfDoc.addPage(pageSize);
let { width, height } = page.getSize();
const textWidth = width - margin * 2;
const lineHeight = fontSize * 1.3;
let y = height - margin;
const paragraphs = text.split('\n');
for (const paragraph of paragraphs) {
const words = paragraph.split(' ');
let currentLine = '';
for (const word of words) {
const testLine = currentLine.length > 0 ? `${currentLine} ${word}` : word;
if (font.widthOfTextAtSize(testLine, fontSize) <= textWidth) {
currentLine = testLine;
} else {
if (y < margin + lineHeight) {
page = pdfDoc.addPage(pageSize);
y = page.getHeight() - margin;
}
page.drawText(currentLine, { x: margin, y, font, size: fontSize, color: rgb(textColor.r, textColor.g, textColor.b) });
y -= lineHeight;
currentLine = word;
}
}
if (currentLine.length > 0) {
if (y < margin + lineHeight) {
page = pdfDoc.addPage(pageSize);
y = page.getHeight() - margin;
}
page.drawText(currentLine, { x: margin, y, font, size: fontSize, color: rgb(textColor.r, textColor.g, textColor.b) });
y -= lineHeight;
}
}
const pdfBytes = await pdfDoc.save();
downloadFile(new Blob([new Uint8Array(pdfBytes)], { type: 'application/pdf' }), 'text-document.pdf');
} catch (e) {
console.error(e);
showAlert('Error', 'Failed to create PDF from text.');
} finally {
hideLoader();
showLoader('Creating PDF...');
try {
// @ts-expect-error TS(2339) FIXME: Property 'value' does not exist on type 'HTMLEleme... Remove this comment to see the full error message
const text = document.getElementById('text-input').value;
if (!text.trim()) {
showAlert('Input Required', 'Please enter some text to convert.');
hideLoader();
return;
}
}
// @ts-expect-error TS(2339) FIXME: Property 'value' does not exist on type 'HTMLEleme... Remove this comment to see the full error message
const fontFamilyKey = document.getElementById('font-family').value;
// @ts-expect-error TS(2339) FIXME: Property 'value' does not exist on type 'HTMLEleme... Remove this comment to see the full error message
const fontSize = parseInt(document.getElementById('font-size').value) || 12;
// @ts-expect-error TS(2339) FIXME: Property 'value' does not exist on type 'HTMLEleme... Remove this comment to see the full error message
const pageSizeKey = document.getElementById('page-size').value;
// @ts-expect-error TS(2339) FIXME: Property 'value' does not exist on type 'HTMLEleme... Remove this comment to see the full error message
const colorHex = document.getElementById('text-color').value;
const textColor = hexToRgb(colorHex);
const pdfDoc = await PDFLibDocument.create();
const font = await pdfDoc.embedFont(StandardFonts[fontFamilyKey]);
const pageSize = PageSizes[pageSizeKey];
const margin = 72; // 1 inch
let page = pdfDoc.addPage(pageSize);
let { width, height } = page.getSize();
const textWidth = width - margin * 2;
const lineHeight = fontSize * 1.3;
let y = height - margin;
const paragraphs = text.split('\n');
for (const paragraph of paragraphs) {
const words = paragraph.split(' ');
let currentLine = '';
for (const word of words) {
const testLine =
currentLine.length > 0 ? `${currentLine} ${word}` : word;
if (font.widthOfTextAtSize(testLine, fontSize) <= textWidth) {
currentLine = testLine;
} else {
if (y < margin + lineHeight) {
page = pdfDoc.addPage(pageSize);
y = page.getHeight() - margin;
}
page.drawText(currentLine, {
x: margin,
y,
font,
size: fontSize,
color: rgb(textColor.r, textColor.g, textColor.b),
});
y -= lineHeight;
currentLine = word;
}
}
if (currentLine.length > 0) {
if (y < margin + lineHeight) {
page = pdfDoc.addPage(pageSize);
y = page.getHeight() - margin;
}
page.drawText(currentLine, {
x: margin,
y,
font,
size: fontSize,
color: rgb(textColor.r, textColor.g, textColor.b),
});
y -= lineHeight;
}
}
const pdfBytes = await pdfDoc.save();
downloadFile(
new Blob([new Uint8Array(pdfBytes)], { type: 'application/pdf' }),
'text-document.pdf'
);
} catch (e) {
console.error(e);
showAlert('Error', 'Failed to create PDF from text.');
} finally {
hideLoader();
}
}

View File

@@ -1,4 +1,4 @@
// This tool doesn't have a "process" button. Its logic is handled directly in fileHandler.js after a file is uploaded.
export function viewMetadata() {
console.log("");
}
console.log('');
}

View File

@@ -5,44 +5,52 @@ import { state } from '../state.js';
import { PDFDocument as PDFLibDocument } from 'pdf-lib';
export async function webpToPdf() {
if (state.files.length === 0) {
showAlert('No Files', 'Please select at least one WebP file.');
return;
}
showLoader('Converting WebP to PDF...');
try {
const pdfDoc = await PDFLibDocument.create();
for (const file of state.files) {
const webpBytes = await readFileAsArrayBuffer(file);
// @ts-expect-error TS(2322) FIXME: Type 'unknown' is not assignable to type 'BlobPart... Remove this comment to see the full error message
const imageBitmap = await createImageBitmap(new Blob([webpBytes]));
const canvas = document.createElement('canvas');
canvas.width = imageBitmap.width;
canvas.height = imageBitmap.height;
const ctx = canvas.getContext('2d');
ctx.drawImage(imageBitmap, 0, 0);
if (state.files.length === 0) {
showAlert('No Files', 'Please select at least one WebP file.');
return;
}
showLoader('Converting WebP to PDF...');
try {
const pdfDoc = await PDFLibDocument.create();
for (const file of state.files) {
const webpBytes = await readFileAsArrayBuffer(file);
// @ts-expect-error TS(2322) FIXME: Type 'unknown' is not assignable to type 'BlobPart... Remove this comment to see the full error message
const imageBitmap = await createImageBitmap(new Blob([webpBytes]));
const pngBlob = await new Promise(resolve => canvas.toBlob(resolve, 'image/png'));
// @ts-expect-error TS(2339) FIXME: Property 'arrayBuffer' does not exist on type 'unk... Remove this comment to see the full error message
const pngBytes = await pngBlob.arrayBuffer();
const canvas = document.createElement('canvas');
canvas.width = imageBitmap.width;
canvas.height = imageBitmap.height;
const ctx = canvas.getContext('2d');
ctx.drawImage(imageBitmap, 0, 0);
// Embed the converted PNG into the PDF
const pngImage = await pdfDoc.embedPng(pngBytes);
const page = pdfDoc.addPage([pngImage.width, pngImage.height]);
page.drawImage(pngImage, {
x: 0,
y: 0,
width: pngImage.width,
height: pngImage.height,
});
}
const pdfBytes = await pdfDoc.save();
downloadFile(new Blob([new Uint8Array(pdfBytes)], { type: 'application/pdf' }), 'from_webp.pdf');
} catch (e) {
console.error(e);
showAlert('Error', 'Failed to convert WebP to PDF. Ensure all files are valid WebP images.');
} finally {
hideLoader();
const pngBlob = await new Promise((resolve) =>
canvas.toBlob(resolve, 'image/png')
);
// @ts-expect-error TS(2339) FIXME: Property 'arrayBuffer' does not exist on type 'unk... Remove this comment to see the full error message
const pngBytes = await pngBlob.arrayBuffer();
// Embed the converted PNG into the PDF
const pngImage = await pdfDoc.embedPng(pngBytes);
const page = pdfDoc.addPage([pngImage.width, pngImage.height]);
page.drawImage(pngImage, {
x: 0,
y: 0,
width: pngImage.width,
height: pngImage.height,
});
}
}
const pdfBytes = await pdfDoc.save();
downloadFile(
new Blob([new Uint8Array(pdfBytes)], { type: 'application/pdf' }),
'from_webp.pdf'
);
} catch (e) {
console.error(e);
showAlert(
'Error',
'Failed to convert WebP to PDF. Ensure all files are valid WebP images.'
);
} finally {
hideLoader();
}
}

View File

@@ -4,37 +4,39 @@ import { readFileAsArrayBuffer } from '../utils/helpers.js';
import { state } from '../state.js';
export async function wordToPdf() {
const file = state.files[0];
if (!file) {
showAlert('No File', 'Please upload a .docx file first.');
return;
}
const file = state.files[0];
if (!file) {
showAlert('No File', 'Please upload a .docx file first.');
return;
}
showLoader('Preparing preview...');
try {
showLoader('Preparing preview...');
const mammothOptions = {
// @ts-expect-error TS(2304) FIXME: Cannot find name 'mammoth'.
convertImage: mammoth.images.inline((element: any) => {
return element.read("base64").then((imageBuffer: any) => {
return {
src: `data:${element.contentType};base64,${imageBuffer}`
};
});
})
};
const arrayBuffer = await readFileAsArrayBuffer(file);
// @ts-expect-error TS(2304) FIXME: Cannot find name 'mammoth'.
const { value: html } = await mammoth.convertToHtml({ arrayBuffer }, mammothOptions);
// Get references to our modal elements from index.html
const previewModal = document.getElementById('preview-modal');
const previewContent = document.getElementById('preview-content');
const downloadBtn = document.getElementById('preview-download-btn');
const closeBtn = document.getElementById('preview-close-btn');
try {
const mammothOptions = {
// @ts-expect-error TS(2304) FIXME: Cannot find name 'mammoth'.
convertImage: mammoth.images.inline((element: any) => {
return element.read('base64').then((imageBuffer: any) => {
return {
src: `data:${element.contentType};base64,${imageBuffer}`,
};
});
}),
};
const arrayBuffer = await readFileAsArrayBuffer(file);
// @ts-expect-error TS(2304) FIXME: Cannot find name 'mammoth'.
const { value: html } = await mammoth.convertToHtml(
{ arrayBuffer },
mammothOptions
);
const styledHtml = `
// Get references to our modal elements from index.html
const previewModal = document.getElementById('preview-modal');
const previewContent = document.getElementById('preview-content');
const downloadBtn = document.getElementById('preview-download-btn');
const closeBtn = document.getElementById('preview-close-btn');
const styledHtml = `
<style>
#preview-content { font-family: 'Times New Roman', Times, serif; font-size: 12pt; line-height: 1.5; color: black; }
#preview-content table { border-collapse: collapse; width: 100%; }
@@ -44,88 +46,95 @@ export async function wordToPdf() {
</style>
${html}
`;
previewContent.innerHTML = styledHtml;
const marginDiv = document.createElement('div');
marginDiv.style.height = '100px';
previewContent.appendChild(marginDiv);
previewContent.innerHTML = styledHtml;
const images = previewContent.querySelectorAll('img');
const imagePromises = Array.from(images).map(img => {
return new Promise((resolve) => {
// @ts-expect-error TS(2794) FIXME: Expected 1 arguments, but got 0. Did you forget to... Remove this comment to see the full error message
if (img.complete) resolve();
else img.onload = resolve;
});
});
await Promise.all(imagePromises);
const marginDiv = document.createElement('div');
marginDiv.style.height = '100px';
previewContent.appendChild(marginDiv);
previewModal.classList.remove('hidden');
hideLoader();
const images = previewContent.querySelectorAll('img');
const imagePromises = Array.from(images).map((img) => {
return new Promise((resolve) => {
// @ts-expect-error TS(2794) FIXME: Expected 1 arguments, but got 0. Did you forget to... Remove this comment to see the full error message
if (img.complete) resolve();
else img.onload = resolve;
});
});
await Promise.all(imagePromises);
const downloadHandler = async () => {
showLoader('Generating High-Quality PDF...');
// @ts-expect-error TS(2339) FIXME: Property 'jspdf' does not exist on type 'Window & ... Remove this comment to see the full error message
const { jsPDF } = window.jspdf;
const doc = new jsPDF({
orientation: 'p',
unit: 'pt',
format: 'letter'
});
previewModal.classList.remove('hidden');
hideLoader();
await doc.html(previewContent, {
callback: function (doc: any) {
const links = previewContent.querySelectorAll('a');
const pageHeight = doc.internal.pageSize.getHeight();
const containerRect = previewContent.getBoundingClientRect(); // Get container's position
const downloadHandler = async () => {
showLoader('Generating High-Quality PDF...');
links.forEach(link => {
if (!link.href) return;
const linkRect = link.getBoundingClientRect();
// Calculate position relative to the preview container's top-left
const relativeX = linkRect.left - containerRect.left;
const relativeY = linkRect.top - containerRect.top;
// @ts-expect-error TS(2339) FIXME: Property 'jspdf' does not exist on type 'Window & ... Remove this comment to see the full error message
const { jsPDF } = window.jspdf;
const doc = new jsPDF({
orientation: 'p',
unit: 'pt',
format: 'letter',
});
const pageNum = Math.floor(relativeY / pageHeight) + 1;
const yOnPage = relativeY % pageHeight;
await doc.html(previewContent, {
callback: function (doc: any) {
const links = previewContent.querySelectorAll('a');
const pageHeight = doc.internal.pageSize.getHeight();
const containerRect = previewContent.getBoundingClientRect(); // Get container's position
doc.setPage(pageNum);
try {
doc.link(relativeX + 45, yOnPage + 45, linkRect.width, linkRect.height, { url: link.href });
} catch (e) {
console.warn("Could not add link:", link.href, e);
}
});
links.forEach((link) => {
if (!link.href) return;
const outputFileName = `${file.name.replace(/\.[^/.]+$/, "")}.pdf`;
doc.save(outputFileName);
hideLoader();
},
autoPaging: 'slice',
x: 45,
y: 45,
width: 522,
windowWidth: previewContent.scrollWidth
});
};
const linkRect = link.getBoundingClientRect();
const closeHandler = () => {
previewModal.classList.add('hidden');
previewContent.innerHTML = '';
downloadBtn.removeEventListener('click', downloadHandler);
closeBtn.removeEventListener('click', closeHandler);
};
// Calculate position relative to the preview container's top-left
const relativeX = linkRect.left - containerRect.left;
const relativeY = linkRect.top - containerRect.top;
downloadBtn.addEventListener('click', downloadHandler);
closeBtn.addEventListener('click', closeHandler);
const pageNum = Math.floor(relativeY / pageHeight) + 1;
const yOnPage = relativeY % pageHeight;
} catch (e) {
console.error(e);
hideLoader();
showAlert('Preview Error', `Could not generate a preview. The file may be corrupt or contain unsupported features. Error: ${e.message}`);
}
}
doc.setPage(pageNum);
try {
doc.link(
relativeX + 45,
yOnPage + 45,
linkRect.width,
linkRect.height,
{ url: link.href }
);
} catch (e) {
console.warn('Could not add link:', link.href, e);
}
});
const outputFileName = `${file.name.replace(/\.[^/.]+$/, '')}.pdf`;
doc.save(outputFileName);
hideLoader();
},
autoPaging: 'slice',
x: 45,
y: 45,
width: 522,
windowWidth: previewContent.scrollWidth,
});
};
const closeHandler = () => {
previewModal.classList.add('hidden');
previewContent.innerHTML = '';
downloadBtn.removeEventListener('click', downloadHandler);
closeBtn.removeEventListener('click', closeHandler);
};
downloadBtn.addEventListener('click', downloadHandler);
closeBtn.addEventListener('click', closeHandler);
} catch (e) {
console.error(e);
hideLoader();
showAlert(
'Preview Error',
`Could not generate a preview. The file may be corrupt or contain unsupported features. Error: ${e.message}`
);
}
}