You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

400 lines
10 KiB

This file contains ambiguous Unicode characters!

This file contains ambiguous Unicode characters that may be confused with others in your current locale. If your use case is intentional and legitimate, you can safely ignore this warning. Use the Escape button to highlight these characters.

"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.01.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 };