|
|
"use strict";
|
|
|
|
|
|
// const { basename, extname } = require("path");
|
|
|
import { basename, extname } from "../../path-browserify/index.js";
|
|
|
//
|
|
|
// 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;
|
|
|
}
|
|
|
// const TextEncoder=require('util').TextEncoder
|
|
|
|
|
|
class Zip {
|
|
|
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" });
|
|
|
}
|
|
|
}
|
|
|
Zip.encoder = new TextEncoder();
|
|
|
|
|
|
//
|
|
|
// 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;
|
|
|
});
|
|
|
const obj = { asBuffer, asDownload, asZipDownload, atScale, options };
|
|
|
export default obj;
|
|
|
// module.exports = { asBuffer, asDownload, asZipDownload, atScale, options };
|