Skip to content

Instantly share code, notes, and snippets.

@jarek-foksa
Created May 15, 2023 22:12
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save jarek-foksa/aaad250720a961e518de1eb4c9ce9352 to your computer and use it in GitHub Desktop.
Save jarek-foksa/aaad250720a961e518de1eb4c9ce9352 to your computer and use it in GitHub Desktop.
// @copyright
// © 2012-2022 Jarosław Foksa
// © 2015 Hugh Kennedy
//
// @license
// MIT License (MIT)
//
// Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated
// documentation files (the "Software"), to deal in the Software without restriction, including without
// limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the
// Software, and to permit persons to whom the Software is furnished to do so, subject to the following
// conditions:
//
// The above copyright notice and this permission notice shall be included in all copies or substantial portions
// of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED
// TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
// THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF
// CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
// DEALINGS IN THE SOFTWARE.
// @type (string, "png" || "jpeg" || "jpg" || "webp", Object) => Blob
//
// Rasterize the artwork to bitmap, represented as a binary blob.
// Passed serialized artwork must use XML serialization.
// This method should use the SVG 2 "flatten to image" API in future:
// http://www.w3.org/Graphics/SVG/WG/wiki/SVG2_Requirements_Input#Flatten_to_image
export let rasterizeArtwork = (serializedArtwork, format = "png", options = {}) => {
return new Promise( async (resolve) => {
if (format === "png") {
let blob = new Blob([serializedArtwork], {type: "image/svg+xml;charset=utf-8"});
let url = URL.createObjectURL(blob);
let img = new Image();
img.src = url;
img.addEventListener("load", async () => {
let dpi = options.dpi || 96;
let colors = options.colors || 0;
let background = options.background || "rgba(0, 0, 0, 0)";
let renderingCanvas = new OffscreenCanvas(img.naturalWidth, img.naturalHeight);
let renderingContext = renderingCanvas.getContext("2d");
renderingContext.fillStyle = background;
renderingContext.fillRect(0, 0, img.naturalWidth, img.naturalHeight);
renderingContext.drawImage(img, 0, 0);
let imageData = renderingContext.getImageData(0, 0, renderingCanvas.width, renderingCanvas.height);
let UPNG = (await import("/libs/upng/upng.js")).default;
let imageArrayBuffer = UPNG.encode([imageData.data.buffer], imageData.width, imageData.height, colors);
let imageArray = new Uint8Array(imageArrayBuffer);
imageArray = rewritePngDpi(imageArray, dpi);
let imageBlob = new Blob([imageArray], {type: "image/png"});
URL.revokeObjectURL(url);
resolve(imageBlob);
}, {once: true});
}
else if (format === "jpeg" || format === "jpg") {
let blob = new Blob([serializedArtwork], {type: "image/svg+xml;charset=utf-8"});
let url = URL.createObjectURL(blob);
let img = new Image();
img.src = url;
img.addEventListener("load", async () => {
let background = options.background || "rgb(255, 255, 255)";
let compression = options.compression || 1;
let renderingCanvas = new OffscreenCanvas(img.naturalWidth, img.naturalHeight);
let renderingContext = renderingCanvas.getContext("2d");
renderingContext.fillStyle = "white";
renderingContext.fillRect(0, 0, img.naturalWidth, img.naturalHeight);
renderingContext.fillStyle = background;
renderingContext.fillRect(0, 0, img.naturalWidth, img.naturalHeight);
renderingContext.drawImage(img, 0, 0);
let imageBlob = await renderingCanvas.convertToBlob({type: "image/jpeg", quality: compression});
URL.revokeObjectURL(url);
resolve(imageBlob);
}, {once: true});
}
else if (format === "webp") {
let blob = new Blob([serializedArtwork], {type: "image/svg+xml;charset=utf-8"});
let url = URL.createObjectURL(blob);
let img = new Image();
img.src = url;
img.addEventListener("load", async () => {
let background = options.background || "rgba(255, 255, 255, 0)";
let compression = options.compression || 1;
let renderingCanvas = new OffscreenCanvas(img.naturalWidth, img.naturalHeight);
let renderingContext = renderingCanvas.getContext("2d");
renderingContext.fillStyle = background;
renderingContext.fillRect(0, 0, img.naturalWidth, img.naturalHeight);
renderingContext.drawImage(img, 0, 0);
let imageBlob = await renderingCanvas.convertToBlob({type: "image/webp", quality: compression});
URL.revokeObjectURL(url);
resolve(imageBlob);
}, {once: true});
}
});
};
// @type (string) => Blob
//
// Same as "rasterizeArtwork", but uses the faster native rasterizer which can output only PNG without options
export let fastRasterizeArtwork = (serializedArtwork) => {
return new Promise( async (resolve) => {
let blob = new Blob([serializedArtwork], {type: "image/svg+xml;charset=utf-8"});
let url = URL.createObjectURL(blob);
let img = new Image();
img.src = url;
img.addEventListener("load", async () => {
let renderingCanvas = new OffscreenCanvas(img.naturalWidth, img.naturalHeight);
let renderingContext = renderingCanvas.getContext("2d");
renderingContext.fillStyle = "rgba(0, 0, 0, 0)";
renderingContext.fillRect(0, 0, img.naturalWidth, img.naturalHeight);
renderingContext.drawImage(img, 0, 0);
let imageBlob = await renderingCanvas.convertToBlob({type: "image/png"});
URL.revokeObjectURL(url);
resolve(imageBlob);
});
});
};
// @type (Uint8Array, number) => imageArray
// @src https://github.com/murkle/rewrite-png-pHYs-chunk
let rewritePngDpi = (data, dpi = 96) => {
let ppmx = Math.round(dpi / 2.54 * 100);
let ppmy = ppmx;
var CRC32;
(function (factory) {
/*jshint ignore:start */
if(typeof DO_NOT_EXPORT_CRC === 'undefined') {
if('object' === typeof exports) {
factory(exports);
} else if ('function' === typeof define && define.amd) {
define(function () {
var module = {};
factory(module);
return module;
});
} else {
factory(CRC32 = {});
}
} else {
factory(CRC32 = {});
}
/*jshint ignore:end */
}(function(CRC32) {
CRC32.version = '1.1.1';
/* see perf/crc32table.js */
/*global Int32Array */
function signed_crc_table() {
var c = 0, table = new Array(256);
for(var n =0; n != 256; ++n){
c = n;
c = ((c&1) ? (-306674912 ^ (c >>> 1)) : (c >>> 1));
c = ((c&1) ? (-306674912 ^ (c >>> 1)) : (c >>> 1));
c = ((c&1) ? (-306674912 ^ (c >>> 1)) : (c >>> 1));
c = ((c&1) ? (-306674912 ^ (c >>> 1)) : (c >>> 1));
c = ((c&1) ? (-306674912 ^ (c >>> 1)) : (c >>> 1));
c = ((c&1) ? (-306674912 ^ (c >>> 1)) : (c >>> 1));
c = ((c&1) ? (-306674912 ^ (c >>> 1)) : (c >>> 1));
c = ((c&1) ? (-306674912 ^ (c >>> 1)) : (c >>> 1));
table[n] = c;
}
return typeof Int32Array !== 'undefined' ? new Int32Array(table) : table;
}
var T = signed_crc_table();
function crc32_bstr(bstr, seed) {
var C = seed ^ -1, L = bstr.length - 1;
for(var i = 0; i < L;) {
C = (C>>>8) ^ T[(C^bstr.charCodeAt(i++))&0xFF];
C = (C>>>8) ^ T[(C^bstr.charCodeAt(i++))&0xFF];
}
if(i === L) C = (C>>>8) ^ T[(C ^ bstr.charCodeAt(i))&0xFF];
return C ^ -1;
}
function crc32_buf(buf, seed) {
if(buf.length > 10000) return crc32_buf_8(buf, seed);
var C = seed ^ -1, L = buf.length - 3;
for(var i = 0; i < L;) {
C = (C>>>8) ^ T[(C^buf[i++])&0xFF];
C = (C>>>8) ^ T[(C^buf[i++])&0xFF];
C = (C>>>8) ^ T[(C^buf[i++])&0xFF];
C = (C>>>8) ^ T[(C^buf[i++])&0xFF];
}
while(i < L+3) C = (C>>>8) ^ T[(C^buf[i++])&0xFF];
return C ^ -1;
}
function crc32_buf_8(buf, seed) {
var C = seed ^ -1, L = buf.length - 7;
for(var i = 0; i < L;) {
C = (C>>>8) ^ T[(C^buf[i++])&0xFF];
C = (C>>>8) ^ T[(C^buf[i++])&0xFF];
C = (C>>>8) ^ T[(C^buf[i++])&0xFF];
C = (C>>>8) ^ T[(C^buf[i++])&0xFF];
C = (C>>>8) ^ T[(C^buf[i++])&0xFF];
C = (C>>>8) ^ T[(C^buf[i++])&0xFF];
C = (C>>>8) ^ T[(C^buf[i++])&0xFF];
C = (C>>>8) ^ T[(C^buf[i++])&0xFF];
}
while(i < L+7) C = (C>>>8) ^ T[(C^buf[i++])&0xFF];
return C ^ -1;
}
function crc32_str(str, seed) {
var C = seed ^ -1;
for(var i = 0, L=str.length, c, d; i < L;) {
c = str.charCodeAt(i++);
if(c < 0x80) {
C = (C>>>8) ^ T[(C ^ c)&0xFF];
} else if(c < 0x800) {
C = (C>>>8) ^ T[(C ^ (192|((c>>6)&31)))&0xFF];
C = (C>>>8) ^ T[(C ^ (128|(c&63)))&0xFF];
} else if(c >= 0xD800 && c < 0xE000) {
c = (c&1023)+64; d = str.charCodeAt(i++)&1023;
C = (C>>>8) ^ T[(C ^ (240|((c>>8)&7)))&0xFF];
C = (C>>>8) ^ T[(C ^ (128|((c>>2)&63)))&0xFF];
C = (C>>>8) ^ T[(C ^ (128|((d>>6)&15)|((c&3)<<4)))&0xFF];
C = (C>>>8) ^ T[(C ^ (128|(d&63)))&0xFF];
} else {
C = (C>>>8) ^ T[(C ^ (224|((c>>12)&15)))&0xFF];
C = (C>>>8) ^ T[(C ^ (128|((c>>6)&63)))&0xFF];
C = (C>>>8) ^ T[(C ^ (128|(c&63)))&0xFF];
}
}
return C ^ -1;
}
CRC32.table = T;
CRC32.bstr = crc32_bstr;
CRC32.buf = crc32_buf;
CRC32.str = crc32_str;
}));
// Used for fast-ish conversion between uint8s and uint32s/int32s.
// Also required in order to remain agnostic for both Node Buffers and
// Uint8Arrays.
var uint8 = new Uint8Array(4);
var int32 = new Int32Array(uint8.buffer);
var uint32 = new Uint32Array(uint8.buffer);
var pHYsFound = false;
if (
data[0] !== 0x89 || data[1] !== 0x50 || data[2] !== 0x4E || data[3] !== 0x47 || data[4] !== 0x0D ||
data[5] !== 0x0A || data[6] !== 0x1A || data[7] !== 0x0A
) {
throw new Error('Invalid .png file header: possibly caused by DOS-Unix line ending conversion?');
}
var ended = false
var idx = 8
while (idx < data.length) {
// Read the length of the current chunk,
// which is stored as a Uint32.
uint8[3] = data[idx++]
uint8[2] = data[idx++]
uint8[1] = data[idx++]
uint8[0] = data[idx++]
// Chunk includes name/type for CRC check (see below).
var length = uint32[0] + 4
var chunk = new Uint8Array(length)
chunk[0] = data[idx++]
chunk[1] = data[idx++]
chunk[2] = data[idx++]
chunk[3] = data[idx++]
// Get the name in ASCII for identification.
var name = (
String.fromCharCode(chunk[0]) +
String.fromCharCode(chunk[1]) +
String.fromCharCode(chunk[2]) +
String.fromCharCode(chunk[3])
);
var chunkDataStart = idx;
// Read the contents of the chunk out of the main buffer.
for (var i = 4; i < length; i++) {
chunk[i] = data[idx++];
}
var crcStart = idx;
// Read out the CRC value for comparison.
// It's stored as an Int32.
uint8[3] = data[idx++];
uint8[2] = data[idx++];
uint8[1] = data[idx++];
uint8[0] = data[idx++];
var crcActual = int32[0];
var crcExpect = CRC32.buf(chunk);
if (crcExpect !== crcActual) {
throw new Error(
'CRC values for ' + name + ' header do not match, PNG file is likely corrupted'
)
} else {
}
if (name == "IDAT") {
chunkDataStart = chunkDataStart - 8;
var len = data.length;
// create new array with pHYs chunk inserted
// 4+4+13
var data2 = new Uint8Array(len + 21);
// copy before IEND
for (var i = 0 ; i < chunkDataStart ; i++) {
data2[i] = data[i];
}
// copy IEND to end
for (var i = chunkDataStart ; i < len ; i++) {
data2[i+21] = data[i];
}
var phys = new Uint8Array(13);
var i = 0;
// length of pHYs chunk
int32[0] = 9;
data2[chunkDataStart++] = uint8[3];
data2[chunkDataStart++] = uint8[2];
data2[chunkDataStart++] = uint8[1];
data2[chunkDataStart++] = uint8[0];
// pHYs (chunk name)
phys[i++] = data2[chunkDataStart++] = 'p'.charCodeAt(0);
phys[i++] = data2[chunkDataStart++] = 'H'.charCodeAt(0);
phys[i++] = data2[chunkDataStart++] = 'Y'.charCodeAt(0);
phys[i++] = data2[chunkDataStart++] = 's'.charCodeAt(0);
// x
uint32[0] = ppmx;
phys[i++] = data2[chunkDataStart++] = uint8[3];
phys[i++] = data2[chunkDataStart++] = uint8[2];
phys[i++] = data2[chunkDataStart++] = uint8[1];
phys[i++] = data2[chunkDataStart++] = uint8[0];
// y
uint32[0] = ppmy;
phys[i++] = data2[chunkDataStart++] = uint8[3];
phys[i++] = data2[chunkDataStart++] = uint8[2];
phys[i++] = data2[chunkDataStart++] = uint8[1];
phys[i++] = data2[chunkDataStart++] = uint8[0];
// unit = meters
phys[i++] = data2[chunkDataStart++] = 1;
var physCRC = CRC32.buf(phys);
int32[0] = physCRC;
data2[chunkDataStart++] = uint8[3];
data2[chunkDataStart++] = uint8[2];
data2[chunkDataStart++] = uint8[1];
data2[chunkDataStart++] = uint8[0];
return data2;
}
if (name == "pHYs") {
var phys = new Uint8Array(13);
var i = 0;
// pHYs (chunk name)
phys[i++] = 'p'.charCodeAt(0);
phys[i++] = 'H'.charCodeAt(0);
phys[i++] = 'Y'.charCodeAt(0);
phys[i++] = 's'.charCodeAt(0);
// x
uint32[0] = ppmx;
phys[i++] = data[chunkDataStart++] = uint8[3];
phys[i++] = data[chunkDataStart++] = uint8[2];
phys[i++] = data[chunkDataStart++] = uint8[1];
phys[i++] = data[chunkDataStart++] = uint8[0];
// y
uint32[0] = ppmy;
phys[i++] = data[chunkDataStart++] = uint8[3];
phys[i++] = data[chunkDataStart++] = uint8[2];
phys[i++] = data[chunkDataStart++] = uint8[1];
phys[i++] = data[chunkDataStart++] = uint8[0];
// unit = meters
phys[i++] = data[chunkDataStart++] = 1;
var physCRC = CRC32.buf(phys);
int32[0] = physCRC;
data[crcStart++] = uint8[3];
data[crcStart++] = uint8[2];
data[crcStart++] = uint8[1];
data[crcStart++] = uint8[0];
return data;
}
}
throw new Error('.png file ended prematurely: no IEND or pHYs header was found');
};
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment