feat: enhance PDF tools with new features and UI improvements

- Added 'Extract Attachments' and 'Edit Attachments' functionalities to manage embedded files in PDFs.
- Introduced new splitting options: by bookmarks and by a specified number of pages (N times).
- Updated the user interface to include new options and improved layout for better usability.
- Enhanced the GitHub link display with dynamic star count retrieval.
- Bumped version to 1.4.0 and updated footer to reflect the new version.
- Refactored existing code for better maintainability and added new TypeScript definitions for the new features.
This commit is contained in:
abdullahalam123
2025-11-10 18:41:48 +05:30
parent 4b5b29bf9a
commit 0634600073
20 changed files with 3719 additions and 306 deletions

View File

@@ -1,133 +1,151 @@
import { showLoader, hideLoader, showAlert } from '../ui.js';
import { downloadFile, readFileAsArrayBuffer } from '../utils/helpers.js';
import { state } from '../state.js';
import { jpgToPdf } from './jpg-to-pdf.js';
import { pngToPdf } from './png-to-pdf.js';
import { webpToPdf } from './webp-to-pdf.js';
import { bmpToPdf } from './bmp-to-pdf.js';
import { tiffToPdf } from './tiff-to-pdf.js';
import { svgToPdf } from './svg-to-pdf.js';
import { heicToPdf } from './heic-to-pdf.js';
import { PDFDocument as PDFLibDocument } from 'pdf-lib';
/**
* Converts any image into a standard, web-friendly JPEG. Loses transparency.
* @param {Uint8Array} imageBytes The raw bytes of the image file.
* @returns {Promise<Uint8Array>} A promise that resolves with sanitized JPEG bytes.
*/
function sanitizeImageAsJpeg(imageBytes: any) {
return new Promise((resolve, reject) => {
const blob = new Blob([imageBytes]);
const imageUrl = URL.createObjectURL(blob);
const img = new Image();
img.onload = () => {
const canvas = document.createElement('canvas');
canvas.width = img.naturalWidth;
canvas.height = img.naturalHeight;
const ctx = canvas.getContext('2d');
ctx.fillStyle = 'white';
ctx.fillRect(0, 0, canvas.width, canvas.height);
ctx.drawImage(img, 0, 0);
canvas.toBlob(
async (jpegBlob) => {
if (!jpegBlob)
return reject(new Error('Canvas to JPEG conversion failed.'));
resolve(new Uint8Array(await jpegBlob.arrayBuffer()));
},
'image/jpeg',
0.9
);
URL.revokeObjectURL(imageUrl);
};
img.onerror = () => {
URL.revokeObjectURL(imageUrl);
reject(new Error('File could not be loaded as an image.'));
};
img.src = imageUrl;
});
}
/**
* Converts any image into a standard PNG. Preserves transparency.
* @param {Uint8Array} imageBytes The raw bytes of the image file.
* @returns {Promise<Uint8Array>} A promise that resolves with sanitized PNG bytes.
*/
function sanitizeImageAsPng(imageBytes: any) {
return new Promise((resolve, reject) => {
const blob = new Blob([imageBytes]);
const imageUrl = URL.createObjectURL(blob);
const img = new Image();
img.onload = () => {
const canvas = document.createElement('canvas');
canvas.width = img.naturalWidth;
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');
URL.revokeObjectURL(imageUrl);
};
img.onerror = () => {
URL.revokeObjectURL(imageUrl);
reject(new Error('File could not be loaded as an image.'));
};
img.src = imageUrl;
});
}
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...');
const filesByType: { [key: string]: File[] } = {};
for (const file of state.files) {
const type = file.type || '';
if (!filesByType[type]) {
filesByType[type] = [];
}
filesByType[type].push(file);
}
const types = Object.keys(filesByType);
if (types.length === 1) {
const type = types[0];
const originalFiles = state.files;
if (type === 'image/jpeg' || type === 'image/jpg') {
state.files = filesByType[type] as File[];
await jpgToPdf();
} else if (type === 'image/png') {
state.files = filesByType[type] as File[];
await pngToPdf();
} else if (type === 'image/webp') {
state.files = filesByType[type] as File[];
await webpToPdf();
} else if (type === 'image/bmp') {
state.files = filesByType[type] as File[];
await bmpToPdf();
} else if (type === 'image/tiff' || type === 'image/tif') {
state.files = filesByType[type] as File[];
await tiffToPdf();
} else if (type === 'image/svg+xml') {
state.files = filesByType[type] as File[];
await svgToPdf();
} else {
const firstFile = filesByType[type][0];
if (firstFile.name.toLowerCase().endsWith('.heic') ||
firstFile.name.toLowerCase().endsWith('.heif')) {
state.files = filesByType[type] as File[];
await heicToPdf();
} else {
showLoader('Converting images to PDF...');
try {
const pdfDoc = await PDFLibDocument.create();
for (const file of filesByType[type]) {
const imageBitmap = await createImageBitmap(file);
const canvas = document.createElement('canvas');
canvas.width = imageBitmap.width;
canvas.height = imageBitmap.height;
const ctx = canvas.getContext('2d');
ctx.drawImage(imageBitmap, 0, 0);
const pngBlob = await new Promise<Blob>((resolve) =>
canvas.toBlob(resolve, '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,
});
imageBitmap.close();
}
const pdfBytes = await pdfDoc.save();
downloadFile(
new Blob([new Uint8Array(pdfBytes)], { type: 'application/pdf' }),
'from-images.pdf'
);
} catch (e) {
console.error(e);
showAlert('Error', 'Failed to convert images to PDF.');
} finally {
hideLoader();
}
}
}
state.files = originalFiles;
return;
}
showLoader('Converting mixed image types 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);
const sortedFiles = imageList
? 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)
: state.files;
const qualityInput = document.getElementById('image-pdf-quality') as HTMLInputElement;
const quality = qualityInput ? Math.max(0.3, Math.min(1.0, parseFloat(qualityInput.value))) : 0.9;
for (const file of sortedFiles) {
const fileBuffer = await readFileAsArrayBuffer(file);
const type = file.type || '';
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...`
try {
const imageBitmap = await createImageBitmap(file);
const canvas = document.createElement('canvas');
canvas.width = imageBitmap.width;
canvas.height = imageBitmap.height;
const ctx = canvas.getContext('2d');
ctx.drawImage(imageBitmap, 0, 0);
const jpegBlob = await new Promise<Blob>((resolve) =>
canvas.toBlob(resolve, 'image/jpeg', quality)
);
const sanitizedBytes = await sanitizeImageAsPng(fileBuffer);
image = await pdfDoc.embedPng(sanitizedBytes as Uint8Array);
}
const jpegBytes = new Uint8Array(await jpegBlob.arrayBuffer());
image = await pdfDoc.embedJpg(jpegBytes);
imageBitmap.close();
const page = pdfDoc.addPage([image.width, image.height]);
page.drawImage(image, {
x: 0,
y: 0,
width: image.width,
height: image.height,
});
const page = pdfDoc.addPage([image.width, image.height]);
page.drawImage(image, {
x: 0,
y: 0,
width: image.width,
height: image.height,
});
} catch (e) {
console.warn(`Failed to process ${file.name}:`, e);
// Continue with next file
}
}
if (pdfDoc.getPageCount() === 0) {