Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1,2 +1,3 @@
__pycache__/
*.pyc
node_modules
143 changes: 79 additions & 64 deletions docs/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -270,6 +270,79 @@ <h2>Online Converter</h2>
}
}

async function processImage(file, targetExt, isBatch, zip, downloadsContainer) {
try {
const arrayBuffer = await file.arrayBuffer();
const sourceBytes = new Uint8Array(arrayBuffer);

// Await a Promise that resolves when the setTimeout for this image finishes
return await new Promise((resolve) => {
setTimeout(() => {
try {
ImageMagick.read(sourceBytes, (image) => {
if (targetExt === 'jpg' || targetExt === 'jpeg') {
image.hasAlpha = false;
}

// Title Case for enum
const titleCaseFormat = targetExt.charAt(0).toUpperCase() + targetExt.slice(1).toLowerCase();
const formatEnum = MagickFormat[titleCaseFormat] || targetExt.toUpperCase();

image.write(formatEnum, (data) => {
let originalName = file.name.split('.').slice(0, -1).join('.');
if (!originalName) originalName = "image"; // fallback if no extension
const outFileName = `${originalName}.${targetExt}`;

if (isBatch) {
// Add to zip
// CRITICAL: Must clone the Uint8Array because 'data' is a view into
// WebAssembly memory which will be overwritten by the next image processing.
zip.file(outFileName, new Uint8Array(data));
} else {
// Single file, provide direct link
const blob = new Blob([data], { type: 'application/octet-stream' });
const url = URL.createObjectURL(blob);
const dlLink = document.createElement('a');
dlLink.href = url;
dlLink.download = outFileName;
dlLink.textContent = `⬇️ Download ${outFileName}`;
dlLink.style.fontWeight = 'bold';
dlLink.style.color = '#10b981';
downloadsContainer.appendChild(dlLink);
}
resolve(true);
});
});
} catch(err) {
console.error(`Error processing ${file.name}:`, err);
resolve(false);
}
}, 50);
});
} catch (err) {
console.error(`Error reading ${file.name}:`, err);
return false;
}
}

async function generateAndDownloadZip(zip, successCount, downloadsContainer) {
try {
const content = await zip.generateAsync({ type: "blob" });
const url = URL.createObjectURL(content);
const dlLink = document.createElement('a');
dlLink.href = url;
dlLink.download = `converted_images.zip`;
dlLink.textContent = `📦 Download All as ZIP (${successCount} files)`;
dlLink.style.fontWeight = 'bold';
dlLink.style.color = '#10b981';
downloadsContainer.appendChild(dlLink);
return true;
} catch (err) {
console.error("ZIP Generation Failed:", err);
return false;
}
}

// 3. Conversion Logic
convertBtn.addEventListener('click', async () => {
if (selectedFiles.length === 0) {
Expand All @@ -292,76 +365,18 @@ <h2>Online Converter</h2>
const file = files[i];
statusDiv.textContent = `Processing file ${i + 1} of ${files.length}: ${file.name}...`;

try {
const arrayBuffer = await file.arrayBuffer();
const sourceBytes = new Uint8Array(arrayBuffer);

// Await a Promise that resolves when the setTimeout for this image finishes
await new Promise((resolve) => {
setTimeout(() => {
try {
ImageMagick.read(sourceBytes, (image) => {
if (targetExt === 'jpg' || targetExt === 'jpeg') {
image.hasAlpha = false;
}

// Title Case for enum
const titleCaseFormat = targetExt.charAt(0).toUpperCase() + targetExt.slice(1).toLowerCase();
const formatEnum = MagickFormat[titleCaseFormat] || targetExt.toUpperCase();

image.write(formatEnum, (data) => {
let originalName = file.name.split('.').slice(0, -1).join('.');
if (!originalName) originalName = "image"; // fallback if no extension
const outFileName = `${originalName}.${targetExt}`;

if (isBatch) {
// Add to zip
// CRITICAL: Must clone the Uint8Array because 'data' is a view into
// WebAssembly memory which will be overwritten by the next image processing.
zip.file(outFileName, new Uint8Array(data));
} else {
// Single file, provide direct link
const blob = new Blob([data], { type: 'application/octet-stream' });
const url = URL.createObjectURL(blob);
const dlLink = document.createElement('a');
dlLink.href = url;
dlLink.download = outFileName;
dlLink.textContent = `⬇️ Download ${outFileName}`;
dlLink.style.fontWeight = 'bold';
dlLink.style.color = '#10b981';
downloadsContainer.appendChild(dlLink);
}
successCount++;
});
});
} catch(err) {
console.error(`Error processing ${file.name}:`, err);
failCount++;
}
resolve();
}, 50);
});
} catch (err) {
console.error(`Error reading ${file.name}:`, err);
const success = await processImage(file, targetExt, isBatch, zip, downloadsContainer);
if (success) {
successCount++;
} else {
failCount++;
}
}

if (isBatch && successCount > 0) {
statusDiv.textContent = `Compressing ${successCount} files into ZIP...`;

try {
const content = await zip.generateAsync({ type: "blob" });
const url = URL.createObjectURL(content);
const dlLink = document.createElement('a');
dlLink.href = url;
dlLink.download = `converted_images.zip`;
dlLink.textContent = `📦 Download All as ZIP (${successCount} files)`;
dlLink.style.fontWeight = 'bold';
dlLink.style.color = '#10b981';
downloadsContainer.appendChild(dlLink);
} catch (err) {
console.error("ZIP Generation Failed:", err);
const zipSuccess = await generateAndDownloadZip(zip, successCount, downloadsContainer);
if (!zipSuccess) {
failCount += successCount; // Consider it a failure if we can't zip
}
}
Expand Down
56 changes: 56 additions & 0 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

5 changes: 5 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
{
"dependencies": {
"playwright": "^1.61.1"
}
}
47 changes: 47 additions & 0 deletions test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
const { chromium } = require('playwright');
const path = require('path');
const fs = require('fs');

(async () => {
const browser = await chromium.launch({ headless: true });
const page = await browser.newPage();

try {
// Navigate to the local server
await page.goto('http://localhost:8000/docs/index.html');

// Wait for the WASM engine to initialize
await page.waitForFunction(() => document.getElementById('status').textContent.includes('Engine Ready'), { timeout: 10000 });

// Select a file
const filePath = path.resolve(__dirname, 'docs/dummy.png');
const fileChooserPromise = page.waitForEvent('filechooser');

// Click the file input label to trigger the file chooser
await page.click('.custom-file-btn');
const fileChooser = await fileChooserPromise;
await fileChooser.setFiles(filePath);

// Wait for preview to render
await page.waitForSelector('.thumbnail-wrapper');

// Click Convert
await page.click('#convertBtn');

// Wait for download link to appear
await page.waitForSelector('#downloads-container a');

// Get status text
const statusText = await page.textContent('#status');
if (!statusText.includes('Job complete! Successfully processed 1 files.')) {
throw new Error(`Unexpected status: ${statusText}`);
}

console.log("Test passed successfully.");
} catch (error) {
console.error("Test failed:", error);
process.exit(1);
} finally {
await browser.close();
}
})();