Created
May 15, 2023 22:12
-
-
Save jarek-foksa/aaad250720a961e518de1eb4c9ce9352 to your computer and use it in GitHub Desktop.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// @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