feat:Setup Prettier for code formatting

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

View File

@@ -1,458 +1,468 @@
@import "tailwindcss";
@import 'tailwindcss';
html {
scroll-behavior: smooth;
scroll-behavior: smooth;
}
body {
font-family: 'DM Sans', sans-serif;
background-color: #111827;
color: #d1d5db;
font-family: 'DM Sans', sans-serif;
background-color: #111827;
color: #d1d5db;
}
.tool-card {
transition: transform 0.2s ease-in-out, box-shadow 0.2s ease-in-out;
border: 1px solid #374151;
transition:
transform 0.2s ease-in-out,
box-shadow 0.2s ease-in-out;
border: 1px solid #374151;
}
.tool-card:hover {
transform: translateY(-5px);
box-shadow: 0 10px 15px -3px rgba(0, 0, 0, 0.2), 0 4px 6px -2px rgba(0, 0, 0, 0.1);
border-color: #4f46e5;
transform: translateY(-5px);
box-shadow:
0 10px 15px -3px rgba(0, 0, 0, 0.2),
0 4px 6px -2px rgba(0, 0, 0, 0.1);
border-color: #4f46e5;
}
.btn {
transition: background-color 0.2s ease-in-out, transform 0.1s ease-in-out;
transition:
background-color 0.2s ease-in-out,
transform 0.1s ease-in-out;
}
.btn:active {
transform: scale(0.98);
transform: scale(0.98);
}
/* Custom file input */
input[type="file"]::file-selector-button {
@apply bg-indigo-600 text-white font-semibold py-2 px-4 rounded-lg cursor-pointer hover:bg-indigo-700 transition-colors duration-200 mr-4;
input[type='file']::file-selector-button {
@apply bg-indigo-600 text-white font-semibold py-2 px-4 rounded-lg cursor-pointer hover:bg-indigo-700 transition-colors duration-200 mr-4;
}
/* Custom scrollbar */
::-webkit-scrollbar {
width: 8px;
width: 8px;
}
::-webkit-scrollbar-track {
background: #1f2937;
background: #1f2937;
}
::-webkit-scrollbar-thumb {
background: #4f46e5;
border-radius: 4px;
background: #4f46e5;
border-radius: 4px;
}
::-webkit-scrollbar-thumb:hover {
background: #4338ca;
/* indigo-700 */
background: #4338ca;
/* indigo-700 */
}
/* Style for drag-and-drop placeholder */
.sortable-ghost {
opacity: 0.4;
border: 2px dashed #4f46e5;
opacity: 0.4;
border: 2px dashed #4f46e5;
}
#embed-pdf-container>div {
width: 100%;
height: 100%;
#embed-pdf-container > div {
width: 100%;
height: 100%;
}
#tool-interface {
color: #39A0ED;
color: #39a0ed;
}
.page-thumbnail,
#file-list>li {
cursor: grab;
#file-list > li {
cursor: grab;
}
.sortable-chosen {
cursor: grabbing;
cursor: grabbing;
}
.compare-viewer-wrapper.overlay-mode {
position: relative;
width: 100%;
height: 75vh;
overflow: auto;
border: 2px solid #374151;
border-radius: 0.5rem;
background-color: #1f2937;
position: relative;
width: 100%;
height: 75vh;
overflow: auto;
border: 2px solid #374151;
border-radius: 0.5rem;
background-color: #1f2937;
}
/* This rule now ONLY applies to canvases in overlay mode */
.compare-viewer-wrapper.overlay-mode canvas {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: auto;
position: absolute;
top: 0;
left: 0;
width: 100%;
height: auto;
}
.compare-viewer-wrapper.side-by-side-mode {
display: flex;
gap: 1rem;
width: 100%;
display: flex;
gap: 1rem;
width: 100%;
}
.pdf-panel {
flex: 1;
min-width: 0;
overflow: auto;
height: 75vh;
border: 2px solid #374151;
border-radius: 0.5rem;
background-color: #1f2937;
flex: 1;
min-width: 0;
overflow: auto;
height: 75vh;
border: 2px solid #374151;
border-radius: 0.5rem;
background-color: #1f2937;
}
/* This rule ensures canvases in side-by-side panels display at their natural rendered size. */
.pdf-panel canvas {
display: block;
margin: 0 auto;
display: block;
margin: 0 auto;
}
footer a {
transition: color 0.2s ease-in-out;
transition: color 0.2s ease-in-out;
}
.marker {
position: relative;
display: inline-block;
position: relative;
display: inline-block;
}
.marker::after {
content: '';
position: absolute;
bottom: -5px;
left: 0;
width: 100%;
/* height: 30px; */
background-color: orange;
/* Yellow marker color */
z-index: -1;
transform: skew(-20deg);
content: '';
position: absolute;
bottom: -5px;
left: 0;
width: 100%;
/* height: 30px; */
background-color: orange;
/* Yellow marker color */
z-index: -1;
transform: skew(-20deg);
}
.pill {
background-color: #374151;
/* bg-gray-700 */
color: #d1d5db;
/* text-gray-300 */
padding: 8px 16px;
border-radius: 9999px;
/* rounded-full */
font-size: 14px;
font-weight: 500;
background-color: #374151;
/* bg-gray-700 */
color: #d1d5db;
/* text-gray-300 */
padding: 8px 16px;
border-radius: 9999px;
/* rounded-full */
font-size: 14px;
font-weight: 500;
}
.cta-button {
background-color: #4f46e5;
/* indigo-600 */
color: white;
font-weight: 600;
padding: 12px 24px;
border-radius: 8px;
/* rounded-lg */
transition: background-color 0.2s ease-in-out;
background-color: #4f46e5;
/* indigo-600 */
color: white;
font-weight: 600;
padding: 12px 24px;
border-radius: 8px;
/* rounded-lg */
transition: background-color 0.2s ease-in-out;
}
.cta-button:hover {
background-color: #4338ca;
/* indigo-700 */
background-color: #4338ca;
/* indigo-700 */
}
.marker-text {
background-color: rgba(255, 255, 0, 0.5);
/* Yellow marker color */
padding: 0 5px;
background-color: rgba(255, 255, 0, 0.5);
/* Yellow marker color */
padding: 0 5px;
}
.feature-card {
background-color: #1f2937;
/* bg-gray-800 */
padding: 24px;
border-radius: 8px;
/* rounded-lg */
text-align: center;
background-color: #1f2937;
/* bg-gray-800 */
padding: 24px;
border-radius: 8px;
/* rounded-lg */
text-align: center;
}
.nav-link {
@apply text-gray-300 hover:bg-gray-700 hover:text-white px-3 py-2 rounded-md text-sm font-medium transition-colors;
@apply text-gray-300 hover:bg-gray-700 hover:text-white px-3 py-2 rounded-md text-sm font-medium transition-colors;
}
.mobile-nav-link {
@apply text-gray-300 hover:bg-gray-700 hover:text-white block px-3 py-2 rounded-md text-base font-medium transition-colors;
@apply text-gray-300 hover:bg-gray-700 hover:text-white block px-3 py-2 rounded-md text-base font-medium transition-colors;
}
.marker-slanted {
position: relative;
display: inline-block;
position: relative;
display: inline-block;
}
.marker-slanted::before {
content: "";
position: absolute;
left: 0;
right: 0;
bottom: 0;
height: 6px;
background: linear-gradient(120deg, #6366f1, #8b5cf6);
z-index: -1;
content: '';
position: absolute;
left: 0;
right: 0;
bottom: 0;
height: 6px;
background: linear-gradient(120deg, #6366f1, #8b5cf6);
z-index: -1;
}
.section-divider {
height: 1px;
background: linear-gradient(to right, transparent, #4f46e5, transparent);
/* Fades from transparent to indigo and back */
margin: 2rem auto;
/* my-16 */
max-width: 42rem;
/* max-w-xl */
opacity: 0.5;
height: 1px;
background: linear-gradient(to right, transparent, #4f46e5, transparent);
/* Fades from transparent to indigo and back */
margin: 2rem auto;
/* my-16 */
max-width: 42rem;
/* max-w-xl */
opacity: 0.5;
}
.btn-gradient {
display: inline-block;
padding: 0.75rem 2rem;
/* py-3 px-8 */
border-radius: 0.5rem;
/* rounded-lg */
background-image: linear-gradient(to bottom, #6366f1, #4f46e5);
/* from-indigo-500 to-indigo-600 */
color: #ffffff;
/* text-white */
font-weight: 600;
/* font-semibold */
transition-property: all;
transition-duration: 200ms;
transform: translateY(0);
display: inline-block;
padding: 0.75rem 2rem;
/* py-3 px-8 */
border-radius: 0.5rem;
/* rounded-lg */
background-image: linear-gradient(to bottom, #6366f1, #4f46e5);
/* from-indigo-500 to-indigo-600 */
color: #ffffff;
/* text-white */
font-weight: 600;
/* font-semibold */
transition-property: all;
transition-duration: 200ms;
transform: translateY(0);
}
.btn-gradient:hover {
box-shadow: 0 10px 15px -3px rgba(79, 70, 229, 0.3), 0 4px 6px -4px rgba(79, 70, 229, 0.3);
/* hover:shadow-xl hover:shadow-indigo-500/30 */
transform: translateY(-0.25rem);
/* hover:-translate-y-1 */
box-shadow:
0 10px 15px -3px rgba(79, 70, 229, 0.3),
0 4px 6px -4px rgba(79, 70, 229, 0.3);
/* hover:shadow-xl hover:shadow-indigo-500/30 */
transform: translateY(-0.25rem);
/* hover:-translate-y-1 */
}
.btn-gradient:focus {
outline: 2px solid transparent;
outline-offset: 2px;
box-shadow: 0 0 0 2px #111827, 0 0 0 4px #818cf8;
/* focus:ring-2 focus:ring-offset-2 focus:ring-offset-gray-900 focus:ring-indigo-400 */
outline: 2px solid transparent;
outline-offset: 2px;
box-shadow:
0 0 0 2px #111827,
0 0 0 4px #818cf8;
/* focus:ring-2 focus:ring-offset-2 focus:ring-offset-gray-900 focus:ring-indigo-400 */
}
.btn-gradient:disabled {
opacity: 0.5;
cursor: not-allowed;
transform: none;
box-shadow: none;
opacity: 0.5;
cursor: not-allowed;
transform: none;
box-shadow: none;
}
.solid-spinner {
width: 64px;
/* w-16 */
height: 64px;
/* h-16 */
border: 5px solid #374151;
/* border-gray-700 */
border-bottom-color: #4f46e5;
/* border-indigo-600 */
border-radius: 50%;
display: inline-block;
box-sizing: border-box;
animation: spin 1s linear infinite;
width: 64px;
/* w-16 */
height: 64px;
/* h-16 */
border: 5px solid #374151;
/* border-gray-700 */
border-bottom-color: #4f46e5;
/* border-indigo-600 */
border-radius: 50%;
display: inline-block;
box-sizing: border-box;
animation: spin 1s linear infinite;
}
@keyframes spin {
0% {
transform: rotate(0deg);
}
0% {
transform: rotate(0deg);
}
100% {
transform: rotate(360deg);
}
100% {
transform: rotate(360deg);
}
}
#signature-ghost {
position: absolute;
z-index: 100;
pointer-events: none;
/* Allows clicks to pass through to the canvas */
opacity: 0.6;
background-size: contain;
background-repeat: no-repeat;
position: absolute;
z-index: 100;
pointer-events: none;
/* Allows clicks to pass through to the canvas */
opacity: 0.6;
background-size: contain;
background-repeat: no-repeat;
}
/* Highlight for selected signature in the saved list */
.saved-signature.selected {
border-color: #4f46e5;
/* indigo-600 */
box-shadow: 0 0 10px rgba(79, 70, 229, 0.5);
border-color: #4f46e5;
/* indigo-600 */
box-shadow: 0 0 10px rgba(79, 70, 229, 0.5);
}
/* Cursor change when hovering over a placed signature */
#canvas-sign.movable {
cursor: move;
cursor: move;
}
#canvas-sign.resize-ns {
cursor: ns-resize;
cursor: ns-resize;
}
#canvas-sign.resize-ew {
cursor: ew-resize;
cursor: ew-resize;
}
#canvas-sign.resize-nesw {
cursor: nesw-resize;
cursor: nesw-resize;
}
#canvas-sign.resize-nwse {
cursor: nwse-resize;
cursor: nwse-resize;
}
.faq-item.open .faq-question {
color: #818cf8;
/* indigo-400 */
color: #818cf8;
/* indigo-400 */
}
.faq-item.open .faq-icon {
transform: rotate(180deg);
transform: rotate(180deg);
}
/* Testimonial Card Styles */
.testimonial-card {
background-color: #1f2937;
/* bg-gray-800 */
padding: 24px;
border-radius: 0.75rem;
/* rounded-xl */
border: 1px solid #374151;
/* border-gray-700 */
display: flex;
flex-direction: column;
background-color: #1f2937;
/* bg-gray-800 */
padding: 24px;
border-radius: 0.75rem;
/* rounded-xl */
border: 1px solid #374151;
/* border-gray-700 */
display: flex;
flex-direction: column;
}
.pill {
background-color: #374151;
/* bg-gray-700 */
color: #d1d5db;
/* text-gray-300 */
padding: 8px 16px;
border-radius: 9999px;
/* rounded-full */
font-size: 14px;
font-weight: 500;
background-color: #374151;
/* bg-gray-700 */
color: #d1d5db;
/* text-gray-300 */
padding: 8px 16px;
border-radius: 9999px;
/* rounded-full */
font-size: 14px;
font-weight: 500;
}
.pill {
background-color: #374151;
/* bg-gray-700 */
color: #d1d5db;
/* text-gray-300 */
/* Smaller padding and font size by default for mobile */
padding: 6px 12px;
font-size: 12px;
border-radius: 9999px;
/* rounded-full */
font-weight: 500;
background-color: #374151;
/* bg-gray-700 */
color: #d1d5db;
/* text-gray-300 */
/* Smaller padding and font size by default for mobile */
padding: 6px 12px;
font-size: 12px;
border-radius: 9999px;
/* rounded-full */
font-weight: 500;
}
/* On small screens (640px) and up, revert to the larger size */
@media (min-width: 640px) {
.pill {
padding: 8px 16px;
font-size: 14px;
}
.pill {
padding: 8px 16px;
font-size: 14px;
}
}
/* Ensure form-field-group contents don't overflow on small screens */
.form-field-group .capitalize {
word-break: break-all;
word-break: break-all;
}
@media (max-width: 1023px) {
#form-fields-container {
overflow-x: auto;
white-space: nowrap;
}
#form-fields-container {
overflow-x: auto;
white-space: nowrap;
}
.form-field-group {
display: inline-block;
width: 90%;
/* Adjust as needed */
margin-right: 1rem;
}
.form-field-group {
display: inline-block;
width: 90%;
/* Adjust as needed */
margin-right: 1rem;
}
}
#page-merge-preview {
display: grid;
gap: 1rem;
padding: 1rem;
grid-template-columns: repeat(auto-fit, minmax(100px, 1fr));
display: grid;
gap: 1rem;
padding: 1rem;
grid-template-columns: repeat(auto-fit, minmax(100px, 1fr));
}
.legal-content h2 {
font-size: 1.5rem;
line-height: 2rem;
font-weight: 700;
color: white;
margin-top: 2rem;
margin-bottom: 1rem;
font-size: 1.5rem;
line-height: 2rem;
font-weight: 700;
color: white;
margin-top: 2rem;
margin-bottom: 1rem;
}
.legal-content h3 {
font-size: 1.25rem;
line-height: 1.75rem;
font-weight: 600;
color: rgb(129 140 248);
margin-top: 1.5rem;
margin-bottom: 0.75rem;
font-size: 1.25rem;
line-height: 1.75rem;
font-weight: 600;
color: rgb(129 140 248);
margin-top: 1.5rem;
margin-bottom: 0.75rem;
}
.legal-content p {
margin-bottom: 1rem;
line-height: 1.625;
color: rgb(156 163 175);
margin-bottom: 1rem;
line-height: 1.625;
color: rgb(156 163 175);
}
.legal-content ul {
list-style-type: disc;
list-style-position: inside;
margin-bottom: 1rem;
padding-left: 1rem;
color: rgb(156 163 175);
list-style-type: disc;
list-style-position: inside;
margin-bottom: 1rem;
padding-left: 1rem;
color: rgb(156 163 175);
}
.legal-content a {
color: rgb(129 140 248);
text-decoration: underline;
color: rgb(129 140 248);
text-decoration: underline;
}
.legal-content a:hover {
color: rgb(165 180 252);
color: rgb(165 180 252);
}
details>summary {
list-style: none;
details > summary {
list-style: none;
}
details>summary::-webkit-details-marker {
display: none;
details > summary::-webkit-details-marker {
display: none;
}
details>summary .icon {
transition: transform 0.2s ease-in-out;
details > summary .icon {
transition: transform 0.2s ease-in-out;
}
details[open]>summary .icon {
transform: rotate(45deg);
}
details[open] > summary .icon {
transform: rotate(45deg);
}

View File

@@ -1,23 +1,23 @@
import { showLoader, hideLoader, showAlert } from './ui.js';
import { state } from './state.js';
import { toolLogic } from './logic/index.js';
import { icons, createIcons } from "lucide";
import { icons, createIcons } from 'lucide';
const editorState = {
pdf: null,
canvas: null,
context: null,
container: null,
currentPageNum: 1,
pageRendering: false,
pageNumPending: null,
scale: 1.0,
pageSnapshot: null,
isDrawing: false,
startX: 0,
startY: 0,
cropBoxes: {},
lastInteractionRect: null, // Used to store the rectangle from the last move event
pdf: null,
canvas: null,
context: null,
container: null,
currentPageNum: 1,
pageRendering: false,
pageNumPending: null,
scale: 1.0,
pageSnapshot: null,
isDrawing: false,
startX: 0,
startY: 0,
cropBoxes: {},
lastInteractionRect: null, // Used to store the rectangle from the last move event
};
/**
@@ -25,9 +25,9 @@ const editorState = {
* @param {PDFPageProxy} page - The PDF.js page object.
*/
function calculateFitScale(page: any) {
const containerWidth = editorState.container.clientWidth;
const viewport = page.getViewport({ scale: 1.0 });
return containerWidth / viewport.width;
const containerWidth = editorState.container.clientWidth;
const viewport = page.getViewport({ scale: 1.0 });
return containerWidth / viewport.width;
}
/**
@@ -35,239 +35,269 @@ function calculateFitScale(page: any) {
* @param {number} num The page number to render.
*/
async function renderPage(num: any) {
editorState.pageRendering = true;
showLoader(`Loading page ${num}...`);
editorState.pageRendering = true;
showLoader(`Loading page ${num}...`);
try {
const page = await editorState.pdf.getPage(num);
try {
const page = await editorState.pdf.getPage(num);
// @ts-expect-error TS(2367) FIXME: This condition will always return 'false' since th... Remove this comment to see the full error message
if (editorState.scale === 'fit') {
editorState.scale = calculateFitScale(page);
}
const viewport = page.getViewport({ scale: editorState.scale });
editorState.canvas.height = viewport.height;
editorState.canvas.width = viewport.width;
const renderContext = {
canvasContext: editorState.context,
viewport: viewport
};
await page.render(renderContext).promise;
editorState.pageSnapshot = editorState.context.getImageData(0, 0, editorState.canvas.width, editorState.canvas.height);
redrawShapes();
} catch (error) {
console.error("Error rendering page:", error);
showAlert('Render Error', 'Could not display the page.');
} finally {
editorState.pageRendering = false;
hideLoader();
document.getElementById('current-page-display').textContent = num;
// @ts-expect-error TS(2339) FIXME: Property 'disabled' does not exist on type 'HTMLEl... Remove this comment to see the full error message
document.getElementById('prev-page').disabled = num <= 1;
// @ts-expect-error TS(2339) FIXME: Property 'disabled' does not exist on type 'HTMLEl... Remove this comment to see the full error message
document.getElementById('next-page').disabled = num >= editorState.pdf.numPages;
if (editorState.pageNumPending !== null) {
const pendingPage = editorState.pageNumPending;
editorState.pageNumPending = null;
queueRenderPage(pendingPage);
}
// @ts-expect-error TS(2367) FIXME: This condition will always return 'false' since th... Remove this comment to see the full error message
if (editorState.scale === 'fit') {
editorState.scale = calculateFitScale(page);
}
const viewport = page.getViewport({ scale: editorState.scale });
editorState.canvas.height = viewport.height;
editorState.canvas.width = viewport.width;
const renderContext = {
canvasContext: editorState.context,
viewport: viewport,
};
await page.render(renderContext).promise;
editorState.pageSnapshot = editorState.context.getImageData(
0,
0,
editorState.canvas.width,
editorState.canvas.height
);
redrawShapes();
} catch (error) {
console.error('Error rendering page:', error);
showAlert('Render Error', 'Could not display the page.');
} finally {
editorState.pageRendering = false;
hideLoader();
document.getElementById('current-page-display').textContent = num;
// @ts-expect-error TS(2339) FIXME: Property 'disabled' does not exist on type 'HTMLEl... Remove this comment to see the full error message
document.getElementById('prev-page').disabled = num <= 1;
// @ts-expect-error TS(2339) FIXME: Property 'disabled' does not exist on type 'HTMLEl... Remove this comment to see the full error message
document.getElementById('next-page').disabled =
num >= editorState.pdf.numPages;
if (editorState.pageNumPending !== null) {
const pendingPage = editorState.pageNumPending;
editorState.pageNumPending = null;
queueRenderPage(pendingPage);
}
}
}
function queueRenderPage(num: any) {
if (editorState.pageRendering) {
editorState.pageNumPending = num;
} else {
editorState.currentPageNum = num;
renderPage(num);
}
if (editorState.pageRendering) {
editorState.pageNumPending = num;
} else {
editorState.currentPageNum = num;
renderPage(num);
}
}
function redrawShapes() {
if (editorState.pageSnapshot) {
editorState.context.putImageData(editorState.pageSnapshot, 0, 0);
}
const currentCropBox = editorState.cropBoxes[editorState.currentPageNum - 1];
if (currentCropBox) {
editorState.context.strokeStyle = 'rgba(79, 70, 229, 0.9)';
editorState.context.lineWidth = 2;
editorState.context.setLineDash([8, 4]);
editorState.context.strokeRect(currentCropBox.x, currentCropBox.y, currentCropBox.width, currentCropBox.height);
editorState.context.setLineDash([]);
}
}
function getEventCoordinates(e: any) {
const rect = editorState.canvas.getBoundingClientRect();
const touch = e.touches ? e.touches[0] : e;
const scaleX = editorState.canvas.width / rect.width;
const scaleY = editorState.canvas.height / rect.height;
return {
x: (touch.clientX - rect.left) * scaleX,
y: (touch.clientY - rect.top) * scaleY,
};
}
function handleInteractionStart(e: any) {
e.preventDefault();
const coords = getEventCoordinates(e);
editorState.isDrawing = true;
editorState.startX = coords.x;
editorState.startY = coords.y;
}
function handleInteractionMove(e: any) {
if (!editorState.isDrawing) return;
e.preventDefault();
redrawShapes();
const coords = getEventCoordinates(e);
const x = Math.min(editorState.startX, coords.x);
const y = Math.min(editorState.startY, coords.y);
const width = Math.abs(editorState.startX - coords.x);
const height = Math.abs(editorState.startY - coords.y);
if (editorState.pageSnapshot) {
editorState.context.putImageData(editorState.pageSnapshot, 0, 0);
}
const currentCropBox = editorState.cropBoxes[editorState.currentPageNum - 1];
if (currentCropBox) {
editorState.context.strokeStyle = 'rgba(79, 70, 229, 0.9)';
editorState.context.lineWidth = 2;
editorState.context.setLineDash([8, 4]);
editorState.context.strokeRect(x, y, width, height);
editorState.context.strokeRect(
currentCropBox.x,
currentCropBox.y,
currentCropBox.width,
currentCropBox.height
);
editorState.context.setLineDash([]);
}
}
// Store the last valid rectangle drawn during the move event
editorState.lastInteractionRect = { x, y, width, height };
function getEventCoordinates(e: any) {
const rect = editorState.canvas.getBoundingClientRect();
const touch = e.touches ? e.touches[0] : e;
const scaleX = editorState.canvas.width / rect.width;
const scaleY = editorState.canvas.height / rect.height;
return {
x: (touch.clientX - rect.left) * scaleX,
y: (touch.clientY - rect.top) * scaleY,
};
}
function handleInteractionStart(e: any) {
e.preventDefault();
const coords = getEventCoordinates(e);
editorState.isDrawing = true;
editorState.startX = coords.x;
editorState.startY = coords.y;
}
function handleInteractionMove(e: any) {
if (!editorState.isDrawing) return;
e.preventDefault();
redrawShapes();
const coords = getEventCoordinates(e);
const x = Math.min(editorState.startX, coords.x);
const y = Math.min(editorState.startY, coords.y);
const width = Math.abs(editorState.startX - coords.x);
const height = Math.abs(editorState.startY - coords.y);
editorState.context.strokeStyle = 'rgba(79, 70, 229, 0.9)';
editorState.context.lineWidth = 2;
editorState.context.setLineDash([8, 4]);
editorState.context.strokeRect(x, y, width, height);
editorState.context.setLineDash([]);
// Store the last valid rectangle drawn during the move event
editorState.lastInteractionRect = { x, y, width, height };
}
function handleInteractionEnd() {
if (!editorState.isDrawing) return;
editorState.isDrawing = false;
if (!editorState.isDrawing) return;
editorState.isDrawing = false;
const finalRect = editorState.lastInteractionRect;
const finalRect = editorState.lastInteractionRect;
if (!finalRect || finalRect.width < 5 || finalRect.height < 5) {
redrawShapes(); // Redraw to clear any invalid, tiny box
editorState.lastInteractionRect = null;
return;
}
if (!finalRect || finalRect.width < 5 || finalRect.height < 5) {
redrawShapes(); // Redraw to clear any invalid, tiny box
editorState.lastInteractionRect = null;
return;
}
editorState.cropBoxes[editorState.currentPageNum - 1] = {
...finalRect,
scale: editorState.scale
};
editorState.cropBoxes[editorState.currentPageNum - 1] = {
...finalRect,
scale: editorState.scale,
};
editorState.lastInteractionRect = null; // Reset for the next drawing action
redrawShapes();
editorState.lastInteractionRect = null; // Reset for the next drawing action
redrawShapes();
}
export async function setupCanvasEditor(toolId: any) {
editorState.canvas = document.getElementById('canvas-editor');
if (!editorState.canvas) return;
editorState.container = document.getElementById('canvas-container');
editorState.context = editorState.canvas.getContext('2d');
editorState.canvas = document.getElementById('canvas-editor');
if (!editorState.canvas) return;
editorState.container = document.getElementById('canvas-container');
editorState.context = editorState.canvas.getContext('2d');
const pageNav = document.getElementById('page-nav');
const pdfData = await state.pdfDoc.save();
// @ts-expect-error TS(2304) FIXME: Cannot find name 'pdfjsLib'.
editorState.pdf = await pdfjsLib.getDocument({ data: pdfData }).promise;
const pageNav = document.getElementById('page-nav');
const pdfData = await state.pdfDoc.save();
// @ts-expect-error TS(2304) FIXME: Cannot find name 'pdfjsLib'.
editorState.pdf = await pdfjsLib.getDocument({ data: pdfData }).promise;
editorState.cropBoxes = {};
editorState.currentPageNum = 1;
// @ts-expect-error TS(2322) FIXME: Type 'string' is not assignable to type 'number'.
editorState.scale = 'fit';
editorState.cropBoxes = {};
editorState.currentPageNum = 1;
// @ts-expect-error TS(2322) FIXME: Type 'string' is not assignable to type 'number'.
editorState.scale = 'fit';
pageNav.textContent = '';
pageNav.textContent = '';
const prevButton = document.createElement('button');
prevButton.id = 'prev-page';
prevButton.className = 'btn p-2 rounded-full bg-gray-700 hover:bg-gray-600 disabled:opacity-50';
prevButton.innerHTML = '<i data-lucide="chevron-left"></i>';
const prevButton = document.createElement('button');
prevButton.id = 'prev-page';
prevButton.className =
'btn p-2 rounded-full bg-gray-700 hover:bg-gray-600 disabled:opacity-50';
prevButton.innerHTML = '<i data-lucide="chevron-left"></i>';
const pageInfo = document.createElement('span');
pageInfo.className = 'text-white font-medium';
const currentPageDisplay = document.createElement('span');
currentPageDisplay.id = 'current-page-display';
currentPageDisplay.textContent = '1';
const pageInfo = document.createElement('span');
pageInfo.className = 'text-white font-medium';
pageInfo.append('Page ', currentPageDisplay, ` of ${editorState.pdf.numPages}`);
const currentPageDisplay = document.createElement('span');
currentPageDisplay.id = 'current-page-display';
currentPageDisplay.textContent = '1';
const nextButton = document.createElement('button');
nextButton.id = 'next-page';
nextButton.className = 'btn p-2 rounded-full bg-gray-700 hover:bg-gray-600 disabled:opacity-50';
nextButton.innerHTML = '<i data-lucide="chevron-right"></i>';
pageInfo.append(
'Page ',
currentPageDisplay,
` of ${editorState.pdf.numPages}`
);
pageNav.append(prevButton, pageInfo, nextButton);
const nextButton = document.createElement('button');
nextButton.id = 'next-page';
nextButton.className =
'btn p-2 rounded-full bg-gray-700 hover:bg-gray-600 disabled:opacity-50';
nextButton.innerHTML = '<i data-lucide="chevron-right"></i>';
createIcons({icons});
pageNav.append(prevButton, pageInfo, nextButton);
document.getElementById('prev-page').addEventListener('click', () => {
if (editorState.currentPageNum > 1) queueRenderPage(editorState.currentPageNum - 1);
});
document.getElementById('next-page').addEventListener('click', () => {
if (editorState.currentPageNum < editorState.pdf.numPages) queueRenderPage(editorState.currentPageNum + 1);
});
createIcons({ icons });
// To prevent stacking multiple listeners, we replace the canvas element with a clone
const newCanvas = editorState.canvas.cloneNode(true);
editorState.canvas.parentNode.replaceChild(newCanvas, editorState.canvas);
editorState.canvas = newCanvas;
editorState.context = newCanvas.getContext('2d');
document.getElementById('prev-page').addEventListener('click', () => {
if (editorState.currentPageNum > 1)
queueRenderPage(editorState.currentPageNum - 1);
});
document.getElementById('next-page').addEventListener('click', () => {
if (editorState.currentPageNum < editorState.pdf.numPages)
queueRenderPage(editorState.currentPageNum + 1);
});
// Mouse Events
editorState.canvas.addEventListener('mousedown', handleInteractionStart);
editorState.canvas.addEventListener('mousemove', handleInteractionMove);
editorState.canvas.addEventListener('mouseup', handleInteractionEnd);
editorState.canvas.addEventListener('mouseleave', handleInteractionEnd);
// To prevent stacking multiple listeners, we replace the canvas element with a clone
const newCanvas = editorState.canvas.cloneNode(true);
editorState.canvas.parentNode.replaceChild(newCanvas, editorState.canvas);
editorState.canvas = newCanvas;
editorState.context = newCanvas.getContext('2d');
// Touch Events
editorState.canvas.addEventListener('touchstart', handleInteractionStart, { passive: false });
editorState.canvas.addEventListener('touchmove', handleInteractionMove, { passive: false });
editorState.canvas.addEventListener('touchend', handleInteractionEnd);
// Mouse Events
editorState.canvas.addEventListener('mousedown', handleInteractionStart);
editorState.canvas.addEventListener('mousemove', handleInteractionMove);
editorState.canvas.addEventListener('mouseup', handleInteractionEnd);
editorState.canvas.addEventListener('mouseleave', handleInteractionEnd);
if (toolId === 'crop') {
document.getElementById('zoom-in-btn').onclick = () => {
editorState.scale += 0.25;
renderPage(editorState.currentPageNum);
};
document.getElementById('zoom-out-btn').onclick = () => {
if (editorState.scale > 0.25) {
editorState.scale -= 0.25;
renderPage(editorState.currentPageNum);
}
};
document.getElementById('fit-page-btn').onclick = async () => {
const page = await editorState.pdf.getPage(editorState.currentPageNum);
editorState.scale = calculateFitScale(page);
renderPage(editorState.currentPageNum);
};
document.getElementById('clear-crop-btn').onclick = () => {
delete editorState.cropBoxes[editorState.currentPageNum - 1];
redrawShapes();
};
document.getElementById('clear-all-crops-btn').onclick = () => {
editorState.cropBoxes = {};
redrawShapes();
};
// Touch Events
editorState.canvas.addEventListener('touchstart', handleInteractionStart, {
passive: false,
});
editorState.canvas.addEventListener('touchmove', handleInteractionMove, {
passive: false,
});
editorState.canvas.addEventListener('touchend', handleInteractionEnd);
document.getElementById('process-btn').onclick = async () => {
if (Object.keys(editorState.cropBoxes).length === 0) {
showAlert('No Area Selected', 'Please draw a rectangle on at least one page to select the crop area.');
return;
}
const success = await toolLogic['crop-pdf'].process(editorState.cropBoxes);
if (success) {
showAlert('Success!', 'Your PDF has been cropped and the download has started.');
}
};
}
if (toolId === 'crop') {
document.getElementById('zoom-in-btn').onclick = () => {
editorState.scale += 0.25;
renderPage(editorState.currentPageNum);
};
document.getElementById('zoom-out-btn').onclick = () => {
if (editorState.scale > 0.25) {
editorState.scale -= 0.25;
renderPage(editorState.currentPageNum);
}
};
document.getElementById('fit-page-btn').onclick = async () => {
const page = await editorState.pdf.getPage(editorState.currentPageNum);
editorState.scale = calculateFitScale(page);
renderPage(editorState.currentPageNum);
};
document.getElementById('clear-crop-btn').onclick = () => {
delete editorState.cropBoxes[editorState.currentPageNum - 1];
redrawShapes();
};
document.getElementById('clear-all-crops-btn').onclick = () => {
editorState.cropBoxes = {};
redrawShapes();
};
queueRenderPage(1);
}
document.getElementById('process-btn').onclick = async () => {
if (Object.keys(editorState.cropBoxes).length === 0) {
showAlert(
'No Area Selected',
'Please draw a rectangle on at least one page to select the crop area.'
);
return;
}
const success = await toolLogic['crop-pdf'].process(
editorState.cropBoxes
);
if (success) {
showAlert(
'Success!',
'Your PDF has been cropped and the download has started.'
);
}
};
}
queueRenderPage(1);
}

View File

@@ -1,18 +1,63 @@
export const singlePdfLoadTools = [
'split', 'organize', 'rotate', 'add-page-numbers',
'pdf-to-jpg', 'pdf-to-png', 'pdf-to-webp', 'compress', 'pdf-to-greyscale',
'edit-metadata', 'remove-metadata', 'flatten', 'delete-pages', 'add-blank-page',
'extract-pages', 'add-watermark', 'add-header-footer', 'invert-colors', 'view-metadata',
'reverse-pages', 'crop', 'redact', 'pdf-to-bmp', 'pdf-to-tiff', 'split-in-half',
'page-dimensions', 'n-up', 'duplicate-organize', 'combine-single-page', 'fix-dimensions', 'change-background-color',
'change-text-color', 'ocr-pdf', 'sign-pdf', 'remove-annotations', 'cropper', 'form-filler', 'posterize', 'remove-blank-pages'
'split',
'organize',
'rotate',
'add-page-numbers',
'pdf-to-jpg',
'pdf-to-png',
'pdf-to-webp',
'compress',
'pdf-to-greyscale',
'edit-metadata',
'remove-metadata',
'flatten',
'delete-pages',
'add-blank-page',
'extract-pages',
'add-watermark',
'add-header-footer',
'invert-colors',
'view-metadata',
'reverse-pages',
'crop',
'redact',
'pdf-to-bmp',
'pdf-to-tiff',
'split-in-half',
'page-dimensions',
'n-up',
'duplicate-organize',
'combine-single-page',
'fix-dimensions',
'change-background-color',
'change-text-color',
'ocr-pdf',
'sign-pdf',
'remove-annotations',
'cropper',
'form-filler',
'posterize',
'remove-blank-pages',
];
export const simpleTools = [
'encrypt', 'decrypt', 'change-permissions', 'pdf-to-markdown', 'word-to-pdf',
'encrypt',
'decrypt',
'change-permissions',
'pdf-to-markdown',
'word-to-pdf',
];
export const multiFileTools = [
'merge', 'pdf-to-zip', 'jpg-to-pdf', 'png-to-pdf', 'webp-to-pdf', 'image-to-pdf', 'svg-to-pdf', 'bmp-to-pdf', 'heic-to-pdf', 'tiff-to-pdf',
'alternate-merge'
];
'merge',
'pdf-to-zip',
'jpg-to-pdf',
'png-to-pdf',
'webp-to-pdf',
'image-to-pdf',
'svg-to-pdf',
'bmp-to-pdf',
'heic-to-pdf',
'tiff-to-pdf',
'alternate-merge',
];

View File

@@ -1,22 +1,104 @@
export const tesseractLanguages = {
"eng": "English", "afr": "Afrikaans", "amh": "Amharic", "ara": "Arabic", "asm": "Assamese", "aze": "Azerbaijani",
"aze_cyrl": "Azerbaijani - Cyrillic", "bel": "Belarusian", "ben": "Bengali", "bod": "Tibetan", "bos": "Bosnian",
"bul": "Bulgarian", "cat": "Catalan; Valencian", "ceb": "Cebuano", "ces": "Czech", "chi_sim": "Chinese - Simplified",
"chi_tra": "Chinese - Traditional", "chr": "Cherokee", "cym": "Welsh", "dan": "Danish", "deu": "German",
"dzo": "Dzongkha", "ell": "Greek, Modern (1453-)", "enm": "English, Middle (1100-1500)", "epo": "Esperanto",
"est": "Estonian", "eus": "Basque", "fas": "Persian", "fin": "Finnish", "fra": "French", "frk": "German Fraktur",
"frm": "French, Middle (ca. 1400-1600)", "gle": "Irish", "glg": "Galician", "grc": "Greek, Ancient (-1453)",
"guj": "Gujarati", "hat": "Haitian; Haitian Creole", "heb": "Hebrew", "hin": "Hindi", "hrv": "Croatian",
"hun": "Hungarian", "iku": "Inuktitut", "ind": "Indonesian", "isl": "Icelandic", "ita": "Italian",
"ita_old": "Italian - Old", "jav": "Javanese", "jpn": "Japanese", "kan": "Kannada", "kat": "Georgian",
"kat_old": "Georgian - Old", "kaz": "Kazakh", "khm": "Central Khmer", "kir": "Kirghiz; Kyrgyz",
"kor": "Korean", "kur": "Kurdish", "lao": "Lao", "lat": "Latin", "lav": "Latvian", "lit": "Lithuanian",
"mal": "Malayalam", "mar": "Marathi", "mkd": "Macedonian", "mlt": "Maltese", "msa": "Malay", "mya": "Burmese",
"nep": "Nepali", "nld": "Dutch; Flemish", "nor": "Norwegian", "ori": "Oriya", "pan": "Panjabi; Punjabi",
"pol": "Polish", "por": "Portuguese", "pus": "Pushto; Pashto", "ron": "Romanian; Moldavian; Moldovan",
"rus": "Russian", "san": "Sanskrit", "sin": "Sinhala; Sinhalese", "slk": "Slovak", "slv": "Slovenian",
"spa": "Spanish; Castilian", "spa_old": "Spanish; Castilian - Old", "sqi": "Albanian", "srp": "Serbian",
"srp_latn": "Serbian - Latin", "swa": "Swahili", "swe": "Swedish", "syr": "Syriac", "tam": "Tamil", "tel": "Telugu",
"tgk": "Tajik", "tgl": "Tagalog", "tha": "Thai", "tir": "Tigrinya", "tur": "Turkish", "uig": "Uighur; Uyghur",
"ukr": "Ukrainian", "urd": "Urdu", "uzb": "Uzbek", "uzb_cyrl": "Uzbek - Cyrillic", "vie": "Vietnamese", "yid": "Yiddish"
};
eng: 'English',
afr: 'Afrikaans',
amh: 'Amharic',
ara: 'Arabic',
asm: 'Assamese',
aze: 'Azerbaijani',
aze_cyrl: 'Azerbaijani - Cyrillic',
bel: 'Belarusian',
ben: 'Bengali',
bod: 'Tibetan',
bos: 'Bosnian',
bul: 'Bulgarian',
cat: 'Catalan; Valencian',
ceb: 'Cebuano',
ces: 'Czech',
chi_sim: 'Chinese - Simplified',
chi_tra: 'Chinese - Traditional',
chr: 'Cherokee',
cym: 'Welsh',
dan: 'Danish',
deu: 'German',
dzo: 'Dzongkha',
ell: 'Greek, Modern (1453-)',
enm: 'English, Middle (1100-1500)',
epo: 'Esperanto',
est: 'Estonian',
eus: 'Basque',
fas: 'Persian',
fin: 'Finnish',
fra: 'French',
frk: 'German Fraktur',
frm: 'French, Middle (ca. 1400-1600)',
gle: 'Irish',
glg: 'Galician',
grc: 'Greek, Ancient (-1453)',
guj: 'Gujarati',
hat: 'Haitian; Haitian Creole',
heb: 'Hebrew',
hin: 'Hindi',
hrv: 'Croatian',
hun: 'Hungarian',
iku: 'Inuktitut',
ind: 'Indonesian',
isl: 'Icelandic',
ita: 'Italian',
ita_old: 'Italian - Old',
jav: 'Javanese',
jpn: 'Japanese',
kan: 'Kannada',
kat: 'Georgian',
kat_old: 'Georgian - Old',
kaz: 'Kazakh',
khm: 'Central Khmer',
kir: 'Kirghiz; Kyrgyz',
kor: 'Korean',
kur: 'Kurdish',
lao: 'Lao',
lat: 'Latin',
lav: 'Latvian',
lit: 'Lithuanian',
mal: 'Malayalam',
mar: 'Marathi',
mkd: 'Macedonian',
mlt: 'Maltese',
msa: 'Malay',
mya: 'Burmese',
nep: 'Nepali',
nld: 'Dutch; Flemish',
nor: 'Norwegian',
ori: 'Oriya',
pan: 'Panjabi; Punjabi',
pol: 'Polish',
por: 'Portuguese',
pus: 'Pushto; Pashto',
ron: 'Romanian; Moldavian; Moldovan',
rus: 'Russian',
san: 'Sanskrit',
sin: 'Sinhala; Sinhalese',
slk: 'Slovak',
slv: 'Slovenian',
spa: 'Spanish; Castilian',
spa_old: 'Spanish; Castilian - Old',
sqi: 'Albanian',
srp: 'Serbian',
srp_latn: 'Serbian - Latin',
swa: 'Swahili',
swe: 'Swedish',
syr: 'Syriac',
tam: 'Tamil',
tel: 'Telugu',
tgk: 'Tajik',
tgl: 'Tagalog',
tha: 'Thai',
tir: 'Tigrinya',
tur: 'Turkish',
uig: 'Uighur; Uyghur',
ukr: 'Ukrainian',
urd: 'Urdu',
uzb: 'Uzbek',
uzb_cyrl: 'Uzbek - Cyrillic',
vie: 'Vietnamese',
yid: 'Yiddish',
};

View File

@@ -1,107 +1,429 @@
// This file centralizes the definition of all available tools, organized by category.
export const categories = [
{
name: 'Popular Tools',
tools: [
{ id: 'merge', name: 'Merge PDF', icon: 'combine', subtitle: 'Combine multiple PDFs into one file.' },
{ id: 'split', name: 'Split PDF', icon: 'scissors', subtitle: 'Extract a range of pages into a new PDF.' },
{ id: 'compress', name: 'Compress PDF', icon: 'zap', subtitle: 'Reduce the file size of your PDF.' },
{ id: 'edit', name: 'PDF Editor', icon: 'pocket-knife', subtitle: 'Annotate, highlight, redact, comment, add shapes/images, search, and view PDFs' },
{ id: 'jpg-to-pdf', name: 'JPG to PDF', icon: 'image-up', subtitle: 'Create a PDF from one or more JPG images.' },
{ id: 'sign-pdf', name: 'Sign PDF', icon: 'pen-tool', subtitle: 'Draw, type, or upload your signature.' },
{ id: 'cropper', name: 'Crop PDF', icon: 'crop', subtitle: 'Trim the margins of every page in your PDF.' },
{ id: 'extract-pages', name: 'Extract Pages', icon: 'ungroup', subtitle: 'Save a selection of pages as new files.' },
{ id: 'duplicate-organize', name: 'Duplicate & Organize', icon: 'files', subtitle: 'Duplicate, reorder, and delete pages.' },
{ id: 'delete-pages', name: 'Delete Pages', icon: 'trash-2', subtitle: 'Remove specific pages from your document.' },
]
},
{
name: 'Edit & Annotate',
tools: [
{ id: 'edit', name: 'PDF Editor', icon: 'pocket-knife', subtitle: 'Annotate, highlight, redact, comment, add shapes/images, search, and view PDFs.' },
// { id: 'crop', name: 'Crop PDF', icon: 'crop', subtitle: 'Trim the margins of every page in your PDF.' },
{ id: 'add-page-numbers', name: 'Page Numbers', icon: 'list-ordered', subtitle: 'Insert page numbers into your document.' },
{ id: 'add-watermark', name: 'Add Watermark', icon: 'droplets', subtitle: 'Stamp text or an image over your PDF pages.' },
{ id: 'add-header-footer', name: 'Header & Footer', icon: 'pilcrow', subtitle: 'Add text to the top and bottom of pages.' },
{ id: 'invert-colors', name: 'Invert Colors', icon: 'contrast', subtitle: 'Create a "dark mode" version of your PDF.' },
{ id: 'change-background-color', name: 'Background Color', icon: 'palette', subtitle: 'Change the background color of your PDF.' },
{ id: 'change-text-color', name: 'Change Text Color', icon: 'type', subtitle: 'Change the color of text in your PDF.' },
{ id: 'sign-pdf', name: 'Sign PDF', icon: 'pen-tool', subtitle: 'Draw, type, or upload your signature.' },
{ id: 'remove-annotations', name: 'Remove Annotations', icon: 'eraser', subtitle: 'Strip comments, highlights, and links.' },
{ id: 'cropper', name: 'Crop PDF', icon: 'crop', subtitle: 'Trim the margins of every page in your PDF.' },
{ id: 'form-filler', name: 'PDF Form Filler', icon: 'square-pen', subtitle: 'Fill in forms directly in the browser.' },
{ id: 'remove-blank-pages', name: 'Remove Blank Pages', icon: 'file-minus-2', subtitle: 'Automatically detect and delete blank pages.' },
]
},
{
name: 'Convert to PDF',
tools: [
{ id: 'image-to-pdf', name: 'Image to PDF', icon: 'images', subtitle: 'Combine various images into one PDF.' },
{ id: 'jpg-to-pdf', name: 'JPG to PDF', icon: 'image-up', subtitle: 'Create a PDF from one or more JPG images.' },
{ id: 'png-to-pdf', name: 'PNG to PDF', icon: 'image-up', subtitle: 'Create a PDF from one or more PNG images.' },
{ id: 'webp-to-pdf', name: 'WebP to PDF', icon: 'image-up', subtitle: 'Create a PDF from one or more WebP images.' },
{ id: 'svg-to-pdf', name: 'SVG to PDF', icon: 'pen-tool', subtitle: 'Create a PDF from one or more SVG images.' },
{ id: 'bmp-to-pdf', name: 'BMP to PDF', icon: 'image', subtitle: 'Create a PDF from one or more BMP images.' },
{ id: 'heic-to-pdf', name: 'HEIC to PDF', icon: 'smartphone', subtitle: 'Create a PDF from one or more HEIC images.' },
{ id: 'tiff-to-pdf', name: 'TIFF to PDF', icon: 'layers', subtitle: 'Create a PDF from one or more TIFF images.' },
{ id: 'txt-to-pdf', name: 'Text to PDF', icon: 'file-pen', subtitle: 'Convert a plain text file into a PDF.' },
// { id: 'md-to-pdf', name: 'Markdown to PDF', icon: 'file-text', subtitle: 'Convert a Markdown file into a PDF.' },
// { id: 'scan-to-pdf', name: 'Scan to PDF', icon: 'camera', subtitle: 'Use your camera to create a scanned PDF.' },
// { id: 'word-to-pdf', name: 'Word to PDF', icon: 'file-text', subtitle: 'Convert .docx documents to PDF.' },
]
},
{
name: 'Convert from PDF',
tools: [
{ id: 'pdf-to-jpg', name: 'PDF to JPG', icon: 'file-image', subtitle: 'Convert each PDF page into a JPG image.' },
{ id: 'pdf-to-png', name: 'PDF to PNG', icon: 'file-image', subtitle: 'Convert each PDF page into a PNG image.' },
{ id: 'pdf-to-webp', name: 'PDF to WebP', icon: 'file-image', subtitle: 'Convert each PDF page into a WebP image.' },
{ id: 'pdf-to-bmp', name: 'PDF to BMP', icon: 'file-image', subtitle: 'Convert each PDF page into a BMP image.' },
{ id: 'pdf-to-tiff', name: 'PDF to TIFF', icon: 'file-image', subtitle: 'Convert each PDF page into a TIFF image.' },
{ id: 'pdf-to-greyscale', name: 'PDF to Greyscale', icon: 'palette', subtitle: 'Convert all colors to black and white.' },
// { id: 'pdf-to-markdown', name: 'PDF to Markdown', icon: 'file-pen', subtitle: 'Extract text into a Markdown file.' },
]
},
{
name: 'Organize & Manage',
tools: [
{ id: 'ocr-pdf', name: 'OCR PDF', icon: 'scan-text', subtitle: 'Make a PDF searchable and copyable.' },
{ id: 'merge', name: 'Merge PDF', icon: 'combine', subtitle: 'Combine multiple PDFs into one file.' },
{ id: 'alternate-merge', name: 'Alternate & Mix Pages', icon: 'shuffle', subtitle: 'Combine PDFs by alternating pages from each.' },
{ id: 'organize', name: 'Organize PDF', icon: 'grip', subtitle: 'Reorder pages by dragging and dropping.' },
{ id: 'duplicate-organize', name: 'Duplicate & Organize', icon: 'files', subtitle: 'Duplicate, reorder, and delete pages.' },
{ id: 'split', name: 'Split PDF', icon: 'scissors', subtitle: 'Extract a range of pages into a new PDF.' },
{ id: 'split-in-half', name: 'Divide Pages', icon: 'table-columns-split', subtitle: 'Divide pages horizontally or vertically.' },
{ id: 'extract-pages', name: 'Extract Pages', icon: 'ungroup', subtitle: 'Save a selection of pages as new files.' },
{ id: 'delete-pages', name: 'Delete Pages', icon: 'trash-2', subtitle: 'Remove specific pages from your document.' },
{ id: 'add-blank-page', name: 'Add Blank Page', icon: 'file-plus-2', subtitle: 'Insert an empty page anywhere in your PDF.' },
{ id: 'reverse-pages', name: 'Reverse Pages', icon: 'arrow-down-z-a', subtitle: 'Flip the order of all pages in your document.' },
{ id: 'rotate', name: 'Rotate PDF', icon: 'rotate-cw', subtitle: 'Turn pages in 90-degree increments.' },
{ id: 'n-up', name: 'N-Up PDF', icon: 'layout-grid', subtitle: 'Arrange multiple pages onto a single sheet.' },
{ id: 'combine-single-page', name: 'Combine to Single Page', icon: 'unfold-vertical', subtitle: 'Stitch all pages into one continuous scroll.' },
{ id: 'view-metadata', name: 'View Metadata', icon: 'info', subtitle: 'Inspect the hidden properties of your PDF.' },
{ id: 'edit-metadata', name: 'Edit Metadata', icon: 'file-cog', subtitle: 'Change the author, title, and other properties.' },
{ id: 'pdf-to-zip', name: 'PDFs to ZIP', icon: 'stretch-horizontal', subtitle: 'Package multiple PDF files into a ZIP archive.' },
{ id: 'compare-pdfs', name: 'Compare PDFs', icon: 'git-compare', subtitle: 'Compare two PDFs side by side.' },
{ id: 'posterize', name: 'Posterize PDF', icon: 'layout-grid', subtitle: 'Split a large page into multiple smaller pages.' },
]
},
{
name: 'Optimize & Repair',
tools: [
{ id: 'compress', name: 'Compress PDF', icon: 'zap', subtitle: 'Reduce the file size of your PDF.' },
{ id: 'fix-dimensions', name: 'Fix Page Size', icon: 'ruler-dimension-line', subtitle: 'Standardize all pages to a uniform size.' },
{ id: 'page-dimensions', name: 'Page Dimensions', icon: 'ruler', subtitle: 'Analyze page size, orientation, and units.' },
]
},
{
name: 'Secure PDF',
tools: [
{ id: 'encrypt', name: 'Encrypt PDF', icon: 'lock', subtitle: 'Add a password to protect your PDF.' },
{ id: 'decrypt', name: 'Decrypt PDF', icon: 'unlock', subtitle: 'Remove password protection from a PDF.' },
{ id: 'flatten', name: 'Flatten PDF', icon: 'layers', subtitle: 'Make form fields and annotations non-editable.' },
{ id: 'remove-metadata', name: 'Remove Metadata', icon: 'file-x', subtitle: 'Strip hidden data from your PDF.' },
{ id: 'change-permissions', name: 'Change Permissions', icon: 'shield-check', subtitle: 'Set or change user permissions on a PDF.' },
]
},
];
{
name: 'Popular Tools',
tools: [
{
id: 'merge',
name: 'Merge PDF',
icon: 'combine',
subtitle: 'Combine multiple PDFs into one file.',
},
{
id: 'split',
name: 'Split PDF',
icon: 'scissors',
subtitle: 'Extract a range of pages into a new PDF.',
},
{
id: 'compress',
name: 'Compress PDF',
icon: 'zap',
subtitle: 'Reduce the file size of your PDF.',
},
{
id: 'edit',
name: 'PDF Editor',
icon: 'pocket-knife',
subtitle:
'Annotate, highlight, redact, comment, add shapes/images, search, and view PDFs',
},
{
id: 'jpg-to-pdf',
name: 'JPG to PDF',
icon: 'image-up',
subtitle: 'Create a PDF from one or more JPG images.',
},
{
id: 'sign-pdf',
name: 'Sign PDF',
icon: 'pen-tool',
subtitle: 'Draw, type, or upload your signature.',
},
{
id: 'cropper',
name: 'Crop PDF',
icon: 'crop',
subtitle: 'Trim the margins of every page in your PDF.',
},
{
id: 'extract-pages',
name: 'Extract Pages',
icon: 'ungroup',
subtitle: 'Save a selection of pages as new files.',
},
{
id: 'duplicate-organize',
name: 'Duplicate & Organize',
icon: 'files',
subtitle: 'Duplicate, reorder, and delete pages.',
},
{
id: 'delete-pages',
name: 'Delete Pages',
icon: 'trash-2',
subtitle: 'Remove specific pages from your document.',
},
],
},
{
name: 'Edit & Annotate',
tools: [
{
id: 'edit',
name: 'PDF Editor',
icon: 'pocket-knife',
subtitle:
'Annotate, highlight, redact, comment, add shapes/images, search, and view PDFs.',
},
// { id: 'crop', name: 'Crop PDF', icon: 'crop', subtitle: 'Trim the margins of every page in your PDF.' },
{
id: 'add-page-numbers',
name: 'Page Numbers',
icon: 'list-ordered',
subtitle: 'Insert page numbers into your document.',
},
{
id: 'add-watermark',
name: 'Add Watermark',
icon: 'droplets',
subtitle: 'Stamp text or an image over your PDF pages.',
},
{
id: 'add-header-footer',
name: 'Header & Footer',
icon: 'pilcrow',
subtitle: 'Add text to the top and bottom of pages.',
},
{
id: 'invert-colors',
name: 'Invert Colors',
icon: 'contrast',
subtitle: 'Create a "dark mode" version of your PDF.',
},
{
id: 'change-background-color',
name: 'Background Color',
icon: 'palette',
subtitle: 'Change the background color of your PDF.',
},
{
id: 'change-text-color',
name: 'Change Text Color',
icon: 'type',
subtitle: 'Change the color of text in your PDF.',
},
{
id: 'sign-pdf',
name: 'Sign PDF',
icon: 'pen-tool',
subtitle: 'Draw, type, or upload your signature.',
},
{
id: 'remove-annotations',
name: 'Remove Annotations',
icon: 'eraser',
subtitle: 'Strip comments, highlights, and links.',
},
{
id: 'cropper',
name: 'Crop PDF',
icon: 'crop',
subtitle: 'Trim the margins of every page in your PDF.',
},
{
id: 'form-filler',
name: 'PDF Form Filler',
icon: 'square-pen',
subtitle: 'Fill in forms directly in the browser.',
},
{
id: 'remove-blank-pages',
name: 'Remove Blank Pages',
icon: 'file-minus-2',
subtitle: 'Automatically detect and delete blank pages.',
},
],
},
{
name: 'Convert to PDF',
tools: [
{
id: 'image-to-pdf',
name: 'Image to PDF',
icon: 'images',
subtitle: 'Combine various images into one PDF.',
},
{
id: 'jpg-to-pdf',
name: 'JPG to PDF',
icon: 'image-up',
subtitle: 'Create a PDF from one or more JPG images.',
},
{
id: 'png-to-pdf',
name: 'PNG to PDF',
icon: 'image-up',
subtitle: 'Create a PDF from one or more PNG images.',
},
{
id: 'webp-to-pdf',
name: 'WebP to PDF',
icon: 'image-up',
subtitle: 'Create a PDF from one or more WebP images.',
},
{
id: 'svg-to-pdf',
name: 'SVG to PDF',
icon: 'pen-tool',
subtitle: 'Create a PDF from one or more SVG images.',
},
{
id: 'bmp-to-pdf',
name: 'BMP to PDF',
icon: 'image',
subtitle: 'Create a PDF from one or more BMP images.',
},
{
id: 'heic-to-pdf',
name: 'HEIC to PDF',
icon: 'smartphone',
subtitle: 'Create a PDF from one or more HEIC images.',
},
{
id: 'tiff-to-pdf',
name: 'TIFF to PDF',
icon: 'layers',
subtitle: 'Create a PDF from one or more TIFF images.',
},
{
id: 'txt-to-pdf',
name: 'Text to PDF',
icon: 'file-pen',
subtitle: 'Convert a plain text file into a PDF.',
},
// { id: 'md-to-pdf', name: 'Markdown to PDF', icon: 'file-text', subtitle: 'Convert a Markdown file into a PDF.' },
// { id: 'scan-to-pdf', name: 'Scan to PDF', icon: 'camera', subtitle: 'Use your camera to create a scanned PDF.' },
// { id: 'word-to-pdf', name: 'Word to PDF', icon: 'file-text', subtitle: 'Convert .docx documents to PDF.' },
],
},
{
name: 'Convert from PDF',
tools: [
{
id: 'pdf-to-jpg',
name: 'PDF to JPG',
icon: 'file-image',
subtitle: 'Convert each PDF page into a JPG image.',
},
{
id: 'pdf-to-png',
name: 'PDF to PNG',
icon: 'file-image',
subtitle: 'Convert each PDF page into a PNG image.',
},
{
id: 'pdf-to-webp',
name: 'PDF to WebP',
icon: 'file-image',
subtitle: 'Convert each PDF page into a WebP image.',
},
{
id: 'pdf-to-bmp',
name: 'PDF to BMP',
icon: 'file-image',
subtitle: 'Convert each PDF page into a BMP image.',
},
{
id: 'pdf-to-tiff',
name: 'PDF to TIFF',
icon: 'file-image',
subtitle: 'Convert each PDF page into a TIFF image.',
},
{
id: 'pdf-to-greyscale',
name: 'PDF to Greyscale',
icon: 'palette',
subtitle: 'Convert all colors to black and white.',
},
// { id: 'pdf-to-markdown', name: 'PDF to Markdown', icon: 'file-pen', subtitle: 'Extract text into a Markdown file.' },
],
},
{
name: 'Organize & Manage',
tools: [
{
id: 'ocr-pdf',
name: 'OCR PDF',
icon: 'scan-text',
subtitle: 'Make a PDF searchable and copyable.',
},
{
id: 'merge',
name: 'Merge PDF',
icon: 'combine',
subtitle: 'Combine multiple PDFs into one file.',
},
{
id: 'alternate-merge',
name: 'Alternate & Mix Pages',
icon: 'shuffle',
subtitle: 'Combine PDFs by alternating pages from each.',
},
{
id: 'organize',
name: 'Organize PDF',
icon: 'grip',
subtitle: 'Reorder pages by dragging and dropping.',
},
{
id: 'duplicate-organize',
name: 'Duplicate & Organize',
icon: 'files',
subtitle: 'Duplicate, reorder, and delete pages.',
},
{
id: 'split',
name: 'Split PDF',
icon: 'scissors',
subtitle: 'Extract a range of pages into a new PDF.',
},
{
id: 'split-in-half',
name: 'Divide Pages',
icon: 'table-columns-split',
subtitle: 'Divide pages horizontally or vertically.',
},
{
id: 'extract-pages',
name: 'Extract Pages',
icon: 'ungroup',
subtitle: 'Save a selection of pages as new files.',
},
{
id: 'delete-pages',
name: 'Delete Pages',
icon: 'trash-2',
subtitle: 'Remove specific pages from your document.',
},
{
id: 'add-blank-page',
name: 'Add Blank Page',
icon: 'file-plus-2',
subtitle: 'Insert an empty page anywhere in your PDF.',
},
{
id: 'reverse-pages',
name: 'Reverse Pages',
icon: 'arrow-down-z-a',
subtitle: 'Flip the order of all pages in your document.',
},
{
id: 'rotate',
name: 'Rotate PDF',
icon: 'rotate-cw',
subtitle: 'Turn pages in 90-degree increments.',
},
{
id: 'n-up',
name: 'N-Up PDF',
icon: 'layout-grid',
subtitle: 'Arrange multiple pages onto a single sheet.',
},
{
id: 'combine-single-page',
name: 'Combine to Single Page',
icon: 'unfold-vertical',
subtitle: 'Stitch all pages into one continuous scroll.',
},
{
id: 'view-metadata',
name: 'View Metadata',
icon: 'info',
subtitle: 'Inspect the hidden properties of your PDF.',
},
{
id: 'edit-metadata',
name: 'Edit Metadata',
icon: 'file-cog',
subtitle: 'Change the author, title, and other properties.',
},
{
id: 'pdf-to-zip',
name: 'PDFs to ZIP',
icon: 'stretch-horizontal',
subtitle: 'Package multiple PDF files into a ZIP archive.',
},
{
id: 'compare-pdfs',
name: 'Compare PDFs',
icon: 'git-compare',
subtitle: 'Compare two PDFs side by side.',
},
{
id: 'posterize',
name: 'Posterize PDF',
icon: 'layout-grid',
subtitle: 'Split a large page into multiple smaller pages.',
},
],
},
{
name: 'Optimize & Repair',
tools: [
{
id: 'compress',
name: 'Compress PDF',
icon: 'zap',
subtitle: 'Reduce the file size of your PDF.',
},
{
id: 'fix-dimensions',
name: 'Fix Page Size',
icon: 'ruler-dimension-line',
subtitle: 'Standardize all pages to a uniform size.',
},
{
id: 'page-dimensions',
name: 'Page Dimensions',
icon: 'ruler',
subtitle: 'Analyze page size, orientation, and units.',
},
],
},
{
name: 'Secure PDF',
tools: [
{
id: 'encrypt',
name: 'Encrypt PDF',
icon: 'lock',
subtitle: 'Add a password to protect your PDF.',
},
{
id: 'decrypt',
name: 'Decrypt PDF',
icon: 'unlock',
subtitle: 'Remove password protection from a PDF.',
},
{
id: 'flatten',
name: 'Flatten PDF',
icon: 'layers',
subtitle: 'Make form fields and annotations non-editable.',
},
{
id: 'remove-metadata',
name: 'Remove Metadata',
icon: 'file-x',
subtitle: 'Strip hidden data from your PDF.',
},
{
id: 'change-permissions',
name: 'Change Permissions',
icon: 'shield-check',
subtitle: 'Set or change user permissions on a PDF.',
},
],
},
];

View File

@@ -1,5 +1,12 @@
import { state } from '../state.js';
import { showLoader, hideLoader, showAlert, renderPageThumbnails, renderFileDisplay, switchView } from '../ui.js';
import {
showLoader,
hideLoader,
showAlert,
renderPageThumbnails,
renderFileDisplay,
switchView,
} from '../ui.js';
import { readFileAsArrayBuffer } from '../utils/helpers.js';
import { setupCanvasEditor } from '../canvasEditor.js';
import { toolLogic } from '../logic/index.js';
@@ -7,352 +14,426 @@ import { renderDuplicateOrganizeThumbnails } from '../logic/duplicate-organize.j
import { PDFDocument as PDFLibDocument } from 'pdf-lib';
import { icons, createIcons } from 'lucide';
import Sortable from 'sortablejs';
import { multiFileTools, simpleTools, singlePdfLoadTools } from '../config/pdf-tools.js';
import {
multiFileTools,
simpleTools,
singlePdfLoadTools,
} from '../config/pdf-tools.js';
import * as pdfjsLib from 'pdfjs-dist';
async function handleSinglePdfUpload(toolId, file) {
showLoader('Loading PDF...');
try {
const pdfBytes = await readFileAsArrayBuffer(file);
state.pdfDoc = await PDFLibDocument.load(pdfBytes as ArrayBuffer, {
ignoreEncryption: true
});
hideLoader();
showLoader('Loading PDF...');
try {
const pdfBytes = await readFileAsArrayBuffer(file);
state.pdfDoc = await PDFLibDocument.load(pdfBytes as ArrayBuffer, {
ignoreEncryption: true,
});
hideLoader();
if (state.pdfDoc.isEncrypted && toolId !== 'decrypt' && toolId !== 'change-permissions') {
showAlert('Protected PDF', 'This PDF is password-protected. Please use the Decrypt or Change Permissions tool first.');
switchView('grid');
return;
}
const optionsDiv = document.querySelector('[id$="-options"], [id$="-preview"], [id$="-organizer"], [id$="-rotator"], [id$="-editor"]');
if (optionsDiv) optionsDiv.classList.remove('hidden');
const processBtn = document.getElementById('process-btn');
if (processBtn) {
(processBtn as HTMLButtonElement).disabled = false;
processBtn.classList.remove('hidden');
const logic = toolLogic[toolId];
if (logic) {
const func = typeof logic.process === 'function' ? logic.process : logic;
processBtn.onclick = func;
}
}
if (['split', 'delete-pages', 'add-blank-page', 'extract-pages', 'add-header-footer'].includes(toolId)) {
document.getElementById('total-pages').textContent = state.pdfDoc.getPageCount().toString();
}
if (toolId === 'organize' || toolId === 'rotate') {
await renderPageThumbnails(toolId, state.pdfDoc);
if (toolId === 'rotate') {
const rotateAllControls = document.getElementById('rotate-all-controls');
const rotateAllLeftBtn = document.getElementById('rotate-all-left-btn');
const rotateAllRightBtn = document.getElementById('rotate-all-right-btn');
rotateAllControls.classList.remove('hidden');
createIcons({ icons });
const rotateAll = (direction) => {
document.querySelectorAll('.page-rotator-item').forEach(item => {
const currentRotation = parseInt((item as HTMLElement).dataset.rotation || '0');
const newRotation = (currentRotation + (direction * 90) + 360) % 360;
(item as HTMLElement).dataset.rotation = newRotation.toString();
const thumbnail = item.querySelector('canvas, img');
if (thumbnail) {
(thumbnail as HTMLElement).style.transform = `rotate(${newRotation}deg)`;
}
});
};
rotateAllLeftBtn.onclick = () => rotateAll(-1);
rotateAllRightBtn.onclick = () => rotateAll(1);
}
}
if (toolId === 'duplicate-organize') {
await renderDuplicateOrganizeThumbnails();
}
if (['crop', 'redact'].includes(toolId)) {
await setupCanvasEditor(toolId);
}
if (toolId === 'view-metadata') {
const resultsDiv = document.getElementById('metadata-results');
showLoader('Analyzing full PDF metadata...');
try {
const pdfBytes = await readFileAsArrayBuffer(state.files[0]);
const pdfjsDoc = await pdfjsLib.getDocument({ data: pdfBytes as ArrayBuffer }).promise;
const [metadata, fieldObjects] = await Promise.all([
pdfjsDoc.getMetadata(),
pdfjsDoc.getFieldObjects()
]);
const { info, metadata: rawXmpString } = metadata;
resultsDiv.textContent = ''; // Clear safely
const createSection = (title) => {
const wrapper = document.createElement('div');
wrapper.className = 'mb-4';
const h3 = document.createElement('h3');
h3.className = 'text-lg font-semibold text-white mb-2';
h3.textContent = title;
const ul = document.createElement('ul');
ul.className = 'space-y-3 text-sm bg-gray-900 p-4 rounded-lg border border-gray-700';
wrapper.append(h3, ul);
return { wrapper, ul };
};
const createListItem = (key, value) => {
const li = document.createElement('li');
li.className = 'flex flex-col sm:flex-row';
const strong = document.createElement('strong');
strong.className = 'w-40 flex-shrink-0 text-gray-400';
strong.textContent = key;
const div = document.createElement('div');
div.className = 'flex-grow text-white break-all';
div.textContent = value;
li.append(strong, div);
return li;
};
const parsePdfDate = (pdfDate) => {
if (!pdfDate || typeof pdfDate !== 'string' || !pdfDate.startsWith('D:')) return pdfDate;
try {
const year = pdfDate.substring(2, 6);
const month = pdfDate.substring(6, 8);
const day = pdfDate.substring(8, 10);
const hour = pdfDate.substring(10, 12);
const minute = pdfDate.substring(12, 14);
const second = pdfDate.substring(14, 16);
return new Date(`${year}-${month}-${day}T${hour}:${minute}:${second}Z`).toLocaleString();
} catch { return pdfDate; }
};
const infoSection = createSection('Info Dictionary');
if (info && Object.keys(info).length > 0) {
for (const key in info) {
let value = info[key] || '- Not Set -';
if ((key === 'CreationDate' || key === 'ModDate') && typeof value === 'string') {
value = parsePdfDate(value);
}
infoSection.ul.appendChild(createListItem(key, String(value)));
}
} else {
infoSection.ul.innerHTML = `<li><span class="text-gray-500 italic">- No Info Dictionary data found -</span></li>`;
}
resultsDiv.appendChild(infoSection.wrapper);
const fieldsSection = createSection('Interactive Form Fields');
if (fieldObjects && Object.keys(fieldObjects).length > 0) {
for (const fieldName in fieldObjects) {
const field = fieldObjects[fieldName][0];
const value = (field as any).fieldValue || '- Not Set -';
fieldsSection.ul.appendChild(createListItem(fieldName, String(value)));
}
} else {
fieldsSection.ul.innerHTML = `<li><span class="text-gray-500 italic">- No interactive form fields found -</span></li>`;
}
resultsDiv.appendChild(fieldsSection.wrapper);
const xmpSection = createSection('XMP Metadata (Raw XML)');
const xmpContainer = document.createElement('div');
xmpContainer.className = 'bg-gray-900 p-4 rounded-lg border border-gray-700';
if (rawXmpString) {
const pre = document.createElement('pre');
pre.className = 'text-xs text-gray-300 whitespace-pre-wrap break-all';
pre.textContent = String(rawXmpString);
xmpContainer.appendChild(pre);
} else {
xmpContainer.innerHTML = `<p class="text-gray-500 italic">- No XMP metadata found -</p>`;
}
xmpSection.wrapper.appendChild(xmpContainer);
resultsDiv.appendChild(xmpSection.wrapper);
resultsDiv.classList.remove('hidden');
} catch (e) {
console.error("Failed to view metadata or fields:", e);
showAlert('Error', 'Could not fully analyze the PDF. It may be corrupted or have an unusual structure.');
} finally {
hideLoader();
}
}
if (toolId === 'edit-metadata') {
const form = document.getElementById('metadata-form');
const container = document.getElementById('custom-metadata-container');
const addBtn = document.getElementById('add-custom-meta-btn');
const formatDateForInput = (date) => {
if (!date) return '';
const pad = (num) => num.toString().padStart(2, '0');
return `${date.getFullYear()}-${pad(date.getMonth() + 1)}-${pad(date.getDate())}T${pad(date.getHours())}:${pad(date.getMinutes())}`;
};
(document.getElementById('meta-title') as HTMLInputElement).value = state.pdfDoc.getTitle() || '';
(document.getElementById('meta-author') as HTMLInputElement).value = state.pdfDoc.getAuthor() || '';
(document.getElementById('meta-subject') as HTMLInputElement).value = state.pdfDoc.getSubject() || '';
(document.getElementById('meta-keywords') as HTMLInputElement).value = state.pdfDoc.getKeywords() || '';
(document.getElementById('meta-creator') as HTMLInputElement).value = state.pdfDoc.getCreator() || '';
(document.getElementById('meta-producer') as HTMLInputElement).value = state.pdfDoc.getProducer() || '';
(document.getElementById('meta-creation-date') as HTMLInputElement).value = formatDateForInput(state.pdfDoc.getCreationDate());
(document.getElementById('meta-mod-date') as HTMLInputElement).value = formatDateForInput(state.pdfDoc.getModificationDate());
addBtn.onclick = () => {
const fieldWrapper = document.createElement('div');
fieldWrapper.className = 'flex items-center gap-2 custom-field-wrapper';
const keyInput = document.createElement('input');
keyInput.type = 'text';
keyInput.placeholder = 'Key (e.g., Department)';
keyInput.className = 'custom-meta-key w-1/3 bg-gray-800 border border-gray-600 text-white rounded-lg p-2';
const valueInput = document.createElement('input');
valueInput.type = 'text';
valueInput.placeholder = 'Value (e.g., Marketing)';
valueInput.className = 'custom-meta-value flex-grow bg-gray-800 border border-gray-600 text-white rounded-lg p-2';
const removeBtn = document.createElement('button');
removeBtn.type = 'button';
removeBtn.className = 'btn p-2 text-red-500 hover:bg-gray-700 rounded-full';
removeBtn.innerHTML = '<i data-lucide="trash-2"></i>';
removeBtn.addEventListener('click', () => fieldWrapper.remove());
fieldWrapper.append(keyInput, valueInput, removeBtn);
container.appendChild(fieldWrapper);
createIcons({ icons });
};
form.classList.remove('hidden');
createIcons({ icons });
}
if (toolId === 'cropper') {
document.getElementById('cropper-ui-container').classList.remove('hidden');
}
if (toolId === 'page-dimensions') {
toolLogic['page-dimensions']();
}
if (toolLogic[toolId] && typeof toolLogic[toolId].setup === 'function') {
toolLogic[toolId].setup();
}
} catch (e) {
hideLoader();
showAlert('Error', 'Could not load PDF. The file may be invalid, corrupted, or password-protected.');
console.error(e);
if (
state.pdfDoc.isEncrypted &&
toolId !== 'decrypt' &&
toolId !== 'change-permissions'
) {
showAlert(
'Protected PDF',
'This PDF is password-protected. Please use the Decrypt or Change Permissions tool first.'
);
switchView('grid');
return;
}
const optionsDiv = document.querySelector(
'[id$="-options"], [id$="-preview"], [id$="-organizer"], [id$="-rotator"], [id$="-editor"]'
);
if (optionsDiv) optionsDiv.classList.remove('hidden');
const processBtn = document.getElementById('process-btn');
if (processBtn) {
(processBtn as HTMLButtonElement).disabled = false;
processBtn.classList.remove('hidden');
const logic = toolLogic[toolId];
if (logic) {
const func =
typeof logic.process === 'function' ? logic.process : logic;
processBtn.onclick = func;
}
}
if (
[
'split',
'delete-pages',
'add-blank-page',
'extract-pages',
'add-header-footer',
].includes(toolId)
) {
document.getElementById('total-pages').textContent = state.pdfDoc
.getPageCount()
.toString();
}
if (toolId === 'organize' || toolId === 'rotate') {
await renderPageThumbnails(toolId, state.pdfDoc);
if (toolId === 'rotate') {
const rotateAllControls = document.getElementById(
'rotate-all-controls'
);
const rotateAllLeftBtn = document.getElementById('rotate-all-left-btn');
const rotateAllRightBtn = document.getElementById(
'rotate-all-right-btn'
);
rotateAllControls.classList.remove('hidden');
createIcons({ icons });
const rotateAll = (direction) => {
document.querySelectorAll('.page-rotator-item').forEach((item) => {
const currentRotation = parseInt(
(item as HTMLElement).dataset.rotation || '0'
);
const newRotation = (currentRotation + direction * 90 + 360) % 360;
(item as HTMLElement).dataset.rotation = newRotation.toString();
const thumbnail = item.querySelector('canvas, img');
if (thumbnail) {
(thumbnail as HTMLElement).style.transform =
`rotate(${newRotation}deg)`;
}
});
};
rotateAllLeftBtn.onclick = () => rotateAll(-1);
rotateAllRightBtn.onclick = () => rotateAll(1);
}
}
if (toolId === 'duplicate-organize') {
await renderDuplicateOrganizeThumbnails();
}
if (['crop', 'redact'].includes(toolId)) {
await setupCanvasEditor(toolId);
}
if (toolId === 'view-metadata') {
const resultsDiv = document.getElementById('metadata-results');
showLoader('Analyzing full PDF metadata...');
try {
const pdfBytes = await readFileAsArrayBuffer(state.files[0]);
const pdfjsDoc = await pdfjsLib.getDocument({
data: pdfBytes as ArrayBuffer,
}).promise;
const [metadata, fieldObjects] = await Promise.all([
pdfjsDoc.getMetadata(),
pdfjsDoc.getFieldObjects(),
]);
const { info, metadata: rawXmpString } = metadata;
resultsDiv.textContent = ''; // Clear safely
const createSection = (title) => {
const wrapper = document.createElement('div');
wrapper.className = 'mb-4';
const h3 = document.createElement('h3');
h3.className = 'text-lg font-semibold text-white mb-2';
h3.textContent = title;
const ul = document.createElement('ul');
ul.className =
'space-y-3 text-sm bg-gray-900 p-4 rounded-lg border border-gray-700';
wrapper.append(h3, ul);
return { wrapper, ul };
};
const createListItem = (key, value) => {
const li = document.createElement('li');
li.className = 'flex flex-col sm:flex-row';
const strong = document.createElement('strong');
strong.className = 'w-40 flex-shrink-0 text-gray-400';
strong.textContent = key;
const div = document.createElement('div');
div.className = 'flex-grow text-white break-all';
div.textContent = value;
li.append(strong, div);
return li;
};
const parsePdfDate = (pdfDate) => {
if (
!pdfDate ||
typeof pdfDate !== 'string' ||
!pdfDate.startsWith('D:')
)
return pdfDate;
try {
const year = pdfDate.substring(2, 6);
const month = pdfDate.substring(6, 8);
const day = pdfDate.substring(8, 10);
const hour = pdfDate.substring(10, 12);
const minute = pdfDate.substring(12, 14);
const second = pdfDate.substring(14, 16);
return new Date(
`${year}-${month}-${day}T${hour}:${minute}:${second}Z`
).toLocaleString();
} catch {
return pdfDate;
}
};
const infoSection = createSection('Info Dictionary');
if (info && Object.keys(info).length > 0) {
for (const key in info) {
let value = info[key] || '- Not Set -';
if (
(key === 'CreationDate' || key === 'ModDate') &&
typeof value === 'string'
) {
value = parsePdfDate(value);
}
infoSection.ul.appendChild(createListItem(key, String(value)));
}
} else {
infoSection.ul.innerHTML = `<li><span class="text-gray-500 italic">- No Info Dictionary data found -</span></li>`;
}
resultsDiv.appendChild(infoSection.wrapper);
const fieldsSection = createSection('Interactive Form Fields');
if (fieldObjects && Object.keys(fieldObjects).length > 0) {
for (const fieldName in fieldObjects) {
const field = fieldObjects[fieldName][0];
const value = (field as any).fieldValue || '- Not Set -';
fieldsSection.ul.appendChild(
createListItem(fieldName, String(value))
);
}
} else {
fieldsSection.ul.innerHTML = `<li><span class="text-gray-500 italic">- No interactive form fields found -</span></li>`;
}
resultsDiv.appendChild(fieldsSection.wrapper);
const xmpSection = createSection('XMP Metadata (Raw XML)');
const xmpContainer = document.createElement('div');
xmpContainer.className =
'bg-gray-900 p-4 rounded-lg border border-gray-700';
if (rawXmpString) {
const pre = document.createElement('pre');
pre.className = 'text-xs text-gray-300 whitespace-pre-wrap break-all';
pre.textContent = String(rawXmpString);
xmpContainer.appendChild(pre);
} else {
xmpContainer.innerHTML = `<p class="text-gray-500 italic">- No XMP metadata found -</p>`;
}
xmpSection.wrapper.appendChild(xmpContainer);
resultsDiv.appendChild(xmpSection.wrapper);
resultsDiv.classList.remove('hidden');
} catch (e) {
console.error('Failed to view metadata or fields:', e);
showAlert(
'Error',
'Could not fully analyze the PDF. It may be corrupted or have an unusual structure.'
);
} finally {
hideLoader();
}
}
if (toolId === 'edit-metadata') {
const form = document.getElementById('metadata-form');
const container = document.getElementById('custom-metadata-container');
const addBtn = document.getElementById('add-custom-meta-btn');
const formatDateForInput = (date) => {
if (!date) return '';
const pad = (num) => num.toString().padStart(2, '0');
return `${date.getFullYear()}-${pad(date.getMonth() + 1)}-${pad(date.getDate())}T${pad(date.getHours())}:${pad(date.getMinutes())}`;
};
(document.getElementById('meta-title') as HTMLInputElement).value =
state.pdfDoc.getTitle() || '';
(document.getElementById('meta-author') as HTMLInputElement).value =
state.pdfDoc.getAuthor() || '';
(document.getElementById('meta-subject') as HTMLInputElement).value =
state.pdfDoc.getSubject() || '';
(document.getElementById('meta-keywords') as HTMLInputElement).value =
state.pdfDoc.getKeywords() || '';
(document.getElementById('meta-creator') as HTMLInputElement).value =
state.pdfDoc.getCreator() || '';
(document.getElementById('meta-producer') as HTMLInputElement).value =
state.pdfDoc.getProducer() || '';
(
document.getElementById('meta-creation-date') as HTMLInputElement
).value = formatDateForInput(state.pdfDoc.getCreationDate());
(document.getElementById('meta-mod-date') as HTMLInputElement).value =
formatDateForInput(state.pdfDoc.getModificationDate());
addBtn.onclick = () => {
const fieldWrapper = document.createElement('div');
fieldWrapper.className = 'flex items-center gap-2 custom-field-wrapper';
const keyInput = document.createElement('input');
keyInput.type = 'text';
keyInput.placeholder = 'Key (e.g., Department)';
keyInput.className =
'custom-meta-key w-1/3 bg-gray-800 border border-gray-600 text-white rounded-lg p-2';
const valueInput = document.createElement('input');
valueInput.type = 'text';
valueInput.placeholder = 'Value (e.g., Marketing)';
valueInput.className =
'custom-meta-value flex-grow bg-gray-800 border border-gray-600 text-white rounded-lg p-2';
const removeBtn = document.createElement('button');
removeBtn.type = 'button';
removeBtn.className =
'btn p-2 text-red-500 hover:bg-gray-700 rounded-full';
removeBtn.innerHTML = '<i data-lucide="trash-2"></i>';
removeBtn.addEventListener('click', () => fieldWrapper.remove());
fieldWrapper.append(keyInput, valueInput, removeBtn);
container.appendChild(fieldWrapper);
createIcons({ icons });
};
form.classList.remove('hidden');
createIcons({ icons });
}
if (toolId === 'cropper') {
document
.getElementById('cropper-ui-container')
.classList.remove('hidden');
}
if (toolId === 'page-dimensions') {
toolLogic['page-dimensions']();
}
if (toolLogic[toolId] && typeof toolLogic[toolId].setup === 'function') {
toolLogic[toolId].setup();
}
} catch (e) {
hideLoader();
showAlert(
'Error',
'Could not load PDF. The file may be invalid, corrupted, or password-protected.'
);
console.error(e);
}
}
function handleMultiFileUpload(toolId) {
const processBtn = document.getElementById('process-btn');
if (processBtn) {
(processBtn as HTMLButtonElement).disabled = false;
const logic = toolLogic[toolId];
if (logic) {
const func = typeof logic.process === 'function' ? logic.process : logic;
processBtn.onclick = func;
}
const processBtn = document.getElementById('process-btn');
if (processBtn) {
(processBtn as HTMLButtonElement).disabled = false;
const logic = toolLogic[toolId];
if (logic) {
const func = typeof logic.process === 'function' ? logic.process : logic;
processBtn.onclick = func;
}
}
if (toolId === 'merge') {
toolLogic.merge.setup();
} else if (toolId === 'alternate-merge') {
toolLogic['alternate-merge'].setup();
} else if (toolId === 'image-to-pdf') {
const imageList = document.getElementById('image-list');
imageList.textContent = ''; // Clear safely
if (toolId === 'merge') {
toolLogic.merge.setup();
} else if (toolId === 'alternate-merge') {
toolLogic['alternate-merge'].setup();
} else if (toolId === 'image-to-pdf') {
const imageList = document.getElementById('image-list');
imageList.textContent = ''; // Clear safely
state.files.forEach(file => {
const url = URL.createObjectURL(file);
const li = document.createElement('li');
li.className = "relative group cursor-move";
li.dataset.fileName = file.name;
state.files.forEach((file) => {
const url = URL.createObjectURL(file);
const li = document.createElement('li');
li.className = 'relative group cursor-move';
li.dataset.fileName = file.name;
const img = document.createElement('img');
img.src = url;
img.className = "w-full h-full object-cover rounded-md border-2 border-gray-600";
const img = document.createElement('img');
img.src = url;
img.className =
'w-full h-full object-cover rounded-md border-2 border-gray-600';
const p = document.createElement('p');
p.className = "absolute bottom-0 left-0 right-0 bg-black bg-opacity-50 text-white text-xs text-center truncate p-1";
p.textContent = file.name; // Safe insertion
const p = document.createElement('p');
p.className =
'absolute bottom-0 left-0 right-0 bg-black bg-opacity-50 text-white text-xs text-center truncate p-1';
p.textContent = file.name; // Safe insertion
li.append(img, p);
imageList.appendChild(li);
});
li.append(img, p);
imageList.appendChild(li);
});
Sortable.create(imageList);
}
Sortable.create(imageList);
}
}
export function setupFileInputHandler(toolId) {
const fileInput = document.getElementById('file-input');
const isMultiFileTool = multiFileTools.includes(toolId);
let isFirstUpload = true;
const fileInput = document.getElementById('file-input');
const isMultiFileTool = multiFileTools.includes(toolId);
let isFirstUpload = true;
const processFiles = async (newFiles) => {
if (newFiles.length === 0) return;
const processFiles = async (newFiles) => {
if (newFiles.length === 0) return;
if (!isMultiFileTool || isFirstUpload) {
state.files = newFiles;
} else {
state.files = [...state.files, ...newFiles];
}
isFirstUpload = false;
if (!isMultiFileTool || isFirstUpload) {
state.files = newFiles;
} else {
state.files = [...state.files, ...newFiles];
}
isFirstUpload = false;
const fileDisplayArea = document.getElementById('file-display-area');
if (fileDisplayArea) {
renderFileDisplay(fileDisplayArea, state.files);
}
const fileDisplayArea = document.getElementById('file-display-area');
if (fileDisplayArea) {
renderFileDisplay(fileDisplayArea, state.files);
}
const fileControls = document.getElementById('file-controls');
if (fileControls) {
fileControls.classList.remove('hidden');
createIcons({ icons });
}
const fileControls = document.getElementById('file-controls');
if (fileControls) {
fileControls.classList.remove('hidden');
createIcons({ icons });
}
if (isMultiFileTool) {
handleMultiFileUpload(toolId);
} else if (singlePdfLoadTools.includes(toolId)) {
await handleSinglePdfUpload(toolId, state.files[0]);
} else if (simpleTools.includes(toolId)) {
const optionsDivId = toolId === 'change-permissions' ? 'permissions-options' : `${toolId}-options`;
const optionsDiv = document.getElementById(optionsDivId);
if (optionsDiv) optionsDiv.classList.remove('hidden');
const processBtn = document.getElementById('process-btn');
if (processBtn) {
(processBtn as HTMLButtonElement).disabled = false;
processBtn.onclick = () => {
const logic = toolLogic[toolId];
if (logic) {
const func = typeof logic.process === 'function' ? logic.process : logic;
func();
}
};
}
} else if (toolId === 'edit') {
const file = state.files[0];
if (!file) return;
if (isMultiFileTool) {
handleMultiFileUpload(toolId);
} else if (singlePdfLoadTools.includes(toolId)) {
await handleSinglePdfUpload(toolId, state.files[0]);
} else if (simpleTools.includes(toolId)) {
const optionsDivId =
toolId === 'change-permissions'
? 'permissions-options'
: `${toolId}-options`;
const optionsDiv = document.getElementById(optionsDivId);
if (optionsDiv) optionsDiv.classList.remove('hidden');
const processBtn = document.getElementById('process-btn');
if (processBtn) {
(processBtn as HTMLButtonElement).disabled = false;
processBtn.onclick = () => {
const logic = toolLogic[toolId];
if (logic) {
const func =
typeof logic.process === 'function' ? logic.process : logic;
func();
}
};
}
} else if (toolId === 'edit') {
const file = state.files[0];
if (!file) return;
const pdfWrapper = document.getElementById('embed-pdf-wrapper');
const pdfContainer = document.getElementById('embed-pdf-container');
const pdfWrapper = document.getElementById('embed-pdf-wrapper');
const pdfContainer = document.getElementById('embed-pdf-container');
pdfContainer.textContent = ''; // Clear safely
pdfContainer.textContent = ''; // Clear safely
if (state.currentPdfUrl) {
URL.revokeObjectURL(state.currentPdfUrl);
}
pdfWrapper.classList.remove('hidden');
const fileURL = URL.createObjectURL(file);
state.currentPdfUrl = fileURL;
if (state.currentPdfUrl) {
URL.revokeObjectURL(state.currentPdfUrl);
}
pdfWrapper.classList.remove('hidden');
const fileURL = URL.createObjectURL(file);
state.currentPdfUrl = fileURL;
const script = document.createElement('script');
script.type = 'module';
script.textContent = `
const script = document.createElement('script');
script.type = 'module';
script.textContent = `
import EmbedPDF from 'https://snippet.embedpdf.com/embedpdf.js';
EmbedPDF.init({
type: 'container',
@@ -361,55 +442,62 @@ export function setupFileInputHandler(toolId) {
theme: 'dark',
});
`;
document.head.appendChild(script);
document.head.appendChild(script);
const backBtn = document.getElementById('back-to-grid');
const urlRevoker = () => {
URL.revokeObjectURL(fileURL);
state.currentPdfUrl = null;
backBtn.removeEventListener('click', urlRevoker);
};
backBtn.addEventListener('click', urlRevoker);
}
};
const backBtn = document.getElementById('back-to-grid');
const urlRevoker = () => {
URL.revokeObjectURL(fileURL);
state.currentPdfUrl = null;
backBtn.removeEventListener('click', urlRevoker);
};
backBtn.addEventListener('click', urlRevoker);
}
};
fileInput.addEventListener('change', (e) => processFiles(Array.from((e.target as HTMLInputElement).files || [])));
fileInput.addEventListener('change', (e) =>
processFiles(Array.from((e.target as HTMLInputElement).files || []))
);
const setupAddMoreButton = () => {
const addMoreBtn = document.getElementById('add-more-btn');
if (addMoreBtn) {
addMoreBtn.addEventListener('click', () => fileInput.click());
}
};
const setupAddMoreButton = () => {
const addMoreBtn = document.getElementById('add-more-btn');
if (addMoreBtn) {
addMoreBtn.addEventListener('click', () => fileInput.click());
}
};
const setupClearButton = () => {
const clearBtn = document.getElementById('clear-files-btn');
if (clearBtn) {
clearBtn.addEventListener('click', () => {
state.files = [];
isFirstUpload = true;
(fileInput as HTMLInputElement).value = '';
const setupClearButton = () => {
const clearBtn = document.getElementById('clear-files-btn');
if (clearBtn) {
clearBtn.addEventListener('click', () => {
state.files = [];
isFirstUpload = true;
(fileInput as HTMLInputElement).value = '';
const fileDisplayArea = document.getElementById('file-display-area');
if (fileDisplayArea) fileDisplayArea.textContent = '';
const fileDisplayArea = document.getElementById('file-display-area');
if (fileDisplayArea) fileDisplayArea.textContent = '';
const fileControls = document.getElementById('file-controls');
if (fileControls) fileControls.classList.add('hidden');
const fileControls = document.getElementById('file-controls');
if (fileControls) fileControls.classList.add('hidden');
const toolSpecificUI = ['file-list', 'page-merge-preview', 'image-list', 'alternate-file-list'];
toolSpecificUI.forEach(id => {
const el = document.getElementById(id);
if (el) el.textContent = '';
});
const toolSpecificUI = [
'file-list',
'page-merge-preview',
'image-list',
'alternate-file-list',
];
toolSpecificUI.forEach((id) => {
const el = document.getElementById(id);
if (el) el.textContent = '';
});
const processBtn = document.getElementById('process-btn');
if (processBtn) (processBtn as HTMLButtonElement).disabled = true;
});
}
};
const processBtn = document.getElementById('process-btn');
if (processBtn) (processBtn as HTMLButtonElement).disabled = true;
});
}
};
setTimeout(() => {
setupAddMoreButton();
setupClearButton();
}, 100);
setTimeout(() => {
setupAddMoreButton();
setupClearButton();
}, 100);
}

View File

@@ -7,35 +7,35 @@ import { createIcons, icons } from 'lucide';
const SETUP_AFTER_UPLOAD = ['sign-pdf'];
export function setupToolInterface(toolId: any) {
window.scrollTo({
top: 0,
left: 0,
behavior: 'instant' as ScrollBehavior
});
state.activeTool = toolId;
dom.toolContent.innerHTML = toolTemplates[toolId]();
createIcons({icons});
switchView('tool');
window.scrollTo({
top: 0,
left: 0,
behavior: 'instant' as ScrollBehavior,
});
const fileInput = document.getElementById('file-input');
const processBtn = document.getElementById('process-btn');
state.activeTool = toolId;
dom.toolContent.innerHTML = toolTemplates[toolId]();
createIcons({ icons });
switchView('tool');
if (!fileInput && processBtn) {
const logic = toolLogic[toolId];
if (logic) {
const func = typeof logic.process === 'function' ? logic.process : logic;
processBtn.onclick = func;
}
const fileInput = document.getElementById('file-input');
const processBtn = document.getElementById('process-btn');
if (!fileInput && processBtn) {
const logic = toolLogic[toolId];
if (logic) {
const func = typeof logic.process === 'function' ? logic.process : logic;
processBtn.onclick = func;
}
}
if (toolLogic[toolId] && typeof toolLogic[toolId].setup === 'function') {
if (!SETUP_AFTER_UPLOAD.includes(toolId)) {
toolLogic[toolId].setup();
}
if (toolLogic[toolId] && typeof toolLogic[toolId].setup === 'function') {
if (!SETUP_AFTER_UPLOAD.includes(toolId)) {
toolLogic[toolId].setup();
}
}
if (fileInput) {
setupFileInputHandler(toolId);
}
}
if (fileInput) {
setupFileInputHandler(toolId);
}
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -3,115 +3,119 @@ import { dom, switchView, hideAlert } from './ui.js';
import { setupToolInterface } from './handlers/toolSelectionHandler.js';
import { createIcons, icons } from 'lucide';
import * as pdfjsLib from 'pdfjs-dist';
import "../css/styles.css";
import '../css/styles.css';
const init = () => {
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();
dom.toolGrid.textContent = '';
dom.toolGrid.textContent = '';
categories.forEach(category => {
const categoryGroup = document.createElement('div');
categoryGroup.className = 'category-group col-span-full';
categories.forEach((category) => {
const categoryGroup = document.createElement('div');
categoryGroup.className = 'category-group col-span-full';
const title = document.createElement('h2');
title.className = 'text-xl font-bold text-indigo-400 mb-4 mt-8 first:mt-0';
title.textContent = category.name;
const title = document.createElement('h2');
title.className = 'text-xl font-bold text-indigo-400 mb-4 mt-8 first:mt-0';
title.textContent = category.name;
const toolsContainer = document.createElement('div');
toolsContainer.className = 'grid grid-cols-2 sm:grid-cols-3 md:grid-cols-4 lg:grid-cols-5 gap-4 md:gap-6';
const toolsContainer = document.createElement('div');
toolsContainer.className =
'grid grid-cols-2 sm:grid-cols-3 md:grid-cols-4 lg:grid-cols-5 gap-4 md:gap-6';
category.tools.forEach(tool => {
const toolCard = document.createElement('div');
toolCard.className = 'tool-card bg-gray-800 rounded-xl p-4 cursor-pointer flex flex-col items-center justify-center text-center';
toolCard.dataset.toolId = tool.id;
category.tools.forEach((tool) => {
const toolCard = document.createElement('div');
toolCard.className =
'tool-card bg-gray-800 rounded-xl p-4 cursor-pointer flex flex-col items-center justify-center text-center';
toolCard.dataset.toolId = tool.id;
const icon = document.createElement('i');
icon.className = 'w-10 h-10 mb-3 text-indigo-400';
icon.setAttribute('data-lucide', tool.icon);
const icon = document.createElement('i');
icon.className = 'w-10 h-10 mb-3 text-indigo-400';
icon.setAttribute('data-lucide', tool.icon);
const toolName = document.createElement('h3');
toolName.className = 'font-semibold text-white';
toolName.textContent = tool.name;
const toolName = document.createElement('h3');
toolName.className = 'font-semibold text-white';
toolName.textContent = tool.name;
toolCard.append(icon, toolName);
toolCard.append(icon, toolName);
if (tool.subtitle) {
const toolSubtitle = document.createElement('p');
toolSubtitle.className = 'text-xs text-gray-400 mt-1 px-2';
toolSubtitle.textContent = tool.subtitle;
toolCard.appendChild(toolSubtitle);
}
if (tool.subtitle) {
const toolSubtitle = document.createElement('p');
toolSubtitle.className = 'text-xs text-gray-400 mt-1 px-2';
toolSubtitle.textContent = tool.subtitle;
toolCard.appendChild(toolSubtitle);
}
toolsContainer.appendChild(toolCard);
});
categoryGroup.append(title, toolsContainer);
dom.toolGrid.appendChild(categoryGroup);
toolsContainer.appendChild(toolCard);
});
const searchBar = document.getElementById('search-bar');
const categoryGroups = dom.toolGrid.querySelectorAll('.category-group');
categoryGroup.append(title, toolsContainer);
dom.toolGrid.appendChild(categoryGroup);
});
searchBar.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 = searchBar.value.toLowerCase().trim();
const searchBar = document.getElementById('search-bar');
const categoryGroups = dom.toolGrid.querySelectorAll('.category-group');
categoryGroups.forEach(group => {
const toolCards = group.querySelectorAll('.tool-card');
let visibleToolsInCategory = 0;
searchBar.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 = searchBar.value.toLowerCase().trim();
toolCards.forEach(card => {
const toolName = card.querySelector('h3').textContent.toLowerCase();
const toolSubtitle = card.querySelector('p')?.textContent.toLowerCase() || '';
const isMatch = toolName.includes(searchTerm) || toolSubtitle.includes(searchTerm);
categoryGroups.forEach((group) => {
const toolCards = group.querySelectorAll('.tool-card');
let visibleToolsInCategory = 0;
card.classList.toggle('hidden', !isMatch);
if (isMatch) {
visibleToolsInCategory++;
}
});
toolCards.forEach((card) => {
const toolName = card.querySelector('h3').textContent.toLowerCase();
const toolSubtitle =
card.querySelector('p')?.textContent.toLowerCase() || '';
const isMatch =
toolName.includes(searchTerm) || toolSubtitle.includes(searchTerm);
group.classList.toggle('hidden', visibleToolsInCategory === 0);
});
});
dom.toolGrid.addEventListener('click', (e) => {
// @ts-expect-error TS(2339) FIXME: Property 'closest' does not exist on type 'EventTa... Remove this comment to see the full error message
const card = e.target.closest('.tool-card');
if (card) {
const toolId = card.dataset.toolId;
setupToolInterface(toolId);
card.classList.toggle('hidden', !isMatch);
if (isMatch) {
visibleToolsInCategory++;
}
});
group.classList.toggle('hidden', visibleToolsInCategory === 0);
});
dom.backToGridBtn.addEventListener('click', () => switchView('grid'));
dom.alertOkBtn.addEventListener('click', hideAlert);
});
const faqAccordion = document.getElementById('faq-accordion');
if (faqAccordion) {
faqAccordion.addEventListener('click', (e) => {
// @ts-expect-error TS(2339) FIXME: Property 'closest' does not exist on type 'EventTa... Remove this comment to see the full error message
const questionButton = e.target.closest('.faq-question');
if (!questionButton) return;
const faqItem = questionButton.parentElement;
const answer = faqItem.querySelector('.faq-answer');
faqItem.classList.toggle('open');
if (faqItem.classList.contains('open')) {
answer.style.maxHeight = answer.scrollHeight + 'px';
} else {
answer.style.maxHeight = '0px';
}
});
dom.toolGrid.addEventListener('click', (e) => {
// @ts-expect-error TS(2339) FIXME: Property 'closest' does not exist on type 'EventTa... Remove this comment to see the full error message
const card = e.target.closest('.tool-card');
if (card) {
const toolId = card.dataset.toolId;
setupToolInterface(toolId);
}
});
dom.backToGridBtn.addEventListener('click', () => switchView('grid'));
dom.alertOkBtn.addEventListener('click', hideAlert);
createIcons({ icons });
console.log('Please share our tool and share the love!');
const faqAccordion = document.getElementById('faq-accordion');
if (faqAccordion) {
faqAccordion.addEventListener('click', (e) => {
// @ts-expect-error TS(2339) FIXME: Property 'closest' does not exist on type 'EventTa... Remove this comment to see the full error message
const questionButton = e.target.closest('.faq-question');
if (!questionButton) return;
const faqItem = questionButton.parentElement;
const answer = faqItem.querySelector('.faq-answer');
faqItem.classList.toggle('open');
if (faqItem.classList.contains('open')) {
answer.style.maxHeight = answer.scrollHeight + 'px';
} else {
answer.style.maxHeight = '0px';
}
});
}
createIcons({ icons });
console.log('Please share our tool and share the love!');
};
document.addEventListener('DOMContentLoaded', init);
document.addEventListener('DOMContentLoaded', init);

View File

@@ -1,17 +1,17 @@
export const state = {
activeTool: null,
files: [],
pdfDoc: null,
pdfPages: [],
currentPdfUrl: null,
activeTool: null,
files: [],
pdfDoc: null,
pdfPages: [],
currentPdfUrl: null,
};
// Resets the state when switching views or completing an operation.
export function resetState() {
state.activeTool = null;
state.files = [];
state.pdfDoc = null;
state.pdfPages = [];
state.currentPdfUrl = null;
document.getElementById('tool-content').innerHTML = '';
}
state.activeTool = null;
state.files = [];
state.pdfDoc = null;
state.pdfPages = [];
state.currentPdfUrl = null;
document.getElementById('tool-content').innerHTML = '';
}

View File

@@ -1,107 +1,104 @@
import { resetState } from './state.js';
import { formatBytes } from './utils/helpers.js';
import { tesseractLanguages } from './config/tesseract-languages.js';
import { icons, createIcons } from "lucide";
import { icons, createIcons } from 'lucide';
import Sortable from 'sortablejs';
// Centralizing DOM element selection
export const dom = {
gridView: document.getElementById('grid-view'),
toolGrid: document.getElementById('tool-grid'),
toolInterface: document.getElementById('tool-interface'),
toolContent: document.getElementById('tool-content'),
backToGridBtn: document.getElementById('back-to-grid'),
loaderModal: document.getElementById('loader-modal'),
loaderText: document.getElementById('loader-text'),
alertModal: document.getElementById('alert-modal'),
alertTitle: document.getElementById('alert-title'),
alertMessage: document.getElementById('alert-message'),
alertOkBtn: document.getElementById('alert-ok'),
heroSection: document.getElementById('hero-section'),
featuresSection: document.getElementById('features-section'),
toolsHeader: document.getElementById('tools-header'),
dividers: document.querySelectorAll('.section-divider'),
hideSections: document.querySelectorAll('.hide-section'),
gridView: document.getElementById('grid-view'),
toolGrid: document.getElementById('tool-grid'),
toolInterface: document.getElementById('tool-interface'),
toolContent: document.getElementById('tool-content'),
backToGridBtn: document.getElementById('back-to-grid'),
loaderModal: document.getElementById('loader-modal'),
loaderText: document.getElementById('loader-text'),
alertModal: document.getElementById('alert-modal'),
alertTitle: document.getElementById('alert-title'),
alertMessage: document.getElementById('alert-message'),
alertOkBtn: document.getElementById('alert-ok'),
heroSection: document.getElementById('hero-section'),
featuresSection: document.getElementById('features-section'),
toolsHeader: document.getElementById('tools-header'),
dividers: document.querySelectorAll('.section-divider'),
hideSections: document.querySelectorAll('.hide-section'),
};
export const showLoader = (text = 'Processing...') => {
dom.loaderText.textContent = text;
dom.loaderModal.classList.remove('hidden');
dom.loaderText.textContent = text;
dom.loaderModal.classList.remove('hidden');
};
export const hideLoader = () => dom.loaderModal.classList.add('hidden');
export const showAlert = (title: any, message: any) => {
dom.alertTitle.textContent = title;
dom.alertMessage.textContent = message;
dom.alertModal.classList.remove('hidden');
dom.alertTitle.textContent = title;
dom.alertMessage.textContent = message;
dom.alertModal.classList.remove('hidden');
};
export const hideAlert = () => dom.alertModal.classList.add('hidden');
export const switchView = (view: any) => {
if (view === 'grid') {
dom.gridView.classList.remove('hidden');
dom.toolInterface.classList.add('hidden');
// show hero and features and header
dom.heroSection.classList.remove('hidden');
dom.featuresSection.classList.remove('hidden');
dom.toolsHeader.classList.remove('hidden');
// show dividers
dom.dividers.forEach(divider => {
divider.classList.remove('hidden');
});
// show hideSections
dom.hideSections.forEach(section => {
section.classList.remove('hidden');
});
if (view === 'grid') {
dom.gridView.classList.remove('hidden');
dom.toolInterface.classList.add('hidden');
// show hero and features and header
dom.heroSection.classList.remove('hidden');
dom.featuresSection.classList.remove('hidden');
dom.toolsHeader.classList.remove('hidden');
// show dividers
dom.dividers.forEach((divider) => {
divider.classList.remove('hidden');
});
// show hideSections
dom.hideSections.forEach((section) => {
section.classList.remove('hidden');
});
resetState();
} else {
dom.gridView.classList.add('hidden');
dom.toolInterface.classList.remove('hidden');
dom.featuresSection.classList.add('hidden');
dom.heroSection.classList.add('hidden');
dom.toolsHeader.classList.add('hidden');
dom.dividers.forEach(divider => {
divider.classList.add('hidden');
});
dom.hideSections.forEach(section => {
section.classList.add('hidden');
});
}
resetState();
} else {
dom.gridView.classList.add('hidden');
dom.toolInterface.classList.remove('hidden');
dom.featuresSection.classList.add('hidden');
dom.heroSection.classList.add('hidden');
dom.toolsHeader.classList.add('hidden');
dom.dividers.forEach((divider) => {
divider.classList.add('hidden');
});
dom.hideSections.forEach((section) => {
section.classList.add('hidden');
});
}
};
const thumbnailState = {
sortableInstances: {}
sortableInstances: {},
};
function initializeOrganizeSortable(containerId: any) {
const container = document.getElementById(containerId);
if (!container) return;
if (thumbnailState.sortableInstances[containerId]) {
thumbnailState.sortableInstances[containerId].destroy();
}
thumbnailState.sortableInstances[containerId] = Sortable.create(container, {
animation: 150,
ghostClass: 'sortable-ghost',
chosenClass: 'sortable-chosen',
dragClass: 'sortable-drag',
filter: '.delete-page-btn',
preventOnFilter: true,
onStart: function(evt: any) {
evt.item.style.opacity = '0.5';
},
onEnd: function(evt: any) {
evt.item.style.opacity = '1';
}
});
}
const container = document.getElementById(containerId);
if (!container) return;
if (thumbnailState.sortableInstances[containerId]) {
thumbnailState.sortableInstances[containerId].destroy();
}
thumbnailState.sortableInstances[containerId] = Sortable.create(container, {
animation: 150,
ghostClass: 'sortable-ghost',
chosenClass: 'sortable-chosen',
dragClass: 'sortable-drag',
filter: '.delete-page-btn',
preventOnFilter: true,
onStart: function (evt: any) {
evt.item.style.opacity = '0.5';
},
onEnd: function (evt: any) {
evt.item.style.opacity = '1';
},
});
}
/**
* Renders page thumbnails for tools like 'Organize' and 'Rotate'.
@@ -109,97 +106,103 @@ function initializeOrganizeSortable(containerId: any) {
* @param {object} pdfDoc The loaded pdf-lib document instance.
*/
export const renderPageThumbnails = async (toolId: any, pdfDoc: any) => {
const containerId = toolId === 'organize' ? 'page-organizer' : 'page-rotator';
const container = document.getElementById(containerId);
if (!container) return;
const containerId = toolId === 'organize' ? 'page-organizer' : 'page-rotator';
const container = document.getElementById(containerId);
if (!container) return;
container.innerHTML = '';
showLoader('Rendering page previews...');
container.innerHTML = '';
showLoader('Rendering page previews...');
const pdfData = await pdfDoc.save();
// @ts-expect-error TS(2304) FIXME: Cannot find name 'pdfjsLib'.
const pdf = await pdfjsLib.getDocument({ data: pdfData }).promise;
const pdfData = await pdfDoc.save();
// @ts-expect-error TS(2304) FIXME: Cannot find name 'pdfjsLib'.
const pdf = await pdfjsLib.getDocument({ data: pdfData }).promise;
for (let i = 1; i <= pdf.numPages; i++) {
const page = await pdf.getPage(i);
const viewport = page.getViewport({ scale: 0.5 });
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;
for (let i = 1; i <= pdf.numPages; i++) {
const page = await pdf.getPage(i);
const viewport = page.getViewport({ scale: 0.5 });
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 wrapper = document.createElement('div');
wrapper.className = 'page-thumbnail relative group';
// @ts-expect-error TS(2322) FIXME: Type 'number' is not assignable to type 'string'.
wrapper.dataset.pageIndex = i - 1;
const wrapper = document.createElement('div');
wrapper.className = 'page-thumbnail relative group';
// @ts-expect-error TS(2322) FIXME: Type 'number' is not assignable to type 'string'.
wrapper.dataset.pageIndex = 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';
const img = document.createElement('img');
img.src = canvas.toDataURL();
img.className = 'max-w-full max-h-full object-contain';
imgContainer.appendChild(img);
imgContainer.appendChild(img);
if (toolId === 'organize') {
wrapper.className = 'page-thumbnail relative group';
wrapper.appendChild(imgContainer);
const pageNumSpan = document.createElement('span');
pageNumSpan.className = 'absolute top-1 left-1 bg-gray-900 bg-opacity-75 text-white text-xs rounded-full px-2 py-1';
pageNumSpan.textContent = i.toString();
const deleteBtn = document.createElement('button');
deleteBtn.className = 'delete-page-btn absolute top-1 right-1 bg-red-600 text-white rounded-full w-6 h-6 flex items-center justify-center';
deleteBtn.innerHTML = '&times;';
deleteBtn.addEventListener('click', (e) => {
(e.currentTarget as HTMLElement).parentElement.remove();
initializeOrganizeSortable(containerId);
});
wrapper.append(pageNumSpan, deleteBtn);
} else if (toolId === 'rotate') {
wrapper.className = 'page-rotator-item flex flex-col items-center gap-2';
wrapper.dataset.rotation = '0';
img.classList.add('transition-transform', 'duration-300');
wrapper.appendChild(imgContainer);
const controlsDiv = document.createElement('div');
controlsDiv.className = 'flex items-center justify-center gap-3 w-full';
const pageNumSpan = document.createElement('span');
pageNumSpan.className = 'font-medium text-sm text-white';
pageNumSpan.textContent = i.toString();
const rotateBtn = document.createElement('button');
rotateBtn.className = 'rotate-btn btn bg-gray-700 hover:bg-gray-600 p-2 rounded-full';
rotateBtn.title = 'Rotate 90°';
rotateBtn.innerHTML = '<i data-lucide="rotate-cw" class="w-5 h-5"></i>';
rotateBtn.addEventListener('click', (e) => {
e.stopPropagation();
const card = (e.currentTarget as HTMLElement).closest('.page-rotator-item') as HTMLElement;
const imgEl = card.querySelector('img');
let currentRotation = parseInt(card.dataset.rotation);
currentRotation = (currentRotation + 90) % 360;
card.dataset.rotation = currentRotation.toString();
imgEl.style.transform = `rotate(${currentRotation}deg)`;
});
controlsDiv.append(pageNumSpan, rotateBtn);
wrapper.appendChild(controlsDiv);
}
container.appendChild(wrapper);
createIcons({icons});
}
if (toolId === 'organize') {
wrapper.className = 'page-thumbnail relative group';
wrapper.appendChild(imgContainer);
const pageNumSpan = document.createElement('span');
pageNumSpan.className =
'absolute top-1 left-1 bg-gray-900 bg-opacity-75 text-white text-xs rounded-full px-2 py-1';
pageNumSpan.textContent = i.toString();
const deleteBtn = document.createElement('button');
deleteBtn.className =
'delete-page-btn absolute top-1 right-1 bg-red-600 text-white rounded-full w-6 h-6 flex items-center justify-center';
deleteBtn.innerHTML = '&times;';
deleteBtn.addEventListener('click', (e) => {
(e.currentTarget as HTMLElement).parentElement.remove();
initializeOrganizeSortable(containerId);
});
wrapper.append(pageNumSpan, deleteBtn);
} else if (toolId === 'rotate') {
wrapper.className = 'page-rotator-item flex flex-col items-center gap-2';
wrapper.dataset.rotation = '0';
img.classList.add('transition-transform', 'duration-300');
wrapper.appendChild(imgContainer);
const controlsDiv = document.createElement('div');
controlsDiv.className = 'flex items-center justify-center gap-3 w-full';
const pageNumSpan = document.createElement('span');
pageNumSpan.className = 'font-medium text-sm text-white';
pageNumSpan.textContent = i.toString();
const rotateBtn = document.createElement('button');
rotateBtn.className =
'rotate-btn btn bg-gray-700 hover:bg-gray-600 p-2 rounded-full';
rotateBtn.title = 'Rotate 90°';
rotateBtn.innerHTML = '<i data-lucide="rotate-cw" class="w-5 h-5"></i>';
rotateBtn.addEventListener('click', (e) => {
e.stopPropagation();
const card = (e.currentTarget as HTMLElement).closest(
'.page-rotator-item'
) as HTMLElement;
const imgEl = card.querySelector('img');
let currentRotation = parseInt(card.dataset.rotation);
currentRotation = (currentRotation + 90) % 360;
card.dataset.rotation = currentRotation.toString();
imgEl.style.transform = `rotate(${currentRotation}deg)`;
});
controlsDiv.append(pageNumSpan, rotateBtn);
wrapper.appendChild(controlsDiv);
}
hideLoader();
container.appendChild(wrapper);
createIcons({ icons });
}
if (toolId === 'organize') {
initializeOrganizeSortable(containerId);
}
hideLoader();
};
/**
@@ -208,35 +211,36 @@ export const renderPageThumbnails = async (toolId: any, pdfDoc: any) => {
* @param {File[]} files The array of file objects.
*/
export const renderFileDisplay = (container: any, files: any) => {
container.textContent = '';
if (files.length > 0) {
files.forEach((file: any) => {
const fileDiv = document.createElement('div');
fileDiv.className = 'flex items-center justify-between bg-gray-700 p-3 rounded-lg text-sm';
container.textContent = '';
if (files.length > 0) {
files.forEach((file: any) => {
const fileDiv = document.createElement('div');
fileDiv.className =
'flex items-center justify-between bg-gray-700 p-3 rounded-lg text-sm';
const nameSpan = document.createElement('span');
nameSpan.className = 'truncate font-medium text-gray-200';
nameSpan.textContent = file.name;
const nameSpan = document.createElement('span');
nameSpan.className = 'truncate font-medium text-gray-200';
nameSpan.textContent = file.name;
const sizeSpan = document.createElement('span');
sizeSpan.className = 'flex-shrink-0 ml-4 text-gray-400';
sizeSpan.textContent = formatBytes(file.size);
const sizeSpan = document.createElement('span');
sizeSpan.className = 'flex-shrink-0 ml-4 text-gray-400';
sizeSpan.textContent = formatBytes(file.size);
fileDiv.append(nameSpan, sizeSpan);
container.appendChild(fileDiv);
});
}
fileDiv.append(nameSpan, sizeSpan);
container.appendChild(fileDiv);
});
}
};
const createFileInputHTML = (options = {}) => {
// @ts-expect-error TS(2339) FIXME: Property 'multiple' does not exist on type '{}'.
const multiple = options.multiple ? 'multiple' : '';
// @ts-expect-error TS(2339) FIXME: Property 'accept' does not exist on type '{}'.
const acceptedFiles = options.accept || 'application/pdf';
// @ts-expect-error TS(2339) FIXME: Property 'showControls' does not exist on type '{}... Remove this comment to see the full error message
const showControls = options.showControls || false; // NEW: Add this parameter
// @ts-expect-error TS(2339) FIXME: Property 'multiple' does not exist on type '{}'.
const multiple = options.multiple ? 'multiple' : '';
// @ts-expect-error TS(2339) FIXME: Property 'accept' does not exist on type '{}'.
const acceptedFiles = options.accept || 'application/pdf';
// @ts-expect-error TS(2339) FIXME: Property 'showControls' does not exist on type '{}... Remove this comment to see the full error message
const showControls = options.showControls || false; // NEW: Add this parameter
return `
return `
<div id="drop-zone" class="relative flex flex-col items-center justify-center w-full h-48 border-2 border-dashed border-gray-600 rounded-xl cursor-pointer bg-gray-900 hover:bg-gray-700 transition-colors duration-300">
<div class="flex flex-col items-center justify-center pt-5 pb-6">
<i data-lucide="upload-cloud" class="w-10 h-10 mb-3 text-gray-400"></i>
@@ -247,7 +251,9 @@ const createFileInputHTML = (options = {}) => {
<input id="file-input" type="file" class="absolute top-0 left-0 w-full h-full opacity-0 cursor-pointer" ${multiple} accept="${acceptedFiles}">
</div>
${showControls ? `
${
showControls
? `
<!-- NEW: Add control buttons for multi-file uploads -->
<div id="file-controls" class="hidden mt-4 flex gap-3">
<button id="add-more-btn" class="btn bg-indigo-600 hover:bg-indigo-700 text-white font-semibold px-4 py-2 rounded-lg flex items-center gap-2">
@@ -257,12 +263,14 @@ const createFileInputHTML = (options = {}) => {
<i data-lucide="x"></i> Clear All
</button>
</div>
` : ''}
`
: ''
}
`;
};
export const toolTemplates = {
merge: () => `
merge: () => `
<h2 class="text-2xl font-bold text-white mb-4">Merge PDFs</h2>
<p class="mb-6 text-gray-400">Combine whole files, or select specific pages to merge into a new document.</p>
${createFileInputHTML({ multiple: true, showControls: true })}
@@ -300,7 +308,7 @@ export const toolTemplates = {
</div>
`,
split: () => `
split: () => `
<h2 class="text-2xl font-bold text-white mb-4">Split PDF</h2>
<p class="mb-6 text-gray-400">Extract pages from a PDF using various methods.</p>
${createFileInputHTML()}
@@ -370,7 +378,7 @@ export const toolTemplates = {
</div>
`,
encrypt: () => `
encrypt: () => `
<h2 class="text-2xl font-bold text-white mb-4">Encrypt PDF</h2>
<p class="mb-6 text-gray-400">Upload a PDF to create a new, password-protected version.</p>
${createFileInputHTML()}
@@ -388,7 +396,7 @@ export const toolTemplates = {
</div>
<canvas id="pdf-canvas" class="hidden"></canvas>
`,
decrypt: () => `
decrypt: () => `
<h2 class="text-2xl font-bold text-white mb-4">Decrypt PDF</h2>
<p class="mb-6 text-gray-400">Upload an encrypted PDF and provide its password to create an unlocked version.</p>
${createFileInputHTML()}
@@ -402,7 +410,7 @@ export const toolTemplates = {
</div>
<canvas id="pdf-canvas" class="hidden"></canvas>
`,
organize: () => `
organize: () => `
<h2 class="text-2xl font-bold text-white mb-4">Organize PDF</h2>
<p class="mb-6 text-gray-400">Reorder, rotate, or delete pages. Drag and drop pages to reorder them.</p>
${createFileInputHTML()}
@@ -411,7 +419,7 @@ export const toolTemplates = {
<button id="process-btn" class="btn-gradient w-full mt-6">Save Changes</button>
`,
rotate: () => `
rotate: () => `
<h2 class="text-2xl font-bold text-white mb-4">Rotate PDF</h2>
<p class="mb-6 text-gray-400">Rotate all or specific pages in a PDF document.</p>
${createFileInputHTML()}
@@ -436,7 +444,7 @@ export const toolTemplates = {
<button id="process-btn" class="btn-gradient w-full mt-6">Save Rotations</button>
`,
'add-page-numbers': () => `
'add-page-numbers': () => `
<h2 class="text-2xl font-bold text-white mb-4">Add Page Numbers</h2>
<p class="mb-6 text-gray-400">Add customizable page numbers to your PDF file.</p>
${createFileInputHTML()}
@@ -471,7 +479,7 @@ export const toolTemplates = {
</div>
<button id="process-btn" class="btn-gradient w-full mt-6">Add Page Numbers</button>
`,
'pdf-to-jpg': () => `
'pdf-to-jpg': () => `
<h2 class="text-2xl font-bold text-white mb-4">PDF to JPG</h2>
<p class="mb-6 text-gray-400">Convert each page of a PDF file into a high-quality JPG image.</p>
${createFileInputHTML()}
@@ -481,14 +489,14 @@ export const toolTemplates = {
<button id="process-btn" class="btn-gradient w-full mt-6">Download All as ZIP</button>
</div>
`,
'jpg-to-pdf': () => `
'jpg-to-pdf': () => `
<h2 class="text-2xl font-bold text-white mb-4">JPG to PDF</h2>
<p class="mb-6 text-gray-400">Convert one or more JPG images into a single PDF file.</p>
${createFileInputHTML({ multiple: true, accept: 'image/jpeg', showControls: true })}
<div id="file-display-area" class="mt-4 space-y-2"></div>
<button id="process-btn" class="btn-gradient w-full mt-6">Convert to PDF</button>
`,
'scan-to-pdf': () => `
'scan-to-pdf': () => `
<h2 class="text-2xl font-bold text-white mb-4">Scan to PDF</h2>
<p class="mb-6 text-gray-400">Use your device's camera to scan documents and save them as a PDF. On desktop, this will open a file picker.</p>
${createFileInputHTML({ accept: 'image/*' })}
@@ -496,7 +504,7 @@ export const toolTemplates = {
<button id="process-btn" class="btn-gradient w-full mt-6">Create PDF from Scans</button>
`,
crop: () => `
crop: () => `
<h2 class="text-2xl font-bold text-white mb-4">Crop PDF</h2>
<p class="mb-6 text-gray-400">Click and drag to select a crop area on any page. You can set different crop areas for each page.</p>
${createFileInputHTML()}
@@ -521,7 +529,7 @@ export const toolTemplates = {
<button id="process-btn" class="btn-gradient w-full mt-6">Apply Crop & Save PDF</button>
</div>
`,
compress: () => `
compress: () => `
<h2 class="text-2xl font-bold text-white mb-4">Compress PDF</h2>
<p class="mb-6 text-gray-400">Reduce file size by choosing the compression method that best suits your document.</p>
${createFileInputHTML()}
@@ -551,14 +559,14 @@ compress: () => `
<button id="process-btn" class="btn-gradient w-full mt-4" disabled>Compress PDF</button>
</div>
`,
'pdf-to-greyscale': () => `
'pdf-to-greyscale': () => `
<h2 class="text-2xl font-bold text-white mb-4">PDF to Greyscale</h2>
<p class="mb-6 text-gray-400">Convert all pages of a PDF to greyscale. This is done by rendering each page, applying a filter, and rebuilding the PDF.</p>
${createFileInputHTML()}
<div id="file-display-area" class="mt-4 space-y-2"></div>
<button id="process-btn" class="btn-gradient w-full mt-6">Convert to Greyscale</button>
`,
'pdf-to-zip': () => `
'pdf-to-zip': () => `
<h2 class="text-2xl font-bold text-white mb-4">Combine PDFs into ZIP</h2>
<p class="mb-6 text-gray-400">Select multiple PDF files to download them together in a single ZIP archive.</p>
${createFileInputHTML({ multiple: true, showControls: true })}
@@ -566,7 +574,7 @@ compress: () => `
<button id="process-btn" class="btn-gradient w-full mt-6">Create ZIP File</button>
`,
'edit-metadata': () => `
'edit-metadata': () => `
<h2 class="text-2xl font-bold text-white mb-4">Edit PDF Metadata</h2>
<p class="mb-6 text-gray-400">Modify the core metadata fields of your PDF. Leave a field blank to clear it.</p>
@@ -629,21 +637,21 @@ compress: () => `
<button id="process-btn" class="hidden btn-gradient w-full mt-6">Update Metadata & Download</button>
`,
'remove-metadata': () => `
'remove-metadata': () => `
<h2 class="text-2xl font-bold text-white mb-4">Remove PDF Metadata</h2>
<p class="mb-6 text-gray-400">Completely remove identifying metadata from your PDF.</p>
${createFileInputHTML()}
<div id="file-display-area" class="mt-4 space-y-2"></div>
<button id="process-btn" class="hidden mt-6 btn-gradient w-full">Remove Metadata & Download</button>
`,
flatten: () => `
flatten: () => `
<h2 class="text-2xl font-bold text-white mb-4">Flatten PDF</h2>
<p class="mb-6 text-gray-400">Make PDF forms and annotations non-editable by flattening them.</p>
${createFileInputHTML()}
<div id="file-display-area" class="mt-4 space-y-2"></div>
<button id="process-btn" class="hidden mt-6 btn-gradient w-full">Flatten PDF</button>
`,
'pdf-to-png': () => `
'pdf-to-png': () => `
<h2 class="text-2xl font-bold text-white mb-4">PDF to PNG</h2>
<p class="mb-6 text-gray-400">Convert each page of a PDF file into a high-quality PNG image.</p>
${createFileInputHTML()}
@@ -653,14 +661,14 @@ compress: () => `
<button id="process-btn" class="btn-gradient w-full">Download All as ZIP</button>
</div>
`,
'png-to-pdf': () => `
'png-to-pdf': () => `
<h2 class="text-2xl font-bold text-white mb-4">PNG to PDF</h2>
<p class="mb-6 text-gray-400">Convert one or more PNG images into a single PDF file.</p>
${createFileInputHTML({ multiple: true, accept: 'image/png', showControls: true })}
<div id="file-display-area" class="mt-4 space-y-2"></div>
<button id="process-btn" class="btn-gradient w-full mt-6">Convert to PDF</button>
`,
'pdf-to-webp': () => `
'pdf-to-webp': () => `
<h2 class="text-2xl font-bold text-white mb-4">PDF to WebP</h2>
<p class="mb-6 text-gray-400">Convert each page of a PDF file into a modern WebP image.</p>
${createFileInputHTML()}
@@ -670,14 +678,14 @@ compress: () => `
<button id="process-btn" class="btn-gradient w-full">Download All as ZIP</button>
</div>
`,
'webp-to-pdf': () => `
'webp-to-pdf': () => `
<h2 class="text-2xl font-bold text-white mb-4">WebP to PDF</h2>
<p class="mb-6 text-gray-400">Convert one or more WebP images into a single PDF file.</p>
${createFileInputHTML({ multiple: true, accept: 'image/webp', showControls: true })}
<div id="file-display-area" class="mt-4 space-y-2"></div>
<button id="process-btn" class="btn-gradient w-full mt-6">Convert to PDF</button>
`,
edit: () => `
edit: () => `
<h2 class="text-2xl font-bold text-white mb-4">PDF Studio</h2>
<p class="mb-6 text-gray-400">An all-in-one PDF workspace where you can annotate, draw, highlight, redact, add comments and shapes, take screenshots, and view PDFs.</p>
${createFileInputHTML()}
@@ -686,7 +694,7 @@ compress: () => `
<div id="embed-pdf-container" class="w-full h-full"></div>
</div>
`,
'delete-pages': () => `
'delete-pages': () => `
<h2 class="text-2xl font-bold text-white mb-4">Delete Pages</h2>
<p class="mb-6 text-gray-400">Remove specific pages or ranges of pages from your PDF file.</p>
${createFileInputHTML()}
@@ -698,7 +706,7 @@ compress: () => `
<button id="process-btn" class="btn-gradient w-full">Delete Pages & Download</button>
</div>
`,
'add-blank-page': () => `
'add-blank-page': () => `
<h2 class="text-2xl font-bold text-white mb-4">Add Blank Page</h2>
<p class="mb-6 text-gray-400">Insert a new blank page at a specific position in your document.</p>
${createFileInputHTML()}
@@ -710,7 +718,7 @@ compress: () => `
<button id="process-btn" class="btn-gradient w-full">Add Page & Download</button>
</div>
`,
'extract-pages': () => `
'extract-pages': () => `
<h2 class="text-2xl font-bold text-white mb-4">Extract Pages</h2>
<p class="mb-6 text-gray-400">Extract specific pages from a PDF into separate files. Your files will download in a ZIP archive.</p>
${createFileInputHTML()}
@@ -723,7 +731,7 @@ compress: () => `
</div>
`,
'add-watermark': () => `
'add-watermark': () => `
<h2 class="text-2xl font-bold text-white mb-4">Add Watermark</h2>
<p class="mb-6 text-gray-400">Apply a text or image watermark to every page of your PDF document.</p>
${createFileInputHTML()}
@@ -787,8 +795,7 @@ compress: () => `
<button id="process-btn" class="hidden btn-gradient w-full mt-6">Add Watermark & Download</button>
`,
'add-header-footer': () => `
'add-header-footer': () => `
<h2 class="text-2xl font-bold text-white mb-4">Add Header & Footer</h2>
<p class="mb-6 text-gray-400">Add custom text to the top and bottom margins of every page.</p>
${createFileInputHTML()}
@@ -846,8 +853,7 @@ compress: () => `
<button id="process-btn" class="hidden btn-gradient w-full mt-6">Apply Header & Footer</button>
`,
'image-to-pdf': () => `
'image-to-pdf': () => `
<h2 class="text-2xl font-bold text-white mb-4">Image to PDF Converter</h2>
<p class="mb-6 text-gray-400">Combine multiple images into a single PDF. Drag and drop to reorder.</p>
${createFileInputHTML({ multiple: true, accept: 'image/jpeg,image/png,image/webp', showControls: true })}
@@ -855,7 +861,7 @@ compress: () => `
<button id="process-btn" class="btn-gradient w-full mt-6">Convert to PDF</button>
`,
'change-permissions': () => `
'change-permissions': () => `
<h2 class="text-2xl font-bold text-white mb-4">Change PDF Permissions</h2>
<p class="mb-6 text-gray-400">Unlock a PDF and re-save it with new passwords and permissions.</p>
${createFileInputHTML()}
@@ -903,8 +909,7 @@ compress: () => `
<button id="process-btn" class="hidden btn-gradient w-full mt-6">Save New Permissions</button>
`,
'pdf-to-markdown': () => `
'pdf-to-markdown': () => `
<h2 class="text-2xl font-bold text-white mb-4">PDF to Markdown</h2>
<p class="mb-6 text-gray-400">Convert a PDF's text content into a structured Markdown file.</p>
${createFileInputHTML({ accept: '.pdf' })}
@@ -914,7 +919,7 @@ compress: () => `
</div>
<button id="process-btn" class="hidden btn-gradient w-full mt-6">Convert to Markdown</button>
`,
'txt-to-pdf': () => `
'txt-to-pdf': () => `
<h2 class="text-2xl font-bold text-white mb-4">Text to PDF</h2>
<p class="mb-6 text-gray-400">Type or paste your text below and convert it to a PDF with custom formatting.</p>
<textarea id="text-input" rows="12" class="w-full bg-gray-900 border border-gray-600 text-gray-300 rounded-lg p-2.5 font-sans" placeholder="Start typing here..."></textarea>
@@ -945,28 +950,28 @@ compress: () => `
</div>
<button id="process-btn" class="btn-gradient w-full mt-6">Create PDF</button>
`,
'invert-colors': () => `
'invert-colors': () => `
<h2 class="text-2xl font-bold text-white mb-4">Invert PDF Colors</h2>
<p class="mb-6 text-gray-400">Convert your PDF to a "dark mode" by inverting its colors. This works best on simple text and image documents.</p>
${createFileInputHTML()}
<div id="file-display-area" class="mt-4 space-y-2"></div>
<button id="process-btn" class="hidden btn-gradient w-full mt-6">Invert Colors & Download</button>
`,
'view-metadata': () => `
'view-metadata': () => `
<h2 class="text-2xl font-bold text-white mb-4">View PDF Metadata</h2>
<p class="mb-6 text-gray-400">Upload a PDF to view its internal properties, such as Title, Author, and Creation Date.</p>
${createFileInputHTML()}
<div id="file-display-area" class="mt-4 space-y-2"></div>
<div id="metadata-results" class="hidden mt-6 p-4 bg-gray-900 border border-gray-700 rounded-lg"></div>
`,
'reverse-pages': () => `
'reverse-pages': () => `
<h2 class="text-2xl font-bold text-white mb-4">Reverse PDF Pages</h2>
<p class="mb-6 text-gray-400">Flip the order of all pages in your document, making the last page the first.</p>
${createFileInputHTML()}
<div id="file-display-area" class="mt-4 space-y-2"></div>
<button id="process-btn" class="hidden btn-gradient w-full mt-6">Reverse & Download</button>
`,
'md-to-pdf': () => `
'md-to-pdf': () => `
<h2 class="text-2xl font-bold text-white mb-4">Markdown to PDF</h2>
<p class="mb-6 text-gray-400">Write in Markdown, select your formatting options, and get a high-quality, multi-page PDF. <br><strong class="text-gray-300">Note:</strong> Images linked from the web (e.g., https://...) require an internet connection to be rendered.</p>
<div class="grid grid-cols-1 md:grid-cols-3 gap-4 mb-6">
@@ -999,42 +1004,42 @@ compress: () => `
</div>
<button id="process-btn" class="btn-gradient w-full mt-6">Create PDF from Markdown</button>
`,
'svg-to-pdf': () => `
'svg-to-pdf': () => `
<h2 class="text-2xl font-bold text-white mb-4">SVG to PDF</h2>
<p class="mb-6 text-gray-400">Convert one or more SVG vector images into a single PDF file.</p>
${createFileInputHTML({ multiple: true, accept: 'image/svg+xml', showControls: true })}
<div id="file-display-area" class="mt-4 space-y-2"></div>
<button id="process-btn" class="btn-gradient w-full mt-6">Convert to PDF</button>
`,
'bmp-to-pdf': () => `
'bmp-to-pdf': () => `
<h2 class="text-2xl font-bold text-white mb-4">BMP to PDF</h2>
<p class="mb-6 text-gray-400">Convert one or more BMP images into a single PDF file.</p>
${createFileInputHTML({ multiple: true, accept: 'image/bmp', showControls: true })}
<div id="file-display-area" class="mt-4 space-y-2"></div>
<button id="process-btn" class="btn-gradient w-full mt-6">Convert to PDF</button>
`,
'heic-to-pdf': () => `
'heic-to-pdf': () => `
<h2 class="text-2xl font-bold text-white mb-4">HEIC to PDF</h2>
<p class="mb-6 text-gray-400">Convert one or more HEIC (High Efficiency) images from your iPhone or camera into a single PDF file.</p>
${createFileInputHTML({ multiple: true, accept: '.heic,.heif', showControls: true })}
<div id="file-display-area" class="mt-4 space-y-2"></div>
<button id="process-btn" class="btn-gradient w-full mt-6">Convert to PDF</button>
`,
'tiff-to-pdf': () => `
'tiff-to-pdf': () => `
<h2 class="text-2xl font-bold text-white mb-4">TIFF to PDF</h2>
<p class="mb-6 text-gray-400">Convert one or more single or multi-page TIFF images into a single PDF file.</p>
${createFileInputHTML({ multiple: true, accept: 'image/tiff', showControls: true })}
<div id="file-display-area" class="mt-4 space-y-2"></div>
<button id="process-btn" class="btn-gradient w-full mt-6">Convert to PDF</button>
`,
'pdf-to-bmp': () => `
'pdf-to-bmp': () => `
<h2 class="text-2xl font-bold text-white mb-4">PDF to BMP</h2>
<p class="mb-6 text-gray-400">Convert each page of a PDF file into a BMP image. Your files will be downloaded in a ZIP archive.</p>
${createFileInputHTML()}
<div id="file-display-area" class="mt-4 space-y-2"></div>
<button id="process-btn" class="btn-gradient w-full mt-6">Convert to BMP & Download ZIP</button>
`,
'pdf-to-tiff': () => `
'pdf-to-tiff': () => `
<h2 class="text-2xl font-bold text-white mb-4">PDF to TIFF</h2>
<p class="mb-6 text-gray-400">Convert each page of a PDF file into a high-quality TIFF image. Your files will be downloaded in a ZIP archive.</p>
${createFileInputHTML()}
@@ -1042,7 +1047,7 @@ compress: () => `
<button id="process-btn" class="btn-gradient w-full mt-6">Convert to TIFF & Download ZIP</button>
`,
'split-in-half': () => `
'split-in-half': () => `
<h2 class="text-2xl font-bold text-white mb-4">Split Pages in Half</h2>
<p class="mb-6 text-gray-400">Choose a method to divide every page of your document into two separate pages.</p>
${createFileInputHTML()}
@@ -1058,7 +1063,7 @@ compress: () => `
<button id="process-btn" class="btn-gradient w-full mt-6">Split PDF</button>
</div>
`,
'page-dimensions': () => `
'page-dimensions': () => `
<h2 class="text-2xl font-bold text-white mb-4">Analyze Page Dimensions</h2>
<p class="mb-6 text-gray-400">Upload a PDF to see the precise dimensions, standard size, and orientation of every page.</p>
${createFileInputHTML()}
@@ -1091,7 +1096,7 @@ compress: () => `
</div>
`,
'n-up': () => `
'n-up': () => `
<h2 class="text-2xl font-bold text-white mb-4">N-Up Page Arrangement</h2>
<p class="mb-6 text-gray-400">Combine multiple pages from your PDF onto a single sheet. This is great for creating booklets or proof sheets.</p>
${createFileInputHTML()}
@@ -1154,7 +1159,7 @@ compress: () => `
</div>
`,
'duplicate-organize': () => `
'duplicate-organize': () => `
<h2 class="text-2xl font-bold text-white mb-4">Page Manager</h2>
<p class="mb-6 text-gray-400">Drag pages to reorder them. Use the <i data-lucide="copy-plus" class="inline-block w-4 h-4 text-green-400"></i> icon to duplicate a page or the <i data-lucide="x-circle" class="inline-block w-4 h-4 text-red-400"></i> icon to delete it.</p>
${createFileInputHTML()}
@@ -1167,7 +1172,7 @@ compress: () => `
</div>
`,
'combine-single-page': () => `
'combine-single-page': () => `
<h2 class="text-2xl font-bold text-white mb-4">Combine to a Single Page</h2>
<p class="mb-6 text-gray-400">Stitch all pages of your PDF together vertically to create one continuous, scrollable page.</p>
${createFileInputHTML()}
@@ -1194,7 +1199,7 @@ compress: () => `
</div>
`,
'fix-dimensions': () => `
'fix-dimensions': () => `
<h2 class="text-2xl font-bold text-white mb-4">Standardize Page Dimensions</h2>
<p class="mb-6 text-gray-400">Convert all pages in your PDF to a uniform size. Choose a standard format or define a custom dimension.</p>
${createFileInputHTML()}
@@ -1270,7 +1275,7 @@ compress: () => `
</div>
`,
'change-background-color': () => `
'change-background-color': () => `
<h2 class="text-2xl font-bold text-white mb-4">Change Background Color</h2>
<p class="mb-6 text-gray-400">Select a new background color for every page of your PDF.</p>
${createFileInputHTML()}
@@ -1282,7 +1287,7 @@ compress: () => `
</div>
`,
'change-text-color': () => `
'change-text-color': () => `
<h2 class="text-2xl font-bold text-white mb-4">Change Text Color</h2>
<p class="mb-6 text-gray-400">Change the color of dark text in your PDF. This process converts pages to images, so text will not be selectable in the final file.</p>
${createFileInputHTML()}
@@ -1306,7 +1311,7 @@ compress: () => `
</div>
`,
'compare-pdfs': () => `
'compare-pdfs': () => `
<h2 class="text-2xl font-bold text-white mb-4">Compare PDFs</h2>
<p class="mb-6 text-gray-400">Upload two files to visually compare them using either an overlay or a side-by-side view.</p>
@@ -1357,7 +1362,7 @@ compress: () => `
</div>
`,
'ocr-pdf': () => `
'ocr-pdf': () => `
<h2 class="text-2xl font-bold text-white mb-4">OCR PDF</h2>
<p class="mb-6 text-gray-400">Convert scanned PDFs into searchable documents. Select one or more languages present in your file for the best results.</p>
${createFileInputHTML()}
@@ -1369,12 +1374,16 @@ compress: () => `
<div class="relative">
<input type="text" id="lang-search" class="w-full bg-gray-900 border border-gray-600 text-white rounded-lg p-2.5 mb-2" placeholder="Search for languages...">
<div id="lang-list" class="max-h-48 overflow-y-auto border border-gray-600 rounded-lg p-2 bg-gray-900">
${Object.entries(tesseractLanguages).map(([code, name]) => `
${Object.entries(tesseractLanguages)
.map(
([code, name]) => `
<label class="flex items-center gap-2 p-2 rounded-md hover:bg-gray-700 cursor-pointer">
<input type="checkbox" value="${code}" class="lang-checkbox w-4 h-4 rounded text-indigo-600 bg-gray-700 border-gray-600 focus:ring-indigo-500">
${name}
</label>
`).join('')}
`
)
.join('')}
</div>
</div>
<p class="text-xs text-gray-500 mt-1">Selected: <span id="selected-langs-display" class="font-semibold">None</span></p>
@@ -1433,7 +1442,7 @@ compress: () => `
</div>
`,
'word-to-pdf': () => `
'word-to-pdf': () => `
<h2 class="text-2xl font-bold text-white mb-4">Word to PDF Converter</h2>
<p class="mb-6 text-gray-400">Upload a .docx file to convert it into a high-quality PDF with selectable text. Complex layouts may not be perfectly preserved.</p>
@@ -1452,7 +1461,7 @@ compress: () => `
<button id="process-btn" class="btn-gradient w-full mt-6" disabled>Preview & Convert</button>
`,
'sign-pdf': () => `
'sign-pdf': () => `
<h2 class="text-2xl font-bold text-white mb-4">Sign PDF</h2>
<p class="mb-6 text-gray-400">Create your signature, select it, then click on the document to place. You can drag to move placed signatures.</p>
${createFileInputHTML()}
@@ -1543,7 +1552,7 @@ compress: () => `
<button id="process-btn" class="hidden btn-gradient w-full mt-6">Apply Signatures & Download PDF</button>
`,
'remove-annotations': () => `
'remove-annotations': () => `
<h2 class="text-2xl font-bold text-white mb-4">Remove Annotations</h2>
<p class="mb-6 text-gray-400">Select the types of annotations to remove from all pages or a specific range.</p>
${createFileInputHTML()}
@@ -1602,8 +1611,7 @@ compress: () => `
<button id="process-btn" class="hidden btn-gradient w-full mt-6">Remove Selected Annotations</button>
`,
cropper: () => `
cropper: () => `
<h2 class="text-2xl font-bold text-white mb-4">PDF Cropper</h2>
<p class="mb-6 text-gray-400">Upload a PDF to visually crop one or more pages. This tool offers a live preview and two distinct cropping modes.</p>
@@ -1647,7 +1655,7 @@ cropper: () => `
</div>
`,
'form-filler': () => `
'form-filler': () => `
<h2 class="text-2xl font-bold text-white mb-4">PDF Form Filler</h2>
<p class="mb-6 text-gray-400">Upload a PDF to fill in existing form fields. The PDF view on the right will update as you type.</p>
${createFileInputHTML()}
@@ -1694,7 +1702,7 @@ cropper: () => `
</div>
`,
posterize: () => `
posterize: () => `
<h2 class="text-2xl font-bold text-white mb-4">Posterize PDF</h2>
<p class="mb-6 text-gray-400">Split pages into multiple smaller sheets to print as a poster. Navigate the preview and see the grid update based on your settings.</p>
${createFileInputHTML()}
@@ -1794,7 +1802,7 @@ posterize: () => `
</div>
`,
'remove-blank-pages': () => `
'remove-blank-pages': () => `
<h2 class="text-2xl font-bold text-white mb-4">Remove Blank Pages</h2>
<p class="mb-6 text-gray-400">Automatically detect and remove blank or nearly blank pages from your PDF. Adjust the sensitivity to control what is considered "blank".</p>
${createFileInputHTML()}
@@ -1819,7 +1827,7 @@ posterize: () => `
</div>
`,
'alternate-merge': () => `
'alternate-merge': () => `
<h2 class="text-2xl font-bold text-white mb-4">Alternate & Mix Pages</h2>
<p class="mb-6 text-gray-400">Combine pages from 2 or more documents, alternating between them. Drag the files to set the mixing order (e.g., Page 1 from Doc A, Page 1 from Doc B, Page 2 from Doc A, Page 2 from Doc B, etc.).</p>
${createFileInputHTML({ multiple: true, accept: 'application/pdf', showControls: true })}
@@ -1836,5 +1844,4 @@ posterize: () => `
<button id="process-btn" class="btn-gradient w-full mt-6" disabled>Alternate & Mix PDFs</button>
</div>
`,
};
};

View File

@@ -1,114 +1,125 @@
const STANDARD_SIZES = {
'A4': { width: 595.28, height: 841.89 },
'Letter': { width: 612, height: 792 },
'Legal': { width: 612, height: 1008 },
'Tabloid': { width: 792, height: 1224 },
'A3': { width: 841.89, height: 1190.55 },
'A5': { width: 419.53, height: 595.28 },
A4: { width: 595.28, height: 841.89 },
Letter: { width: 612, height: 792 },
Legal: { width: 612, height: 1008 },
Tabloid: { width: 792, height: 1224 },
A3: { width: 841.89, height: 1190.55 },
A5: { width: 419.53, height: 595.28 },
};
export function getStandardPageName(width: any, height: any) {
const tolerance = 1; // Allow for minor floating point variations
for (const [name, size] of Object.entries(STANDARD_SIZES)) {
if ((Math.abs(width - size.width) < tolerance && Math.abs(height - size.height) < tolerance) ||
(Math.abs(width - size.height) < tolerance && Math.abs(height - size.width) < tolerance)) {
return name;
}
const tolerance = 1; // Allow for minor floating point variations
for (const [name, size] of Object.entries(STANDARD_SIZES)) {
if (
(Math.abs(width - size.width) < tolerance &&
Math.abs(height - size.height) < tolerance) ||
(Math.abs(width - size.height) < tolerance &&
Math.abs(height - size.width) < tolerance)
) {
return name;
}
return 'Custom';
}
return 'Custom';
}
export function convertPoints(points: any, unit: any) {
let result = 0;
switch (unit) {
case 'in':
result = points / 72;
break;
case 'mm':
result = (points / 72) * 25.4;
break;
case 'px':
result = points * (96 / 72); // Assuming 96 DPI
break;
default: // 'pt'
result = points;
break;
}
return result.toFixed(2);
let result = 0;
switch (unit) {
case 'in':
result = points / 72;
break;
case 'mm':
result = (points / 72) * 25.4;
break;
case 'px':
result = points * (96 / 72); // Assuming 96 DPI
break;
default: // 'pt'
result = points;
break;
}
return result.toFixed(2);
}
export const hexToRgb = (hex: any) => {
const result = /^#?([a-f\d]{2})([a-f\d]{2})([a-f\d]{2})$/i.exec(hex);
return result ? {
const result = /^#?([a-f\d]{2})([a-f\d]{2})([a-f\d]{2})$/i.exec(hex);
return result
? {
r: parseInt(result[1], 16) / 255,
g: parseInt(result[2], 16) / 255,
b: parseInt(result[3], 16) / 255,
} : { r: 0, g: 0, b: 0 }; // Default to black
}
: { r: 0, g: 0, b: 0 }; // Default to black
};
export const formatBytes = (bytes: any, decimals = 1) => {
if (bytes === 0) return '0 Bytes';
const k = 1024;
const dm = decimals < 0 ? 0 : decimals;
const sizes = ['Bytes', 'KB', 'MB', 'GB', 'TB'];
const i = Math.floor(Math.log(bytes) / Math.log(k));
return parseFloat((bytes / Math.pow(k, i)).toFixed(dm)) + ' ' + sizes[i];
if (bytes === 0) return '0 Bytes';
const k = 1024;
const dm = decimals < 0 ? 0 : decimals;
const sizes = ['Bytes', 'KB', 'MB', 'GB', 'TB'];
const i = Math.floor(Math.log(bytes) / Math.log(k));
return parseFloat((bytes / Math.pow(k, i)).toFixed(dm)) + ' ' + sizes[i];
};
export const downloadFile = (blob: any, filename: any) => {
const url = URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url;
a.download = filename;
document.body.appendChild(a);
a.click();
document.body.removeChild(a);
URL.revokeObjectURL(url);
const url = URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url;
a.download = filename;
document.body.appendChild(a);
a.click();
document.body.removeChild(a);
URL.revokeObjectURL(url);
};
export const readFileAsArrayBuffer = (file: any) => {
return new Promise((resolve, reject) => {
const reader = new FileReader();
reader.onload = () => resolve(reader.result);
reader.onerror = (error) => reject(error);
reader.readAsArrayBuffer(file);
});
return new Promise((resolve, reject) => {
const reader = new FileReader();
reader.onload = () => resolve(reader.result);
reader.onerror = (error) => reject(error);
reader.readAsArrayBuffer(file);
});
};
export function parsePageRanges(rangeString: any, totalPages: any) {
if (!rangeString || rangeString.trim() === '') {
return Array.from({ length: totalPages }, (_, i) => i);
if (!rangeString || rangeString.trim() === '') {
return Array.from({ length: totalPages }, (_, i) => i);
}
const indices = new Set();
const parts = rangeString.split(',');
for (const part of parts) {
const trimmedPart = part.trim();
if (!trimmedPart) continue;
if (trimmedPart.includes('-')) {
const [start, end] = trimmedPart.split('-').map(Number);
if (
isNaN(start) ||
isNaN(end) ||
start < 1 ||
end > totalPages ||
start > end
) {
console.warn(`Invalid range skipped: ${trimmedPart}`);
continue;
}
for (let i = start; i <= end; i++) {
indices.add(i - 1);
}
} else {
const pageNum = Number(trimmedPart);
if (isNaN(pageNum) || pageNum < 1 || pageNum > totalPages) {
console.warn(`Invalid page number skipped: ${trimmedPart}`);
continue;
}
indices.add(pageNum - 1);
}
}
const indices = new Set();
const parts = rangeString.split(',');
for (const part of parts) {
const trimmedPart = part.trim();
if (!trimmedPart) continue;
if (trimmedPart.includes('-')) {
const [start, end] = trimmedPart.split('-').map(Number);
if (isNaN(start) || isNaN(end) || start < 1 || end > totalPages || start > end) {
console.warn(`Invalid range skipped: ${trimmedPart}`);
continue;
}
for (let i = start; i <= end; i++) {
indices.add(i - 1);
}
} else {
const pageNum = Number(trimmedPart);
if (isNaN(pageNum) || pageNum < 1 || pageNum > totalPages) {
console.warn(`Invalid page number skipped: ${trimmedPart}`);
continue;
}
indices.add(pageNum - 1);
}
}
// @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);
}

View File

@@ -4,7 +4,10 @@ import { PDFDocument } from 'pdf-lib';
import Sortable from 'sortablejs';
import * as helpers from '@/js/utils/helpers';
import * as ui from '@/js/ui';
import { setupAlternateMergeTool, alternateMerge } from '@/js/logic/alternate-merge';
import {
setupAlternateMergeTool,
alternateMerge,
} from '@/js/logic/alternate-merge';
vi.mock('pdf-lib', () => ({
PDFDocument: {
@@ -47,7 +50,9 @@ describe('Alternate Merge Tool', () => {
mockPdfDoc1 = { getPageCount: vi.fn(() => 2) };
mockPdfDoc2 = { getPageCount: vi.fn(() => 3) };
vi.mocked(helpers.readFileAsArrayBuffer).mockResolvedValue(new ArrayBuffer(8));
vi.mocked(helpers.readFileAsArrayBuffer).mockResolvedValue(
new ArrayBuffer(8)
);
vi.mocked(PDFDocument.load)
.mockResolvedValueOnce(mockPdfDoc1)
.mockResolvedValueOnce(mockPdfDoc2);
@@ -66,16 +71,18 @@ describe('Alternate Merge Tool', () => {
expect(ui.showLoader).toHaveBeenCalledWith('Loading PDF documents...');
expect(ui.hideLoader).toHaveBeenCalled();
expect(PDFDocument.load).toHaveBeenCalledTimes(2);
expect(document.querySelectorAll('#alternate-file-list li').length).toBe(2);
expect(document.querySelectorAll('#alternate-file-list li').length).toBe(
2
);
expect(Sortable.create).toHaveBeenCalled();
});
it('should show alert on load failure', async () => {
vi.mocked(PDFDocument.load).mockReset();
vi.mocked(PDFDocument.load).mockRejectedValueOnce(new Error('bad pdf'));
await setupAlternateMergeTool();
expect(ui.showAlert).toHaveBeenCalledWith(
'Error',
expect.stringContaining('Failed to load one or more PDF files')
@@ -90,12 +97,12 @@ describe('Alternate Merge Tool', () => {
state.files = [new File(['dummy1'], 'file1.pdf')];
vi.mocked(PDFDocument.load).mockReset();
vi.mocked(PDFDocument.load).mockResolvedValueOnce(mockPdfDoc1);
await setupAlternateMergeTool();
vi.clearAllMocks(); // Clear the setup calls
await alternateMerge();
expect(ui.showAlert).toHaveBeenCalledWith(
'Not Enough Files',
expect.stringContaining('Please upload at least two PDF files')
@@ -106,7 +113,7 @@ describe('Alternate Merge Tool', () => {
// First setup the tool to populate internal state
await setupAlternateMergeTool();
vi.clearAllMocks(); // Clear setup calls
const mockCopyPages = vi.fn(() =>
Promise.resolve([{ page: 'mockPage' }] as any)
);
@@ -129,12 +136,17 @@ describe('Alternate Merge Tool', () => {
await alternateMerge();
expect(ui.showLoader).toHaveBeenCalledWith(expect.stringContaining('Alternating'));
expect(ui.showLoader).toHaveBeenCalledWith(
expect.stringContaining('Alternating')
);
expect(mockCopyPages).toHaveBeenCalled();
expect(mockAddPage).toHaveBeenCalled();
expect(mockSave).toHaveBeenCalled();
expect(helpers.downloadFile).toHaveBeenCalled();
expect(ui.showAlert).toHaveBeenCalledWith('Success', expect.stringContaining('mixed successfully'));
expect(ui.showAlert).toHaveBeenCalledWith(
'Success',
expect.stringContaining('mixed successfully')
);
expect(ui.hideLoader).toHaveBeenCalled();
});
@@ -142,7 +154,7 @@ describe('Alternate Merge Tool', () => {
// Setup the tool first to populate internal state with 2 PDFs
await setupAlternateMergeTool();
vi.clearAllMocks(); // Clear setup calls
// Mock PDFDocument.create to reject
vi.mocked(PDFDocument.create).mockRejectedValue(new Error('broken'));
@@ -155,4 +167,4 @@ describe('Alternate Merge Tool', () => {
expect(ui.hideLoader).toHaveBeenCalled();
});
});
});
});

View File

@@ -211,4 +211,4 @@ describe('helpers', () => {
expect(result).toEqual([0, 4]);
});
});
});
});

View File

@@ -1,8 +1,11 @@
import { describe, it, expect } from 'vitest';
import { singlePdfLoadTools, simpleTools, multiFileTools } from '@/js/config/pdf-tools';
import {
singlePdfLoadTools,
simpleTools,
multiFileTools,
} from '@/js/config/pdf-tools';
describe('Tool Configuration Arrays', () => {
// --- Tests for singlePdfLoadTools ---
describe('singlePdfLoadTools', () => {
it('should be an array of non-empty strings', () => {
@@ -30,7 +33,7 @@ describe('Tool Configuration Arrays', () => {
it('should be an array of non-empty strings', () => {
expect(Array.isArray(simpleTools)).toBe(true);
expect(simpleTools.length).toBeGreaterThan(0);
simpleTools.forEach(tool => {
simpleTools.forEach((tool) => {
expect(typeof tool).toBe('string');
expect(tool.length).toBeGreaterThan(0);
});
@@ -51,7 +54,7 @@ describe('Tool Configuration Arrays', () => {
it('should be an array of non-empty strings', () => {
expect(Array.isArray(multiFileTools)).toBe(true);
expect(multiFileTools.length).toBeGreaterThan(0);
multiFileTools.forEach(tool => {
multiFileTools.forEach((tool) => {
expect(typeof tool).toBe('string');
expect(tool.length).toBeGreaterThan(0);
});
@@ -82,5 +85,4 @@ describe('Tool Configuration Arrays', () => {
expect(uniqueTools.size).toBe(allTools.length);
});
});
});
});

View File

@@ -1,5 +1,8 @@
import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
import { setupRemoveBlankPagesTool, removeBlankPages } from '@/js/logic/remove-blank-pages';
import {
setupRemoveBlankPagesTool,
removeBlankPages,
} from '@/js/logic/remove-blank-pages';
import * as ui from '@/js/ui';
import * as helpers from '@/js/utils/helpers';
import { state } from '@/js/state';
@@ -11,7 +14,12 @@ if (typeof ImageData === 'undefined') {
width: number;
height: number;
colorSpace: string;
constructor(data: Uint8ClampedArray, width: number, height: number, options?: { colorSpace: string }) {
constructor(
data: Uint8ClampedArray,
width: number,
height: number,
options?: { colorSpace: string }
) {
this.data = data;
this.width = width;
this.height = height;
@@ -27,11 +35,17 @@ const mockContext: CanvasRenderingContext2D = {
} as unknown as CanvasRenderingContext2D;
HTMLCanvasElement.prototype.getContext = vi.fn().mockReturnValue(mockContext);
HTMLCanvasElement.prototype.toDataURL = vi.fn().mockReturnValue('data:image/png;base64,mock');
HTMLCanvasElement.prototype.toDataURL = vi
.fn()
.mockReturnValue('data:image/png;base64,mock');
function createMockPage(isBlank: boolean) {
return {
getViewport: vi.fn(({ scale }) => ({ width: 800 * scale, height: 600 * scale, scale })),
getViewport: vi.fn(({ scale }) => ({
width: 800 * scale,
height: 600 * scale,
scale,
})),
render: vi.fn(() => {
// Return ImageData depending on blank/content
mockContext.getImageData = vi.fn(() => {
@@ -187,4 +201,4 @@ describe('Remove Blank Pages Tool', () => {
);
expect(helpers.downloadFile).not.toHaveBeenCalled();
});
});
});

View File

@@ -13,7 +13,7 @@ global.ResizeObserver = vi.fn().mockImplementation(() => ({
Object.defineProperty(window, 'matchMedia', {
writable: true,
value: vi.fn().mockImplementation(query => ({
value: vi.fn().mockImplementation((query) => ({
matches: false,
media: query,
onchange: null,
@@ -29,4 +29,4 @@ global.IntersectionObserver = vi.fn().mockImplementation(() => ({
observe: vi.fn(),
unobserve: vi.fn(),
disconnect: vi.fn(),
}));
}));

View File

@@ -2,7 +2,6 @@ import { state, resetState } from '@/js/state';
import { describe, it, expect, beforeEach } from 'vitest';
describe('State Management', () => {
// Test the initial state on import
describe('Initial State', () => {
it('should have the correct initial values', () => {
@@ -16,7 +15,6 @@ describe('State Management', () => {
// Test the resetState function
describe('resetState function', () => {
// Before each test in this block, we'll "dirty" the state
// to ensure the reset function is actually doing something.
beforeEach(() => {
@@ -29,7 +27,8 @@ describe('State Management', () => {
// 2. Create the DOM element that the function interacts with
// The setup.ts file will clean this up automatically after each test.
document.body.innerHTML = '<div id="tool-content">Some old tool content</div>';
document.body.innerHTML =
'<div id="tool-content">Some old tool content</div>';
});
it('should reset all state properties to their initial values', () => {
@@ -57,4 +56,4 @@ describe('State Management', () => {
expect(toolContentElement?.innerHTML).toBe('');
});
});
});
});

View File

@@ -4,7 +4,6 @@ import { categories } from '@/js/config/tools';
import { describe, it, expect } from 'vitest';
describe('Tool Categories Configuration', () => {
// 1. Basic Structure and Type Checking
it('should be an array of category objects', () => {
expect(Array.isArray(categories)).toBe(true);
@@ -13,7 +12,6 @@ describe('Tool Categories Configuration', () => {
// 2. Loop through each category to perform specific checks
describe.each(categories)('Category: "$name"', (category) => {
// Check that the category object itself is well-formed
it('should have a non-empty "name" string and a non-empty "tools" array', () => {
expect(typeof category.name).toBe('string');
@@ -24,7 +22,7 @@ describe('Tool Categories Configuration', () => {
// **KEY CHANGE**: This test now ensures IDs are unique only WITHIN this specific category.
it('should not contain any duplicate tool IDs within its own list', () => {
const toolIds = category.tools.map(tool => tool.id);
const toolIds = category.tools.map((tool) => tool.id);
const uniqueToolIds = new Set(toolIds);
// This assertion checks for duplicates inside THIS category only.
@@ -49,4 +47,4 @@ describe('Tool Categories Configuration', () => {
});
});
});
});
});