Download decode.js from VladZpp/waste-sorting-classifier: direct link, hf CLI and curl.
- Browser
- Download file 6.16 kB
-
https://huggingface.co/spaces/VladZpp/waste-sorting-classifier/resolve/main/decode.js
- Command line
-
hf download hf://spaces/VladZpp/waste-sorting-classifier/decode.js
-
curl -L -o decode.js https://huggingface.co/spaces/VladZpp/waste-sorting-classifier/resolve/main/decode.js
6.16 kB
| // Uploaded bytes -> upright 8-bit RGB, exactly as preprocess.py's load_rgb produces it, in | |
| // every browser: the page decodes images itself instead of asking the browser, whose | |
| // decoders differ (Safari's JPEG decoder, colour management, premultiplied alpha). | |
| // - JPEG and WebP: libjpeg-turbo 3 and libwebp compiled to WebAssembly, the libraries | |
| // Pillow uses; PNG: png.js. `codecs` supplies the two WebAssembly decoders. | |
| // - Transparency is flattened onto white with Pillow's arithmetic and the EXIF orientation | |
| // applied like ImageOps.exif_transpose, in one pass. | |
| // - Refused before decoding: unknown formats, out-of-limit sizes (as the API) and | |
| // incomplete files. Refused although Python accepts them (a known, documented | |
| // difference): CMYK JPEGs and animated WebP, which the WebAssembly decoders do not read. | |
| import { ImageError, readHeader } from "./image-header.js"; | |
| import { decodePng } from "./png.js"; | |
| import { checkSize } from "./preprocess.js"; | |
| const damaged = () => new ImageError("That file could not be read as an image: it may be damaged."); | |
| // Source pixel index for output pixel (x, y) under each orientation (2-8), like Pillow's | |
| // transpose methods; w, h are the stored (unrotated) width and height. | |
| const SOURCE = { | |
| 1: (x, y, w) => y * w + x, | |
| 2: (x, y, w) => y * w + (w - 1 - x), // FLIP_LEFT_RIGHT | |
| 3: (x, y, w, h) => (h - 1 - y) * w + (w - 1 - x), // ROTATE_180 | |
| 4: (x, y, w, h) => (h - 1 - y) * w + x, // FLIP_TOP_BOTTOM | |
| 5: (x, y, w) => x * w + y, // TRANSPOSE | |
| 6: (x, y, w, h) => (h - 1 - x) * w + y, // ROTATE_270 | |
| 7: (x, y, w, h) => (h - 1 - x) * w + (w - 1 - y), // TRANSVERSE | |
| 8: (x, y, w) => x * w + (w - 1 - y), // ROTATE_90 | |
| }; | |
| // Decoded pixels (1, 3 or 4 channels) -> upright RGB. Alpha is flattened onto white with | |
| // Pillow's alpha_composite arithmetic (checked against Pillow for every colour and alpha). | |
| function toUprightRgb({ width: w, height: h, channels, data }, orientation) { | |
| const swap = orientation >= 5; | |
| const W = swap ? h : w, H = swap ? w : h; | |
| const source = SOURCE[orientation]; | |
| const out = new Uint8Array(W * H * 3); | |
| for (let y = 0, o = 0; y < H; y++) { | |
| for (let x = 0; x < W; x++, o += 3) { | |
| const i = source(x, y, w, h) * channels; | |
| if (channels === 1) { | |
| out[o] = out[o + 1] = out[o + 2] = data[i]; | |
| } else if (channels === 3) { | |
| out[o] = data[i]; | |
| out[o + 1] = data[i + 1]; | |
| out[o + 2] = data[i + 2]; | |
| } else { | |
| const a = data[i + 3], back = 255 * (255 - a) + 127; | |
| out[o] = Math.floor((data[i] * a + back) / 255); | |
| out[o + 1] = Math.floor((data[i + 1] * a + back) / 255); | |
| out[o + 2] = Math.floor((data[i + 2] * a + back) / 255); | |
| } | |
| } | |
| } | |
| return { rgb: out, width: W, height: H }; | |
| } | |
| // bytes (Uint8Array) -> { rgb, width, height }; throws ImageError (or checkSize's Error) | |
| // with a message for the user. | |
| export async function decodeImage(bytes, codecs) { | |
| const header = await readHeader(bytes); | |
| if (!header.format) throw new ImageError("Use a JPEG, PNG or WebP photo."); | |
| if (!header.complete) { | |
| throw new ImageError("That file is incomplete: it may have been cut off while downloading."); | |
| } | |
| checkSize(header.width, header.height); // before anything is decoded | |
| if (header.format === "jpeg" && header.components === 4) { | |
| throw new ImageError("CMYK JPEGs (from print workflows) are not supported here: save the photo as RGB."); | |
| } | |
| if (header.format === "webp" && header.animated) { | |
| throw new ImageError("Animated images are not supported here."); | |
| } | |
| let image; | |
| if (header.format === "png") { | |
| image = await decodePng(bytes); | |
| } else { | |
| try { | |
| image = await codecs[header.format](header.decodeBytes ?? bytes); | |
| } catch { | |
| throw damaged(); | |
| } | |
| // WebP without an alpha channel in Pillow's eyes: its colours are used as they are. | |
| if (header.format === "webp" && !header.alpha) image = dropAlpha(image); | |
| } | |
| if (image.width !== header.width || image.height !== header.height) throw damaged(); | |
| return toUprightRgb(image, header.orientation); | |
| } | |
| function dropAlpha({ width, height, data }) { | |
| const rgb = new Uint8Array(width * height * 3); | |
| for (let i = 0, j = 0; j < rgb.length; i += 4, j += 3) { | |
| rgb[j] = data[i]; | |
| rgb[j + 1] = data[i + 1]; | |
| rgb[j + 2] = data[i + 2]; | |
| } | |
| return { width, height, channels: 3, data: rgb }; | |
| } | |
| // The two WebAssembly decoders from their Emscripten factories (the vendored | |
| // libjpegturbowasm_decode.js and webp_dec.js) and their already verified .wasm bytes. | |
| // Each returns { width, height, channels, data }. A decoder that fails is rebuilt for the | |
| // next file. | |
| export function makeCodecs({ jpegFactory, jpegWasm, webpFactory, webpWasm }) { | |
| const options = (wasmBinary) => ({ noInitialRun: true, wasmBinary, print: () => {}, printErr: () => {} }); | |
| const wrap = (factory, wasm, call) => { | |
| let module = null; | |
| return async (bytes) => { | |
| module ??= await factory(options(wasm)); | |
| try { | |
| return call(module, bytes); | |
| } catch (err) { | |
| module = null; | |
| throw err; | |
| } | |
| }; | |
| }; | |
| return { | |
| // libjpeg-turbo: RGB or greyscale output, no EXIF rotation. | |
| jpeg: wrap(jpegFactory, jpegWasm, (m, bytes) => { | |
| const decoder = new m.JPEGDecoder(); | |
| try { | |
| decoder.getEncodedBuffer(bytes.length).set(bytes); | |
| decoder.decode(); | |
| const info = decoder.getFrameInfo(); | |
| if (info.componentCount !== 1 && info.componentCount !== 3) throw new Error("unsupported"); | |
| return { width: info.width, height: info.height, channels: info.componentCount, | |
| data: new Uint8Array(decoder.getDecodedBuffer()) }; // copied out of the WebAssembly heap | |
| } finally { | |
| decoder.delete(); | |
| } | |
| }), | |
| // libwebp: RGBA output. | |
| webp: wrap(webpFactory, webpWasm, (m, bytes) => { | |
| const buffer = bytes.buffer.slice(bytes.byteOffset, bytes.byteOffset + bytes.byteLength); | |
| const out = m.decode(buffer); | |
| if (!out) throw new Error("decoding failed"); | |
| return { width: out.width, height: out.height, channels: 4, data: out.data }; | |
| }), | |
| }; | |
| } | |