feat:Setup Prettier for code formatting
This commit is contained in:
@@ -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();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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}`);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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.');
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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.');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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.');
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
},
|
||||
};
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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');
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
});
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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('');
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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}`
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user