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:
@@ -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) {
|
||||
|
||||
Reference in New Issue
Block a user