UNPKG

@julusian/skia-canvas

Version:

A GPU-accelerated Canvas Graphics API for Node

313 lines (261 loc) 9.72 kB
"use strict" const {basename, extname} = require('path') // // Mime type <-> File extension mappings // class Format{ constructor(){ let isWeb = (() => typeof global=='undefined')(), png = "image/png", jpg = "image/jpeg", jpeg = "image/jpeg", webp = "image/webp", pdf = "application/pdf", svg = "image/svg+xml" Object.assign(this, { toMime: this.toMime.bind(this), fromMime: this.fromMime.bind(this), expected: isWeb ? `"png", "jpg", or "webp"` : `"png", "jpg", "pdf", or "svg"`, formats: isWeb ? {png, jpg, jpeg, webp} : {png, jpg, jpeg, pdf, svg}, mimes: isWeb ? {[png]: "png", [jpg]: "jpg", [webp]: "webp"} : {[png]: "png", [jpg]: "jpg", [pdf]: "pdf", [svg]: "svg"}, }) } toMime(ext){ return this.formats[(ext||'').replace(/^\./, '').toLowerCase()] } fromMime(mime){ return this.mimes[mime] } } // // Validation of the options dict shared by the Canvas saveAs, toBuffer, and toDataURL methods // function options(pages, {filename='', extension='', format, page, quality, matte, density, outline, archive}={}){ var {fromMime, toMime, expected} = new Format(), archive = archive || 'canvas', ext = format || extension.replace(/@\d+x$/i,'') || extname(filename), format = fromMime(toMime(ext) || ext), mime = toMime(format), pp = pages.length if(!ext) throw new Error(`Cannot determine image format (use a filename extension or 'format' argument)`) if (!format) throw new Error(`Unsupported file format "${ext}" (expected ${expected})`) if (!pp) throw new RangeError(`Canvas has no associated contexts (try calling getContext or newPage first)`) let padding, isSequence, pattern = filename.replace(/{(\d*)}/g, (_, width) => { isSequence = true width = parseInt(width, 10) padding = isFinite(width) ? width : isFinite(padding) ? padding : -1 return "{}" }) // allow negative indexing if a specific page is specified let idx = page > 0 ? page - 1 : page < 0 ? pp + page : undefined; if (isFinite(idx) && idx < 0 || idx >= pp) throw new RangeError( pp == 1 ? `Canvas only has a ‘page 1’ (${idx} is out of bounds)` : `Canvas has pages 1–${pp} (${idx} is out of bounds)` ) pages = isFinite(idx) ? [pages[idx]] : isSequence || format=='pdf' ? pages : pages.slice(-1) // default to the 'current' context if (quality===undefined){ quality = 0.92 }else{ if (typeof quality!='number' || !isFinite(quality) || quality<0 || quality>1){ throw new TypeError("The quality option must be an number in the 0.0–1.0 range") } } if (density===undefined){ let m = (extension || basename(filename, ext)).match(/@(\d+)x$/i) density = m ? parseInt(m[1], 10) : 1 }else if (typeof density!='number' || !Number.isInteger(density) || density<1){ throw new TypeError("The density option must be a non-negative integer") } if (outline===undefined){ outline = true }else if (format == 'svg'){ outline = !!outline } return {filename, pattern, format, mime, pages, padding, quality, matte, density, outline, archive} } // // Zip (pace Phil Katz & q.v. https://github.com/jimmywarting/StreamSaver.js) // class Crc32 { static for(data){ return new Crc32().append(data).get() } constructor(){ this.crc = -1 } get(){ return ~this.crc } append(data){ var crc = this.crc | 0, table = this.table for (var offset = 0, len = data.length | 0; offset < len; offset++) { crc = (crc >>> 8) ^ table[(crc ^ data[offset]) & 0xFF] } this.crc = crc return this } } Crc32.prototype.table = (() => { var i, j, t, table = [] for (i = 0; i < 256; i++) { t = i for (j = 0; j < 8; j++) { t = (t & 1) ? (t >>> 1) ^ 0xEDB88320 : t >>> 1 } table[i] = t } return table })() function calloc(size){ let array = new Uint8Array(size), view = new DataView(array.buffer), buf = { array, view, size, set8(at, to){ view.setUint8(at, to); return buf }, set16(at, to){ view.setUint16(at, to, true); return buf }, set32(at, to){ view.setUint32(at, to, true); return buf }, bytes(at, to){ array.set(to, at); return buf }, } return buf } class Zip{ static encoder = new TextEncoder() constructor(directory){ let now = new Date() Object.assign(this, { directory, offset: 0, files: [], time: (((now.getHours() << 6) | now.getMinutes()) << 5) | now.getSeconds() / 2, date: ((((now.getFullYear() - 1980) << 4) | (now.getMonth() + 1)) << 5) | now.getDate(), }) this.add(directory) } async add(filename, blob){ let folder = !blob, name = Zip.encoder.encode(`${this.directory}/${folder ? '' : filename}`), data = new Uint8Array(folder ? 0 : await blob.arrayBuffer()), preamble = 30 + name.length, descriptor = preamble + data.length, postamble = 16, {offset} = this let header = calloc(26) .set32(0, 0x08080014) // zip version .set16(6, this.time) // time .set16(8, this.date) // date .set32(10, Crc32.for(data)) // checksum .set32(14, data.length) // compressed size (w/ zero compression) .set32(18, data.length) // un-compressed size .set16(22, name.length) // filename length (utf8 bytes) offset += preamble let payload = calloc(preamble + data.length + postamble) .set32(0, 0x04034b50) // local header signature .bytes(4, header.array) // ...header fields... .bytes(30, name) // filename .bytes(preamble, data) // blob bytes offset += data.length payload .set32(descriptor, 0x08074b50) // signature .bytes(descriptor + 4, header.array.slice(10,22)) // length & filemame offset += postamble this.files.push({offset, folder, name, header, payload}) this.offset = offset } toBuffer(){ // central directory record let length = this.files.reduce((len, {name}) => 46 + name.length + len, 0), cdr = calloc(length + 22), index = 0 for (var {offset, name, header, folder} of this.files){ cdr.set32(index, 0x02014b50) // archive file signature .set16(index + 4, 0x0014) // version .bytes(index + 6, header.array) // ...header fields... .set8(index + 38, folder ? 0x10 : 0) // is_dir flag .set32(index + 42, offset) // file offset .bytes(index + 46, name) // filename index += 46 + name.length } cdr.set32(index, 0x06054b50) // signature .set16(index + 8, this.files.length) // № files per-segment .set16(index + 10, this.files.length) // № files this segment .set32(index + 12, length) // central directory length .set32(index + 16, this.offset) // file-offset of directory // concatenated zipfile data let output = new Uint8Array(this.offset + cdr.size), cursor = 0; for (var {payload} of this.files){ output.set(payload.array, cursor) cursor += payload.size } output.set(cdr.array, cursor) return output } get blob(){ return new Blob([this.toBuffer()], {type:"application/zip"}) } } // // Browser helpers for converting canvas elements to blobs/buffers/files/zips // const asBlob = (canvas, mime, quality, matte) => { if (matte){ let {width, height} = canvas, comp = Object.assign(document.createElement('canvas'), {width, height}), ctx = comp.getContext("2d") ctx.fillStyle = matte ctx.fillRect(0, 0, width, height) ctx.drawImage(canvas, 0, 0) canvas = comp } return new Promise((res, rej) => canvas.toBlob(res, mime, quality)) } const asBuffer = (...args) => asBlob(...args).then(b => b.arrayBuffer()) const asDownload = async (canvas, mime, quality, matte, filename) => { _download(filename, await asBlob(canvas, mime, quality, matte)) } const asZipDownload = async (pages, mime, quality, matte, archive, pattern, padding) => { let filenames = i => pattern.replace('{}', String(i+1).padStart(padding, '0')), folder = basename(archive, '.zip') || 'archive', zip = new Zip(folder) await Promise.all(pages.map(async (page, i) => { let filename = filenames(i) // serialize filename(s) before awaiting await zip.add(filename, await asBlob(page, mime, quality, matte)) })) _download(`${folder}.zip`, zip.blob) } const _download = (filename, blob) => { const href = window.URL.createObjectURL(blob), link = document.createElement('a') link.style.display = 'none' link.href = href link.setAttribute('download', filename) if (typeof link.download === 'undefined') { link.setAttribute('target', '_blank') } document.body.appendChild(link) link.click() document.body.removeChild(link) setTimeout(() => window.URL.revokeObjectURL(href), 100) } const atScale = (pages, density, matte) => pages.map(page => { if (density == 1 && !matte) return page.canvas let scaled = document.createElement('canvas'), ctx = scaled.getContext("2d"), src = page.canvas ? page.canvas : page scaled.width = src.width * density scaled.height = src.height * density if (matte){ ctx.fillStyle = matte ctx.fillRect(0, 0, scaled.width, scaled.height) } ctx.scale(density, density) ctx.drawImage(src, 0, 0) return scaled }) module.exports = {asBuffer, asDownload, asZipDownload, atScale, options}