Build a Chrome Extension to Auto‑Compress Downloaded Images with WebAssembly
This guide explains how to create a Chrome extension that intercepts image downloads from design tools, compresses PNG files using WebAssembly‑compiled pngquant and advpng, uploads the optimized assets to a CDN, and manages them with IndexedDB for efficient front‑end workflows.
Overview
This guide describes how to build a Chrome extension that automatically intercepts image download requests from design tools (e.g., Lanhu, Figma), compresses PNG assets in‑memory using WebAssembly‑compiled pngquant (lossy) and advpng (lossless), uploads the compressed files to a CDN (Qiniu), and stores metadata in IndexedDB via dexie.
Intercepting Original Images
Design platforms expose download URLs in the page source. By overriding HTMLAnchorElement.dispatchEvent you can capture the click on an <a> element that triggers a blob: download and retrieve the filename and URL.
const originDispatchEvent = EventTarget.prototype.dispatchEvent;
Object.defineProperty(HTMLAnchorElement.prototype, 'dispatchEvent', {
writable: true,
configurable: true,
enumerable: true,
value: function (event) {
const nodeName = this.nodeName;
const href = this.href;
const filename = this.download;
if (nodeName === 'A' && filename && /^blob:/.test(href)) {
console.warn('Intercepted:', filename, href);
return false; // prevent default download
}
return originDispatchEvent.apply(this, [event]);
}
});Injecting the Interception Script via Chrome Extension
The script is injected with the scripting API. Setting world: 'MAIN' ensures the code runs in the page’s JavaScript context.
function inject() {
// same dispatchEvent override as above
}
chrome.tabs.onUpdated.addListener((tabId, changeInfo, tab) => {
if (tab.status === 'complete' && /^https?/.test(tab.url || '')) {
chrome.scripting.executeScript({
func: inject,
target: { tabId },
world: 'MAIN'
}).catch(err => console.error(err));
}
});WebAssembly Compression Engine
Open‑source tools are used:
pngquant – lossy PNG compression (up to 70% size reduction) with adaptive dithering.
advpng – lossless PNG compression that removes auxiliary chunks, merges IDAT blocks, and applies 7‑zip Deflate.
Both tools are originally file‑based C programs. To run them in the browser they are compiled to WebAssembly with Emscripten. The source is modified to operate on in‑memory buffers instead of filesystem files.
// Original file read
FILE *infile = fopen(filename, "rb");
// Modified for WebAssembly
FILE *infile = fmemopen(file_buffer, file_size, "rb");
if (!infile) return READ_ERROR;During the build the required libraries ( libpng and zlib) are compiled statically to avoid dynamic linking.
file(GLOB PNG_SOURCE libpng/*.c)
file(GLOB ZLIB_SOURCE zlib/*.c)
add_library(${PROJECT_NAME} STATIC pngquant.c rwpng.c ${PNG_SOURCE} ${ZLIB_SOURCE} ${QUA_SOURCE} ${ADVPNG_SOURCE})Uploading Compressed Images to Qiniu CDN
The extension must declare the upload endpoint in host_permissions. The upload token is generated on the front‑end using the algorithm from Qiniu’s documentation.
import { urlSafeBase64Encode } from 'qiniu-js';
import HmacSHA1 from 'crypto-js/hmac-sha1';
import encBase64 from 'crypto-js/enc-base64';
function getUploadToken(bucket, secretKey) {
const returnBody = {
key: '$(key)',
hash: '$(etag)',
name: '$(fname)',
size: '$(fsize)',
width: '$(imageInfo.width)',
height: '$(imageInfo.height)'
};
const putPolicy = JSON.stringify({
scope: bucket,
deadline: Math.floor(Date.now() / 1000) + 3600,
returnBody: JSON.stringify(returnBody)
});
const encodedPolicy = urlSafeBase64Encode(putPolicy);
const hash = HmacSHA1(encodedPolicy, secretKey);
const encodedSigned = hash.toString(encBase64);
return `${accessKey}:${safe64(encodedSigned)}:${encodedPolicy}`;
}Managing Image Records with IndexedDB
IndexedDB provides large, persistent storage and efficient pagination. The dexie wrapper simplifies CRUD operations.
interface ImageEntry {
name: string;
width: number;
height: number;
size: number;
cdnUrl: string;
uploadTime: number;
}
class ImageDB {
constructor(name = 'zimagedb') {
const db = new Dexie(name);
db.version(1).stores({ images: '++_id,name,cdnUrl' });
this.db = db;
}
async add(...items) {
for (const item of items) {
await this.db.images.add(item);
}
}
async findPage(page = 0, limit = 10) {
const offset = page * limit;
return this.db.images.limit(limit).offset(offset).toArray();
}
}
// Background script listener
chrome.runtime.onMessage.addListener((msg, sender, respond) => {
if (msg.type === 'getImages') {
db.findPage(msg.page).then(images => respond({ success: true, images }))
.catch(err => respond({ success: false, error: err }));
return true; // keep channel open
}
});
// Content script helper
function getImages(page = 0) {
return new Promise((resolve, reject) => {
chrome.runtime.sendMessage({ type: 'getImages', page }, resp => {
if (resp?.success) resolve(resp.images);
else reject(resp?.error);
});
});
}Complete Workflow
Chrome extension injects the dispatchEvent override into the design‑tool page.
When a user clicks a download link, the script captures the filename and blob URL.
The blob is fetched, converted to an Uint8Array, and passed to the WebAssembly module that runs pngquant (lossy) and then advpng (lossless) on the in‑memory data.
The compressed byte array is uploaded to Qiniu using the generated token.
Metadata (original name, dimensions, size, CDN URL, timestamp) is stored in IndexedDB via dexie.
Content scripts retrieve paginated image lists from the background page for UI display.
References
Chrome Extensions (MV3) – https://developer.chrome.com/docs/extensions/mv3/
pngquant repository – https://github.com/kornelski/pngquant
advpng documentation – https://www.advancemame.it/doc-advpng.html
WebAssembly – https://webassembly.org/
IndexedDB API – https://developer.mozilla.org/en-US/docs/Web/API/IndexedDB_API
Qiniu JavaScript SDK – https://developer.qiniu.com/kodo/1283/javascript
Dexie.js – https://dexie.org/
How this landed with the community
Was this worth your time?
0 Comments
Thoughtful readers leave field notes, pushback, and hard-won operational detail here.
