feat(rotate,delete-pages): Add custom rotation angles and enhance page management UI
- Add TypeScript type definitions for merge and alternate-merge Web Workers - Implement custom rotation angle input with increment/decrement controls for rotate tool - Extend delete-pages tool to render page thumbnails for better UX - Hide number input spin buttons across all number inputs via CSS - Refactor rotateAll function to accept angle parameter instead of direction multiplier - Update fileHandler to support delete-pages tool thumbnail rendering - Improve type safety in alternate-merge logic with proper interface definitions - Enhance rotate tool UI with custom angle input field and adjustment buttons
This commit is contained in:
@@ -3,9 +3,14 @@ import { downloadFile, readFileAsArrayBuffer, getPDFDocument } from '../utils/he
|
||||
import { state } from '../state.js';
|
||||
import Sortable from 'sortablejs';
|
||||
|
||||
const alternateMergeState = {
|
||||
pdfDocs: {} as Record<string, any>,
|
||||
pdfBytes: {} as Record<string, ArrayBuffer>,
|
||||
interface AlternateMergeState {
|
||||
pdfDocs: Record<string, any>;
|
||||
pdfBytes: Record<string, ArrayBuffer>;
|
||||
}
|
||||
|
||||
const alternateMergeState: AlternateMergeState = {
|
||||
pdfDocs: {},
|
||||
pdfBytes: {},
|
||||
};
|
||||
|
||||
const alternateMergeWorker = new Worker('/workers/alternate-merge.worker.js');
|
||||
@@ -98,7 +103,7 @@ export async function alternateMerge() {
|
||||
(li) => (li as HTMLElement).dataset.fileName
|
||||
).filter(Boolean) as string[];
|
||||
|
||||
const filesToMerge: { name: string; data: ArrayBuffer }[] = [];
|
||||
const filesToMerge: InterleaveFile[] = [];
|
||||
for (const name of sortedFileNames) {
|
||||
const bytes = alternateMergeState.pdfBytes[name];
|
||||
if (bytes) {
|
||||
@@ -112,12 +117,14 @@ export async function alternateMerge() {
|
||||
return;
|
||||
}
|
||||
|
||||
alternateMergeWorker.postMessage({
|
||||
const message: InterleaveMessage = {
|
||||
command: 'interleave',
|
||||
files: filesToMerge
|
||||
}, filesToMerge.map(f => f.data));
|
||||
};
|
||||
|
||||
alternateMergeWorker.onmessage = (e) => {
|
||||
alternateMergeWorker.postMessage(message, filesToMerge.map(f => f.data));
|
||||
|
||||
alternateMergeWorker.onmessage = (e: MessageEvent<InterleaveResponse>) => {
|
||||
hideLoader();
|
||||
if (e.data.status === 'success') {
|
||||
const blob = new Blob([e.data.pdfBytes], { type: 'application/pdf' });
|
||||
|
||||
@@ -68,3 +68,44 @@ export async function deletePages() {
|
||||
hideLoader();
|
||||
}
|
||||
}
|
||||
|
||||
export function setupDeletePagesTool() {
|
||||
const input = document.getElementById('pages-to-delete') as HTMLInputElement;
|
||||
if (!input) return;
|
||||
|
||||
const updateHighlights = () => {
|
||||
const val = input.value;
|
||||
const pagesToDelete = new Set<number>();
|
||||
|
||||
const parts = val.split(',');
|
||||
for (const part of parts) {
|
||||
const trimmed = part.trim();
|
||||
if (trimmed.includes('-')) {
|
||||
const [start, end] = trimmed.split('-').map(Number);
|
||||
if (!isNaN(start) && !isNaN(end) && start <= end) {
|
||||
for (let i = start; i <= end; i++) pagesToDelete.add(i);
|
||||
}
|
||||
} else {
|
||||
const num = Number(trimmed);
|
||||
if (!isNaN(num)) pagesToDelete.add(num);
|
||||
}
|
||||
}
|
||||
|
||||
const thumbnails = document.querySelectorAll('#delete-pages-preview .page-thumbnail');
|
||||
thumbnails.forEach((thumb) => {
|
||||
const pageNum = parseInt((thumb as HTMLElement).dataset.pageNumber || '0');
|
||||
const innerContainer = thumb.querySelector('div.relative');
|
||||
|
||||
if (pagesToDelete.has(pageNum)) {
|
||||
innerContainer?.classList.add('border-red-500');
|
||||
innerContainer?.classList.remove('border-gray-600');
|
||||
} else {
|
||||
innerContainer?.classList.remove('border-red-500');
|
||||
innerContainer?.classList.add('border-gray-600');
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
input.addEventListener('input', updateHighlights);
|
||||
updateHighlights();
|
||||
}
|
||||
|
||||
@@ -18,7 +18,7 @@ import { pdfToPng } from './pdf-to-png.js';
|
||||
import { pngToPdf } from './png-to-pdf.js';
|
||||
import { pdfToWebp } from './pdf-to-webp.js';
|
||||
import { webpToPdf } from './webp-to-pdf.js';
|
||||
import { deletePages } from './delete-pages.js';
|
||||
import { deletePages, setupDeletePagesTool } from './delete-pages.js';
|
||||
import { addBlankPage } from './add-blank-page.js';
|
||||
import { extractPages } from './extract-pages.js';
|
||||
import { addWatermark, setupWatermarkUI } from './add-watermark.js';
|
||||
@@ -92,7 +92,7 @@ export const toolLogic = {
|
||||
'png-to-pdf': pngToPdf,
|
||||
'pdf-to-webp': pdfToWebp,
|
||||
'webp-to-pdf': webpToPdf,
|
||||
'delete-pages': deletePages,
|
||||
'delete-pages': { process: deletePages, setup: setupDeletePagesTool },
|
||||
'add-blank-page': addBlankPage,
|
||||
'extract-pages': extractPages,
|
||||
'add-watermark': { process: addWatermark, setup: setupWatermarkUI },
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { showLoader, hideLoader, showAlert } from '../ui.ts';
|
||||
import { downloadFile, readFileAsArrayBuffer, getPDFDocument } from '../utils/helpers.ts';
|
||||
import { state } from '../state.ts';
|
||||
import { renderPagesProgressively, cleanupLazyRendering, createPlaceholder } from '../utils/render-utils.ts';
|
||||
import { renderPagesProgressively, cleanupLazyRendering } from '../utils/render-utils.ts';
|
||||
|
||||
import { createIcons, icons } from 'lucide';
|
||||
import * as pdfjsLib from 'pdfjs-dist';
|
||||
@@ -9,9 +9,22 @@ import Sortable from 'sortablejs';
|
||||
|
||||
pdfjsLib.GlobalWorkerOptions.workerSrc = new URL('pdfjs-dist/build/pdf.worker.min.mjs', import.meta.url).toString();
|
||||
|
||||
const mergeState = {
|
||||
pdfDocs: {} as Record<string, any>,
|
||||
pdfBytes: {} as Record<string, ArrayBuffer>,
|
||||
interface MergeState {
|
||||
pdfDocs: Record<string, any>;
|
||||
pdfBytes: Record<string, ArrayBuffer>;
|
||||
activeMode: 'file' | 'page';
|
||||
sortableInstances: {
|
||||
fileList?: Sortable;
|
||||
pageThumbnails?: Sortable;
|
||||
};
|
||||
isRendering: boolean;
|
||||
cachedThumbnails: boolean | null;
|
||||
lastFileHash: string | null;
|
||||
}
|
||||
|
||||
const mergeState: MergeState = {
|
||||
pdfDocs: {},
|
||||
pdfBytes: {},
|
||||
activeMode: 'file',
|
||||
sortableInstances: {},
|
||||
isRendering: false,
|
||||
@@ -21,47 +34,14 @@ const mergeState = {
|
||||
|
||||
const mergeWorker = new Worker('/workers/merge.worker.js');
|
||||
|
||||
function parsePageRanges(rangeString: any, totalPages: any) {
|
||||
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);
|
||||
}
|
||||
}
|
||||
// @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;
|
||||
|
||||
// @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();
|
||||
}
|
||||
|
||||
// @ts-expect-error TS(2339) FIXME: Property 'fileList' does not exist on type '{}'.
|
||||
mergeState.sortableInstances.fileList = Sortable.create(fileList, {
|
||||
handle: '.drag-handle',
|
||||
animation: 150,
|
||||
@@ -81,13 +61,10 @@ function initializePageThumbnailsSortable() {
|
||||
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
|
||||
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',
|
||||
@@ -224,8 +201,8 @@ async function renderPageMergeThumbnails() {
|
||||
export async function merge() {
|
||||
showLoader('Merging PDFs...');
|
||||
try {
|
||||
const jobs: any[] = [];
|
||||
const filesToMerge: any[] = [];
|
||||
const jobs: MergeJob[] = [];
|
||||
const filesToMerge: MergeFile[] = [];
|
||||
const uniqueFileNames = new Set<string>();
|
||||
|
||||
if (mergeState.activeMode === 'file') {
|
||||
@@ -234,8 +211,7 @@ export async function merge() {
|
||||
|
||||
const sortedFiles = Array.from(fileList.children)
|
||||
.map((li) => {
|
||||
// @ts-expect-error TS(2339) FIXME: Property 'dataset' does not exist on type 'Element...
|
||||
return state.files.find((f) => f.name === li.dataset.fileName);
|
||||
return state.files.find((f) => f.name === (li as HTMLElement).dataset.fileName);
|
||||
})
|
||||
.filter(Boolean);
|
||||
|
||||
@@ -267,10 +243,9 @@ export async function merge() {
|
||||
|
||||
const rawPages: { fileName: string; pageIndex: number }[] = [];
|
||||
for (const el of pageElements) {
|
||||
// @ts-expect-error TS(2339)
|
||||
const fileName = el.dataset.fileName;
|
||||
// @ts-expect-error TS(2339)
|
||||
const pageIndex = parseInt(el.dataset.pageIndex, 10); // 0-based index from dataset
|
||||
const element = el as HTMLElement;
|
||||
const fileName = element.dataset.fileName;
|
||||
const pageIndex = parseInt(element.dataset.pageIndex || '', 10); // 0-based index from dataset
|
||||
|
||||
if (fileName && !isNaN(pageIndex)) {
|
||||
uniqueFileNames.add(fileName);
|
||||
@@ -304,8 +279,8 @@ export async function merge() {
|
||||
jobs.push({
|
||||
fileName: current.fileName,
|
||||
rangeType: 'range',
|
||||
startPage: current.pageIndex + 1,
|
||||
endPage: endPage + 1
|
||||
startPage: current.pageIndex + 1,
|
||||
endPage: endPage + 1
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -324,13 +299,15 @@ export async function merge() {
|
||||
}
|
||||
}
|
||||
|
||||
mergeWorker.postMessage({
|
||||
const message: MergeMessage = {
|
||||
command: 'merge',
|
||||
files: filesToMerge,
|
||||
jobs: jobs
|
||||
}, filesToMerge.map(f => f.data));
|
||||
};
|
||||
|
||||
mergeWorker.onmessage = (e) => {
|
||||
mergeWorker.postMessage(message, filesToMerge.map(f => f.data));
|
||||
|
||||
mergeWorker.onmessage = (e: MessageEvent<MergeResponse>) => {
|
||||
hideLoader();
|
||||
if (e.data.status === 'success') {
|
||||
const blob = new Blob([e.data.pdfBytes], { type: 'application/pdf' });
|
||||
|
||||
@@ -3,25 +3,56 @@ import { downloadFile, resetAndReloadTool } from '../utils/helpers.js';
|
||||
import { state } from '../state.js';
|
||||
import { getRotationState, resetRotationState } from '../handlers/fileHandler.js';
|
||||
|
||||
import { degrees } from 'pdf-lib';
|
||||
import { PDFDocument, degrees } from 'pdf-lib';
|
||||
|
||||
export async function rotate() {
|
||||
showLoader('Applying rotations...');
|
||||
try {
|
||||
const pages = state.pdfDoc.getPages();
|
||||
const originalPdf = state.pdfDoc;
|
||||
const pageCount = originalPdf.getPageCount();
|
||||
const rotationStateArray = getRotationState();
|
||||
|
||||
// Apply rotations from state (not DOM) to ensure all pages including lazy-loaded ones are rotated
|
||||
rotationStateArray.forEach((rotation, pageIndex) => {
|
||||
if (rotation !== 0 && pages[pageIndex]) {
|
||||
const currentRotation = pages[pageIndex].getRotation().angle;
|
||||
pages[pageIndex].setRotation(degrees(currentRotation + rotation));
|
||||
}
|
||||
});
|
||||
const newPdfDoc = await PDFDocument.create();
|
||||
|
||||
const rotatedPdfBytes = await state.pdfDoc.save();
|
||||
for (let i = 0; i < pageCount; i++) {
|
||||
const rotation = rotationStateArray[i] || 0;
|
||||
const originalPage = originalPdf.getPage(i);
|
||||
const currentRotation = originalPage.getRotation().angle;
|
||||
const totalRotation = currentRotation + rotation;
|
||||
|
||||
if (totalRotation % 90 === 0) {
|
||||
const [copiedPage] = await newPdfDoc.copyPages(originalPdf, [i]);
|
||||
copiedPage.setRotation(degrees(totalRotation));
|
||||
newPdfDoc.addPage(copiedPage);
|
||||
} else {
|
||||
const embeddedPage = await newPdfDoc.embedPage(originalPage);
|
||||
const { width, height } = embeddedPage.scale(1);
|
||||
|
||||
const angleRad = (totalRotation * Math.PI) / 180;
|
||||
const absCos = Math.abs(Math.cos(angleRad));
|
||||
const absSin = Math.abs(Math.sin(angleRad));
|
||||
|
||||
const newWidth = width * absCos + height * absSin;
|
||||
const newHeight = width * absSin + height * absCos;
|
||||
|
||||
const newPage = newPdfDoc.addPage([newWidth, newHeight]);
|
||||
|
||||
const x = newWidth / 2 - (width / 2 * Math.cos(angleRad) - height / 2 * Math.sin(angleRad));
|
||||
const y = newHeight / 2 - (width / 2 * Math.sin(angleRad) + height / 2 * Math.cos(angleRad));
|
||||
|
||||
newPage.drawPage(embeddedPage, {
|
||||
x,
|
||||
y,
|
||||
width,
|
||||
height,
|
||||
rotate: degrees(totalRotation),
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
const rotatedPdfBytes = await newPdfDoc.save();
|
||||
downloadFile(
|
||||
new Blob([rotatedPdfBytes], { type: 'application/pdf' }),
|
||||
new Blob([rotatedPdfBytes as any], { type: 'application/pdf' }),
|
||||
'rotated.pdf'
|
||||
);
|
||||
|
||||
|
||||
Reference in New Issue
Block a user