patx/makeitpdf
makeitpdf v0.2
Commit 69e5c3a · patx · 2026-08-17T23:37:40-04:00
Comments
No comments yet.
Diff
diff --git a/docs/README.md b/docs/README.md
index bf6ab00..2cdfd24 100644
--- a/docs/README.md
+++ b/docs/README.md
@@ -1,121 +1,126 @@
# MakeItPDF
-Combine up to 10 images into a single PDF. Everything runs in the browser —
-no server, no upload, no account.
+Turn images, Word documents, Excel workbooks, Markdown, and CSV files into one
+PDF. Everything runs in the browser: no upload, account, server-side converter,
+or third-party runtime request.
## Run it
-It's a static site. Any file server will do:
+Serve the directory over HTTP:
```bash
-npm run serve # python3 -m http.server 8000
+npm run serve
```
-Then open http://127.0.0.1:8000.
+Open <http://127.0.0.1:8000>. The app uses a Web Worker and lazy-loaded local
+fonts, so opening `index.html` directly with a `file:` URL is not supported.
-Deploying is copying the folder to GitHub Pages, Netlify, S3, or any CDN.
-There is nothing to install and nothing to keep running.
+The production app is static and all runtime libraries and fonts are committed
+under `vendor/`. `npm install` is only needed to run the browser integration
+test or refresh vendored assets.
-## Tests
-
-```bash
-npm test # node --test "tests/*.test.js"
-```
+## Supported files
-16 tests, no dependencies — they cover `pages.js`, which holds the page
-geometry and is deliberately free of DOM and jsPDF so it runs straight through
-node. The parts that need a real browser (decoding, rasterising, jsPDF output)
-are verified by using the thing; see *Verified behaviour* below.
+- Images: JPEG, PNG, WebP, GIF, BMP, TIFF
+- Documents: DOCX
+- Spreadsheets: XLSX and UTF-8 CSV
+- Text: Markdown (`.md` and `.markdown`)
-## How it works
+Select or drop up to 10 files, reorder them, and convert them into one PDF.
+Files begin new PDF sections in queue order. Legacy DOC/XLS and encrypted
+Office files are rejected with a specific message.
-`app.js` walks the images in order and, for each one:
+### Images keep their physical size
-1. **Decodes** it with `createImageBitmap(file, { imageOrientation: "from-image" })`.
- That option applies the EXIF orientation tag. Without it, phone photos come
- out sideways — a PDF page has no orientation metadata to defer to, so the
- rotation has to be baked into the pixels.
-2. **Places** it on an A4 page turned to match the image, scaled to fit inside
- a 24 pt margin and centred (`pages.js`).
-3. **Rasterises** it to the size it will occupy at 150 DPI. The resize happens
- inside `createImageBitmap`, not `drawImage`: a single `drawImage` step from
- 24 MP down to ~1100 px aliases badly, while the bitmap resizer downsamples
- properly. The canvas is filled white first, because JPEG has no alpha and
- transparency would otherwise come out black.
-4. **Adds** it to the jsPDF document as a JPEG at quality 0.9.
+Each image gets a borderless page sized from its pixel dimensions and embedded
+print resolution:
-Images are processed one at a time and each bitmap is `close()`d immediately,
-so peak memory is roughly one decoded image rather than the whole batch.
+```text
+page points = image pixels / image DPI × 72
+```
-An A4 content area is only about 1140 px wide at 150 DPI, so large camera
-images are downscaled to that. This is the same output the previous
-server-side version produced.
+Resolution is read from JPEG JFIF/EXIF, PNG `pHYs`, TIFF resolution tags, and
+BMP pixels-per-metre fields. Images without valid resolution metadata use 96
+DPI. EXIF rotation is applied before page dimensions are calculated.
-### Formats
+Compatible JPEG and PNG data is embedded directly when no rotation is needed.
+Other image types and rotated images are decoded at their original pixel grid;
+they are never reduced to an A4 raster. Pages over the common PDF limit of 200
+inches per side are proportionally capped without discarding source pixels.
-JPEG, PNG, WebP, GIF, and BMP go through the browser's own decoder.
+Animated GIFs use their first frame. Multi-page TIFFs currently use their first
+image directory.
-**TIFF has no browser decoder**, so `UTIF.js` unpacks it to RGBA and it gets
-handed back through a canvas. TIFF is detected by magic bytes (`II*\0` or
-`MM\0*`), not by filename or content-type, because browsers frequently report
-an empty type for `.tif` files.
+### DOCX and Markdown
-`pako` must load before `UTIF` — UTIF picks it up as `self.pako` at load time
-and needs it to inflate deflate-compressed TIFFs (compression tag 8). Without
-it, LZW TIFFs work and deflate ones fail.
+DOCX conversion is semantic rather than a clone of Word's print engine. It
+preserves ordinary headings, paragraphs, lists, tables, links, embedded images,
+and inline emphasis. Exact Word pagination, installed fonts, text boxes,
+headers/footers, tracked changes, and floating layouts are not reproduced.
-TIFF thumbnails show a blank sheet outline, since `<img>` can't render TIFF
-either. The PDF page is correct regardless.
+Markdown supports headings, emphasis, links, lists and task lists, quotes,
+fenced code, tables, rules, and strikethrough. Raw HTML is disabled. Embedded
+data-URI images are allowed; remote and relative images are represented by alt
+text and their URL and are never fetched.
-### Limits
+### XLSX and CSV
-In `index.html` as `data-` attributes on the form, so the markup and `app.js`
-can't drift apart.
+Every visible XLSX worksheet is included in workbook order. Hidden and very
+hidden worksheets stay out of the PDF. Displayed cell values, cached formula
+results, merges, and stored column widths are retained where available.
+Formulae are not recalculated, and charts, macros, shapes, conditional
+formatting, and Excel print settings are not reproduced.
-- 10 images per PDF
-- 25 MB per image
+Spreadsheets use landscape A4. Wide tables are divided into consecutive column
+groups before type becomes too small to read. Workbooks and CSV files are
+limited to 50,000 rendered cells.
-There is no batch total limit — that existed to bound an HTTP upload, and
-there is no upload any more.
+## Privacy and safety
-## Verified behaviour
+- Runtime code, UI fonts, PDF fonts, and conversion libraries are local files.
+- DOCX and XLSX parsing runs in a worker with a 30-second timeout.
+- Mammoth output is converted through an element and URL allowlist and is never
+ inserted into the live page.
+- Only HTTP, HTTPS, and mailto links are emitted into PDFs.
+- A failed input aborts the whole conversion; no partial PDF is downloaded.
+- Limits are 10 files and 25 MB per file.
-Checked by running it and inspecting the resulting PDF, not just the success
-screen:
+The PDF font set covers Latin, Greek, Cyrillic, Arabic, Hebrew, Devanagari,
+Chinese, Japanese, and Korean. Bidirectional text is reordered for display and
+tagged with its logical source text for copying and search.
-- A JPEG stored 800×400 with EXIF orientation 6 produces a **portrait** page,
- and the extracted page image is rotated 90° clockwise — the correct
- direction, not merely a rotation.
-- Deflate-compressed (tag 8) and LZW-compressed (tag 5) TIFFs both convert.
-- A fully transparent PNG lands with pure white corners, not black.
-- Page geometry is exact: a 1000×1400 image on A4 yields a 1140×1596 px
- raster, matching `pages.js` to the pixel.
-- jsPDF deduplicates identical images, so two pixel-identical pages share one
- embedded image.
+## Tests
-## Layout
+Install development dependencies, then run:
+```bash
+npm test
+npm run test:browser
```
-index.html markup, limits, script order
-app.js intake, reordering, decode -> PDF pipeline
-pages.js pure page geometry (tested)
-style.css one typeface, white ground, one accent
-tests/ node --test, no dependencies
-vendor/ jsPDF 4.2.1, UTIF 3.1.0, pako 2.1.0 (all MIT), with licences
-```
-
-Dependencies are vendored rather than pulled from a CDN, so the page has no
-third-party runtime requests and keeps working offline. The scripts are
-classic UMD builds with relative paths — no ES modules, no `fetch`, no
-workers. (Testing was done over HTTP; opening `index.html` directly should
-work for the same reason but wasn't verified.)
-The only external request on the page is the Google Fonts stylesheet for
-Figtree. Drop that `<link>` and the system sans-serif takes over.
-
-## History
+The unit suite covers image geometry and metadata, TIFF detection, URL safety,
+UTF-8 handling, and CSV parsing. The headless-Chrome integration test converts
+a PNG, DOCX, XLSX, Markdown file, and CSV into one PDF and verifies:
+
+- the image page has its exact 96-DPI fallback dimensions;
+- text from all four text-based formats is searchable;
+- hidden worksheets are excluded;
+- no external network request occurs.
+
+## Project layout
+
+```text
+index.html form and result markup
+app.js intake, queue, workers, and UI states
+pages.js pure image metadata and page geometry
+documents.js safe CSV/HTML/Markdown normalization
+pdf-builder.js PDFKit layout and mixed page writing
+office-worker.js isolated DOCX/XLSX parsing
+style.css responsive visual system
+tests/ node and real-browser coverage
+vendor/ offline libraries, fonts, and licences
+```
-The first implementation was a FastAPI + Jinja2 service doing the conversion
-with Pillow. It's preserved in commit `c073366` if the server-side approach is
-ever wanted back — it supported `fit`/`a4`/`letter` page sizes and had 36
-Python tests.
+Runtime libraries are PDFKit, blob-stream, Mammoth, SheetJS CE, markdown-it,
+bidi-js, UTIF, and pako. SheetJS is vendored from its official 0.20.3 release
+rather than the outdated npm registry build.
diff --git a/docs/app.js b/docs/app.js
index 35cc1ee..6a1f7a7 100644
--- a/docs/app.js
+++ b/docs/app.js
@@ -1,12 +1,8 @@
-/* MakeItPDF -- everything happens on this device.
- Images are decoded, downscaled, and written into a PDF with jsPDF. No
- network request is made at any point. */
+/* MakeItPDF -- one private, local form for images and everyday documents. */
(() => {
"use strict";
- const { jsPDF } = window.jspdf;
-
const form = document.getElementById("composer");
const dropzone = document.getElementById("dropzone");
const tray = document.getElementById("tray");
@@ -20,27 +16,67 @@
const progress = document.getElementById("progress");
const done = document.getElementById("done");
const doneMeta = document.getElementById("done-meta");
+ const doneNotes = document.getElementById("done-notes");
const downloadLink = document.getElementById("download");
const restartBtn = document.getElementById("restart");
const MAX_FILES = Number(form.dataset.maxFiles);
const MAX_FILE_BYTES = Number(form.dataset.maxFileBytes);
- const JPEG_QUALITY = 0.9;
-
- /** @type {{id:number,file:File,url:string}[]} */
- let pages = [];
+ const MAX_SHEET_CELLS = 50000;
+ const OFFICE_TIMEOUT_MS = 30000;
+
+ const FORMAT_REGISTRY = [
+ {
+ kind: "image",
+ label: "Image",
+ extensions: ["jpg", "jpeg", "png", "webp", "gif", "bmp", "tif", "tiff"],
+ mimeTypes: ["image/jpeg", "image/png", "image/webp", "image/gif", "image/bmp", "image/tiff"],
+ },
+ {
+ kind: "docx",
+ label: "Word",
+ extensions: ["docx"],
+ mimeTypes: ["application/vnd.openxmlformats-officedocument.wordprocessingml.document"],
+ },
+ {
+ kind: "xlsx",
+ label: "Excel",
+ extensions: ["xlsx"],
+ mimeTypes: ["application/vnd.openxmlformats-officedocument.spreadsheetml.sheet"],
+ },
+ { kind: "markdown", label: "Markdown", extensions: ["md", "markdown"], mimeTypes: ["text/markdown"] },
+ { kind: "csv", label: "CSV", extensions: ["csv"], mimeTypes: ["text/csv", "application/csv"] },
+ ];
+ const FORMAT_BY_EXTENSION = new Map(
+ FORMAT_REGISTRY.flatMap((format) => format.extensions.map((extension) => [extension, format]))
+ );
+ const FORMAT_BY_MIME = new Map(
+ FORMAT_REGISTRY.flatMap((format) => format.mimeTypes.map((mimeType) => [mimeType, format]))
+ );
+
+ /** @type {{id:number,file:File,kind:string,label:string,previewUrl:string|null}[]} */
+ let files = [];
let nextId = 1;
let dragId = null;
let busy = false;
let lastUrl = null;
const { formatBytes } = Pages;
- const totalBytes = () => pages.reduce((sum, page) => sum + page.file.size, 0);
+ const totalBytes = () => files.reduce((sum, item) => sum + item.file.size, 0);
+
+ function extensionOf(name) {
+ const match = String(name).toLowerCase().match(/\.([^.]+)$/);
+ return match ? match[1] : "";
+ }
+
+ function formatFor(file) {
+ return FORMAT_BY_EXTENSION.get(extensionOf(file.name)) || FORMAT_BY_MIME.get(file.type) || null;
+ }
function setAlert(message) {
let alert = document.querySelector("main > .alert");
if (!message) {
- if (alert) alert.remove();
+ alert?.remove();
return;
}
if (!alert) {
@@ -52,131 +88,19 @@
alert.textContent = message;
}
- /* ---- decoding -------------------------------------------------------- */
-
- /**
- * Decode a file to a bitmap, upright.
- *
- * imageOrientation "from-image" applies the EXIF rotation tag, without which
- * phone photos land sideways -- a PDF has no orientation metadata to defer
- * to, so it has to be baked in here.
- */
- async function decode(file) {
- const buffer = await file.arrayBuffer();
- const head = new Uint8Array(buffer, 0, Math.min(4, buffer.byteLength));
-
- if (Pages.isTiff(head)) return decodeTiff(buffer, file.name);
-
- try {
- return await createImageBitmap(file, { imageOrientation: "from-image" });
- } catch {
- throw new Error(`${file.name} isn't an image this browser can read.`);
- }
- }
-
- /** Browsers have no TIFF decoder, so UTIF unpacks it to RGBA for us. */
- async function decodeTiff(buffer, name) {
- try {
- const ifds = UTIF.decode(buffer);
- if (!ifds.length) throw new Error("no pages");
- UTIF.decodeImage(buffer, ifds[0], ifds);
- const rgba = UTIF.toRGBA8(ifds[0]);
- const { width, height } = ifds[0];
- const canvas = new OffscreenCanvas(width, height);
- canvas
- .getContext("2d")
- .putImageData(new ImageData(new Uint8ClampedArray(rgba), width, height), 0, 0);
- return await createImageBitmap(canvas);
- } catch {
- throw new Error(`${name} is a TIFF we couldn't read.`);
- }
- }
-
- /**
- * Draw a bitmap into a canvas at its final size on the page.
- *
- * The resize happens in createImageBitmap rather than in drawImage: a single
- * drawImage step from 24MP down to ~1100px aliases badly, while the bitmap
- * resizer downsamples properly.
- */
- async function rasterise(bitmap, raster) {
- const scaled =
- bitmap.width === raster.width && bitmap.height === raster.height
- ? bitmap
- : await createImageBitmap(bitmap, {
- resizeWidth: raster.width,
- resizeHeight: raster.height,
- resizeQuality: "high",
- });
-
- const canvas = document.createElement("canvas");
- canvas.width = raster.width;
- canvas.height = raster.height;
- const ctx = canvas.getContext("2d");
- // JPEG has no alpha, so transparency would come out black without this.
- ctx.fillStyle = "#ffffff";
- ctx.fillRect(0, 0, raster.width, raster.height);
- ctx.drawImage(scaled, 0, 0);
- if (scaled !== bitmap) scaled.close();
- return canvas;
- }
-
- /** Build the PDF, one page per image, in list order. */
- async function buildPdf(items, onProgress) {
- let doc = null;
-
- for (let index = 0; index < items.length; index += 1) {
- const bitmap = await decode(items[index].file);
- try {
- const size = Pages.pageSize(bitmap.width, bitmap.height);
- const placement = Pages.placeImage(
- bitmap.width, bitmap.height, size.width, size.height
- );
- const canvas = await rasterise(bitmap, Pages.rasterSize(placement));
-
- if (doc === null) {
- doc = new jsPDF({
- unit: "pt",
- format: Pages.A4_PORTRAIT,
- orientation: size.orientation,
- compress: true,
- });
- } else {
- doc.addPage(Pages.A4_PORTRAIT, size.orientation);
- }
-
- doc.addImage(
- canvas.toDataURL("image/jpeg", JPEG_QUALITY),
- "JPEG",
- placement.x, placement.y, placement.width, placement.height
- );
- } finally {
- bitmap.close();
- }
- onProgress(index + 1, items.length);
- // Yield so the progress bar and counter can actually paint.
- await new Promise((resolve) => setTimeout(resolve, 0));
- }
-
- return doc.output("blob");
- }
-
- /* ---- intake ---------------------------------------------------------- */
-
function accept(fileList) {
const problems = [];
- let room = MAX_FILES - pages.length;
+ let room = MAX_FILES - files.length;
for (const file of Array.from(fileList)) {
if (room <= 0) {
- problems.push(`${MAX_FILES} images is the limit — skipped the rest.`);
+ problems.push(`${MAX_FILES} files is the limit — skipped the rest.`);
break;
}
- // TIFFs often arrive with an empty type, so fall back to the extension.
- const looksLikeImage =
- file.type.startsWith("image/") || /\.tiff?$/i.test(file.name);
- if (!looksLikeImage) {
- problems.push(`${file.name} isn't an image.`);
+ const format = formatFor(file);
+ if (!format) {
+ const extension = extensionOf(file.name).toUpperCase() || "That file type";
+ problems.push(`${extension} isn't supported. Use images, DOCX, XLSX, Markdown, or CSV.`);
continue;
}
if (file.size > MAX_FILE_BYTES) {
@@ -185,144 +109,233 @@
);
continue;
}
- pages.push({ id: nextId++, file, url: URL.createObjectURL(file) });
+ files.push({
+ id: nextId++,
+ file,
+ kind: format.kind,
+ label: format.label,
+ previewUrl: format.kind === "image" ? URL.createObjectURL(file) : null,
+ });
room -= 1;
}
- setAlert(problems.length ? problems[0] : "");
+ setAlert(problems[0] || "");
showComposer();
render();
}
function remove(id) {
- const index = pages.findIndex((page) => page.id === id);
+ if (busy) return;
+ const index = files.findIndex((item) => item.id === id);
if (index === -1) return;
- URL.revokeObjectURL(pages[index].url);
- pages.splice(index, 1);
+ if (files[index].previewUrl) URL.revokeObjectURL(files[index].previewUrl);
+ files.splice(index, 1);
render();
}
function move(id, delta) {
- const from = pages.findIndex((page) => page.id === id);
+ if (busy) return;
+ const from = files.findIndex((item) => item.id === id);
const to = from + delta;
- if (from === -1 || to < 0 || to >= pages.length) return;
- [pages[from], pages[to]] = [pages[to], pages[from]];
+ if (from === -1 || to < 0 || to >= files.length) return;
+ [files[from], files[to]] = [files[to], files[from]];
render();
- const arrow = sheetsEl.querySelector(
- `.card[data-id="${id}"] .card__arrow--${delta < 0 ? "up" : "down"}`
- );
- if (arrow && !arrow.disabled) arrow.focus();
+ sheetsEl
+ .querySelector(`.card[data-id="${id}"] .card__arrow--${delta < 0 ? "up" : "down"}`)
+ ?.focus();
}
function clearAll() {
- pages.forEach((page) => URL.revokeObjectURL(page.url));
- pages = [];
+ if (busy) return;
+ files.forEach((item) => item.previewUrl && URL.revokeObjectURL(item.previewUrl));
+ files = [];
setAlert("");
render();
}
- /* ---- render ---------------------------------------------------------- */
-
function render() {
sheetsEl.textContent = "";
-
- pages.forEach((page, index) => {
+ files.forEach((item, index) => {
const node = template.content.firstElementChild.cloneNode(true);
- node.dataset.id = String(page.id);
+ node.dataset.id = String(item.id);
node.querySelector(".card__num").textContent = String(index + 1);
- node.querySelector(".card__name").textContent = page.file.name;
+ node.querySelector(".card__name").textContent = item.file.name;
+ node.querySelector(".card__meta").textContent = `${item.label} · ${formatBytes(item.file.size)}`;
+ node.querySelector(".card__type").textContent = extensionOf(item.file.name).toUpperCase();
+ node.classList.add(`card--${item.kind}`);
const img = node.querySelector(".card__img");
- // No <img> decoder for TIFF either, so the thumbnail falls back to a
- // plain sheet outline rather than a broken image.
- img.src = page.url;
- img.addEventListener("error", () => {
+ if (item.previewUrl) {
+ img.src = item.previewUrl;
+ img.alt = `Preview of ${item.file.name}`;
+ img.addEventListener("error", () => {
+ img.remove();
+ node.querySelector(".card__frame").classList.add("card__frame--document");
+ }, { once: true });
+ } else {
img.remove();
- node.querySelector(".card__frame").classList.add("card__frame--blank");
- }, { once: true });
+ node.querySelector(".card__frame").classList.add("card__frame--document");
+ }
const up = node.querySelector(".card__arrow--up");
const down = node.querySelector(".card__arrow--down");
- up.disabled = index === 0;
- down.disabled = index === pages.length - 1;
- up.addEventListener("click", () => move(page.id, -1));
- down.addEventListener("click", () => move(page.id, 1));
- node.querySelector(".card__remove").addEventListener("click", () => remove(page.id));
+ up.querySelector(".visually-hidden").textContent = `Move ${item.file.name} earlier`;
+ down.querySelector(".visually-hidden").textContent = `Move ${item.file.name} later`;
+ up.disabled = index === 0 || busy;
+ down.disabled = index === files.length - 1 || busy;
+ up.addEventListener("click", () => move(item.id, -1));
+ down.addEventListener("click", () => move(item.id, 1));
+ const removeButton = node.querySelector(".card__remove");
+ removeButton.querySelector(".visually-hidden").textContent = `Remove ${item.file.name}`;
+ removeButton.disabled = busy;
+ removeButton.addEventListener("click", () => remove(item.id));
node.addEventListener("dragstart", (event) => {
- dragId = page.id;
+ if (busy) return event.preventDefault();
+ dragId = item.id;
node.classList.add("is-lifted");
event.dataTransfer.effectAllowed = "move";
- // Firefox refuses to start a drag without payload.
- event.dataTransfer.setData("text/plain", String(page.id));
+ event.dataTransfer.setData("text/plain", String(item.id));
});
node.addEventListener("dragend", () => {
dragId = null;
node.classList.remove("is-lifted");
- sheetsEl.querySelectorAll(".is-over").forEach((el) => el.classList.remove("is-over"));
+ sheetsEl.querySelectorAll(".is-over").forEach((element) => element.classList.remove("is-over"));
});
node.addEventListener("dragover", (event) => {
- if (dragId === null || dragId === page.id) return;
+ if (dragId === null || dragId === item.id) return;
event.preventDefault();
node.classList.add("is-over");
});
node.addEventListener("dragleave", () => node.classList.remove("is-over"));
node.addEventListener("drop", (event) => {
- if (dragId === null) return;
+ if (dragId === null || busy) return;
event.preventDefault();
event.stopPropagation();
node.classList.remove("is-over");
- const from = pages.findIndex((p) => p.id === dragId);
- const to = pages.findIndex((p) => p.id === page.id);
+ const from = files.findIndex((candidate) => candidate.id === dragId);
+ const to = files.findIndex((candidate) => candidate.id === item.id);
if (from === -1 || to === -1 || from === to) return;
- pages.splice(to, 0, pages.splice(from, 1)[0]);
+ files.splice(to, 0, files.splice(from, 1)[0]);
render();
});
-
sheetsEl.appendChild(node);
});
- const count = pages.length;
+ const count = files.length;
form.classList.toggle("has-pages", count > 0);
tray.hidden = count === 0;
- trayCount.textContent =
- `${count} ${count === 1 ? "image" : "images"} · ${formatBytes(totalBytes())}`;
+ trayCount.textContent = `${count} ${count === 1 ? "file" : "files"} · ${formatBytes(totalBytes())}`;
buildBtn.disabled = count === 0 || busy;
buildBtn.hidden = count === 0;
+ clearBtn.disabled = busy;
}
- /* ---- view switching --------------------------------------------------- */
-
function showComposer() {
done.hidden = true;
form.hidden = false;
}
- function showDone(blob) {
+ function showDone(result) {
if (lastUrl) URL.revokeObjectURL(lastUrl);
- lastUrl = URL.createObjectURL(blob);
+ lastUrl = URL.createObjectURL(result.blob);
const stamp = new Date().toISOString().slice(0, 19).replace(/[-:]/g, "").replace("T", "-");
downloadLink.href = lastUrl;
downloadLink.download = `makeitpdf-${stamp}.pdf`;
- doneMeta.textContent =
- `${pages.length} ${pages.length === 1 ? "page" : "pages"} · ${formatBytes(blob.size)}`;
+ doneMeta.textContent = `${result.pageCount} ${result.pageCount === 1 ? "page" : "pages"} · ${formatBytes(result.blob.size)}`;
+ if (result.warnings.length) {
+ doneNotes.hidden = false;
+ doneNotes.textContent = `${result.warnings.length} conversion ${result.warnings.length === 1 ? "note" : "notes"}: ${result.warnings.slice(0, 3).join(" · ")}`;
+ } else {
+ doneNotes.hidden = true;
+ doneNotes.textContent = "";
+ }
form.hidden = true;
done.hidden = false;
downloadLink.focus();
}
- /* ---- wiring ---------------------------------------------------------- */
+ function runOfficeWorker(kind, file) {
+ return file.arrayBuffer().then(
+ (buffer) =>
+ new Promise((resolve, reject) => {
+ const worker = new Worker("office-worker.js");
+ const timer = setTimeout(() => {
+ worker.terminate();
+ reject(new Error(`${file.name} took longer than 30 seconds to read.`));
+ }, OFFICE_TIMEOUT_MS);
+ worker.addEventListener("message", (event) => {
+ if (event.data.id !== 1) return;
+ clearTimeout(timer);
+ worker.terminate();
+ if (event.data.ok) resolve(event.data.result);
+ else reject(new Error(`${file.name} ${event.data.error}`));
+ });
+ worker.addEventListener("error", () => {
+ clearTimeout(timer);
+ worker.terminate();
+ reject(new Error(`${file.name} could not be read.`));
+ });
+ worker.postMessage({ id: 1, kind, buffer }, [buffer]);
+ })
+ );
+ }
+
+ async function parseItem(item) {
+ if (item.kind === "image") return { ...item, warnings: [] };
+ if (item.kind === "docx") {
+ const result = await runOfficeWorker("docx", item.file);
+ const parsed = Documents.htmlToBlocks(result.html);
+ if (!parsed.blocks.length) throw new Error(`${item.file.name} did not contain readable document content.`);
+ return { ...item, blocks: parsed.blocks, warnings: [...result.warnings, ...parsed.warnings] };
+ }
+ if (item.kind === "xlsx") {
+ const result = await runOfficeWorker("xlsx", item.file);
+ return { ...item, sheets: result.sheets, warnings: [] };
+ }
+
+ let text;
+ try {
+ text = Documents.decodeUtf8(await item.file.arrayBuffer());
+ } catch (error) {
+ throw new Error(`${item.file.name} ${error.message}`);
+ }
+ if (item.kind === "csv") {
+ let parsed;
+ try {
+ parsed = Documents.parseCsv(text);
+ } catch (error) {
+ throw new Error(`${item.file.name} ${error.message}`);
+ }
+ if (parsed.cells > MAX_SHEET_CELLS) {
+ throw new Error(`${item.file.name} has more than ${MAX_SHEET_CELLS.toLocaleString()} rendered cells.`);
+ }
+ const name = item.file.name.replace(/\.csv$/i, "") || "CSV";
+ return { ...item, sheets: [{ name, rows: parsed.rows, merges: [], widths: [] }], warnings: [] };
+ }
+
+ const markdown = window.markdownit({ html: false, linkify: true, typographer: false });
+ markdown.validateLink = (url) => Boolean(Documents.safeUrl(url) || Documents.SAFE_DATA_IMAGE.test(url));
+ const parsed = Documents.htmlToBlocks(markdown.render(text));
+ parsed.blocks.forEach((block) => {
+ if (block.type === "list") {
+ block.items = block.items.map((entry) =>
+ entry.replace(/^\[ \]\s*/, "☐ ").replace(/^\[[xX]\]\s*/, "☒ ")
+ );
+ }
+ });
+ if (!parsed.blocks.length) throw new Error(`${item.file.name} did not contain readable Markdown.`);
+ return { ...item, blocks: parsed.blocks, warnings: parsed.warnings };
+ }
formats.textContent =
- `JPG · PNG · WEBP · GIF · BMP · TIFF · up to ${formatBytes(MAX_FILE_BYTES)} each`;
+ `JPG · PNG · WEBP · GIF · BMP · TIFF · DOCX · XLSX · MD · CSV · ${formatBytes(MAX_FILE_BYTES)} each`;
picker.addEventListener("change", () => {
accept(picker.files);
picker.value = "";
});
-
clearBtn.addEventListener("click", clearAll);
-
restartBtn.addEventListener("click", () => {
clearAll();
showComposer();
@@ -336,43 +349,44 @@
dropzone.classList.add("is-target");
});
});
-
["dragleave", "drop"].forEach((type) => {
dropzone.addEventListener(type, (event) => {
if (type === "dragleave" && dropzone.contains(event.relatedTarget)) return;
dropzone.classList.remove("is-target");
});
});
-
dropzone.addEventListener("drop", (event) => {
if (!event.dataTransfer?.files.length) return;
event.preventDefault();
accept(event.dataTransfer.files);
});
- /* ---- convert --------------------------------------------------------- */
-
form.addEventListener("submit", async (event) => {
event.preventDefault();
- if (busy || !pages.length) return;
-
+ if (busy || !files.length) return;
busy = true;
setAlert("");
progress.classList.add("is-running");
- buildBtn.disabled = true;
+ render();
try {
- const blob = await buildPdf(pages, (current, total) => {
- buildBtn.textContent = `Converting ${current} of ${total}…`;
+ const parsed = [];
+ for (let index = 0; index < files.length; index += 1) {
+ buildBtn.textContent = `Reading ${index + 1} of ${files.length}…`;
+ parsed.push(await parseItem(files[index]));
+ await new Promise((resolve) => setTimeout(resolve, 0));
+ }
+ const result = await PdfBuilder.build(parsed, (_current, _total, label) => {
+ buildBtn.textContent = `${label}…`;
});
- showDone(blob);
+ showDone(result);
} catch (error) {
setAlert(error.message || "Something went wrong building the PDF.");
} finally {
busy = false;
progress.classList.remove("is-running");
buildBtn.textContent = "Convert to PDF";
- buildBtn.disabled = pages.length === 0;
+ render();
}
});
diff --git a/docs/documents.js b/docs/documents.js
new file mode 100644
index 0000000..fc34fda
--- /dev/null
+++ b/docs/documents.js
@@ -0,0 +1,231 @@
+/* Safe, format-neutral document parsing helpers. */
+
+(function (root, factory) {
+ if (typeof module === "object" && module.exports) module.exports = factory();
+ else root.Documents = factory();
+})(typeof self !== "undefined" ? self : globalThis, function () {
+ "use strict";
+
+ const SAFE_LINK = /^(https?:|mailto:)/i;
+ const SAFE_DATA_IMAGE = /^data:image\/(?:png|jpe?g|webp|gif|bmp);base64,/i;
+
+ function safeUrl(value) {
+ const url = String(value || "").trim();
+ return SAFE_LINK.test(url) ? url : null;
+ }
+
+ function decodeUtf8(buffer) {
+ try {
+ return new TextDecoder("utf-8", { fatal: true }).decode(buffer).replace(/^\uFEFF/, "");
+ } catch {
+ throw new Error("isn't valid UTF-8. Save it as UTF-8 and try again.");
+ }
+ }
+
+ function parseDelimited(text, delimiter) {
+ const rows = [];
+ let row = [];
+ let field = "";
+ let quoted = false;
+
+ for (let index = 0; index < text.length; index += 1) {
+ const char = text[index];
+ if (quoted) {
+ if (char === '"' && text[index + 1] === '"') {
+ field += '"';
+ index += 1;
+ } else if (char === '"') {
+ quoted = false;
+ } else {
+ field += char;
+ }
+ } else if (char === '"' && field.length === 0) {
+ quoted = true;
+ } else if (char === delimiter) {
+ row.push(field);
+ field = "";
+ } else if (char === "\n" || char === "\r") {
+ if (char === "\r" && text[index + 1] === "\n") index += 1;
+ row.push(field);
+ rows.push(row);
+ row = [];
+ field = "";
+ } else {
+ field += char;
+ }
+ }
+ if (quoted) throw new Error("has an unclosed quoted field.");
+ if (field.length || row.length || !rows.length) {
+ row.push(field);
+ rows.push(row);
+ }
+ return rows;
+ }
+
+ function detectDelimiter(text) {
+ const candidates = [",", "\t", ";", "|"];
+ let best = { delimiter: ",", score: -Infinity };
+ for (const delimiter of candidates) {
+ let rows;
+ try {
+ rows = parseDelimited(text.slice(0, 65536), delimiter).slice(0, 12);
+ } catch {
+ continue;
+ }
+ const counts = rows.filter((row) => row.some((cell) => cell !== "")).map((row) => row.length);
+ const common = counts.reduce((map, count) => map.set(count, (map.get(count) || 0) + 1), new Map());
+ const consistency = Math.max(0, ...common.values());
+ const width = Math.max(1, ...counts);
+ const score = width > 1 ? consistency * 100 + width : 0;
+ if (score > best.score) best = { delimiter, score };
+ }
+ return best.delimiter;
+ }
+
+ function parseCsv(text) {
+ let inQuotes = false;
+ for (let index = 0; index < text.length; index += 1) {
+ if (text[index] !== '"') continue;
+ if (inQuotes && text[index + 1] === '"') index += 1;
+ else inQuotes = !inQuotes;
+ }
+ if (inQuotes) throw new Error("has an unclosed quoted field.");
+ const delimiter = detectDelimiter(text);
+ const rows = parseDelimited(text, delimiter);
+ const width = rows.reduce((max, row) => Math.max(max, row.length), 0);
+ rows.forEach((row) => {
+ while (row.length < width) row.push("");
+ });
+ return { delimiter, rows, cells: rows.length * width };
+ }
+
+ function cleanText(value) {
+ return String(value || "")
+ .replace(/\u00a0/g, " ")
+ .replace(/[ \t]+\n/g, "\n")
+ .replace(/\n[ \t]+/g, "\n")
+ .trim();
+ }
+
+ function htmlToBlocks(html) {
+ if (typeof DOMParser === "undefined") throw new Error("This browser cannot read document content.");
+ const parsed = new DOMParser().parseFromString(String(html || ""), "text/html");
+ const blocks = [];
+ const warnings = [];
+
+ function inline(node, style, into) {
+ const output = into || [];
+ const current = style || {};
+ if (node.nodeType === 3) {
+ if (node.nodeValue) output.push({ text: node.nodeValue, ...current });
+ return output;
+ }
+ if (node.nodeType !== 1) return output;
+ const tag = node.tagName.toLowerCase();
+ if (tag === "br") {
+ output.push({ text: "\n", ...current });
+ return output;
+ }
+ if (tag === "img") return output;
+
+ const next = { ...current };
+ if (tag === "strong" || tag === "b") next.bold = true;
+ if (tag === "em" || tag === "i") next.italic = true;
+ if (tag === "code") next.code = true;
+ if (tag === "s" || tag === "del" || tag === "strike") next.strike = true;
+ if (tag === "a") {
+ const href = safeUrl(node.getAttribute("href"));
+ if (href) next.link = href;
+ else if (node.getAttribute("href")) warnings.push(`Skipped an unsafe link: ${node.getAttribute("href")}`);
+ }
+ Array.from(node.childNodes).forEach((child) => inline(child, next, output));
+ return output;
+ }
+
+ function imagesFrom(element) {
+ return Array.from(element.querySelectorAll("img")).map((img) => {
+ const src = String(img.getAttribute("src") || "");
+ const alt = cleanText(img.getAttribute("alt") || "Image");
+ if (SAFE_DATA_IMAGE.test(src)) return { type: "image", src, alt };
+ const label = src ? `${alt} — ${src}` : alt;
+ warnings.push(`Did not load linked image: ${src || alt}`);
+ return { type: "paragraph", spans: [{ text: label, italic: true }] };
+ });
+ }
+
+ function addTextBlock(type, element, extra) {
+ const spans = inline(element).filter((span) => span.text);
+ const text = cleanText(spans.map((span) => span.text).join(""));
+ if (text) blocks.push({ type, spans, ...(extra || {}) });
+ blocks.push(...imagesFrom(element));
+ }
+
+ function walk(element) {
+ if (element.nodeType !== 1) return;
+ const tag = element.tagName.toLowerCase();
+ if (/^h[1-6]$/.test(tag)) return addTextBlock("heading", element, { level: Number(tag[1]) });
+ if (tag === "p") return addTextBlock("paragraph", element);
+ if (tag === "pre") {
+ const text = element.textContent || "";
+ if (text) blocks.push({ type: "code", text: text.replace(/\n$/, "") });
+ return;
+ }
+ if (tag === "blockquote") {
+ const text = cleanText(element.textContent);
+ if (text) blocks.push({ type: "quote", text });
+ return;
+ }
+ if (tag === "hr") {
+ blocks.push({ type: "rule" });
+ return;
+ }
+ if (tag === "img") {
+ blocks.push(...imagesFrom(element.parentElement || element));
+ return;
+ }
+ if (tag === "ul" || tag === "ol") {
+ const items = Array.from(element.children)
+ .filter((child) => child.tagName.toLowerCase() === "li")
+ .map((item) => {
+ const clone = item.cloneNode(true);
+ clone.querySelectorAll("ul,ol").forEach((nested) => nested.remove());
+ return cleanText(clone.textContent);
+ })
+ .filter(Boolean);
+ if (items.length) blocks.push({ type: "list", ordered: tag === "ol", items });
+ return;
+ }
+ if (tag === "table") {
+ const rows = Array.from(element.querySelectorAll("tr")).map((row) =>
+ Array.from(row.children)
+ .filter((cell) => /^(td|th)$/i.test(cell.tagName))
+ .map((cell) => ({
+ text: cleanText(cell.textContent),
+ header: cell.tagName.toLowerCase() === "th",
+ colSpan: Math.max(1, Number(cell.getAttribute("colspan")) || 1),
+ rowSpan: Math.max(1, Number(cell.getAttribute("rowspan")) || 1),
+ }))
+ );
+ if (rows.length) blocks.push({ type: "table", rows });
+ return;
+ }
+ if (["body", "main", "article", "section", "div", "figure", "figcaption"].includes(tag)) {
+ Array.from(element.children).forEach(walk);
+ } else if (cleanText(element.textContent)) {
+ addTextBlock("paragraph", element);
+ }
+ }
+
+ Array.from(parsed.body.children).forEach(walk);
+ return { blocks, warnings };
+ }
+
+ return {
+ SAFE_DATA_IMAGE,
+ safeUrl,
+ decodeUtf8,
+ detectDelimiter,
+ parseCsv,
+ htmlToBlocks,
+ };
+});
diff --git a/docs/index.html b/docs/index.html
index a03c147..c4a2371 100644
--- a/docs/index.html
+++ b/docs/index.html
@@ -4,10 +4,7 @@
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>MakeItPDF</title>
-<meta name="description" content="Combine up to 10 images into a single PDF, entirely in your browser.">
-<link rel="preconnect" href="https://fonts.googleapis.com">
-<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
-<link href="https://fonts.googleapis.com/css2?family=Figtree:wght@400;500;600;700&display=swap" rel="stylesheet">
+<meta name="description" content="Turn images, Word, Excel, Markdown, and CSV files into one PDF, entirely in your browser.">
<link rel="stylesheet" href="style.css">
<link rel="icon" href="data:image/svg+xml,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 32 32'><rect width='32' height='32' rx='9' fill='%23f4402d'/><rect x='7.5' y='5' width='11' height='15' rx='2.4' fill='%23fff' opacity='.5'/><rect x='13.5' y='11' width='11' height='15' rx='2.4' fill='%23fff' stroke='%23f4402d' stroke-width='2.2'/></svg>">
</head>
@@ -28,8 +25,8 @@
</svg>
<span class="logo__text">MakeIt<span class="logo__accent">PDF</span></span>
</h1>
- <p class="intro__sub">Combine up to 10 images into a single PDF.
- Nothing leaves your device.</p>
+ <p class="intro__sub">Turn images, Word, Excel, Markdown, and CSV files
+ into one PDF. Nothing leaves your device.</p>
</div>
<noscript>
@@ -41,23 +38,26 @@
<form class="composer" id="composer" data-max-files="10" data-max-file-bytes="26214400">
<input class="picker" id="picker" type="file" multiple
- accept="image/jpeg,image/png,image/webp,image/gif,image/bmp,image/tiff">
+ accept=".jpg,.jpeg,.png,.webp,.gif,.bmp,.tif,.tiff,.docx,.xlsx,.md,.markdown,.csv">
<div class="dropzone" id="dropzone">
- <label class="btn btn--accent btn--lg" for="picker">Select images</label>
+ <div class="format-spine" aria-hidden="true">
+ <span>IMG</span><span>DOCX</span><span>XLSX</span><span>MD</span><span>CSV</span>
+ </div>
+ <label class="btn btn--accent btn--lg" for="picker">Select files</label>
<p class="dropzone__hint">or drop them here</p>
<p class="dropzone__formats" id="formats"></p>
</div>
<section class="tray" id="tray" hidden>
<div class="tray__head">
- <h2 class="tray__count" id="tray-count">0 images</h2>
+ <h2 class="tray__count" id="tray-count">0 files</h2>
<button class="link" id="clear" type="button">Clear all</button>
</div>
<ol class="grid" id="sheets"></ol>
</section>
- <button class="btn btn--accent btn--block" id="build" type="submit" hidden>Convert to PDF</button>
+ <button class="btn btn--accent btn--block" id="build" type="submit" aria-live="polite" hidden>Convert to PDF</button>
</form>
<section class="done" id="done" hidden>
@@ -68,11 +68,12 @@
</svg>
<h2 class="done__title">Your PDF is ready</h2>
<p class="done__meta" id="done-meta"></p>
+ <p class="done__notes" id="done-notes" hidden></p>
<a class="btn btn--accent btn--lg" id="download" download>Download PDF</a>
- <button class="link" id="restart" type="button">Convert more images</button>
+ <button class="link" id="restart" type="button">Convert more files</button>
</section>
- <p class="footnote">Your images are never uploaded. The PDF is built on this
+ <p class="footnote">Your files are never uploaded. The PDF is built on this
device and stays here.</p>
</main>
@@ -80,6 +81,7 @@
<li class="card" draggable="true">
<span class="card__frame">
<img class="card__img" alt="">
+ <span class="card__paper" aria-hidden="true"><span class="card__type"></span></span>
<span class="card__num"></span>
<button class="card__remove" type="button">
<svg viewBox="0 0 16 16" aria-hidden="true">
@@ -104,15 +106,20 @@
</span>
</span>
<span class="card__name"></span>
+ <span class="card__meta"></span>
</li>
</template>
-<script src="vendor/jspdf.umd.min.js"></script>
-<!-- pako must load before UTIF: UTIF picks it up as self.pako at load time and
- needs it to inflate deflate-compressed TIFFs. -->
+<script src="vendor/pdfkit.standalone.js"></script>
+<script src="vendor/blob-stream.js"></script>
+<script src="vendor/bidi.min.js"></script>
+<script src="vendor/markdown-it.min.js"></script>
+<!-- pako must load before UTIF so deflate-compressed TIFFs work. -->
<script src="vendor/pako_inflate.min.js"></script>
<script src="vendor/UTIF.js"></script>
<script src="pages.js"></script>
+<script src="documents.js"></script>
+<script src="pdf-builder.js"></script>
<script src="app.js"></script>
</body>
</html>
diff --git a/docs/office-worker.js b/docs/office-worker.js
new file mode 100644
index 0000000..d747674
--- /dev/null
+++ b/docs/office-worker.js
@@ -0,0 +1,94 @@
+/* DOCX/XLSX parsing is isolated from the UI thread. No network URLs are used;
+ importScripts only reads the vendored files beside this application. */
+
+"use strict";
+
+const MAX_CELLS = 50000;
+
+self.addEventListener("message", async (event) => {
+ const { id, kind, buffer } = event.data || {};
+ try {
+ if (kind === "docx") {
+ importScripts("vendor/mammoth.browser.min.js");
+ const result = await self.mammoth.convertToHtml(
+ { arrayBuffer: buffer },
+ { externalFileAccess: false }
+ );
+ self.postMessage({
+ id,
+ ok: true,
+ result: {
+ html: result.value,
+ warnings: result.messages.map((message) => message.message || String(message)),
+ },
+ });
+ return;
+ }
+
+ if (kind === "xlsx") {
+ importScripts("vendor/xlsx.full.min.js");
+ const workbook = self.XLSX.read(buffer, {
+ type: "array",
+ cellDates: false,
+ cellNF: true,
+ cellStyles: true,
+ });
+ const sheetMeta = new Map(
+ (workbook.Workbook?.Sheets || []).map((sheet) => [sheet.name, sheet])
+ );
+ const sheets = [];
+ let cellCount = 0;
+
+ for (const name of workbook.SheetNames) {
+ if (sheetMeta.get(name)?.Hidden) continue;
+ const worksheet = workbook.Sheets[name];
+ const ref = worksheet["!ref"];
+ if (!ref) {
+ sheets.push({ name, rows: [], merges: [], widths: [] });
+ continue;
+ }
+ const range = self.XLSX.utils.decode_range(ref);
+ const rowCount = range.e.r - range.s.r + 1;
+ const columnCount = range.e.c - range.s.c + 1;
+ cellCount += rowCount * columnCount;
+ if (cellCount > MAX_CELLS) {
+ throw new Error(`has more than ${MAX_CELLS.toLocaleString()} rendered cells.`);
+ }
+
+ const rows = [];
+ for (let rowIndex = range.s.r; rowIndex <= range.e.r; rowIndex += 1) {
+ const row = [];
+ for (let columnIndex = range.s.c; columnIndex <= range.e.c; columnIndex += 1) {
+ const address = self.XLSX.utils.encode_cell({ r: rowIndex, c: columnIndex });
+ const cell = worksheet[address];
+ if (!cell) row.push("");
+ else if (cell.w !== undefined) row.push(String(cell.w));
+ else if (cell.v !== undefined && cell.v !== null) row.push(String(cell.v));
+ else if (cell.f) row.push(`=${cell.f}`);
+ else row.push("");
+ }
+ rows.push(row);
+ }
+
+ const merges = (worksheet["!merges"] || []).map((merge) => ({
+ startRow: merge.s.r - range.s.r,
+ startColumn: merge.s.c - range.s.c,
+ rowSpan: merge.e.r - merge.s.r + 1,
+ colSpan: merge.e.c - merge.s.c + 1,
+ }));
+ const widths = Array.from({ length: columnCount }, (_, index) => {
+ const column = worksheet["!cols"]?.[range.s.c + index];
+ return column?.wpx || (column?.wch ? column.wch * 7 : null);
+ });
+ sheets.push({ name, rows, merges, widths });
+ }
+ if (!sheets.length) throw new Error("has no visible worksheets.");
+ self.postMessage({ id, ok: true, result: { sheets, cellCount } });
+ return;
+ }
+
+ throw new Error("uses an unsupported Office format.");
+ } catch (error) {
+ self.postMessage({ id, ok: false, error: error.message || String(error) });
+ }
+});
diff --git a/docs/package-lock.json b/docs/package-lock.json
new file mode 100644
index 0000000..b650b92
--- /dev/null
+++ b/docs/package-lock.json
@@ -0,0 +1,777 @@
+{
+ "name": "makeitpdf",
+ "version": "2.0.0",
+ "lockfileVersion": 3,
+ "requires": true,
+ "packages": {
+ "": {
+ "name": "makeitpdf",
+ "version": "2.0.0",
+ "dependencies": {
+ "@fontsource/figtree": "^5.3.0",
+ "bidi-js": "^1.0.3",
+ "blob-stream": "^0.1.3",
+ "mammoth": "^1.12.1",
+ "markdown-it": "^15.0.0",
+ "pdfkit": "^0.19.1",
+ "xlsx": "https://cdn.sheetjs.com/xlsx-0.20.3/xlsx-0.20.3.tgz"
+ },
+ "devDependencies": {
+ "pdfjs-dist": "^6.2.108",
+ "playwright-core": "^1.62.1"
+ }
+ },
+ "node_modules/@fontsource/figtree": {
+ "version": "5.3.0",
+ "resolved": "https://registry.npmjs.org/@fontsource/figtree/-/figtree-5.3.0.tgz",
+ "integrity": "sha512-/iTcTXL1eRBGhvD7NN9dnKzXzWqDLVZVEUFI9xIxAC1G/hXU8tCyYeXCAuPOuvSYPInFUP7H6KLGgygmDfy1SA==",
+ "funding": {
+ "url": "https://github.com/sponsors/ayuhito"
+ }
+ },
+ "node_modules/@napi-rs/canvas": {
+ "version": "1.0.7",
+ "resolved": "https://registry.npmjs.org/@napi-rs/canvas/-/canvas-1.0.7.tgz",
+ "integrity": "sha512-26wVFEgs6gbe7wmzCrud1pK8q6oOgcUu7OOF24BuazB8ZUCskU9ZSLrCjoWIFVxx09rjAxsXxPleaWowHvdPCA==",
+ "dev": true,
+ "optional": true,
+ "engines": {
+ "node": ">= 10"
+ },
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/Brooooooklyn"
+ },
+ "optionalDependencies": {
+ "@napi-rs/canvas-android-arm64": "1.0.7",
+ "@napi-rs/canvas-darwin-arm64": "1.0.7",
+ "@napi-rs/canvas-darwin-x64": "1.0.7",
+ "@napi-rs/canvas-linux-arm-gnueabihf": "1.0.7",
+ "@napi-rs/canvas-linux-arm64-gnu": "1.0.7",
+ "@napi-rs/canvas-linux-arm64-musl": "1.0.7",
+ "@napi-rs/canvas-linux-riscv64-gnu": "1.0.7",
+ "@napi-rs/canvas-linux-x64-gnu": "1.0.7",
+ "@napi-rs/canvas-linux-x64-musl": "1.0.7",
+ "@napi-rs/canvas-win32-arm64-msvc": "1.0.7",
+ "@napi-rs/canvas-win32-x64-msvc": "1.0.7"
+ }
+ },
+ "node_modules/@napi-rs/canvas-android-arm64": {
+ "version": "1.0.7",
+ "resolved": "https://registry.npmjs.org/@napi-rs/canvas-android-arm64/-/canvas-android-arm64-1.0.7.tgz",
+ "integrity": "sha512-d5p4hHTykc/9PjiBXkvCH/IC5hT5jH7BjQ1u8ITq+G8x4VmvveOHdy/4BWYcC3ebWRmrG9zEc/oCTy+Yf3iZ4A==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "optional": true,
+ "os": [
+ "android"
+ ],
+ "engines": {
+ "node": ">= 10"
+ },
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/Brooooooklyn"
+ }
+ },
+ "node_modules/@napi-rs/canvas-darwin-arm64": {
+ "version": "1.0.7",
+ "resolved": "https://registry.npmjs.org/@napi-rs/canvas-darwin-arm64/-/canvas-darwin-arm64-1.0.7.tgz",
+ "integrity": "sha512-jKfZl2QDqBr6/Ap/8NKkX0Po9SFfVjUPBJbQQmYGVtQQIazWWIAO60riH3Mz2sOyysa2oJO39sLEfXerypu0vg==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "optional": true,
+ "os": [
+ "darwin"
+ ],
+ "engines": {
+ "node": ">= 10"
+ },
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/Brooooooklyn"
+ }
+ },
+ "node_modules/@napi-rs/canvas-darwin-x64": {
+ "version": "1.0.7",
+ "resolved": "https://registry.npmjs.org/@napi-rs/canvas-darwin-x64/-/canvas-darwin-x64-1.0.7.tgz",
+ "integrity": "sha512-v1asrnKBu0tD+sdSD2qVIUfhoXSrr8LFTn7z76pN+8xsiOrmFcAVty57R/5DB8ZNqNLKsUBDXFSrzf4Wt9NaSg==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "optional": true,
+ "os": [
+ "darwin"
+ ],
+ "engines": {
+ "node": ">= 10"
+ },
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/Brooooooklyn"
+ }
+ },
+ "node_modules/@napi-rs/canvas-linux-arm64-gnu": {
+ "version": "1.0.7",
+ "resolved": "https://registry.npmjs.org/@napi-rs/canvas-linux-arm64-gnu/-/canvas-linux-arm64-gnu-1.0.7.tgz",
+ "integrity": "sha512-OVW6T1x65BTVfWb//xylCoWxbdII+nzHu3L5T035aerBAnZ5e0nmeTMkMEO9qQrVJq4WcWW8hp+T0dF+JvJPUQ==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">= 10"
+ },
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/Brooooooklyn"
+ }
+ },
+ "node_modules/@napi-rs/canvas-linux-arm64-musl": {
+ "version": "1.0.7",
+ "resolved": "https://registry.npmjs.org/@napi-rs/canvas-linux-arm64-musl/-/canvas-linux-arm64-musl-1.0.7.tgz",
+ "integrity": "sha512-KX/k1UO61XdKlXxhtIkq1ZUH8uP+NNK4vSrBcM2/4wWUUCZaG93BU5s0KuE9n+TFn0jEoqLSlXOuH1pYuV5dcQ==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">= 10"
+ },
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/Brooooooklyn"
+ }
+ },
+ "node_modules/@napi-rs/canvas-linux-riscv64-gnu": {
+ "version": "1.0.7",
+ "resolved": "https://registry.npmjs.org/@napi-rs/canvas-linux-riscv64-gnu/-/canvas-linux-riscv64-gnu-1.0.7.tgz",
+ "integrity": "sha512-r+0Bg+fK8r2AsrbeT7JwBUAERZSjlyhfAMQfZE/zxYGmbswS92hN0FPjynVWbeuz5fIrLfZ6JA5pH8V6drN6qw==",
+ "cpu": [
+ "riscv64"
+ ],
+ "dev": true,
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">= 10"
+ },
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/Brooooooklyn"
+ }
+ },
+ "node_modules/@napi-rs/canvas-linux-x64-gnu": {
+ "version": "1.0.7",
+ "resolved": "https://registry.npmjs.org/@napi-rs/canvas-linux-x64-gnu/-/canvas-linux-x64-gnu-1.0.7.tgz",
+ "integrity": "sha512-0tT9KzEcTfb6MWdUWIDfy6uq6UNF691r/9NfXxxl8s9+G5QrD7VP1UC+PAwzsCzJTClIKyidNau/grYTL+QmHw==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">= 10"
+ },
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/Brooooooklyn"
+ }
+ },
+ "node_modules/@napi-rs/canvas-linux-x64-musl": {
+ "version": "1.0.7",
+ "resolved": "https://registry.npmjs.org/@napi-rs/canvas-linux-x64-musl/-/canvas-linux-x64-musl-1.0.7.tgz",
+ "integrity": "sha512-rvT0xo1xA+C7e+U/2ngsxWsl9gC5knHU+z8KTT5VK0Zos4AQbWZvtjvegrmzxm4o2yHE32Lexj6CDEJNvuTczA==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">= 10"
+ },
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/Brooooooklyn"
+ }
+ },
+ "node_modules/@napi-rs/canvas-win32-arm64-msvc": {
+ "version": "1.0.7",
+ "resolved": "https://registry.npmjs.org/@napi-rs/canvas-win32-arm64-msvc/-/canvas-win32-arm64-msvc-1.0.7.tgz",
+ "integrity": "sha512-N6s3yoFFPUDkfRpjUiKERVYkli7ex5Sf0Bu/My6D6gOGYxYolC1KvL6x8U0aN+ScCWAzbkIWcMYR9g0OGsFuVw==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "optional": true,
+ "os": [
+ "win32"
+ ],
+ "engines": {
+ "node": ">= 10"
+ },
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/Brooooooklyn"
+ }
+ },
+ "node_modules/@napi-rs/canvas-win32-x64-msvc": {
+ "version": "1.0.7",
+ "resolved": "https://registry.npmjs.org/@napi-rs/canvas-win32-x64-msvc/-/canvas-win32-x64-msvc-1.0.7.tgz",
+ "integrity": "sha512-SKTNdBV/ljfhkPsLhXjm4x2PQ0ILpc0PhkBzRHuroJXidZUaF5yh0j3s3dIzB8PrRmW+nEASzyMTaWT3nH1ywQ==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "optional": true,
+ "os": [
+ "win32"
+ ],
+ "engines": {
+ "node": ">= 10"
+ },
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/Brooooooklyn"
+ }
+ },
+ "node_modules/@noble/ciphers": {
+ "version": "1.3.0",
+ "resolved": "https://registry.npmjs.org/@noble/ciphers/-/ciphers-1.3.0.tgz",
+ "integrity": "sha512-2I0gnIVPtfnMw9ee9h1dJG7tp81+8Ob3OJb3Mv37rx5L40/b0i7djjCVvGOVqc9AEIQyvyu1i6ypKdFw8R8gQw==",
+ "engines": {
+ "node": "^14.21.3 || >=16"
+ },
+ "funding": {
+ "url": "https://paulmillr.com/funding/"
+ }
+ },
+ "node_modules/@noble/hashes": {
+ "version": "1.8.0",
+ "resolved": "https://registry.npmjs.org/@noble/hashes/-/hashes-1.8.0.tgz",
+ "integrity": "sha512-jCs9ldd7NwzpgXDIf6P3+NrHh9/sD6CQdxHyjQI+h/6rDNo88ypBxxz45UDuZHz9r3tNz7N/VInSVoVdtXEI4A==",
+ "engines": {
+ "node": "^14.21.3 || >=16"
+ },
+ "funding": {
+ "url": "https://paulmillr.com/funding/"
+ }
+ },
+ "node_modules/@swc/helpers": {
+ "version": "0.5.23",
+ "resolved": "https://registry.npmjs.org/@swc/helpers/-/helpers-0.5.23.tgz",
+ "integrity": "sha512-5lSsMOTXURePglDfvuAQUqkGek9Hg2kksOYay2m0+XR++b2NWYL/4sWyuvVBIs8oKnJaxkdi9whaL/sqN13afw==",
+ "dependencies": {
+ "tslib": "^2.8.0"
+ }
+ },
+ "node_modules/@xmldom/xmldom": {
+ "version": "0.8.14",
+ "resolved": "https://registry.npmjs.org/@xmldom/xmldom/-/xmldom-0.8.14.tgz",
+ "integrity": "sha512-T4EDRUBVZYRldYApjEJiU0e1stYWaRAX7CuSnKzrpwdZKo53zGV8/pqfzV6FfwNl9YThD2OumQYvqtvjvgG7aQ==",
+ "engines": {
+ "node": ">=10.0.0"
+ }
+ },
+ "node_modules/argparse": {
+ "version": "1.0.10",
+ "resolved": "https://registry.npmjs.org/argparse/-/argparse-1.0.10.tgz",
+ "integrity": "sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg==",
+ "dependencies": {
+ "sprintf-js": "~1.0.2"
+ }
+ },
+ "node_modules/base64-js": {
+ "version": "1.5.1",
+ "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz",
+ "integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==",
+ "funding": [
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/feross"
+ },
+ {
+ "type": "patreon",
+ "url": "https://www.patreon.com/feross"
+ },
+ {
+ "type": "consulting",
+ "url": "https://feross.org/support"
+ }
+ ]
+ },
+ "node_modules/bidi-js": {
+ "version": "1.0.3",
+ "resolved": "https://registry.npmjs.org/bidi-js/-/bidi-js-1.0.3.tgz",
+ "integrity": "sha512-RKshQI1R3YQ+n9YJz2QQ147P66ELpa1FQEg20Dk8oW9t2KgLbpDLLp9aGZ7y8WHSshDknG0bknqGw5/tyCs5tw==",
+ "dependencies": {
+ "require-from-string": "^2.0.2"
+ }
+ },
+ "node_modules/blob": {
+ "version": "0.0.4",
+ "resolved": "https://registry.npmjs.org/blob/-/blob-0.0.4.tgz",
+ "integrity": "sha512-YRc9zvVz4wNaxcXmiSgb9LAg7YYwqQ2xd0Sj6osfA7k/PKmIGVlnOYs3wOFdkRC9/JpQu8sGt/zHgJV7xzerfg=="
+ },
+ "node_modules/blob-stream": {
+ "version": "0.1.3",
+ "resolved": "https://registry.npmjs.org/blob-stream/-/blob-stream-0.1.3.tgz",
+ "integrity": "sha512-xXwyhgVmPsFVFFvtM5P0syI17/oae+MIjLn5jGhuD86mmSJ61EWMWmbPrV/0+bdcH9jQ2CzIhmTQKNUJL7IPog==",
+ "dependencies": {
+ "blob": "0.0.4"
+ }
+ },
+ "node_modules/bluebird": {
+ "version": "3.4.7",
+ "resolved": "https://registry.npmjs.org/bluebird/-/bluebird-3.4.7.tgz",
+ "integrity": "sha512-iD3898SR7sWVRHbiQv+sHUtHnMvC1o3nW5rAcqnq3uOn07DSAppZYUkIGslDz6gXC7HfunPe7YVBgoEJASPcHA=="
+ },
+ "node_modules/brotli": {
+ "version": "1.3.3",
+ "resolved": "https://registry.npmjs.org/brotli/-/brotli-1.3.3.tgz",
+ "integrity": "sha512-oTKjJdShmDuGW94SyyaoQvAjf30dZaHnjJ8uAF+u2/vGJkJbJPJAT1gDiOJP5v1Zb6f9KEyW/1HpuaWIXtGHPg==",
+ "dependencies": {
+ "base64-js": "^1.1.2"
+ }
+ },
+ "node_modules/browserify-zlib": {
+ "version": "0.2.0",
+ "resolved": "https://registry.npmjs.org/browserify-zlib/-/browserify-zlib-0.2.0.tgz",
+ "integrity": "sha512-Z942RysHXmJrhqk88FmKBVq/v5tqmSkDz7p54G/MGyjMnCFFnC79XWNbg+Vta8W6Wb2qtSZTSxIGkJrRpCFEiA==",
+ "dependencies": {
+ "pako": "~1.0.5"
+ }
+ },
+ "node_modules/clone": {
+ "version": "2.1.2",
+ "resolved": "https://registry.npmjs.org/clone/-/clone-2.1.2.tgz",
+ "integrity": "sha512-3Pe/CF1Nn94hyhIYpjtiLhdCoEoz0DqQ+988E9gmeEdQZlojxnOb74wctFyuwWQHzqyf9X7C7MG8juUpqBJT8w==",
+ "engines": {
+ "node": ">=0.8"
+ }
+ },
+ "node_modules/core-util-is": {
+ "version": "1.0.3",
+ "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.3.tgz",
+ "integrity": "sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ=="
+ },
+ "node_modules/dfa": {
+ "version": "1.2.0",
+ "resolved": "https://registry.npmjs.org/dfa/-/dfa-1.2.0.tgz",
+ "integrity": "sha512-ED3jP8saaweFTjeGX8HQPjeC1YYyZs98jGNZx6IiBvxW7JG5v492kamAQB3m2wop07CvU/RQmzcKr6bgcC5D/Q=="
+ },
+ "node_modules/dingbat-to-unicode": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/dingbat-to-unicode/-/dingbat-to-unicode-1.0.1.tgz",
+ "integrity": "sha512-98l0sW87ZT58pU4i61wa2OHwxbiYSbuxsCBozaVnYX2iCnr3bLM3fIes1/ej7h1YdOKuKt/MLs706TVnALA65w=="
+ },
+ "node_modules/duck": {
+ "version": "0.1.12",
+ "resolved": "https://registry.npmjs.org/duck/-/duck-0.1.12.tgz",
+ "integrity": "sha512-wkctla1O6VfP89gQ+J/yDesM0S7B7XLXjKGzXxMDVFg7uEn706niAtyYovKbyq1oT9YwDcly721/iUWoc8MVRg==",
+ "dependencies": {
+ "underscore": "^1.13.1"
+ }
+ },
+ "node_modules/entities": {
+ "version": "8.0.0",
+ "resolved": "https://registry.npmjs.org/entities/-/entities-8.0.0.tgz",
+ "integrity": "sha512-zwfzJecQ/Uej6tusMqwAqU/6KL2XaB2VZ2Jg54Je6ahNBGNH6Ek6g3jjNCF0fG9EWQKGZNddNjU5F1ZQn/sBnA==",
+ "engines": {
+ "node": ">=20.19.0"
+ },
+ "funding": {
+ "url": "https://github.com/fb55/entities?sponsor=1"
+ }
+ },
+ "node_modules/fast-deep-equal": {
+ "version": "3.1.3",
+ "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz",
+ "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q=="
+ },
+ "node_modules/fontkit": {
+ "version": "2.0.4",
+ "resolved": "https://registry.npmjs.org/fontkit/-/fontkit-2.0.4.tgz",
+ "integrity": "sha512-syetQadaUEDNdxdugga9CpEYVaQIxOwk7GlwZWWZ19//qW4zE5bknOKeMBDYAASwnpaSHKJITRLMF9m1fp3s6g==",
+ "dependencies": {
+ "@swc/helpers": "^0.5.12",
+ "brotli": "^1.3.2",
+ "clone": "^2.1.2",
+ "dfa": "^1.2.0",
+ "fast-deep-equal": "^3.1.3",
+ "restructure": "^3.0.0",
+ "tiny-inflate": "^1.0.3",
+ "unicode-properties": "^1.4.0",
+ "unicode-trie": "^2.0.0"
+ }
+ },
+ "node_modules/immediate": {
+ "version": "3.0.6",
+ "resolved": "https://registry.npmjs.org/immediate/-/immediate-3.0.6.tgz",
+ "integrity": "sha512-XXOFtyqDjNDAQxVfYxuF7g9Il/IbWmmlQg2MYKOH8ExIT1qg6xc4zyS3HaEEATgs1btfzxq15ciUiY7gjSXRGQ=="
+ },
+ "node_modules/inherits": {
+ "version": "2.0.4",
+ "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz",
+ "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ=="
+ },
+ "node_modules/isarray": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz",
+ "integrity": "sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ=="
+ },
+ "node_modules/js-md5": {
+ "version": "0.8.3",
+ "resolved": "https://registry.npmjs.org/js-md5/-/js-md5-0.8.3.tgz",
+ "integrity": "sha512-qR0HB5uP6wCuRMrWPTrkMaev7MJZwJuuw4fnwAzRgP4J4/F8RwtodOKpGp4XpqsLBFzzgqIO42efFAyz2Et6KQ=="
+ },
+ "node_modules/jszip": {
+ "version": "3.10.1",
+ "resolved": "https://registry.npmjs.org/jszip/-/jszip-3.10.1.tgz",
+ "integrity": "sha512-xXDvecyTpGLrqFrvkrUSoxxfJI5AH7U8zxxtVclpsUtMCq4JQ290LY8AW5c7Ggnr/Y/oK+bQMbqK2qmtk3pN4g==",
+ "dependencies": {
+ "lie": "~3.3.0",
+ "pako": "~1.0.2",
+ "readable-stream": "~2.3.6",
+ "setimmediate": "^1.0.5"
+ }
+ },
+ "node_modules/lie": {
+ "version": "3.3.0",
+ "resolved": "https://registry.npmjs.org/lie/-/lie-3.3.0.tgz",
+ "integrity": "sha512-UaiMJzeWRlEujzAuw5LokY1L5ecNQYZKfmyZ9L7wDHb/p5etKaxXhohBcrw0EYby+G/NA52vRSN4N39dxHAIwQ==",
+ "dependencies": {
+ "immediate": "~3.0.5"
+ }
+ },
+ "node_modules/linebreak": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/linebreak/-/linebreak-1.1.0.tgz",
+ "integrity": "sha512-MHp03UImeVhB7XZtjd0E4n6+3xr5Dq/9xI/5FptGk5FrbDR3zagPa2DS6U8ks/3HjbKWG9Q1M2ufOzxV2qLYSQ==",
+ "dependencies": {
+ "base64-js": "0.0.8",
+ "unicode-trie": "^2.0.0"
+ }
+ },
+ "node_modules/linebreak/node_modules/base64-js": {
+ "version": "0.0.8",
+ "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-0.0.8.tgz",
+ "integrity": "sha512-3XSA2cR/h/73EzlXXdU6YNycmYI7+kicTxks4eJg2g39biHR84slg2+des+p7iHYhbRg/udIS4TD53WabcOUkw==",
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
+ "node_modules/linkify-it": {
+ "version": "6.1.0",
+ "resolved": "https://registry.npmjs.org/linkify-it/-/linkify-it-6.1.0.tgz",
+ "integrity": "sha512-wJ/TwpSDTLepCrQoYWYIExIKg5Zchex2Nn5yk2mFnB+6PtdkHtyLx742md9csRjjOnGkKIS/RrbY7l8D6gT9Vw==",
+ "funding": [
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/puzrin"
+ },
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/markdown-it"
+ }
+ ],
+ "dependencies": {
+ "uc.micro": "^3.0.0"
+ }
+ },
+ "node_modules/lop": {
+ "version": "0.4.2",
+ "resolved": "https://registry.npmjs.org/lop/-/lop-0.4.2.tgz",
+ "integrity": "sha512-RefILVDQ4DKoRZsJ4Pj22TxE3omDO47yFpkIBoDKzkqPRISs5U1cnAdg/5583YPkWPaLIYHOKRMQSvjFsO26cw==",
+ "dependencies": {
+ "duck": "^0.1.12",
+ "option": "~0.2.1",
+ "underscore": "^1.13.1"
+ }
+ },
+ "node_modules/mammoth": {
+ "version": "1.12.1",
+ "resolved": "https://registry.npmjs.org/mammoth/-/mammoth-1.12.1.tgz",
+ "integrity": "sha512-nCH9KKjWi3jQ+i8bUKs7k1yrXtSEGpWgF8IYkzsFMcbn+5S6l4bZEBbyx2hOQErFiXPuAs9RPa6qjXVxhyx/8g==",
+ "dependencies": {
+ "@xmldom/xmldom": "^0.8.6",
+ "argparse": "~1.0.3",
+ "base64-js": "^1.5.1",
+ "bluebird": "~3.4.0",
+ "dingbat-to-unicode": "^1.0.1",
+ "jszip": "^3.7.1",
+ "lop": "^0.4.2",
+ "path-is-absolute": "^1.0.0",
+ "underscore": "^1.13.1",
+ "xmlbuilder": "^10.0.0"
+ },
+ "bin": {
+ "mammoth": "bin/mammoth"
+ },
+ "engines": {
+ "node": ">=12.0.0"
+ }
+ },
+ "node_modules/markdown-it": {
+ "version": "15.0.0",
+ "resolved": "https://registry.npmjs.org/markdown-it/-/markdown-it-15.0.0.tgz",
+ "integrity": "sha512-Lf8ajvVNdRpzSNB4VegxNy7gjs8gU35l4b4+ET49LrQC5PKYwLZ72u60LeJ9gv3qiaesuYjJWCyVeQmv/QWKQw==",
+ "funding": [
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/puzrin"
+ },
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/markdown-it"
+ }
+ ],
+ "dependencies": {
+ "argparse": "^3.0.0",
+ "entities": "^8.0.0",
+ "linkify-it": "^6.0.0",
+ "mdurl": "^2.1.0",
+ "punycode.js": "^2.3.1",
+ "uc.micro": "^3.0.0"
+ },
+ "bin": {
+ "markdown-it": "bin/markdown-it.mjs"
+ }
+ },
+ "node_modules/markdown-it/node_modules/argparse": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/argparse/-/argparse-3.0.0.tgz",
+ "integrity": "sha512-BOp5NMrHqKxmq/OLr+clzzrRxgOKSLkcjmkWuChp7Irqwn4s74WjOBPIgWfA/HMcBnVkZ5XEuf9uUqzlpfCQ6A==",
+ "funding": [
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/puzrin"
+ },
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/nodeca"
+ }
+ ]
+ },
+ "node_modules/mdurl": {
+ "version": "2.1.0",
+ "resolved": "https://registry.npmjs.org/mdurl/-/mdurl-2.1.0.tgz",
+ "integrity": "sha512-1+HBaOx0zi/dQWht8rNv9MYf9qqpqL/kxI0hXImU6Y547zM6Sni8BQibt7ifgMcYtQg41ao3Ivd6cnSM86inpg=="
+ },
+ "node_modules/option": {
+ "version": "0.2.4",
+ "resolved": "https://registry.npmjs.org/option/-/option-0.2.4.tgz",
+ "integrity": "sha512-pkEqbDyl8ou5cpq+VsnQbe/WlEy5qS7xPzMS1U55OCG9KPvwFD46zDbxQIj3egJSFc3D+XhYOPUzz49zQAVy7A=="
+ },
+ "node_modules/pako": {
+ "version": "1.0.11",
+ "resolved": "https://registry.npmjs.org/pako/-/pako-1.0.11.tgz",
+ "integrity": "sha512-4hLB8Py4zZce5s4yd9XzopqwVv/yGNhV1Bl8NTmCq1763HeK2+EwVTv+leGeL13Dnh2wfbqowVPXCIO0z4taYw=="
+ },
+ "node_modules/path-is-absolute": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz",
+ "integrity": "sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==",
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/pdfjs-dist": {
+ "version": "6.2.108",
+ "resolved": "https://registry.npmjs.org/pdfjs-dist/-/pdfjs-dist-6.2.108.tgz",
+ "integrity": "sha512-YxFb+SQcodN2rnX9Tn3dHYlqfb7NjlzzfONPpJd+AKoKtUjEdevTfbC07d5TcczzOK6261auRkP/M8OBHs9vFQ==",
+ "dev": true,
+ "engines": {
+ "node": ">=22.13.0 || >=24"
+ },
+ "optionalDependencies": {
+ "@napi-rs/canvas": "^1.0.0"
+ }
+ },
+ "node_modules/pdfkit": {
+ "version": "0.19.1",
+ "resolved": "https://registry.npmjs.org/pdfkit/-/pdfkit-0.19.1.tgz",
+ "integrity": "sha512-6Gzk+wDwTs4VSxsR5rCMTnIl5nlmkye1oWB0l2hDB1EX6ZNSIBroKQEv+2+fPPn+stVjyqzmsqRJVDfB9fo5DA==",
+ "dependencies": {
+ "@noble/ciphers": "^1.0.0",
+ "@noble/hashes": "^1.6.0",
+ "fontkit": "^2.0.4",
+ "js-md5": "^0.8.3",
+ "linebreak": "^1.1.0",
+ "png-js": "^1.1.0"
+ }
+ },
+ "node_modules/playwright-core": {
+ "version": "1.62.1",
+ "resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.62.1.tgz",
+ "integrity": "sha512-wPYSwEBJY9GHraISXqyqtx0na0LpO3XEX7jNDhntbex7tzUS7kLnZsOlFruFJB4Hi/rhDMjXGqHewDZ68nYZVw==",
+ "dev": true,
+ "bin": {
+ "playwright-core": "cli.js"
+ },
+ "engines": {
+ "node": ">=20"
+ }
+ },
+ "node_modules/png-js": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/png-js/-/png-js-1.1.0.tgz",
+ "integrity": "sha512-PM/uYGzGdNSzqeOgly68+6wKQDL1SY0a/N+OEa/+br6LnHWOAJB0Npiamnodfq3jd2LS/i2fMeOKSAILjA+m5Q==",
+ "dependencies": {
+ "browserify-zlib": "^0.2.0"
+ }
+ },
+ "node_modules/process-nextick-args": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.1.tgz",
+ "integrity": "sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag=="
+ },
+ "node_modules/punycode.js": {
+ "version": "2.3.1",
+ "resolved": "https://registry.npmjs.org/punycode.js/-/punycode.js-2.3.1.tgz",
+ "integrity": "sha512-uxFIHU0YlHYhDQtV4R9J6a52SLx28BCjT+4ieh7IGbgwVJWO+km431c4yRlREUAsAmt/uMjQUyQHNEPf0M39CA==",
+ "engines": {
+ "node": ">=6"
+ }
+ },
+ "node_modules/readable-stream": {
+ "version": "2.3.8",
+ "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.8.tgz",
+ "integrity": "sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==",
+ "dependencies": {
+ "core-util-is": "~1.0.0",
+ "inherits": "~2.0.3",
+ "isarray": "~1.0.0",
+ "process-nextick-args": "~2.0.0",
+ "safe-buffer": "~5.1.1",
+ "string_decoder": "~1.1.1",
+ "util-deprecate": "~1.0.1"
+ }
+ },
+ "node_modules/require-from-string": {
+ "version": "2.0.2",
+ "resolved": "https://registry.npmjs.org/require-from-string/-/require-from-string-2.0.2.tgz",
+ "integrity": "sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==",
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/restructure": {
+ "version": "3.0.2",
+ "resolved": "https://registry.npmjs.org/restructure/-/restructure-3.0.2.tgz",
+ "integrity": "sha512-gSfoiOEA0VPE6Tukkrr7I0RBdE0s7H1eFCDBk05l1KIQT1UIKNc5JZy6jdyW6eYH3aR3g5b3PuL77rq0hvwtAw=="
+ },
+ "node_modules/safe-buffer": {
+ "version": "5.1.2",
+ "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz",
+ "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g=="
+ },
+ "node_modules/setimmediate": {
+ "version": "1.0.5",
+ "resolved": "https://registry.npmjs.org/setimmediate/-/setimmediate-1.0.5.tgz",
+ "integrity": "sha512-MATJdZp8sLqDl/68LfQmbP8zKPLQNV6BIZoIgrscFDQ+RsvK/BxeDQOgyxKKoh0y/8h3BqVFnCqQ/gd+reiIXA=="
+ },
+ "node_modules/sprintf-js": {
+ "version": "1.0.3",
+ "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.0.3.tgz",
+ "integrity": "sha512-D9cPgkvLlV3t3IzL0D0YLvGA9Ahk4PcvVwUbN0dSGr1aP0Nrt4AEnTUbuGvquEC0mA64Gqt1fzirlRs5ibXx8g=="
+ },
+ "node_modules/string_decoder": {
+ "version": "1.1.1",
+ "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz",
+ "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==",
+ "dependencies": {
+ "safe-buffer": "~5.1.0"
+ }
+ },
+ "node_modules/tiny-inflate": {
+ "version": "1.0.3",
+ "resolved": "https://registry.npmjs.org/tiny-inflate/-/tiny-inflate-1.0.3.tgz",
+ "integrity": "sha512-pkY1fj1cKHb2seWDy0B16HeWyczlJA9/WW3u3c4z/NiWDsO3DOU5D7nhTLE9CF0yXv/QZFY7sEJmj24dK+Rrqw=="
+ },
+ "node_modules/tslib": {
+ "version": "2.8.1",
+ "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz",
+ "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="
+ },
+ "node_modules/uc.micro": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/uc.micro/-/uc.micro-3.0.0.tgz",
+ "integrity": "sha512-U3PppEkleoTnIfi8BozMx3yju3qc/L6SwqWo2Sw+54PX+PX0q9I+r1Um5HCmqD7n9VDX5/v3vQH/AjA6deDdtw=="
+ },
+ "node_modules/underscore": {
+ "version": "1.13.8",
+ "resolved": "https://registry.npmjs.org/underscore/-/underscore-1.13.8.tgz",
+ "integrity": "sha512-DXtD3ZtEQzc7M8m4cXotyHR+FAS18C64asBYY5vqZexfYryNNnDc02W4hKg3rdQuqOYas1jkseX0+nZXjTXnvQ=="
+ },
+ "node_modules/unicode-properties": {
+ "version": "1.4.1",
+ "resolved": "https://registry.npmjs.org/unicode-properties/-/unicode-properties-1.4.1.tgz",
+ "integrity": "sha512-CLjCCLQ6UuMxWnbIylkisbRj31qxHPAurvena/0iwSVbQ2G1VY5/HjV0IRabOEbDHlzZlRdCrD4NhB0JtU40Pg==",
+ "dependencies": {
+ "base64-js": "^1.3.0",
+ "unicode-trie": "^2.0.0"
+ }
+ },
+ "node_modules/unicode-trie": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/unicode-trie/-/unicode-trie-2.0.0.tgz",
+ "integrity": "sha512-x7bc76x0bm4prf1VLg79uhAzKw8DVboClSN5VxJuQ+LKDOVEW9CdH+VY7SP+vX7xCYQqzzgQpFqz15zeLvAtZQ==",
+ "dependencies": {
+ "pako": "^0.2.5",
+ "tiny-inflate": "^1.0.0"
+ }
+ },
+ "node_modules/unicode-trie/node_modules/pako": {
+ "version": "0.2.9",
+ "resolved": "https://registry.npmjs.org/pako/-/pako-0.2.9.tgz",
+ "integrity": "sha512-NUcwaKxUxWrZLpDG+z/xZaCgQITkA/Dv4V/T6bw7VON6l1Xz/VnrBqrYjZQ12TamKHzITTfOEIYUj48y2KXImA=="
+ },
+ "node_modules/util-deprecate": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz",
+ "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw=="
+ },
+ "node_modules/xlsx": {
+ "version": "0.20.3",
+ "resolved": "https://cdn.sheetjs.com/xlsx-0.20.3/xlsx-0.20.3.tgz",
+ "integrity": "sha512-oLDq3jw7AcLqKWH2AhCpVTZl8mf6X2YReP+Neh0SJUzV/BdZYjth94tG5toiMB1PPrYtxOCfaoUCkvtuH+3AJA==",
+ "license": "Apache-2.0",
+ "bin": {
+ "xlsx": "bin/xlsx.njs"
+ },
+ "engines": {
+ "node": ">=0.8"
+ }
+ },
+ "node_modules/xmlbuilder": {
+ "version": "10.1.1",
+ "resolved": "https://registry.npmjs.org/xmlbuilder/-/xmlbuilder-10.1.1.tgz",
+ "integrity": "sha512-OyzrcFLL/nb6fMGHbiRDuPup9ljBycsdCypwuyg5AAHvyWzGfChJpCXMG88AGTIMFhGZ9RccFN1e6lhg3hkwKg==",
+ "engines": {
+ "node": ">=4.0"
+ }
+ }
+ }
+}
diff --git a/docs/package.json b/docs/package.json
index 81fa59b..384a79d 100644
--- a/docs/package.json
+++ b/docs/package.json
@@ -1,10 +1,24 @@
{
"name": "makeitpdf",
- "version": "2.0.0",
+ "version": "3.0.0",
"private": true,
- "description": "Combine up to 10 images into a single PDF, entirely in the browser",
+ "description": "Turn images, Word, Excel, Markdown, and CSV files into one PDF in the browser",
"scripts": {
- "test": "node --test \"tests/*.test.js\"",
+ "test": "node --test tests/pages.test.js tests/documents.test.js",
+ "test:browser": "node --test tests/browser.test.js",
"serve": "python3 -m http.server 8000"
+ },
+ "dependencies": {
+ "@fontsource/figtree": "^5.3.0",
+ "bidi-js": "^1.0.3",
+ "blob-stream": "^0.1.3",
+ "mammoth": "^1.12.1",
+ "markdown-it": "^15.0.0",
+ "pdfkit": "^0.19.1",
+ "xlsx": "https://cdn.sheetjs.com/xlsx-0.20.3/xlsx-0.20.3.tgz"
+ },
+ "devDependencies": {
+ "pdfjs-dist": "^6.2.108",
+ "playwright-core": "^1.62.1"
}
}
diff --git a/docs/pages.js b/docs/pages.js
index 42966e1..b675ddc 100644
--- a/docs/pages.js
+++ b/docs/pages.js
@@ -1,6 +1,5 @@
-/* Page geometry. No DOM, no jsPDF -- so tests/pages.test.js can run it
- straight through node. Loaded as a plain script in the browser
- (window.Pages) and as CommonJS in the test runner. */
+/* Pure page and image-metadata helpers. This file intentionally has no DOM
+ dependencies so the conversion rules can be exercised with node --test. */
(function (root, factory) {
if (typeof module === "object" && module.exports) module.exports = factory();
@@ -8,20 +7,12 @@
})(typeof self !== "undefined" ? self : globalThis, function () {
"use strict";
- /** A4 in PostScript points (1/72in), portrait. */
const A4_PORTRAIT = [595.28, 841.89];
-
- /** Margin around the image on every page, in points. */
const MARGIN_PT = 24;
-
- /** Pages are rasterised at this density before being placed. */
const RENDER_DPI = 150;
+ const DEFAULT_IMAGE_DPI = 96;
+ const MAX_PAGE_PT = 14400; // 200 inches, the widely-supported PDF limit.
- /**
- * The page an image belongs on: A4, turned to match the image.
- * jsPDF wants the portrait format plus an orientation flag, so both the
- * final dimensions and that flag come back together.
- */
function pageSize(imageWidth, imageHeight) {
const landscape = imageWidth > imageHeight;
const [short, long] = A4_PORTRAIT;
@@ -32,10 +23,6 @@
};
}
- /**
- * Where the image sits on the page: scaled to fit inside the margins,
- * centred, never enlarged past the box. All values in points.
- */
function placeImage(imageWidth, imageHeight, pageWidth, pageHeight, margin) {
const inset = margin === undefined ? MARGIN_PT : margin;
const boxWidth = Math.max(1, pageWidth - inset * 2);
@@ -51,11 +38,6 @@
};
}
- /**
- * Pixel dimensions to rasterise a placed image at, so it lands on the page
- * at RENDER_DPI. Big camera images get downsampled here rather than being
- * embedded whole -- an A4 content area is only ~1140px wide at 150 DPI.
- */
function rasterSize(placement, dpi) {
const density = (dpi === undefined ? RENDER_DPI : dpi) / 72;
return {
@@ -70,28 +52,215 @@
return (bytes / (1024 * 1024)).toFixed(1) + " MB";
}
- /**
- * TIFF has no browser decoder, so it has to be spotted before we hand the
- * file to createImageBitmap. Magic bytes, not the name or content-type:
- * "II*\0" (little-endian) or "MM\0*" (big-endian).
- */
function isTiff(bytes) {
if (!bytes || bytes.length < 4) return false;
- const littleEndian =
- bytes[0] === 0x49 && bytes[1] === 0x49 && bytes[2] === 0x2a && bytes[3] === 0x00;
- const bigEndian =
- bytes[0] === 0x4d && bytes[1] === 0x4d && bytes[2] === 0x00 && bytes[3] === 0x2a;
- return littleEndian || bigEndian;
+ return (
+ (bytes[0] === 0x49 && bytes[1] === 0x49 && bytes[2] === 0x2a && bytes[3] === 0x00) ||
+ (bytes[0] === 0x4d && bytes[1] === 0x4d && bytes[2] === 0x00 && bytes[3] === 0x2a)
+ );
+ }
+
+ const ascii = (bytes, start, length) =>
+ String.fromCharCode.apply(null, Array.from(bytes.slice(start, start + length)));
+
+ function tiffDirectory(bytes, start) {
+ if (start + 8 > bytes.length) return null;
+ const little = bytes[start] === 0x49 && bytes[start + 1] === 0x49;
+ if (!little && !(bytes[start] === 0x4d && bytes[start + 1] === 0x4d)) return null;
+ const view = new DataView(bytes.buffer, bytes.byteOffset, bytes.byteLength);
+ const u16 = (offset) => view.getUint16(offset, little);
+ const u32 = (offset) => view.getUint32(offset, little);
+ if (u16(start + 2) !== 42) return null;
+ const ifd = start + u32(start + 4);
+ if (ifd + 2 > bytes.length) return null;
+
+ const out = {};
+ const count = u16(ifd);
+ const typeBytes = { 1: 1, 2: 1, 3: 2, 4: 4, 5: 8, 9: 4, 10: 8 };
+
+ function valueAt(entry, type, amount) {
+ const size = (typeBytes[type] || 0) * amount;
+ if (!size) return undefined;
+ const offset = size <= 4 ? entry + 8 : start + u32(entry + 8);
+ if (offset < 0 || offset + size > bytes.length) return undefined;
+ if (type === 3) return u16(offset);
+ if (type === 4) return u32(offset);
+ if (type === 5) {
+ const denominator = u32(offset + 4);
+ return denominator ? u32(offset) / denominator : undefined;
+ }
+ return bytes[offset];
+ }
+
+ for (let index = 0; index < count; index += 1) {
+ const entry = ifd + 2 + index * 12;
+ if (entry + 12 > bytes.length) break;
+ const tag = u16(entry);
+ const type = u16(entry + 2);
+ const amount = u32(entry + 4);
+ if ([256, 257, 274, 282, 283, 296].includes(tag)) {
+ out[tag] = valueAt(entry, type, amount);
+ }
+ }
+ return out;
+ }
+
+ function parseJpeg(bytes) {
+ let offset = 2;
+ let jfif = null;
+ let exif = null;
+ let width;
+ let height;
+
+ while (offset + 4 <= bytes.length && bytes[offset] === 0xff) {
+ while (bytes[offset] === 0xff) offset += 1;
+ const marker = bytes[offset++];
+ if (marker === 0xd9 || marker === 0xda) break;
+ if (offset + 2 > bytes.length) break;
+ const length = (bytes[offset] << 8) | bytes[offset + 1];
+ const data = offset + 2;
+ if (length < 2 || data + length - 2 > bytes.length) break;
+
+ if (marker === 0xe0 && ascii(bytes, data, 5) === "JFIF\0" && length >= 14) {
+ const unit = bytes[data + 7];
+ let dpiX = (bytes[data + 8] << 8) | bytes[data + 9];
+ let dpiY = (bytes[data + 10] << 8) | bytes[data + 11];
+ if (unit === 2) {
+ dpiX *= 2.54;
+ dpiY *= 2.54;
+ }
+ if (unit === 1 || unit === 2) jfif = { dpiX, dpiY };
+ }
+ if (marker === 0xe1 && ascii(bytes, data, 6) === "Exif\0\0") {
+ const tags = tiffDirectory(bytes, data + 6);
+ if (tags) {
+ let dpiX = tags[282];
+ let dpiY = tags[283];
+ if (tags[296] === 3) {
+ dpiX *= 2.54;
+ dpiY *= 2.54;
+ }
+ exif = { dpiX, dpiY, orientation: tags[274] };
+ }
+ }
+ if ([0xc0, 0xc1, 0xc2, 0xc3, 0xc5, 0xc6, 0xc7, 0xc9, 0xca, 0xcb, 0xcd, 0xce, 0xcf].includes(marker)) {
+ height = (bytes[data + 1] << 8) | bytes[data + 2];
+ width = (bytes[data + 3] << 8) | bytes[data + 4];
+ }
+ offset += length;
+ }
+ const result = { width, height, ...(jfif || {}) };
+ if (exif) {
+ if (Number.isFinite(exif.dpiX) && exif.dpiX > 0) result.dpiX = exif.dpiX;
+ if (Number.isFinite(exif.dpiY) && exif.dpiY > 0) result.dpiY = exif.dpiY;
+ if (exif.orientation) result.orientation = exif.orientation;
+ }
+ return result;
+ }
+
+ function parsePng(bytes) {
+ const view = new DataView(bytes.buffer, bytes.byteOffset, bytes.byteLength);
+ const out = {};
+ if (bytes.length >= 24) {
+ out.width = view.getUint32(16, false);
+ out.height = view.getUint32(20, false);
+ }
+ let offset = 8;
+ while (offset + 12 <= bytes.length) {
+ const length = view.getUint32(offset, false);
+ const type = ascii(bytes, offset + 4, 4);
+ if (offset + 12 + length > bytes.length) break;
+ if (type === "pHYs" && length >= 9 && bytes[offset + 16] === 1) {
+ out.dpiX = view.getUint32(offset + 8, false) * 0.0254;
+ out.dpiY = view.getUint32(offset + 12, false) * 0.0254;
+ }
+ if (type === "IEND") break;
+ offset += length + 12;
+ }
+ return out;
+ }
+
+ function parseBmp(bytes) {
+ if (bytes.length < 46) return {};
+ const view = new DataView(bytes.buffer, bytes.byteOffset, bytes.byteLength);
+ return {
+ width: Math.abs(view.getInt32(18, true)),
+ height: Math.abs(view.getInt32(22, true)),
+ dpiX: Math.abs(view.getInt32(38, true)) * 0.0254,
+ dpiY: Math.abs(view.getInt32(42, true)) * 0.0254,
+ };
+ }
+
+ function parseTiff(bytes) {
+ const tags = tiffDirectory(bytes, 0) || {};
+ let dpiX = tags[282];
+ let dpiY = tags[283];
+ if (tags[296] === 3) {
+ dpiX *= 2.54;
+ dpiY *= 2.54;
+ }
+ return {
+ width: tags[256],
+ height: tags[257],
+ orientation: tags[274],
+ dpiX,
+ dpiY,
+ };
+ }
+
+ function parseRasterMetadata(input) {
+ const bytes = input instanceof Uint8Array ? input : new Uint8Array(input || 0);
+ if (bytes.length >= 2 && bytes[0] === 0xff && bytes[1] === 0xd8) return parseJpeg(bytes);
+ if (bytes.length >= 8 && ascii(bytes, 1, 3) === "PNG") return parsePng(bytes);
+ if (bytes.length >= 2 && ascii(bytes, 0, 2) === "BM") return parseBmp(bytes);
+ if (isTiff(bytes)) return parseTiff(bytes);
+ return {};
+ }
+
+ function validDpi(value) {
+ return Number.isFinite(value) && value > 0 ? value : DEFAULT_IMAGE_DPI;
+ }
+
+ function imagePage(pixelWidth, pixelHeight, dpiX, dpiY, orientation, maxPagePt) {
+ const turn = orientation >= 5 && orientation <= 8;
+ const sourceWidth = Math.max(1, Number(pixelWidth) || 1);
+ const sourceHeight = Math.max(1, Number(pixelHeight) || 1);
+ const resolvedX = validDpi(dpiX);
+ const resolvedY = validDpi(dpiY);
+ const widthPixels = turn ? sourceHeight : sourceWidth;
+ const heightPixels = turn ? sourceWidth : sourceHeight;
+ const widthDpi = turn ? resolvedY : resolvedX;
+ const heightDpi = turn ? resolvedX : resolvedY;
+ const naturalWidth = (widthPixels / widthDpi) * 72;
+ const naturalHeight = (heightPixels / heightDpi) * 72;
+ const cap = maxPagePt === undefined ? MAX_PAGE_PT : maxPagePt;
+ const scale = Math.min(1, cap / naturalWidth, cap / naturalHeight);
+ return {
+ width: naturalWidth * scale,
+ height: naturalHeight * scale,
+ naturalWidth,
+ naturalHeight,
+ pixelWidth: widthPixels,
+ pixelHeight: heightPixels,
+ dpiX: widthDpi,
+ dpiY: heightDpi,
+ scale,
+ capped: scale < 1,
+ };
}
return {
A4_PORTRAIT,
MARGIN_PT,
RENDER_DPI,
+ DEFAULT_IMAGE_DPI,
+ MAX_PAGE_PT,
pageSize,
placeImage,
rasterSize,
formatBytes,
isTiff,
+ parseRasterMetadata,
+ imagePage,
};
});
diff --git a/docs/pdf-builder.js b/docs/pdf-builder.js
new file mode 100644
index 0000000..dea1eca
--- /dev/null
+++ b/docs/pdf-builder.js
@@ -0,0 +1,518 @@
+/* Mixed-format PDF writer. PDFKit keeps document text searchable while still
+ allowing every image to use its own physical page size. */
+
+(function (root, factory) {
+ if (typeof module === "object" && module.exports) module.exports = factory();
+ else root.PdfBuilder = factory();
+})(typeof self !== "undefined" ? self : globalThis, function () {
+ "use strict";
+
+ const FONT_FILES = {
+ NotoSans: "vendor/fonts/noto-sans-regular.ttf",
+ NotoSansBold: "vendor/fonts/noto-sans-bold.ttf",
+ NotoSansItalic: "vendor/fonts/noto-sans-italic.ttf",
+ NotoSansBoldItalic: "vendor/fonts/noto-sans-bolditalic.ttf",
+ NotoArabic: "vendor/fonts/noto-arabic-regular.ttf",
+ NotoArabicBold: "vendor/fonts/noto-arabic-bold.ttf",
+ NotoHebrew: "vendor/fonts/noto-hebrew-regular.ttf",
+ NotoHebrewBold: "vendor/fonts/noto-hebrew-bold.ttf",
+ NotoDevanagari: "vendor/fonts/noto-devanagari-regular.ttf",
+ NotoDevanagariBold: "vendor/fonts/noto-devanagari-bold.ttf",
+ NotoCJK: "vendor/fonts/noto-cjk-regular.ttc",
+ };
+ const FONT_CACHE = new Map();
+ const INK = "#101113";
+ const MUTED = "#666b72";
+ const LINE = "#dfe2e6";
+ const SURFACE = "#f5f6f7";
+ const ACCENT = "#f4402d";
+ const RTL_RE = /[\u0590-\u08ff\ufb1d-\ufefc]/;
+
+ async function loadFont(name) {
+ if (!FONT_CACHE.has(name)) {
+ FONT_CACHE.set(
+ name,
+ fetch(FONT_FILES[name]).then((response) => {
+ if (!response.ok) throw new Error(`Could not load the offline font ${name}.`);
+ return response.arrayBuffer();
+ })
+ );
+ }
+ return FONT_CACHE.get(name);
+ }
+
+ function scriptsIn(text) {
+ const value = String(text || "");
+ return {
+ arabic: /[\u0600-\u08ff\ufb50-\ufefc]/.test(value),
+ hebrew: /[\u0590-\u05ff\ufb1d-\ufb4f]/.test(value),
+ devanagari: /[\u0900-\u097f]/.test(value),
+ japanese: /[\u3040-\u30ff]/.test(value),
+ korean: /[\uac00-\ud7af\u1100-\u11ff]/.test(value),
+ han: /[\u3400-\u9fff\uf900-\ufaff]/.test(value),
+ };
+ }
+
+ function collectText(items) {
+ const parts = [];
+ for (const item of items) {
+ parts.push(item.file?.name || "");
+ for (const block of item.blocks || []) {
+ parts.push(block.text || "");
+ parts.push((block.spans || []).map((span) => span.text).join(""));
+ parts.push((block.items || []).join(" "));
+ for (const row of block.rows || []) parts.push(row.map((cell) => cell.text || cell).join(" "));
+ }
+ for (const sheet of item.sheets || []) {
+ parts.push(sheet.name);
+ for (const row of sheet.rows || []) parts.push(row.join(" "));
+ }
+ }
+ return parts.join("\n");
+ }
+
+ async function registerFonts(doc, items) {
+ const text = collectText(items);
+ const scripts = scriptsIn(text);
+ const names = ["NotoSans", "NotoSansBold", "NotoSansItalic", "NotoSansBoldItalic"];
+ if (scripts.arabic) names.push("NotoArabic", "NotoArabicBold");
+ if (scripts.hebrew) names.push("NotoHebrew", "NotoHebrewBold");
+ if (scripts.devanagari) names.push("NotoDevanagari", "NotoDevanagariBold");
+ if (scripts.japanese || scripts.korean || scripts.han) names.push("NotoCJK");
+ const loaded = await Promise.all(names.map(async (name) => [name, await loadFont(name)]));
+ for (const [name, buffer] of loaded) {
+ const bytes = new Uint8Array(buffer);
+ if (name === "NotoCJK") {
+ doc.registerFont("NotoCJKSC", bytes, "NotoSansCJKsc-Regular");
+ doc.registerFont("NotoCJKTC", bytes, "NotoSansCJKtc-Regular");
+ doc.registerFont("NotoCJKJP", bytes, "NotoSansCJKjp-Regular");
+ doc.registerFont("NotoCJKKR", bytes, "NotoSansCJKkr-Regular");
+ } else {
+ doc.registerFont(name, bytes);
+ }
+ }
+ }
+
+ function fontFor(text, bold, italic) {
+ const scripts = scriptsIn(text);
+ if (scripts.arabic) return bold ? "NotoArabicBold" : "NotoArabic";
+ if (scripts.hebrew) return bold ? "NotoHebrewBold" : "NotoHebrew";
+ if (scripts.devanagari) return bold ? "NotoDevanagariBold" : "NotoDevanagari";
+ if (scripts.japanese) return "NotoCJKJP";
+ if (scripts.korean) return "NotoCJKKR";
+ if (scripts.han) return "NotoCJKSC";
+ if (bold && italic) return "NotoSansBoldItalic";
+ if (bold) return "NotoSansBold";
+ if (italic) return "NotoSansItalic";
+ return "NotoSans";
+ }
+
+ function visualRtl(text) {
+ if (!RTL_RE.test(text) || typeof bidi_js !== "function") return text;
+ const bidi = bidi_js();
+ const levels = bidi.getEmbeddingLevels(text);
+ const chars = Array.from(text);
+ for (const [start, end] of bidi.getReorderSegments(text, levels)) {
+ const reversed = chars.slice(start, end + 1).reverse();
+ chars.splice(start, reversed.length, ...reversed);
+ }
+ const mirrored = bidi.getMirroredCharactersMap(text, levels);
+ mirrored.forEach((replacement, index) => {
+ if (index < chars.length) chars[index] = replacement;
+ });
+ return chars.join("");
+ }
+
+ function addLogicalText(doc, logical, options) {
+ const rtl = RTL_RE.test(logical);
+ if (rtl) doc.markContent("Span", { actual: logical });
+ doc.text(rtl ? visualRtl(logical) : logical, { ...(options || {}), align: rtl ? "right" : options?.align });
+ if (rtl) doc.endMarkedContent();
+ }
+
+ function fileToDataUrl(file) {
+ return new Promise((resolve, reject) => {
+ const reader = new FileReader();
+ reader.onload = () => resolve(reader.result);
+ reader.onerror = () => reject(new Error(`${file.name} could not be read.`));
+ reader.readAsDataURL(file);
+ });
+ }
+
+ function canvasDataUrl(bitmap, orientation) {
+ const turn = orientation >= 5 && orientation <= 8;
+ const canvas = document.createElement("canvas");
+ canvas.width = turn ? bitmap.height : bitmap.width;
+ canvas.height = turn ? bitmap.width : bitmap.height;
+ const context = canvas.getContext("2d");
+ context.fillStyle = "#ffffff";
+ context.fillRect(0, 0, canvas.width, canvas.height);
+ const transforms = {
+ 2: [-1, 0, 0, 1, bitmap.width, 0],
+ 3: [-1, 0, 0, -1, bitmap.width, bitmap.height],
+ 4: [1, 0, 0, -1, 0, bitmap.height],
+ 5: [0, 1, 1, 0, 0, 0],
+ 6: [0, 1, -1, 0, bitmap.height, 0],
+ 7: [0, -1, -1, 0, bitmap.height, bitmap.width],
+ 8: [0, -1, 1, 0, 0, bitmap.width],
+ };
+ if (transforms[orientation]) context.setTransform(...transforms[orientation]);
+ context.drawImage(bitmap, 0, 0);
+ return canvas.toDataURL("image/png");
+ }
+
+ async function decodeTiff(buffer, name) {
+ try {
+ const ifds = UTIF.decode(buffer);
+ if (!ifds.length) throw new Error("no pages");
+ UTIF.decodeImage(buffer, ifds[0], ifds);
+ const rgba = UTIF.toRGBA8(ifds[0]);
+ const canvas = document.createElement("canvas");
+ canvas.width = ifds[0].width;
+ canvas.height = ifds[0].height;
+ canvas.getContext("2d").putImageData(
+ new ImageData(new Uint8ClampedArray(rgba), ifds[0].width, ifds[0].height),
+ 0,
+ 0
+ );
+ return createImageBitmap(canvas);
+ } catch {
+ throw new Error(`${name} is a TIFF this browser couldn't read.`);
+ }
+ }
+
+ async function prepareImage(file) {
+ const buffer = await file.arrayBuffer();
+ const bytes = new Uint8Array(buffer);
+ const metadata = Pages.parseRasterMetadata(bytes);
+ const tiff = Pages.isTiff(bytes);
+ let bitmap;
+ try {
+ bitmap = tiff
+ ? await decodeTiff(buffer, file.name)
+ : await createImageBitmap(file, { imageOrientation: "from-image" });
+ } catch {
+ throw new Error(`${file.name} isn't an image this browser can read.`);
+ }
+
+ const page = Pages.imagePage(
+ metadata.width || bitmap.width,
+ metadata.height || bitmap.height,
+ metadata.dpiX,
+ metadata.dpiY,
+ metadata.orientation || 1
+ );
+ const simpleJpeg = bytes[0] === 0xff && bytes[1] === 0xd8 && (metadata.orientation || 1) === 1;
+ const simplePng = bytes.length > 8 && String.fromCharCode(...bytes.slice(1, 4)) === "PNG";
+ const src = simpleJpeg || simplePng
+ ? await fileToDataUrl(file)
+ : canvasDataUrl(bitmap, tiff ? metadata.orientation || 1 : 1);
+ bitmap.close();
+ return { src, page };
+ }
+
+ function addImagePage(doc, image) {
+ doc.addPage({ size: [image.page.width, image.page.height], margin: 0 });
+ doc.image(image.src, 0, 0, {
+ width: image.page.width,
+ height: image.page.height,
+ ignoreOrientation: true,
+ });
+ }
+
+ function addTextPage(doc) {
+ doc.addPage({ size: "A4", layout: "portrait", margin: 54 });
+ doc.font("NotoSans").fontSize(11).fillColor(INK);
+ }
+
+ function renderSpans(doc, spans, options) {
+ const logical = spans.map((span) => span.text).join("");
+ if (RTL_RE.test(logical)) {
+ doc.font(fontFor(logical, spans.some((span) => span.bold), false));
+ addLogicalText(doc, logical, options);
+ return;
+ }
+ const usable = spans.filter((span) => span.text);
+ usable.forEach((span, index) => {
+ doc.font(span.code ? "NotoSans" : fontFor(span.text, span.bold, span.italic));
+ doc.fillColor(span.link ? ACCENT : options?.color || INK);
+ doc.text(span.text, {
+ continued: index !== usable.length - 1,
+ link: span.link || undefined,
+ underline: Boolean(span.link),
+ strike: Boolean(span.strike),
+ lineGap: options?.lineGap,
+ });
+ });
+ doc.fillColor(INK);
+ }
+
+ function renderTable(doc, rows, widths) {
+ if (!rows.length) return;
+ const data = rows.map((row, rowIndex) =>
+ row.map((raw) => {
+ const cell = typeof raw === "object" && raw !== null ? raw : { text: String(raw ?? "") };
+ const header = rowIndex === 0 || cell.header;
+ return {
+ text: cell.text || "",
+ colSpan: cell.colSpan || 1,
+ rowSpan: cell.rowSpan || 1,
+ type: header ? "TH" : "TD",
+ scope: header ? "Column" : undefined,
+ backgroundColor: header ? "#eceef1" : undefined,
+ textColor: INK,
+ borderColor: LINE,
+ padding: 4,
+ font: { src: fontFor(cell.text || "", header, false), size: 8 },
+ textOptions: { lineGap: 1 },
+ };
+ })
+ );
+ doc.table({
+ maxWidth: doc.page.contentWidth,
+ columnStyles: widths || Array.from({ length: Math.max(...rows.map((row) => row.length)) }, () => "*"),
+ defaultStyle: { border: 0.5, borderColor: LINE, padding: 4 },
+ data,
+ });
+ doc.moveDown(0.75);
+ }
+
+ function renderBlocks(doc, blocks) {
+ for (const block of blocks) {
+ if (block.type === "heading") {
+ const sizes = [0, 25, 20, 16, 14, 12, 11];
+ const text = block.spans.map((span) => span.text).join("");
+ doc.moveDown(block.level === 1 ? 0.3 : 0.55);
+ doc.font(fontFor(text, true, false)).fontSize(sizes[block.level] || 12).fillColor(INK);
+ addLogicalText(doc, text, { lineGap: 2 });
+ doc.moveDown(0.25);
+ } else if (block.type === "paragraph") {
+ doc.fontSize(11).fillColor(INK);
+ renderSpans(doc, block.spans, { lineGap: 3 });
+ doc.moveDown(0.55);
+ } else if (block.type === "list") {
+ const joined = block.items.join(" ");
+ doc.font(fontFor(joined, false, false)).fontSize(11).fillColor(INK);
+ doc.list(block.items, {
+ listType: block.ordered ? "numbered" : "bullet",
+ indent: 18,
+ textIndent: 10,
+ bulletIndent: 2,
+ lineGap: 3,
+ });
+ doc.moveDown(0.55);
+ } else if (block.type === "quote") {
+ const x = doc.x;
+ doc.save().strokeColor(ACCENT).lineWidth(2).moveTo(x, doc.y).lineTo(x, doc.y + 34).stroke().restore();
+ doc.x += 14;
+ doc.font(fontFor(block.text, false, true)).fontSize(10.5).fillColor(MUTED);
+ addLogicalText(doc, block.text, { lineGap: 3 });
+ doc.x = x;
+ doc.fillColor(INK).moveDown(0.65);
+ } else if (block.type === "code") {
+ const available = doc.page.contentWidth;
+ doc.font(/[^ -\u007f]/.test(block.text) ? "NotoSans" : "Courier").fontSize(9);
+ const height = Math.min(doc.heightOfString(block.text, { width: available - 20 }) + 16, 500);
+ if (doc.y + height > doc.page.height - doc.page.margins.bottom) doc.addPage();
+ const top = doc.y;
+ doc.save().fillColor(SURFACE).roundedRect(doc.x, top, available, height, 5).fill().restore();
+ doc.fillColor(INK).text(block.text, doc.x + 10, top + 8, { width: available - 20, lineGap: 2 });
+ doc.y = top + height + 8;
+ } else if (block.type === "rule") {
+ doc.moveDown(0.4);
+ doc.save().strokeColor(LINE).lineWidth(1).moveTo(doc.x, doc.y).lineTo(doc.x + doc.page.contentWidth, doc.y).stroke().restore();
+ doc.moveDown(0.7);
+ } else if (block.type === "table") {
+ renderTable(doc, block.rows);
+ } else if (block.type === "image") {
+ try {
+ doc.image(block.src, { fit: [doc.page.contentWidth, 430], align: "center" });
+ doc.moveDown(0.65);
+ } catch {
+ doc.font("NotoSansItalic").fontSize(10).fillColor(MUTED).text(block.alt || "Image");
+ doc.moveDown(0.5);
+ }
+ }
+ }
+ }
+
+ function columnGroups(sheet, availableWidth) {
+ const columns = sheet.rows.reduce((max, row) => Math.max(max, row.length), 0);
+ const widths = Array.from({ length: columns }, (_, column) => {
+ if (sheet.widths?.[column]) return Math.max(42, Math.min(160, sheet.widths[column]));
+ const longest = sheet.rows.slice(0, 250).reduce(
+ (max, row) => Math.max(max, String(row[column] || "").length),
+ 0
+ );
+ return Math.max(42, Math.min(160, longest * 4.5 + 12));
+ });
+ const groups = [];
+ let group = [];
+ let used = 0;
+ widths.forEach((width, index) => {
+ if (group.length && used + width > availableWidth) {
+ groups.push(group);
+ group = [];
+ used = 0;
+ }
+ group.push({ index, width: Math.min(width, availableWidth) });
+ used += width;
+ });
+ if (group.length) groups.push(group);
+ return groups;
+ }
+
+ function columnName(index) {
+ let value = index + 1;
+ let result = "";
+ while (value) {
+ value -= 1;
+ result = String.fromCharCode(65 + (value % 26)) + result;
+ value = Math.floor(value / 26);
+ }
+ return result;
+ }
+
+ function sheetRows(sheet, group) {
+ const mergeAt = new Map();
+ const covered = new Set();
+ for (const merge of sheet.merges || []) {
+ const groupStart = group[0].index;
+ const groupEnd = group[group.length - 1].index;
+ const mergeEnd = merge.startColumn + merge.colSpan - 1;
+ if (merge.startColumn < groupStart || mergeEnd > groupEnd) continue;
+ mergeAt.set(`${merge.startRow},${merge.startColumn}`, merge);
+ for (let row = merge.startRow; row < merge.startRow + merge.rowSpan; row += 1) {
+ for (let column = merge.startColumn; column <= mergeEnd; column += 1) {
+ if (row !== merge.startRow || column !== merge.startColumn) covered.add(`${row},${column}`);
+ }
+ }
+ }
+ return sheet.rows.map((row, rowIndex) => {
+ const output = [];
+ for (const { index } of group) {
+ if (covered.has(`${rowIndex},${index}`)) continue;
+ const merge = mergeAt.get(`${rowIndex},${index}`);
+ output.push({
+ text: String(row[index] || ""),
+ header: rowIndex === 0,
+ colSpan: merge?.colSpan || 1,
+ rowSpan: merge?.rowSpan || 1,
+ });
+ }
+ return output;
+ });
+ }
+
+ function sheetTitle(doc, title, continued) {
+ doc.font(fontFor(title, true, false)).fontSize(16).fillColor(INK)
+ .text(title + (continued ? " · continued" : ""));
+ doc.moveDown(0.55).fontSize(8).font("NotoSans");
+ }
+
+ function estimatedRowHeight(doc, row, widths) {
+ let height = 0;
+ row.forEach((cell, index) => {
+ const text = typeof cell === "object" ? cell.text : String(cell || "");
+ doc.font(fontFor(text, Boolean(cell?.header), false)).fontSize(8);
+ height = Math.max(height, doc.heightOfString(text || " ", {
+ width: Math.max(8, (widths[index] || 42) - 8),
+ lineGap: 1,
+ }) + 8);
+ });
+ return Math.min(500, Math.max(17, height));
+ }
+
+ function renderPaginatedSheet(doc, title, rows, widths) {
+ const header = rows[0] || [];
+ let pageRows = [header];
+ let continued = false;
+
+ function newPage() {
+ doc.addPage({ size: "A4", layout: "landscape", margin: 30 });
+ sheetTitle(doc, title, continued);
+ continued = true;
+ pageRows = [header];
+ }
+
+ newPage();
+ let used = estimatedRowHeight(doc, header, widths);
+ for (const row of rows.slice(1)) {
+ const height = estimatedRowHeight(doc, row, widths);
+ const available = doc.page.height - doc.page.margins.bottom - doc.y;
+ if (pageRows.length > 1 && used + height > available) {
+ renderTable(doc, pageRows, widths);
+ newPage();
+ used = estimatedRowHeight(doc, header, widths);
+ }
+ pageRows.push(row);
+ used += height;
+ }
+ renderTable(doc, pageRows, widths);
+ }
+
+ function renderSheets(doc, sheets) {
+ for (const sheet of sheets) {
+ const available = Pages.A4_PORTRAIT[1] - 60;
+ const groups = columnGroups(sheet, available);
+ if (!groups.length) {
+ doc.addPage({ size: "A4", layout: "landscape", margin: 30 });
+ doc.font("NotoSansBold").fontSize(17).fillColor(INK).text(sheet.name);
+ doc.moveDown(0.6).font("NotoSans").fontSize(10).fillColor(MUTED).text("This sheet is empty.");
+ continue;
+ }
+ groups.forEach((group, groupIndex) => {
+ const suffix = groups.length > 1
+ ? ` · columns ${columnName(group[0].index)}–${columnName(group[group.length - 1].index)}`
+ : "";
+ renderPaginatedSheet(
+ doc,
+ sheet.name + suffix,
+ sheetRows(sheet, group),
+ group.map((column) => column.width)
+ );
+ if (groupIndex < groups.length - 1) doc.moveDown(0.1);
+ });
+ }
+ }
+
+ async function build(items, onProgress) {
+ if (typeof PDFDocument !== "function" || typeof blobStream !== "function") {
+ throw new Error("The offline PDF engine did not load. Refresh and try again.");
+ }
+ const doc = new PDFDocument({
+ autoFirstPage: false,
+ bufferPages: true,
+ compress: true,
+ tagged: true,
+ pdfVersion: "1.5",
+ info: { Title: "MakeItPDF conversion", Creator: "MakeItPDF" },
+ });
+ const stream = doc.pipe(blobStream());
+ await registerFonts(doc, items);
+ const warnings = [];
+
+ for (let index = 0; index < items.length; index += 1) {
+ const item = items[index];
+ onProgress?.(index, items.length, `Converting ${item.file.name}`);
+ if (item.kind === "image") addImagePage(doc, await prepareImage(item.file));
+ else if (item.kind === "xlsx" || item.kind === "csv") renderSheets(doc, item.sheets);
+ else {
+ addTextPage(doc);
+ renderBlocks(doc, item.blocks);
+ }
+ warnings.push(...(item.warnings || []).map((warning) => `${item.file.name}: ${warning}`));
+ await new Promise((resolve) => setTimeout(resolve, 0));
+ }
+ const pageCount = doc.bufferedPageRange().count;
+ onProgress?.(items.length, items.length, "Finishing PDF");
+ doc.end();
+ const blob = await new Promise((resolve, reject) => {
+ stream.on("finish", () => resolve(stream.toBlob("application/pdf")));
+ stream.on("error", reject);
+ });
+ return { blob, pageCount, warnings };
+ }
+
+ return { build, scriptsIn, visualRtl, columnGroups };
+});
diff --git a/docs/style.css b/docs/style.css
index 5811adf..0671281 100644
--- a/docs/style.css
+++ b/docs/style.css
@@ -1,5 +1,33 @@
-/* MakeItPDF -- white ground, one typeface, one accent.
- The red appears in exactly two places: the logo and the button you press. */
+/* MakeItPDF -- white paper, black ink, one action red. */
+
+@font-face {
+ font-family: "Figtree";
+ src: url("vendor/fonts/figtree-400.woff2") format("woff2");
+ font-style: normal;
+ font-weight: 400;
+ font-display: swap;
+}
+@font-face {
+ font-family: "Figtree";
+ src: url("vendor/fonts/figtree-500.woff2") format("woff2");
+ font-style: normal;
+ font-weight: 500;
+ font-display: swap;
+}
+@font-face {
+ font-family: "Figtree";
+ src: url("vendor/fonts/figtree-600.woff2") format("woff2");
+ font-style: normal;
+ font-weight: 600;
+ font-display: swap;
+}
+@font-face {
+ font-family: "Figtree";
+ src: url("vendor/fonts/figtree-700.woff2") format("woff2");
+ font-style: normal;
+ font-weight: 700;
+ font-display: swap;
+}
:root {
--bg: #ffffff;
@@ -162,6 +190,7 @@ ol { padding: 0; list-style: none; }
}
.link:hover { color: var(--ink); }
+.link:disabled { opacity: 0.45; cursor: default; }
/* ---- Dropzone ----------------------------------------------------------- */
@@ -185,6 +214,38 @@ ol { padding: 0; list-style: none; }
transition: border-color 130ms ease, background 130ms ease;
}
+.format-spine {
+ display: flex;
+ align-items: flex-end;
+ justify-content: center;
+ min-height: 2.6rem;
+ margin-bottom: 1.05rem;
+}
+
+.format-spine span {
+ display: grid;
+ place-items: center;
+ width: 3.15rem;
+ height: 2.2rem;
+ margin-left: -0.35rem;
+ border: 1px solid var(--line);
+ border-radius: 5px 5px 2px 2px;
+ background: #fff;
+ color: var(--muted);
+ font-size: 0.62rem;
+ font-weight: 700;
+ letter-spacing: 0.035em;
+ transform: rotate(calc((var(--i, 0) - 2) * 2deg));
+ transform-origin: 50% 110%;
+ box-shadow: 0 3px 10px rgb(16 17 19 / 4%);
+}
+
+.format-spine span:nth-child(1) { --i: 0; }
+.format-spine span:nth-child(2) { --i: 1; }
+.format-spine span:nth-child(3) { --i: 2; color: var(--accent); border-color: color-mix(in srgb, var(--accent) 35%, var(--line)); }
+.format-spine span:nth-child(4) { --i: 3; }
+.format-spine span:nth-child(5) { --i: 4; }
+
.dropzone.is-target {
border-color: var(--accent);
background: color-mix(in srgb, var(--accent) 5%, var(--bg));
@@ -207,7 +268,8 @@ ol { padding: 0; list-style: none; }
}
.composer.has-pages .dropzone__hint,
-.composer.has-pages .dropzone__formats { display: none; }
+.composer.has-pages .dropzone__formats,
+.composer.has-pages .format-spine { display: none; }
.composer.has-pages .btn--lg { padding: 0.6rem 1.3rem; font-size: 0.95rem; }
@@ -227,7 +289,7 @@ ol { padding: 0; list-style: none; }
.grid {
display: grid;
- grid-template-columns: repeat(auto-fill, minmax(8.5rem, 1fr));
+ grid-template-columns: repeat(auto-fill, minmax(9rem, 1fr));
gap: 0.9rem;
}
@@ -255,14 +317,39 @@ ol { padding: 0; list-style: none; }
border-radius: 2px;
}
-/* TIFF has no <img> decoder either, so its card shows a blank sheet. */
-.card__frame--blank::after {
- content: "";
- width: 62%;
- height: 76%;
- border: 1px solid var(--line);
- border-radius: 2px;
+.card__paper { display: none; }
+
+.card__frame--document .card__paper {
+ position: relative;
+ display: grid;
+ place-items: end center;
+ width: 60%;
+ height: 73%;
+ padding-bottom: 0.9rem;
+ border: 1px solid #d9dde1;
+ border-radius: 3px;
background: #fff;
+ box-shadow: 0 5px 14px rgb(16 17 19 / 7%);
+}
+
+.card__frame--document .card__paper::before {
+ content: "";
+ position: absolute;
+ top: -1px;
+ right: -1px;
+ width: 1.25rem;
+ height: 1.25rem;
+ background: linear-gradient(225deg, var(--surface) 49%, #d9dde1 50%, #d9dde1 53%, #fff 54%);
+}
+
+.card__type {
+ padding: 0.14rem 0.42rem;
+ border-radius: 4px;
+ background: color-mix(in srgb, var(--accent) 9%, #fff);
+ color: var(--accent);
+ font-size: 0.64rem;
+ font-weight: 700;
+ letter-spacing: 0.035em;
}
.card__num {
@@ -336,6 +423,17 @@ ol { padding: 0; list-style: none; }
white-space: nowrap;
}
+.card__meta {
+ display: block;
+ margin-top: 0.08rem;
+ color: var(--muted);
+ font-size: 0.7rem;
+ opacity: 0.8;
+ overflow: hidden;
+ text-overflow: ellipsis;
+ white-space: nowrap;
+}
+
/* ---- Progress ----------------------------------------------------------- */
.progress {
@@ -376,6 +474,13 @@ ol { padding: 0; list-style: none; }
.done__tick { width: 2.6rem; height: 2.6rem; color: var(--accent); }
.done__title { margin-top: 0.7rem; font-size: 1.4rem; font-weight: 700; letter-spacing: -0.02em; }
.done__meta { color: var(--muted); font-size: 0.92rem; }
+.done__notes {
+ max-width: 29rem;
+ margin-top: 0.65rem;
+ color: var(--muted);
+ font-size: 0.82rem;
+ line-height: 1.45;
+}
.done #download, .done .btn { margin-top: 1.1rem; }
.done .link { margin-top: 0.9rem; }
diff --git a/docs/tests/browser.test.js b/docs/tests/browser.test.js
new file mode 100644
index 0000000..51b4f85
--- /dev/null
+++ b/docs/tests/browser.test.js
@@ -0,0 +1,182 @@
+const test = require("node:test");
+const assert = require("node:assert/strict");
+const http = require("node:http");
+const fs = require("node:fs/promises");
+const path = require("node:path");
+const os = require("node:os");
+const zlib = require("node:zlib");
+const { chromium } = require("playwright-core");
+const XLSX = require("xlsx");
+const JSZip = require("jszip");
+
+const ROOT = path.resolve(__dirname, "..");
+
+function crc32(buffer) {
+ let crc = 0xffffffff;
+ for (const byte of buffer) {
+ crc ^= byte;
+ for (let bit = 0; bit < 8; bit += 1) crc = (crc >>> 1) ^ (0xedb88320 & -(crc & 1));
+ }
+ const output = Buffer.alloc(4);
+ output.writeUInt32BE((crc ^ 0xffffffff) >>> 0);
+ return output;
+}
+
+function pngChunk(type, data) {
+ const name = Buffer.from(type);
+ const length = Buffer.alloc(4);
+ length.writeUInt32BE(data.length);
+ return Buffer.concat([length, name, data, crc32(Buffer.concat([name, data]))]);
+}
+
+function makePng(width, height) {
+ const signature = Buffer.from([137, 80, 78, 71, 13, 10, 26, 10]);
+ const header = Buffer.alloc(13);
+ header.writeUInt32BE(width, 0);
+ header.writeUInt32BE(height, 4);
+ header.set([8, 6, 0, 0, 0], 8);
+ const row = Buffer.alloc(width * 4 + 1);
+ for (let x = 0; x < width; x += 1) row.set([244, 64, 45, 255], 1 + x * 4);
+ const pixels = Buffer.concat(Array.from({ length: height }, () => row));
+ return Buffer.concat([
+ signature,
+ pngChunk("IHDR", header),
+ pngChunk("IDAT", zlib.deflateSync(pixels)),
+ pngChunk("IEND", Buffer.alloc(0)),
+ ]);
+}
+
+async function makeDocx() {
+ const zip = new JSZip();
+ zip.file("[Content_Types].xml", `<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
+ <Types xmlns="http://schemas.openxmlformats.org/package/2006/content-types">
+ <Default Extension="rels" ContentType="application/vnd.openxmlformats-package.relationships+xml"/>
+ <Default Extension="xml" ContentType="application/xml"/>
+ <Override PartName="/word/document.xml" ContentType="application/vnd.openxmlformats-officedocument.wordprocessingml.document.main+xml"/>
+ </Types>`);
+ zip.folder("_rels").file(".rels", `<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
+ <Relationships xmlns="http://schemas.openxmlformats.org/package/2006/relationships">
+ <Relationship Id="rId1" Type="http://schemas.openxmlformats.org/officeDocument/2006/relationships/officeDocument" Target="word/document.xml"/>
+ </Relationships>`);
+ zip.folder("word").file("document.xml", `<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
+ <w:document xmlns:w="http://schemas.openxmlformats.org/wordprocessingml/2006/main">
+ <w:body><w:p><w:r><w:t>Searchable Word phrase</w:t></w:r></w:p><w:sectPr/></w:body>
+ </w:document>`);
+ zip.folder("word").folder("_rels").file("document.xml.rels", `<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
+ <Relationships xmlns="http://schemas.openxmlformats.org/package/2006/relationships"/>`);
+ return zip.generateAsync({ type: "nodebuffer" });
+}
+
+function makeXlsx() {
+ const workbook = XLSX.utils.book_new();
+ XLSX.utils.book_append_sheet(workbook, XLSX.utils.aoa_to_sheet([
+ ["Product", "Amount"],
+ ["Searchable workbook phrase", 42],
+ ]), "Visible sheet");
+ XLSX.utils.book_append_sheet(workbook, XLSX.utils.aoa_to_sheet([["Must stay hidden"]]), "Hidden sheet");
+ workbook.Workbook = { Sheets: [
+ { name: "Visible sheet", Hidden: 0 },
+ { name: "Hidden sheet", Hidden: 1 },
+ ] };
+ return XLSX.write(workbook, { type: "buffer", bookType: "xlsx" });
+}
+
+function mimeFor(filename) {
+ const extension = path.extname(filename);
+ return {
+ ".html": "text/html; charset=utf-8",
+ ".js": "text/javascript; charset=utf-8",
+ ".css": "text/css; charset=utf-8",
+ ".woff2": "font/woff2",
+ ".ttf": "font/ttf",
+ ".ttc": "font/collection",
+ }[extension] || "application/octet-stream";
+}
+
+async function startServer() {
+ const server = http.createServer(async (request, response) => {
+ try {
+ const pathname = decodeURIComponent(new URL(request.url, "http://localhost").pathname);
+ const relative = pathname === "/" ? "index.html" : pathname.replace(/^\/+/, "");
+ const filename = path.resolve(ROOT, relative);
+ if (!filename.startsWith(ROOT + path.sep)) throw new Error("outside root");
+ const body = await fs.readFile(filename);
+ response.writeHead(200, { "content-type": mimeFor(filename), "cache-control": "no-store" });
+ response.end(body);
+ } catch {
+ response.writeHead(404).end("Not found");
+ }
+ });
+ await new Promise((resolve) => server.listen(0, "127.0.0.1", resolve));
+ return { server, origin: `http://127.0.0.1:${server.address().port}` };
+}
+
+test("the browser converts a mixed local queue into a searchable PDF", { timeout: 120000 }, async () => {
+ const { server, origin } = await startServer();
+ const browser = await chromium.launch({
+ executablePath: "/usr/bin/google-chrome",
+ headless: true,
+ args: ["--no-sandbox", "--disable-dev-shm-usage"],
+ });
+ const temp = await fs.mkdtemp(path.join(os.tmpdir(), "makeitpdf-browser-"));
+ const page = await browser.newPage({ acceptDownloads: true });
+ const externalRequests = [];
+ const pageErrors = [];
+ page.on("request", (request) => {
+ if (!request.url().startsWith(origin) && !request.url().startsWith("blob:")) {
+ externalRequests.push(request.url());
+ }
+ });
+ page.on("pageerror", (error) => pageErrors.push(error.message));
+
+ try {
+ await page.goto(origin, { waitUntil: "networkidle" });
+ await page.setInputFiles("#picker", [
+ { name: "original-size.png", mimeType: "image/png", buffer: makePng(100, 50) },
+ { name: "sample.docx", mimeType: "application/vnd.openxmlformats-officedocument.wordprocessingml.document", buffer: await makeDocx() },
+ { name: "sample.xlsx", mimeType: "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet", buffer: makeXlsx() },
+ { name: "notes.md", mimeType: "text/markdown", buffer: Buffer.from(
+ "# Notes\n\nSearchable Markdown phrase.\n\nمرحبا بالعالم\n\nשלום עולם\n\nनमस्ते दुनिया\n\n中文\n\n日本語\n\n한국어"
+ ) },
+ { name: "records.csv", mimeType: "text/csv", buffer: Buffer.from("Name,Value\nSearchable CSV phrase,7") },
+ ]);
+ await page.getByText("5 files", { exact: false }).waitFor();
+ await page.click("#build");
+ await page.locator("#done:not([hidden])").waitFor({ timeout: 90000 });
+ const [download] = await Promise.all([page.waitForEvent("download"), page.click("#download")]);
+ const pdfPath = path.join(temp, "mixed.pdf");
+ await download.saveAs(pdfPath);
+
+ assert.deepEqual(pageErrors, []);
+ assert.deepEqual(externalRequests, []);
+ const pdfData = new Uint8Array(await fs.readFile(pdfPath));
+ const pdfjs = await import("pdfjs-dist/legacy/build/pdf.mjs");
+ const pdf = await pdfjs.getDocument({ data: pdfData, disableWorker: true }).promise;
+ assert.ok(pdf.numPages >= 5);
+ const firstPage = await pdf.getPage(1);
+ const viewport = firstPage.getViewport({ scale: 1 });
+ assert.ok(Math.abs(viewport.width - 75) < 0.1, `expected 75pt, got ${viewport.width}`);
+ assert.ok(Math.abs(viewport.height - 37.5) < 0.1, `expected 37.5pt, got ${viewport.height}`);
+
+ let extracted = "";
+ for (let index = 1; index <= pdf.numPages; index += 1) {
+ const content = await (await pdf.getPage(index)).getTextContent();
+ extracted += content.items.map((item) => item.str).join(" ") + "\n";
+ }
+ assert.match(extracted, /Searchable Word phrase/);
+ assert.match(extracted, /Searchable workbook phrase/);
+ assert.match(extracted, /Searchable Markdown phrase/);
+ assert.match(extracted, /Searchable CSV phrase/);
+ assert.match(extracted, /[\u0600-\u06ff]{5}/);
+ assert.match(extracted, /[\u0590-\u05ff]{4}/);
+ assert.match(extracted, /नमस्ते/);
+ assert.match(extracted, /中文/);
+ assert.match(extracted, /日本語/);
+ assert.match(extracted, /한국어/);
+ assert.doesNotMatch(extracted, /Must stay hidden/);
+ } finally {
+ await browser.close();
+ await new Promise((resolve) => server.close(resolve));
+ await fs.rm(temp, { recursive: true, force: true });
+ }
+});
diff --git a/docs/tests/documents.test.js b/docs/tests/documents.test.js
new file mode 100644
index 0000000..4f4b327
--- /dev/null
+++ b/docs/tests/documents.test.js
@@ -0,0 +1,42 @@
+const test = require("node:test");
+const assert = require("node:assert/strict");
+const Documents = require("../documents.js");
+
+test("safe links only allow web and mail targets", () => {
+ assert.equal(Documents.safeUrl("https://example.com/a"), "https://example.com/a");
+ assert.equal(Documents.safeUrl("mailto:[email protected]"), "mailto:[email protected]");
+ assert.equal(Documents.safeUrl("javascript:alert(1)"), null);
+ assert.equal(Documents.safeUrl("file:///etc/passwd"), null);
+});
+
+test("CSV delimiter detection handles comma, tab, semicolon, and pipe", () => {
+ assert.equal(Documents.parseCsv("a,b\n1,2").delimiter, ",");
+ assert.equal(Documents.parseCsv("a\tb\n1\t2").delimiter, "\t");
+ assert.equal(Documents.parseCsv("a;b\n1;2").delimiter, ";");
+ assert.equal(Documents.parseCsv("a|b\n1|2").delimiter, "|");
+});
+
+test("CSV parsing preserves quoted commas, escaped quotes, and newlines", () => {
+ const parsed = Documents.parseCsv('name,note\n"Ada, A.","line 1\nline 2"\nBob,"said ""hi"""');
+ assert.deepEqual(parsed.rows, [
+ ["name", "note"],
+ ["Ada, A.", "line 1\nline 2"],
+ ["Bob", 'said "hi"'],
+ ]);
+ assert.equal(parsed.cells, 6);
+});
+
+test("CSV rows are padded to a stable table width", () => {
+ const parsed = Documents.parseCsv("a,b,c\n1,2");
+ assert.deepEqual(parsed.rows[1], ["1", "2", ""]);
+});
+
+test("unclosed CSV quotes produce a useful error", () => {
+ assert.throws(() => Documents.parseCsv('a,"broken'), /unclosed quoted field/);
+});
+
+test("UTF-8 decoding strips a BOM and rejects invalid input", () => {
+ const valid = Uint8Array.from([0xef, 0xbb, 0xbf, 0x68, 0x69]);
+ assert.equal(Documents.decodeUtf8(valid), "hi");
+ assert.throws(() => Documents.decodeUtf8(Uint8Array.from([0xc3, 0x28])), /valid UTF-8/);
+});
diff --git a/docs/tests/pages.test.js b/docs/tests/pages.test.js
index f542b32..1cfc52d 100644
--- a/docs/tests/pages.test.js
+++ b/docs/tests/pages.test.js
@@ -111,3 +111,84 @@ test("other formats are not mistaken for TIFF", () => {
assert.equal(Pages.isTiff(new Uint8Array([0x49, 0x49])), false, "truncated");
assert.equal(Pages.isTiff(null), false, "nothing");
});
+
+test("image pages use embedded physical resolution instead of A4", () => {
+ const page = Pages.imagePage(1200, 600, 300, 300, 1);
+ assert.equal(page.width, 288);
+ assert.equal(page.height, 144);
+ assert.equal(page.capped, false);
+});
+
+test("images without usable DPI metadata fall back to 96 DPI", () => {
+ const page = Pages.imagePage(960, 480, undefined, 0, 1);
+ assert.equal(page.width, 720);
+ assert.equal(page.height, 360);
+ assert.equal(page.dpiX, 96);
+ assert.equal(page.dpiY, 96);
+});
+
+test("EXIF rotations swap pixel dimensions and resolution axes", () => {
+ const page = Pages.imagePage(1200, 600, 300, 150, 6);
+ assert.equal(page.pixelWidth, 600);
+ assert.equal(page.pixelHeight, 1200);
+ assert.equal(page.width, 288);
+ assert.equal(page.height, 288);
+});
+
+test("oversized physical pages are capped proportionally", () => {
+ const page = Pages.imagePage(40000, 20000, 1, 1, 1);
+ assert.equal(page.width, Pages.MAX_PAGE_PT);
+ assert.equal(page.height, Pages.MAX_PAGE_PT / 2);
+ assert.equal(page.capped, true);
+ assert.ok(page.scale < 1);
+});
+
+test("PNG pHYs metadata is converted from pixels per metre to DPI", () => {
+ const bytes = new Uint8Array(54);
+ bytes.set([137, 80, 78, 71, 13, 10, 26, 10]);
+ const view = new DataView(bytes.buffer);
+ view.setUint32(8, 13, false);
+ bytes.set([73, 72, 68, 82], 12);
+ view.setUint32(16, 1200, false);
+ view.setUint32(20, 600, false);
+ view.setUint32(33, 9, false);
+ bytes.set([112, 72, 89, 115], 37);
+ view.setUint32(41, 11811, false); // approximately 300 DPI
+ view.setUint32(45, 5906, false); // approximately 150 DPI
+ bytes[49] = 1;
+ const metadata = Pages.parseRasterMetadata(bytes);
+ assert.equal(metadata.width, 1200);
+ assert.equal(metadata.height, 600);
+ assert.ok(Math.abs(metadata.dpiX - 300) < 0.02);
+ assert.ok(Math.abs(metadata.dpiY - 150) < 0.02);
+});
+
+test("BMP pixels-per-metre metadata is read", () => {
+ const bytes = new Uint8Array(46);
+ bytes.set([66, 77]);
+ const view = new DataView(bytes.buffer);
+ view.setInt32(18, 800, true);
+ view.setInt32(22, 400, true);
+ view.setInt32(38, 3780, true);
+ view.setInt32(42, 3780, true);
+ const metadata = Pages.parseRasterMetadata(bytes);
+ assert.equal(metadata.width, 800);
+ assert.equal(metadata.height, 400);
+ assert.ok(Math.abs(metadata.dpiX - 96) < 0.02);
+});
+
+test("JPEG JFIF density and dimensions are read", () => {
+ const bytes = new Uint8Array([
+ 0xff, 0xd8,
+ 0xff, 0xe0, 0x00, 0x10,
+ 0x4a, 0x46, 0x49, 0x46, 0x00, 0x01, 0x01, 0x01, 0x01, 0x2c, 0x00, 0x96, 0x00, 0x00,
+ 0xff, 0xc0, 0x00, 0x11, 0x08, 0x02, 0x58, 0x04, 0xb0,
+ 0x03, 0x01, 0x11, 0x00, 0x02, 0x11, 0x00, 0x03, 0x11, 0x00,
+ 0xff, 0xd9,
+ ]);
+ const metadata = Pages.parseRasterMetadata(bytes);
+ assert.equal(metadata.width, 1200);
+ assert.equal(metadata.height, 600);
+ assert.equal(metadata.dpiX, 300);
+ assert.equal(metadata.dpiY, 150);
+});
diff --git a/docs/vendor/bidi.min.js b/docs/vendor/bidi.min.js
new file mode 100644
index 0000000..bd0ca4d
--- /dev/null
+++ b/docs/vendor/bidi.min.js
@@ -0,0 +1 @@
+!function(r,e){"object"==typeof exports&&"undefined"!=typeof module?module.exports=e():"function"==typeof define&&define.amd?define(e):(r="undefined"!=typeof globalThis?globalThis:r||self).bidi_js=e()}(this,(function(){"use strict";return function(){return function(r){var e={R:"13k,1a,2,3,3,2+1j,ch+16,a+1,5+2,2+n,5,a,4,6+16,4+3,h+1b,4mo,179q,2+9,2+11,2i9+7y,2+68,4,3+4,5+13,4+3,2+4k,3+29,8+cf,1t+7z,w+17,3+3m,1t+3z,16o1+5r,8+30,8+mc,29+1r,29+4v,75+73",EN:"1c+9,3d+1,6,187+9,513,4+5,7+9,sf+j,175h+9,qw+q,161f+1d,4xt+a,25i+9",ES:"17,2,6dp+1,f+1,av,16vr,mx+1,4o,2",ET:"z+2,3h+3,b+1,ym,3e+1,2o,p4+1,8,6u,7c,g6,1wc,1n9+4,30+1b,2n,6d,qhx+1,h0m,a+1,49+2,63+1,4+1,6bb+3,12jj",AN:"16o+5,2j+9,2+1,35,ed,1ff2+9,87+u",CS:"18,2+1,b,2u,12k,55v,l,17v0,2,3,53,2+1,b",B:"a,3,f+2,2v,690",S:"9,2,k",WS:"c,k,4f4,1vk+a,u,1j,335",ON:"x+1,4+4,h+5,r+5,r+3,z,5+3,2+1,2+1,5,2+2,3+4,o,w,ci+1,8+d,3+d,6+8,2+g,39+1,9,6+1,2,33,b8,3+1,3c+1,7+1,5r,b,7h+3,sa+5,2,3i+6,jg+3,ur+9,2v,ij+1,9g+9,7+a,8m,4+1,49+x,14u,2+2,c+2,e+2,e+2,e+1,i+n,e+e,2+p,u+2,e+2,36+1,2+3,2+1,b,2+2,6+5,2,2,2,h+1,5+4,6+3,3+f,16+2,5+3l,3+81,1y+p,2+40,q+a,m+13,2r+ch,2+9e,75+hf,3+v,2+2w,6e+5,f+6,75+2a,1a+p,2+2g,d+5x,r+b,6+3,4+o,g,6+1,6+2,2k+1,4,2j,5h+z,1m+1,1e+f,t+2,1f+e,d+3,4o+3,2s+1,w,535+1r,h3l+1i,93+2,2s,b+1,3l+x,2v,4g+3,21+3,kz+1,g5v+1,5a,j+9,n+v,2,3,2+8,2+1,3+2,2,3,46+1,4+4,h+5,r+5,r+a,3h+2,4+6,b+4,78,1r+24,4+c,4,1hb,ey+6,103+j,16j+c,1ux+7,5+g,fsh,jdq+1t,4,57+2e,p1,1m,1m,1m,1m,4kt+1,7j+17,5+2r,d+e,3+e,2+e,2+10,m+4,w,1n+5,1q,4z+5,4b+rb,9+c,4+c,4+37,d+2g,8+b,l+b,5+1j,9+9,7+13,9+t,3+1,27+3c,2+29,2+3q,d+d,3+4,4+2,6+6,a+o,8+6,a+2,e+6,16+42,2+1i",BN:"0+8,6+d,2s+5,2+p,e,4m9,1kt+2,2b+5,5+5,17q9+v,7k,6p+8,6+1,119d+3,440+7,96s+1,1ekf+1,1ekf+1,1ekf+1,1ekf+1,1ekf+1,1ekf+1,1ekf+1,1ekf+1,1ekf+1,1ekf+1,1ekf+1,1ekf+75,6p+2rz,1ben+1,1ekf+1,1ekf+1",NSM:"lc+33,7o+6,7c+18,2,2+1,2+1,2,21+a,1d+k,h,2u+6,3+5,3+1,2+3,10,v+q,2k+a,1n+8,a,p+3,2+8,2+2,2+4,18+2,3c+e,2+v,1k,2,5+7,5,4+6,b+1,u,1n,5+3,9,l+1,r,3+1,1m,5+1,5+1,3+2,4,v+1,4,c+1,1m,5+4,2+1,5,l+1,n+5,2,1n,3,2+3,9,8+1,c+1,v,1q,d,1f,4,1m+2,6+2,2+3,8+1,c+1,u,1n,g+1,l+1,t+1,1m+1,5+3,9,l+1,u,21,8+2,2,2j,3+6,d+7,2r,3+8,c+5,23+1,s,2,2,1k+d,2+4,2+1,6+a,2+z,a,2v+3,2+5,2+1,3+1,q+1,5+2,h+3,e,3+1,7,g,jk+2,qb+2,u+2,u+1,v+1,1t+1,2+6,9,3+a,a,1a+2,3c+1,z,3b+2,5+1,a,7+2,64+1,3,1n,2+6,2,2,3+7,7+9,3,1d+g,1s+3,1d,2+4,2,6,15+8,d+1,x+3,3+1,2+2,1l,2+1,4,2+2,1n+7,3+1,49+2,2+c,2+6,5,7,4+1,5j+1l,2+4,k1+w,2db+2,3y,2p+v,ff+3,30+1,n9x+3,2+9,x+1,29+1,7l,4,5,q+1,6,48+1,r+h,e,13+7,q+a,1b+2,1d,3+3,3+1,14,1w+5,3+1,3+1,d,9,1c,1g,2+2,3+1,6+1,2,17+1,9,6n,3,5,fn5,ki+f,h+f,r2,6b,46+4,1af+2,2+1,6+3,15+2,5,4m+1,fy+3,as+1,4a+a,4x,1j+e,1l+2,1e+3,3+1,1y+2,11+4,2+7,1r,d+1,1h+8,b+3,3,2o+2,3,2+1,7,4h,4+7,m+1,1m+1,4,12+6,4+4,5g+7,3+2,2,o,2d+5,2,5+1,2+1,6n+3,7+1,2+1,s+1,2e+7,3,2+1,2z,2,3+5,2,2u+2,3+3,2+4,78+8,2+1,75+1,2,5,41+3,3+1,5,x+5,3+1,15+5,3+3,9,a+5,3+2,1b+c,2+1,bb+6,2+5,2d+l,3+6,2+1,2+1,3f+5,4,2+1,2+6,2,21+1,4,2,9o+1,f0c+4,1o+6,t5,1s+3,2a,f5l+1,43t+2,i+7,3+6,v+3,45+2,1j0+1i,5+1d,9,f,n+4,2+e,11t+6,2+g,3+6,2+1,2+4,7a+6,c6+3,15t+6,32+6,gzhy+6n",AL:"16w,3,2,e+1b,z+2,2+2s,g+1,8+1,b+m,2+t,s+2i,c+e,4h+f,1d+1e,1bwe+dp,3+3z,x+c,2+1,35+3y,2rm+z,5+7,b+5,dt+l,c+u,17nl+27,1t+27,4x+6n,3+d",LRO:"6ct",RLO:"6cu",LRE:"6cq",RLE:"6cr",PDF:"6cs",LRI:"6ee",RLI:"6ef",FSI:"6eg",PDI:"6eh"},f={},a={};f.L=1,a[1]="L",Object.keys(e).forEach((function(r,e){f[r]=1<<e+1,a[f[r]]=r})),Object.freeze(f);var n=f.LRI|f.RLI|f.FSI,i=f.L|f.R|f.AL,v=f.B|f.S|f.WS|f.ON|f.FSI|f.LRI|f.RLI|f.PDI,o=f.BN|f.RLE|f.LRE|f.RLO|f.LRO|f.PDF,t=f.S|f.WS|f.B|n|f.PDI|o,u=null;function l(r){return function(){if(!u){u=new Map;var r=function(r){if(e.hasOwnProperty(r)){var a=0;e[r].split(",").forEach((function(e){var n=e.split("+"),i=n[0],v=n[1];i=parseInt(i,36),v=v?parseInt(v,36):0,u.set(a+=i,f[r]);for(var o=0;o<v;o++)u.set(++a,f[r])}))}};for(var a in e)r(a)}}(),u.get(r.codePointAt(0))||f.L}var c,d,b,s="14>1,1e>2,u>2,2wt>1,1>1,1ge>1,1wp>1,1j>1,f>1,hm>1,1>1,u>1,u6>1,1>1,+5,28>1,w>1,1>1,+3,b8>1,1>1,+3,1>3,-1>-1,3>1,1>1,+2,1s>1,1>1,x>1,th>1,1>1,+2,db>1,1>1,+3,3>1,1>1,+2,14qm>1,1>1,+1,4q>1,1e>2,u>2,2>1,+1",h="6f1>-6dx,6dy>-6dx,6ec>-6ed,6ee>-6ed,6ww>2jj,-2ji>2jj,14r4>-1e7l,1e7m>-1e7l,1e7m>-1e5c,1e5d>-1e5b,1e5c>-14qx,14qy>-14qx,14vn>-1ecg,1ech>-1ecg,1edu>-1ecg,1eci>-1ecg,1eda>-1ecg,1eci>-1ecg,1eci>-168q,168r>-168q,168s>-14ye,14yf>-14ye";function k(r,e){var f,a=0,n=new Map,i=e&&new Map;return r.split(",").forEach((function r(v){if(-1!==v.indexOf("+"))for(var o=+v;o--;)r(f);else{f=v;var t=v.split(">"),u=t[0],l=t[1];u=String.fromCodePoint(a+=parseInt(u,36)),l=String.fromCodePoint(a+=parseInt(l,36)),n.set(u,l),e&&i.set(l,u)}})),{map:n,reverseMap:i}}function m(){if(!c){var r=k(s,!0),e=r.map,f=r.reverseMap;c=e,d=f,b=k(h,!1).map}}function j(r){return m(),c.get(r)||null}function g(r){return m(),d.get(r)||null}function p(r){return m(),b.get(r)||null}var q=f.L,w=f.R,y=f.EN,x=f.ES,_=f.ET,M=f.AN,z=f.CS,I=f.B,L=f.S,S=f.ON,R=f.BN,O=f.NSM,E=f.AL,N=f.LRO,T=f.RLO,A=f.LRE,D=f.RLE,P=f.PDF,W=f.LRI,B=f.RLI,F=f.FSI,U=f.PDI;var C;function G(r){return function(){if(!C){var r=k("14>1,j>2,t>2,u>2,1a>g,2v3>1,1>1,1ge>1,1wd>1,b>1,1j>1,f>1,ai>3,-2>3,+1,8>1k0,-1jq>1y7,-1y6>1hf,-1he>1h6,-1h5>1ha,-1h8>1qi,-1pu>1,6>3u,-3s>7,6>1,1>1,f>1,1>1,+2,3>1,1>1,+13,4>1,1>1,6>1eo,-1ee>1,3>1mg,-1me>1mk,-1mj>1mi,-1mg>1mi,-1md>1,1>1,+2,1>10k,-103>1,1>1,4>1,5>1,1>1,+10,3>1,1>8,-7>8,+1,-6>7,+1,a>1,1>1,u>1,u6>1,1>1,+5,26>1,1>1,2>1,2>2,8>1,7>1,4>1,1>1,+5,b8>1,1>1,+3,1>3,-2>1,2>1,1>1,+2,c>1,3>1,1>1,+2,h>1,3>1,a>1,1>1,2>1,3>1,1>1,d>1,f>1,3>1,1a>1,1>1,6>1,7>1,13>1,k>1,1>1,+19,4>1,1>1,+2,2>1,1>1,+18,m>1,a>1,1>1,lk>1,1>1,4>1,2>1,f>1,3>1,1>1,+3,db>1,1>1,+3,3>1,1>1,+2,14qm>1,1>1,+1,6>1,4j>1,j>2,t>2,u>2,2>1,+1",!0),e=r.map;r.reverseMap.forEach((function(r,f){e.set(f,r)})),C=e}}(),C.get(r)||null}function H(r,e,f,a){var n=r.length;f=Math.max(0,null==f?0:+f),a=Math.min(n-1,null==a?n-1:+a);var i=[];return e.paragraphs.forEach((function(n){var v=Math.max(f,n.start),o=Math.min(a,n.end);if(v<o){for(var u=e.levels.slice(v,o+1),c=o;c>=v&&l(r[c])&t;c--)u[c]=n.level;for(var d=n.level,b=1/0,s=0;s<u.length;s++){var h=u[s];h>d&&(d=h),h<b&&(b=1|h)}for(var k=d;k>=b;k--)for(var m=0;m<u.length;m++)if(u[m]>=k){for(var j=m;m+1<u.length&&u[m+1]>=k;)m++;m>j&&i.push([j+v,m+v])}}})),i}function J(r,e,f,a){for(var n=H(r,e,f,a),i=[],v=0;v<r.length;v++)i[v]=v;return n.forEach((function(r){for(var e=r[0],f=r[1],a=i.slice(e,f+1),n=a.length;n--;)i[f-n]=a[n]})),i}return r.closingToOpeningBracket=g,r.getBidiCharType=l,r.getBidiCharTypeName=function(r){return a[l(r)]},r.getCanonicalBracket=p,r.getEmbeddingLevels=function(r,e){for(var f=new Uint32Array(r.length),a=0;a<r.length;a++)f[a]=l(r[a]);var u=new Map;function c(r,e){var a=f[r];f[r]=e,u.set(a,u.get(a)-1),a&v&&u.set(v,u.get(v)-1),u.set(e,(u.get(e)||0)+1),e&v&&u.set(v,(u.get(v)||0)+1)}for(var d=new Uint8Array(r.length),b=new Map,s=[],h=null,k=0;k<r.length;k++)h||s.push(h={start:k,end:r.length-1,level:"rtl"===e?1:"ltr"===e?0:Fe(k,!1)}),f[k]&I&&(h.end=k,h=null);for(var m=D|A|T|N|n|U|P|I,C=function(r){return r+(1&r?1:2)},G=function(r){return r+(1&r?2:1)},H=0;H<s.length;H++){var J=[{i:(h=s[H]).level,v:0,o:0}],K=void 0,Q=0,V=0,X=0;u.clear();for(var Y=h.start;Y<=h.end;Y++){var Z=f[Y];if(K=J[J.length-1],u.set(Z,(u.get(Z)||0)+1),Z&v&&u.set(v,(u.get(v)||0)+1),Z&m)if(Z&(D|A)){d[Y]=K.i;var $=(Z===D?G:C)(K.i);$<=125&&!Q&&!V?J.push({i:$,v:0,o:0}):Q||V++}else if(Z&(T|N)){d[Y]=K.i;var rr=(Z===T?G:C)(K.i);rr<=125&&!Q&&!V?J.push({i:rr,v:Z&T?w:q,o:0}):Q||V++}else if(Z&n){Z&F&&(Z=1===Fe(Y+1,!0)?B:W),d[Y]=K.i,K.v&&c(Y,K.v);var er=(Z===B?G:C)(K.i);er<=125&&0===Q&&0===V?(X++,J.push({i:er,v:0,o:1,t:Y})):Q++}else if(Z&U){if(Q>0)Q--;else if(X>0){for(V=0;!J[J.length-1].o;)J.pop();var fr=J[J.length-1].t;null!=fr&&(b.set(fr,Y),b.set(Y,fr)),J.pop(),X--}K=J[J.length-1],d[Y]=K.i,K.v&&c(Y,K.v)}else Z&P?(0===Q&&(V>0?V--:!K.o&&J.length>1&&(J.pop(),K=J[J.length-1])),d[Y]=K.i):Z&I&&(d[Y]=h.level);else d[Y]=K.i,K.v&&Z!==R&&c(Y,K.v)}for(var ar=[],nr=null,ir=h.start;ir<=h.end;ir++){var vr=f[ir];if(!(vr&o)){var or=d[ir],tr=vr&n,ur=vr===U;nr&&or===nr.i?(nr.u=ir,nr.l=tr):ar.push(nr={h:ir,u:ir,i:or,k:ur,l:tr})}}for(var lr=[],cr=0;cr<ar.length;cr++){var dr=ar[cr];if(!dr.k||dr.k&&!b.has(dr.h)){for(var br=[nr=dr],sr=void 0;nr&&nr.l&&null!=(sr=b.get(nr.u));)for(var hr=cr+1;hr<ar.length;hr++)if(ar[hr].h===sr){br.push(nr=ar[hr]);break}for(var kr=[],mr=0;mr<br.length;mr++)for(var jr=br[mr],gr=jr.h;gr<=jr.u;gr++)kr.push(gr);for(var pr=d[kr[0]],qr=h.level,wr=kr[0]-1;wr>=0;wr--)if(!(f[wr]&o)){qr=d[wr];break}var yr=kr[kr.length-1],xr=d[yr],_r=h.level;if(!(f[yr]&n))for(var Mr=yr+1;Mr<=h.end;Mr++)if(!(f[Mr]&o)){_r=d[Mr];break}lr.push({m:kr,j:Math.max(qr,pr)%2?w:q,g:Math.max(_r,xr)%2?w:q})}}for(var zr=0;zr<lr.length;zr++){var Ir=lr[zr],Lr=Ir.m,Sr=Ir.j,Rr=Ir.g,Or=1&d[Lr[0]]?w:q;if(u.get(O))for(var Er=0;Er<Lr.length;Er++){var Nr=Lr[Er];if(f[Nr]&O){for(var Tr=Sr,Ar=Er-1;Ar>=0;Ar--)if(!(f[Lr[Ar]]&o)){Tr=f[Lr[Ar]];break}c(Nr,Tr&(n|U)?S:Tr)}}if(u.get(y))for(var Dr=0;Dr<Lr.length;Dr++){var Pr=Lr[Dr];if(f[Pr]&y)for(var Wr=Dr-1;Wr>=-1;Wr--){var Br=-1===Wr?Sr:f[Lr[Wr]];if(Br&i){Br===E&&c(Pr,M);break}}}if(u.get(E))for(var Fr=0;Fr<Lr.length;Fr++){var Ur=Lr[Fr];f[Ur]&E&&c(Ur,w)}if(u.get(x)||u.get(z))for(var Cr=1;Cr<Lr.length-1;Cr++){var Gr=Lr[Cr];if(f[Gr]&(x|z)){for(var Hr=0,Jr=0,Kr=Cr-1;Kr>=0&&(Hr=f[Lr[Kr]])&o;Kr--);for(var Qr=Cr+1;Qr<Lr.length&&(Jr=f[Lr[Qr]])&o;Qr++);Hr===Jr&&(f[Gr]===x?Hr===y:Hr&(y|M))&&c(Gr,Hr)}}if(u.get(y))for(var Vr=0;Vr<Lr.length;Vr++){var Xr=Lr[Vr];if(f[Xr]&y){for(var Yr=Vr-1;Yr>=0&&f[Lr[Yr]]&(_|o);Yr--)c(Lr[Yr],y);for(Vr++;Vr<Lr.length&&f[Lr[Vr]]&(_|o|y);Vr++)f[Lr[Vr]]!==y&&c(Lr[Vr],y)}}if(u.get(_)||u.get(x)||u.get(z))for(var Zr=0;Zr<Lr.length;Zr++){var $r=Lr[Zr];if(f[$r]&(_|x|z)){c($r,S);for(var re=Zr-1;re>=0&&f[Lr[re]]&o;re--)c(Lr[re],S);for(var ee=Zr+1;ee<Lr.length&&f[Lr[ee]]&o;ee++)c(Lr[ee],S)}}if(u.get(y))for(var fe=0,ae=Sr;fe<Lr.length;fe++){var ne=Lr[fe],ie=f[ne];ie&y?ae===q&&c(ne,q):ie&i&&(ae=ie)}if(u.get(v)){for(var ve=w|y|M,oe=ve|q,te=[],ue=[],le=0;le<Lr.length;le++)if(f[Lr[le]]&v){var ce=r[Lr[le]],de=void 0;if(null!==j(ce)){if(!(ue.length<63))break;ue.push({char:ce,seqIndex:le})}else if(null!==(de=g(ce)))for(var be=ue.length-1;be>=0;be--){var se=ue[be].char;if(se===de||se===g(p(ce))||j(p(se))===ce){te.push([ue[be].seqIndex,le]),ue.length=be;break}}}te.sort((function(r,e){return r[0]-e[0]}));for(var he=0;he<te.length;he++){for(var ke=te[he],me=ke[0],je=ke[1],ge=!1,pe=0,qe=me+1;qe<je;qe++){var we=Lr[qe];if(f[we]&oe){ge=!0;var ye=f[we]&ve?w:q;if(ye===Or){pe=ye;break}}}if(ge&&!pe){pe=Sr;for(var xe=me-1;xe>=0;xe--){var _e=Lr[xe];if(f[_e]&oe){var Me=f[_e]&ve?w:q;pe=Me!==Or?Me:Or;break}}}if(pe){if(f[Lr[me]]=f[Lr[je]]=pe,pe!==Or)for(var ze=me+1;ze<Lr.length;ze++)if(!(f[Lr[ze]]&o)){l(r[Lr[ze]])&O&&(f[Lr[ze]]=pe);break}if(pe!==Or)for(var Ie=je+1;Ie<Lr.length;Ie++)if(!(f[Lr[Ie]]&o)){l(r[Lr[Ie]])&O&&(f[Lr[Ie]]=pe);break}}}for(var Le=0;Le<Lr.length;Le++)if(f[Lr[Le]]&v){for(var Se=Le,Re=Le,Oe=Sr,Ee=Le-1;Ee>=0;Ee--){if(!(f[Lr[Ee]]&o)){Oe=f[Lr[Ee]]&ve?w:q;break}Se=Ee}for(var Ne=Rr,Te=Le+1;Te<Lr.length;Te++){if(!(f[Lr[Te]]&(v|o))){Ne=f[Lr[Te]]&ve?w:q;break}Re=Te}for(var Ae=Se;Ae<=Re;Ae++)f[Lr[Ae]]=Oe===Ne?Oe:Or;Le=Re}}}for(var De=h.start;De<=h.end;De++){var Pe=d[De],We=f[De];if(1&Pe?We&(q|y|M)&&d[De]++:We&w?d[De]++:We&(M|y)&&(d[De]+=2),We&o&&(d[De]=0===De?h.level:d[De-1]),De===h.end||l(r[De])&(L|I))for(var Be=De;Be>=0&&l(r[Be])&t;Be--)d[Be]=h.level}}return{levels:d,paragraphs:s};function Fe(e,a){for(var i=e;i<r.length;i++){var v=f[i];if(v&(w|E))return 1;if(v&(I|q)||a&&v===U)return 0;if(v&n){var o=Ue(i);i=-1===o?r.length:o}}return 0}function Ue(e){for(var a=1,i=e+1;i<r.length;i++){var v=f[i];if(v&I)break;if(v&U){if(0==--a)return i}else v&n&&a++}return-1}},r.getMirroredCharacter=G,r.getMirroredCharactersMap=function(r,e,f,a){var n=r.length;f=Math.max(0,null==f?0:+f),a=Math.min(n-1,null==a?n-1:+a);for(var i=new Map,v=f;v<=a;v++)if(1&e[v]){var o=G(r[v]);null!==o&&i.set(v,o)}return i},r.getReorderSegments=H,r.getReorderedIndices=J,r.getReorderedString=function(r,e,f,a){var n=J(r,e,f,a),i=[].concat(r);return n.forEach((function(f,a){i[a]=(1&e.levels[f]?G(r[f]):null)||r[f]})),i.join("")},r.openingToClosingBracket=j,Object.defineProperty(r,"p",{value:!0}),r}({})}}));
diff --git a/docs/vendor/blob-stream.js b/docs/vendor/blob-stream.js
new file mode 100644
index 0000000..2bbfee2
--- /dev/null
+++ b/docs/vendor/blob-stream.js
@@ -0,0 +1,4684 @@
+!function(e){if("object"==typeof exports&&"undefined"!=typeof module)module.exports=e();else if("function"==typeof define&&define.amd)define([],e);else{var f;"undefined"!=typeof window?f=window:"undefined"!=typeof global?f=global:"undefined"!=typeof self&&(f=self),f.blobStream=e()}}(function(){var define,module,exports;return (function e(t,n,r){function s(o,u){if(!n[o]){if(!t[o]){var a=typeof require=="function"&&require;if(!u&&a)return a(o,!0);if(i)return i(o,!0);var f=new Error("Cannot find module '"+o+"'");throw f.code="MODULE_NOT_FOUND",f}var l=n[o]={exports:{}};t[o][0].call(l.exports,function(e){var n=t[o][1][e];return s(n?n:e)},l,l.exports,e,t,n,r)}return n[o].exports}var i=typeof require=="function"&&require;for(var o=0;o<r.length;o++)s(r[o]);return s})({1:[function(require,module,exports){
+(function (global){
+/**
+ * Create a blob builder even when vendor prefixes exist
+ */
+
+var BlobBuilder = global.BlobBuilder
+ || global.WebKitBlobBuilder
+ || global.MSBlobBuilder
+ || global.MozBlobBuilder;
+
+/**
+ * Check if Blob constructor is supported
+ */
+
+var blobSupported = (function() {
+ try {
+ var a = new Blob(['hi']);
+ return a.size === 2;
+ } catch(e) {
+ return false;
+ }
+})();
+
+/**
+ * Check if Blob constructor supports ArrayBufferViews
+ * Fails in Safari 6, so we need to map to ArrayBuffers there.
+ */
+
+var blobSupportsArrayBufferView = blobSupported && (function() {
+ try {
+ var b = new Blob([new Uint8Array([1,2])]);
+ return b.size === 2;
+ } catch(e) {
+ return false;
+ }
+})();
+
+/**
+ * Check if BlobBuilder is supported
+ */
+
+var blobBuilderSupported = BlobBuilder
+ && BlobBuilder.prototype.append
+ && BlobBuilder.prototype.getBlob;
+
+/**
+ * Helper function that maps ArrayBufferViews to ArrayBuffers
+ * Used by BlobBuilder constructor and old browsers that didn't
+ * support it in the Blob constructor.
+ */
+
+function mapArrayBufferViews(ary) {
+ for (var i = 0; i < ary.length; i++) {
+ var chunk = ary[i];
+ if (chunk.buffer instanceof ArrayBuffer) {
+ var buf = chunk.buffer;
+
+ // if this is a subarray, make a copy so we only
+ // include the subarray region from the underlying buffer
+ if (chunk.byteLength !== buf.byteLength) {
+ var copy = new Uint8Array(chunk.byteLength);
+ copy.set(new Uint8Array(buf, chunk.byteOffset, chunk.byteLength));
+ buf = copy.buffer;
+ }
+
+ ary[i] = buf;
+ }
+ }
+}
+
+function BlobBuilderConstructor(ary, options) {
+ options = options || {};
+
+ var bb = new BlobBuilder();
+ mapArrayBufferViews(ary);
+
+ for (var i = 0; i < ary.length; i++) {
+ bb.append(ary[i]);
+ }
+
+ return (options.type) ? bb.getBlob(options.type) : bb.getBlob();
+};
+
+function BlobConstructor(ary, options) {
+ mapArrayBufferViews(ary);
+ return new Blob(ary, options || {});
+};
+
+module.exports = (function() {
+ if (blobSupported) {
+ return blobSupportsArrayBufferView ? global.Blob : BlobConstructor;
+ } else if (blobBuilderSupported) {
+ return BlobBuilderConstructor;
+ } else {
+ return undefined;
+ }
+})();
+
+}).call(this,typeof global !== "undefined" ? global : typeof self !== "undefined" ? self : typeof window !== "undefined" ? window : {})
+},{}],2:[function(require,module,exports){
+(function (global){
+var WritableStream = require('stream').Writable;
+var util = require('util');
+var Blob = require('blob');
+var URL = global.URL || global.webkitURL || global.mozURL;
+
+function BlobStream() {
+ if (!(this instanceof BlobStream))
+ return new BlobStream;
+
+ WritableStream.call(this);
+ this._chunks = [];
+ this._blob = null;
+ this.length = 0;
+}
+
+util.inherits(BlobStream, WritableStream);
+
+BlobStream.prototype._write = function(chunk, encoding, callback) {
+ // convert chunks to Uint8Arrays (e.g. Buffer when array fallback is being used)
+ if (!(chunk instanceof Uint8Array))
+ chunk = new Uint8Array(chunk);
+
+ this.length += chunk.length;
+ this._chunks.push(chunk);
+ callback();
+};
+
+BlobStream.prototype.toBlob = function(type) {
+ type = type || 'application/octet-stream';
+
+ // cache the blob if needed
+ if (!this._blob) {
+ this._blob = new Blob(this._chunks, {
+ type: type
+ });
+
+ this._chunks = []; // free memory
+ }
+
+ // if the cached blob's type doesn't match the requested type, make a new blob
+ if (this._blob.type !== type)
+ this._blob = new Blob([this._blob], { type: type });
+
+ return this._blob;
+};
+
+BlobStream.prototype.toBlobURL = function(type) {
+ return URL.createObjectURL(this.toBlob(type));
+};
+
+module.exports = BlobStream;
+
+}).call(this,typeof global !== "undefined" ? global : typeof self !== "undefined" ? self : typeof window !== "undefined" ? window : {})
+},{"blob":1,"stream":22,"util":25}],3:[function(require,module,exports){
+/*!
+ * The buffer module from node.js, for the browser.
+ *
+ * @author Feross Aboukhadijeh <[email protected]> <http://feross.org>
+ * @license MIT
+ */
+
+var base64 = require('base64-js')
+var ieee754 = require('ieee754')
+var isArray = require('is-array')
+
+exports.Buffer = Buffer
+exports.SlowBuffer = Buffer
+exports.INSPECT_MAX_BYTES = 50
+Buffer.poolSize = 8192 // not used by this implementation
+
+var kMaxLength = 0x3fffffff
+
+/**
+ * If `Buffer.TYPED_ARRAY_SUPPORT`:
+ * === true Use Uint8Array implementation (fastest)
+ * === false Use Object implementation (most compatible, even IE6)
+ *
+ * Browsers that support typed arrays are IE 10+, Firefox 4+, Chrome 7+, Safari 5.1+,
+ * Opera 11.6+, iOS 4.2+.
+ *
+ * Note:
+ *
+ * - Implementation must support adding new properties to `Uint8Array` instances.
+ * Firefox 4-29 lacked support, fixed in Firefox 30+.
+ * See: https://bugzilla.mozilla.org/show_bug.cgi?id=695438.
+ *
+ * - Chrome 9-10 is missing the `TypedArray.prototype.subarray` function.
+ *
+ * - IE10 has a broken `TypedArray.prototype.subarray` function which returns arrays of
+ * incorrect length in some situations.
+ *
+ * We detect these buggy browsers and set `Buffer.TYPED_ARRAY_SUPPORT` to `false` so they will
+ * get the Object implementation, which is slower but will work correctly.
+ */
+Buffer.TYPED_ARRAY_SUPPORT = (function () {
+ try {
+ var buf = new ArrayBuffer(0)
+ var arr = new Uint8Array(buf)
+ arr.foo = function () { return 42 }
+ return 42 === arr.foo() && // typed array instances can be augmented
+ typeof arr.subarray === 'function' && // chrome 9-10 lack `subarray`
+ new Uint8Array(1).subarray(1, 1).byteLength === 0 // ie10 has broken `subarray`
+ } catch (e) {
+ return false
+ }
+})()
+
+/**
+ * Class: Buffer
+ * =============
+ *
+ * The Buffer constructor returns instances of `Uint8Array` that are augmented
+ * with function properties for all the node `Buffer` API functions. We use
+ * `Uint8Array` so that square bracket notation works as expected -- it returns
+ * a single octet.
+ *
+ * By augmenting the instances, we can avoid modifying the `Uint8Array`
+ * prototype.
+ */
+function Buffer (subject, encoding, noZero) {
+ if (!(this instanceof Buffer))
+ return new Buffer(subject, encoding, noZero)
+
+ var type = typeof subject
+
+ // Find the length
+ var length
+ if (type === 'number')
+ length = subject > 0 ? subject >>> 0 : 0
+ else if (type === 'string') {
+ if (encoding === 'base64')
+ subject = base64clean(subject)
+ length = Buffer.byteLength(subject, encoding)
+ } else if (type === 'object' && subject !== null) { // assume object is array-like
+ if (subject.type === 'Buffer' && isArray(subject.data))
+ subject = subject.data
+ length = +subject.length > 0 ? Math.floor(+subject.length) : 0
+ } else
+ throw new TypeError('must start with number, buffer, array or string')
+
+ if (this.length > kMaxLength)
+ throw new RangeError('Attempt to allocate Buffer larger than maximum ' +
+ 'size: 0x' + kMaxLength.toString(16) + ' bytes')
+
+ var buf
+ if (Buffer.TYPED_ARRAY_SUPPORT) {
+ // Preferred: Return an augmented `Uint8Array` instance for best performance
+ buf = Buffer._augment(new Uint8Array(length))
+ } else {
+ // Fallback: Return THIS instance of Buffer (created by `new`)
+ buf = this
+ buf.length = length
+ buf._isBuffer = true
+ }
+
+ var i
+ if (Buffer.TYPED_ARRAY_SUPPORT && typeof subject.byteLength === 'number') {
+ // Speed optimization -- use set if we're copying from a typed array
+ buf._set(subject)
+ } else if (isArrayish(subject)) {
+ // Treat array-ish objects as a byte array
+ if (Buffer.isBuffer(subject)) {
+ for (i = 0; i < length; i++)
+ buf[i] = subject.readUInt8(i)
+ } else {
+ for (i = 0; i < length; i++)
+ buf[i] = ((subject[i] % 256) + 256) % 256
+ }
+ } else if (type === 'string') {
+ buf.write(subject, 0, encoding)
+ } else if (type === 'number' && !Buffer.TYPED_ARRAY_SUPPORT && !noZero) {
+ for (i = 0; i < length; i++) {
+ buf[i] = 0
+ }
+ }
+
+ return buf
+}
+
+Buffer.isBuffer = function (b) {
+ return !!(b != null && b._isBuffer)
+}
+
+Buffer.compare = function (a, b) {
+ if (!Buffer.isBuffer(a) || !Buffer.isBuffer(b))
+ throw new TypeError('Arguments must be Buffers')
+
+ var x = a.length
+ var y = b.length
+ for (var i = 0, len = Math.min(x, y); i < len && a[i] === b[i]; i++) {}
+ if (i !== len) {
+ x = a[i]
+ y = b[i]
+ }
+ if (x < y) return -1
+ if (y < x) return 1
+ return 0
+}
+
+Buffer.isEncoding = function (encoding) {
+ switch (String(encoding).toLowerCase()) {
+ case 'hex':
+ case 'utf8':
+ case 'utf-8':
+ case 'ascii':
+ case 'binary':
+ case 'base64':
+ case 'raw':
+ case 'ucs2':
+ case 'ucs-2':
+ case 'utf16le':
+ case 'utf-16le':
+ return true
+ default:
+ return false
+ }
+}
+
+Buffer.concat = function (list, totalLength) {
+ if (!isArray(list)) throw new TypeError('Usage: Buffer.concat(list[, length])')
+
+ if (list.length === 0) {
+ return new Buffer(0)
+ } else if (list.length === 1) {
+ return list[0]
+ }
+
+ var i
+ if (totalLength === undefined) {
+ totalLength = 0
+ for (i = 0; i < list.length; i++) {
+ totalLength += list[i].length
+ }
+ }
+
+ var buf = new Buffer(totalLength)
+ var pos = 0
+ for (i = 0; i < list.length; i++) {
+ var item = list[i]
+ item.copy(buf, pos)
+ pos += item.length
+ }
+ return buf
+}
+
+Buffer.byteLength = function (str, encoding) {
+ var ret
+ str = str + ''
+ switch (encoding || 'utf8') {
+ case 'ascii':
+ case 'binary':
+ case 'raw':
+ ret = str.length
+ break
+ case 'ucs2':
+ case 'ucs-2':
+ case 'utf16le':
+ case 'utf-16le':
+ ret = str.length * 2
+ break
+ case 'hex':
+ ret = str.length >>> 1
+ break
+ case 'utf8':
+ case 'utf-8':
+ ret = utf8ToBytes(str).length
+ break
+ case 'base64':
+ ret = base64ToBytes(str).length
+ break
+ default:
+ ret = str.length
+ }
+ return ret
+}
+
+// pre-set for values that may exist in the future
+Buffer.prototype.length = undefined
+Buffer.prototype.parent = undefined
+
+// toString(encoding, start=0, end=buffer.length)
+Buffer.prototype.toString = function (encoding, start, end) {
+ var loweredCase = false
+
+ start = start >>> 0
+ end = end === undefined || end === Infinity ? this.length : end >>> 0
+
+ if (!encoding) encoding = 'utf8'
+ if (start < 0) start = 0
+ if (end > this.length) end = this.length
+ if (end <= start) return ''
+
+ while (true) {
+ switch (encoding) {
+ case 'hex':
+ return hexSlice(this, start, end)
+
+ case 'utf8':
+ case 'utf-8':
+ return utf8Slice(this, start, end)
+
+ case 'ascii':
+ return asciiSlice(this, start, end)
+
+ case 'binary':
+ return binarySlice(this, start, end)
+
+ case 'base64':
+ return base64Slice(this, start, end)
+
+ case 'ucs2':
+ case 'ucs-2':
+ case 'utf16le':
+ case 'utf-16le':
+ return utf16leSlice(this, start, end)
+
+ default:
+ if (loweredCase)
+ throw new TypeError('Unknown encoding: ' + encoding)
+ encoding = (encoding + '').toLowerCase()
+ loweredCase = true
+ }
+ }
+}
+
+Buffer.prototype.equals = function (b) {
+ if(!Buffer.isBuffer(b)) throw new TypeError('Argument must be a Buffer')
+ return Buffer.compare(this, b) === 0
+}
+
+Buffer.prototype.inspect = function () {
+ var str = ''
+ var max = exports.INSPECT_MAX_BYTES
+ if (this.length > 0) {
+ str = this.toString('hex', 0, max).match(/.{2}/g).join(' ')
+ if (this.length > max)
+ str += ' ... '
+ }
+ return '<Buffer ' + str + '>'
+}
+
+Buffer.prototype.compare = function (b) {
+ if (!Buffer.isBuffer(b)) throw new TypeError('Argument must be a Buffer')
+ return Buffer.compare(this, b)
+}
+
+// `get` will be removed in Node 0.13+
+Buffer.prototype.get = function (offset) {
+ console.log('.get() is deprecated. Access using array indexes instead.')
+ return this.readUInt8(offset)
+}
+
+// `set` will be removed in Node 0.13+
+Buffer.prototype.set = function (v, offset) {
+ console.log('.set() is deprecated. Access using array indexes instead.')
+ return this.writeUInt8(v, offset)
+}
+
+function hexWrite (buf, string, offset, length) {
+ offset = Number(offset) || 0
+ var remaining = buf.length - offset
+ if (!length) {
+ length = remaining
+ } else {
+ length = Number(length)
+ if (length > remaining) {
+ length = remaining
+ }
+ }
+
+ // must be an even number of digits
+ var strLen = string.length
+ if (strLen % 2 !== 0) throw new Error('Invalid hex string')
+
+ if (length > strLen / 2) {
+ length = strLen / 2
+ }
+ for (var i = 0; i < length; i++) {
+ var byte = parseInt(string.substr(i * 2, 2), 16)
+ if (isNaN(byte)) throw new Error('Invalid hex string')
+ buf[offset + i] = byte
+ }
+ return i
+}
+
+function utf8Write (buf, string, offset, length) {
+ var charsWritten = blitBuffer(utf8ToBytes(string), buf, offset, length)
+ return charsWritten
+}
+
+function asciiWrite (buf, string, offset, length) {
+ var charsWritten = blitBuffer(asciiToBytes(string), buf, offset, length)
+ return charsWritten
+}
+
+function binaryWrite (buf, string, offset, length) {
+ return asciiWrite(buf, string, offset, length)
+}
+
+function base64Write (buf, string, offset, length) {
+ var charsWritten = blitBuffer(base64ToBytes(string), buf, offset, length)
+ return charsWritten
+}
+
+function utf16leWrite (buf, string, offset, length) {
+ var charsWritten = blitBuffer(utf16leToBytes(string), buf, offset, length)
+ return charsWritten
+}
+
+Buffer.prototype.write = function (string, offset, length, encoding) {
+ // Support both (string, offset, length, encoding)
+ // and the legacy (string, encoding, offset, length)
+ if (isFinite(offset)) {
+ if (!isFinite(length)) {
+ encoding = length
+ length = undefined
+ }
+ } else { // legacy
+ var swap = encoding
+ encoding = offset
+ offset = length
+ length = swap
+ }
+
+ offset = Number(offset) || 0
+ var remaining = this.length - offset
+ if (!length) {
+ length = remaining
+ } else {
+ length = Number(length)
+ if (length > remaining) {
+ length = remaining
+ }
+ }
+ encoding = String(encoding || 'utf8').toLowerCase()
+
+ var ret
+ switch (encoding) {
+ case 'hex':
+ ret = hexWrite(this, string, offset, length)
+ break
+ case 'utf8':
+ case 'utf-8':
+ ret = utf8Write(this, string, offset, length)
+ break
+ case 'ascii':
+ ret = asciiWrite(this, string, offset, length)
+ break
+ case 'binary':
+ ret = binaryWrite(this, string, offset, length)
+ break
+ case 'base64':
+ ret = base64Write(this, string, offset, length)
+ break
+ case 'ucs2':
+ case 'ucs-2':
+ case 'utf16le':
+ case 'utf-16le':
+ ret = utf16leWrite(this, string, offset, length)
+ break
+ default:
+ throw new TypeError('Unknown encoding: ' + encoding)
+ }
+ return ret
+}
+
+Buffer.prototype.toJSON = function () {
+ return {
+ type: 'Buffer',
+ data: Array.prototype.slice.call(this._arr || this, 0)
+ }
+}
+
+function base64Slice (buf, start, end) {
+ if (start === 0 && end === buf.length) {
+ return base64.fromByteArray(buf)
+ } else {
+ return base64.fromByteArray(buf.slice(start, end))
+ }
+}
+
+function utf8Slice (buf, start, end) {
+ var res = ''
+ var tmp = ''
+ end = Math.min(buf.length, end)
+
+ for (var i = start; i < end; i++) {
+ if (buf[i] <= 0x7F) {
+ res += decodeUtf8Char(tmp) + String.fromCharCode(buf[i])
+ tmp = ''
+ } else {
+ tmp += '%' + buf[i].toString(16)
+ }
+ }
+
+ return res + decodeUtf8Char(tmp)
+}
+
+function asciiSlice (buf, start, end) {
+ var ret = ''
+ end = Math.min(buf.length, end)
+
+ for (var i = start; i < end; i++) {
+ ret += String.fromCharCode(buf[i])
+ }
+ return ret
+}
+
+function binarySlice (buf, start, end) {
+ return asciiSlice(buf, start, end)
+}
+
+function hexSlice (buf, start, end) {
+ var len = buf.length
+
+ if (!start || start < 0) start = 0
+ if (!end || end < 0 || end > len) end = len
+
+ var out = ''
+ for (var i = start; i < end; i++) {
+ out += toHex(buf[i])
+ }
+ return out
+}
+
+function utf16leSlice (buf, start, end) {
+ var bytes = buf.slice(start, end)
+ var res = ''
+ for (var i = 0; i < bytes.length; i += 2) {
+ res += String.fromCharCode(bytes[i] + bytes[i + 1] * 256)
+ }
+ return res
+}
+
+Buffer.prototype.slice = function (start, end) {
+ var len = this.length
+ start = ~~start
+ end = end === undefined ? len : ~~end
+
+ if (start < 0) {
+ start += len;
+ if (start < 0)
+ start = 0
+ } else if (start > len) {
+ start = len
+ }
+
+ if (end < 0) {
+ end += len
+ if (end < 0)
+ end = 0
+ } else if (end > len) {
+ end = len
+ }
+
+ if (end < start)
+ end = start
+
+ if (Buffer.TYPED_ARRAY_SUPPORT) {
+ return Buffer._augment(this.subarray(start, end))
+ } else {
+ var sliceLen = end - start
+ var newBuf = new Buffer(sliceLen, undefined, true)
+ for (var i = 0; i < sliceLen; i++) {
+ newBuf[i] = this[i + start]
+ }
+ return newBuf
+ }
+}
+
+/*
+ * Need to make sure that buffer isn't trying to write out of bounds.
+ */
+function checkOffset (offset, ext, length) {
+ if ((offset % 1) !== 0 || offset < 0)
+ throw new RangeError('offset is not uint')
+ if (offset + ext > length)
+ throw new RangeError('Trying to access beyond buffer length')
+}
+
+Buffer.prototype.readUInt8 = function (offset, noAssert) {
+ if (!noAssert)
+ checkOffset(offset, 1, this.length)
+ return this[offset]
+}
+
+Buffer.prototype.readUInt16LE = function (offset, noAssert) {
+ if (!noAssert)
+ checkOffset(offset, 2, this.length)
+ return this[offset] | (this[offset + 1] << 8)
+}
+
+Buffer.prototype.readUInt16BE = function (offset, noAssert) {
+ if (!noAssert)
+ checkOffset(offset, 2, this.length)
+ return (this[offset] << 8) | this[offset + 1]
+}
+
+Buffer.prototype.readUInt32LE = function (offset, noAssert) {
+ if (!noAssert)
+ checkOffset(offset, 4, this.length)
+
+ return ((this[offset]) |
+ (this[offset + 1] << 8) |
+ (this[offset + 2] << 16)) +
+ (this[offset + 3] * 0x1000000)
+}
+
+Buffer.prototype.readUInt32BE = function (offset, noAssert) {
+ if (!noAssert)
+ checkOffset(offset, 4, this.length)
+
+ return (this[offset] * 0x1000000) +
+ ((this[offset + 1] << 16) |
+ (this[offset + 2] << 8) |
+ this[offset + 3])
+}
+
+Buffer.prototype.readInt8 = function (offset, noAssert) {
+ if (!noAssert)
+ checkOffset(offset, 1, this.length)
+ if (!(this[offset] & 0x80))
+ return (this[offset])
+ return ((0xff - this[offset] + 1) * -1)
+}
+
+Buffer.prototype.readInt16LE = function (offset, noAssert) {
+ if (!noAssert)
+ checkOffset(offset, 2, this.length)
+ var val = this[offset] | (this[offset + 1] << 8)
+ return (val & 0x8000) ? val | 0xFFFF0000 : val
+}
+
+Buffer.prototype.readInt16BE = function (offset, noAssert) {
+ if (!noAssert)
+ checkOffset(offset, 2, this.length)
+ var val = this[offset + 1] | (this[offset] << 8)
+ return (val & 0x8000) ? val | 0xFFFF0000 : val
+}
+
+Buffer.prototype.readInt32LE = function (offset, noAssert) {
+ if (!noAssert)
+ checkOffset(offset, 4, this.length)
+
+ return (this[offset]) |
+ (this[offset + 1] << 8) |
+ (this[offset + 2] << 16) |
+ (this[offset + 3] << 24)
+}
+
+Buffer.prototype.readInt32BE = function (offset, noAssert) {
+ if (!noAssert)
+ checkOffset(offset, 4, this.length)
+
+ return (this[offset] << 24) |
+ (this[offset + 1] << 16) |
+ (this[offset + 2] << 8) |
+ (this[offset + 3])
+}
+
+Buffer.prototype.readFloatLE = function (offset, noAssert) {
+ if (!noAssert)
+ checkOffset(offset, 4, this.length)
+ return ieee754.read(this, offset, true, 23, 4)
+}
+
+Buffer.prototype.readFloatBE = function (offset, noAssert) {
+ if (!noAssert)
+ checkOffset(offset, 4, this.length)
+ return ieee754.read(this, offset, false, 23, 4)
+}
+
+Buffer.prototype.readDoubleLE = function (offset, noAssert) {
+ if (!noAssert)
+ checkOffset(offset, 8, this.length)
+ return ieee754.read(this, offset, true, 52, 8)
+}
+
+Buffer.prototype.readDoubleBE = function (offset, noAssert) {
+ if (!noAssert)
+ checkOffset(offset, 8, this.length)
+ return ieee754.read(this, offset, false, 52, 8)
+}
+
+function checkInt (buf, value, offset, ext, max, min) {
+ if (!Buffer.isBuffer(buf)) throw new TypeError('buffer must be a Buffer instance')
+ if (value > max || value < min) throw new TypeError('value is out of bounds')
+ if (offset + ext > buf.length) throw new TypeError('index out of range')
+}
+
+Buffer.prototype.writeUInt8 = function (value, offset, noAssert) {
+ value = +value
+ offset = offset >>> 0
+ if (!noAssert)
+ checkInt(this, value, offset, 1, 0xff, 0)
+ if (!Buffer.TYPED_ARRAY_SUPPORT) value = Math.floor(value)
+ this[offset] = value
+ return offset + 1
+}
+
+function objectWriteUInt16 (buf, value, offset, littleEndian) {
+ if (value < 0) value = 0xffff + value + 1
+ for (var i = 0, j = Math.min(buf.length - offset, 2); i < j; i++) {
+ buf[offset + i] = (value & (0xff << (8 * (littleEndian ? i : 1 - i)))) >>>
+ (littleEndian ? i : 1 - i) * 8
+ }
+}
+
+Buffer.prototype.writeUInt16LE = function (value, offset, noAssert) {
+ value = +value
+ offset = offset >>> 0
+ if (!noAssert)
+ checkInt(this, value, offset, 2, 0xffff, 0)
+ if (Buffer.TYPED_ARRAY_SUPPORT) {
+ this[offset] = value
+ this[offset + 1] = (value >>> 8)
+ } else objectWriteUInt16(this, value, offset, true)
+ return offset + 2
+}
+
+Buffer.prototype.writeUInt16BE = function (value, offset, noAssert) {
+ value = +value
+ offset = offset >>> 0
+ if (!noAssert)
+ checkInt(this, value, offset, 2, 0xffff, 0)
+ if (Buffer.TYPED_ARRAY_SUPPORT) {
+ this[offset] = (value >>> 8)
+ this[offset + 1] = value
+ } else objectWriteUInt16(this, value, offset, false)
+ return offset + 2
+}
+
+function objectWriteUInt32 (buf, value, offset, littleEndian) {
+ if (value < 0) value = 0xffffffff + value + 1
+ for (var i = 0, j = Math.min(buf.length - offset, 4); i < j; i++) {
+ buf[offset + i] = (value >>> (littleEndian ? i : 3 - i) * 8) & 0xff
+ }
+}
+
+Buffer.prototype.writeUInt32LE = function (value, offset, noAssert) {
+ value = +value
+ offset = offset >>> 0
+ if (!noAssert)
+ checkInt(this, value, offset, 4, 0xffffffff, 0)
+ if (Buffer.TYPED_ARRAY_SUPPORT) {
+ this[offset + 3] = (value >>> 24)
+ this[offset + 2] = (value >>> 16)
+ this[offset + 1] = (value >>> 8)
+ this[offset] = value
+ } else objectWriteUInt32(this, value, offset, true)
+ return offset + 4
+}
+
+Buffer.prototype.writeUInt32BE = function (value, offset, noAssert) {
+ value = +value
+ offset = offset >>> 0
+ if (!noAssert)
+ checkInt(this, value, offset, 4, 0xffffffff, 0)
+ if (Buffer.TYPED_ARRAY_SUPPORT) {
+ this[offset] = (value >>> 24)
+ this[offset + 1] = (value >>> 16)
+ this[offset + 2] = (value >>> 8)
+ this[offset + 3] = value
+ } else objectWriteUInt32(this, value, offset, false)
+ return offset + 4
+}
+
+Buffer.prototype.writeInt8 = function (value, offset, noAssert) {
+ value = +value
+ offset = offset >>> 0
+ if (!noAssert)
+ checkInt(this, value, offset, 1, 0x7f, -0x80)
+ if (!Buffer.TYPED_ARRAY_SUPPORT) value = Math.floor(value)
+ if (value < 0) value = 0xff + value + 1
+ this[offset] = value
+ return offset + 1
+}
+
+Buffer.prototype.writeInt16LE = function (value, offset, noAssert) {
+ value = +value
+ offset = offset >>> 0
+ if (!noAssert)
+ checkInt(this, value, offset, 2, 0x7fff, -0x8000)
+ if (Buffer.TYPED_ARRAY_SUPPORT) {
+ this[offset] = value
+ this[offset + 1] = (value >>> 8)
+ } else objectWriteUInt16(this, value, offset, true)
+ return offset + 2
+}
+
+Buffer.prototype.writeInt16BE = function (value, offset, noAssert) {
+ value = +value
+ offset = offset >>> 0
+ if (!noAssert)
+ checkInt(this, value, offset, 2, 0x7fff, -0x8000)
+ if (Buffer.TYPED_ARRAY_SUPPORT) {
+ this[offset] = (value >>> 8)
+ this[offset + 1] = value
+ } else objectWriteUInt16(this, value, offset, false)
+ return offset + 2
+}
+
+Buffer.prototype.writeInt32LE = function (value, offset, noAssert) {
+ value = +value
+ offset = offset >>> 0
+ if (!noAssert)
+ checkInt(this, value, offset, 4, 0x7fffffff, -0x80000000)
+ if (Buffer.TYPED_ARRAY_SUPPORT) {
+ this[offset] = value
+ this[offset + 1] = (value >>> 8)
+ this[offset + 2] = (value >>> 16)
+ this[offset + 3] = (value >>> 24)
+ } else objectWriteUInt32(this, value, offset, true)
+ return offset + 4
+}
+
+Buffer.prototype.writeInt32BE = function (value, offset, noAssert) {
+ value = +value
+ offset = offset >>> 0
+ if (!noAssert)
+ checkInt(this, value, offset, 4, 0x7fffffff, -0x80000000)
+ if (value < 0) value = 0xffffffff + value + 1
+ if (Buffer.TYPED_ARRAY_SUPPORT) {
+ this[offset] = (value >>> 24)
+ this[offset + 1] = (value >>> 16)
+ this[offset + 2] = (value >>> 8)
+ this[offset + 3] = value
+ } else objectWriteUInt32(this, value, offset, false)
+ return offset + 4
+}
+
+function checkIEEE754 (buf, value, offset, ext, max, min) {
+ if (value > max || value < min) throw new TypeError('value is out of bounds')
+ if (offset + ext > buf.length) throw new TypeError('index out of range')
+}
+
+function writeFloat (buf, value, offset, littleEndian, noAssert) {
+ if (!noAssert)
+ checkIEEE754(buf, value, offset, 4, 3.4028234663852886e+38, -3.4028234663852886e+38)
+ ieee754.write(buf, value, offset, littleEndian, 23, 4)
+ return offset + 4
+}
+
+Buffer.prototype.writeFloatLE = function (value, offset, noAssert) {
+ return writeFloat(this, value, offset, true, noAssert)
+}
+
+Buffer.prototype.writeFloatBE = function (value, offset, noAssert) {
+ return writeFloat(this, value, offset, false, noAssert)
+}
+
+function writeDouble (buf, value, offset, littleEndian, noAssert) {
+ if (!noAssert)
+ checkIEEE754(buf, value, offset, 8, 1.7976931348623157E+308, -1.7976931348623157E+308)
+ ieee754.write(buf, value, offset, littleEndian, 52, 8)
+ return offset + 8
+}
+
+Buffer.prototype.writeDoubleLE = function (value, offset, noAssert) {
+ return writeDouble(this, value, offset, true, noAssert)
+}
+
+Buffer.prototype.writeDoubleBE = function (value, offset, noAssert) {
+ return writeDouble(this, value, offset, false, noAssert)
+}
+
+// copy(targetBuffer, targetStart=0, sourceStart=0, sourceEnd=buffer.length)
+Buffer.prototype.copy = function (target, target_start, start, end) {
+ var source = this
+
+ if (!start) start = 0
+ if (!end && end !== 0) end = this.length
+ if (!target_start) target_start = 0
+
+ // Copy 0 bytes; we're done
+ if (end === start) return
+ if (target.length === 0 || source.length === 0) return
+
+ // Fatal error conditions
+ if (end < start) throw new TypeError('sourceEnd < sourceStart')
+ if (target_start < 0 || target_start >= target.length)
+ throw new TypeError('targetStart out of bounds')
+ if (start < 0 || start >= source.length) throw new TypeError('sourceStart out of bounds')
+ if (end < 0 || end > source.length) throw new TypeError('sourceEnd out of bounds')
+
+ // Are we oob?
+ if (end > this.length)
+ end = this.length
+ if (target.length - target_start < end - start)
+ end = target.length - target_start + start
+
+ var len = end - start
+
+ if (len < 1000 || !Buffer.TYPED_ARRAY_SUPPORT) {
+ for (var i = 0; i < len; i++) {
+ target[i + target_start] = this[i + start]
+ }
+ } else {
+ target._set(this.subarray(start, start + len), target_start)
+ }
+}
+
+// fill(value, start=0, end=buffer.length)
+Buffer.prototype.fill = function (value, start, end) {
+ if (!value) value = 0
+ if (!start) start = 0
+ if (!end) end = this.length
+
+ if (end < start) throw new TypeError('end < start')
+
+ // Fill 0 bytes; we're done
+ if (end === start) return
+ if (this.length === 0) return
+
+ if (start < 0 || start >= this.length) throw new TypeError('start out of bounds')
+ if (end < 0 || end > this.length) throw new TypeError('end out of bounds')
+
+ var i
+ if (typeof value === 'number') {
+ for (i = start; i < end; i++) {
+ this[i] = value
+ }
+ } else {
+ var bytes = utf8ToBytes(value.toString())
+ var len = bytes.length
+ for (i = start; i < end; i++) {
+ this[i] = bytes[i % len]
+ }
+ }
+
+ return this
+}
+
+/**
+ * Creates a new `ArrayBuffer` with the *copied* memory of the buffer instance.
+ * Added in Node 0.12. Only available in browsers that support ArrayBuffer.
+ */
+Buffer.prototype.toArrayBuffer = function () {
+ if (typeof Uint8Array !== 'undefined') {
+ if (Buffer.TYPED_ARRAY_SUPPORT) {
+ return (new Buffer(this)).buffer
+ } else {
+ var buf = new Uint8Array(this.length)
+ for (var i = 0, len = buf.length; i < len; i += 1) {
+ buf[i] = this[i]
+ }
+ return buf.buffer
+ }
+ } else {
+ throw new TypeError('Buffer.toArrayBuffer not supported in this browser')
+ }
+}
+
+// HELPER FUNCTIONS
+// ================
+
+var BP = Buffer.prototype
+
+/**
+ * Augment a Uint8Array *instance* (not the Uint8Array class!) with Buffer methods
+ */
+Buffer._augment = function (arr) {
+ arr.constructor = Buffer
+ arr._isBuffer = true
+
+ // save reference to original Uint8Array get/set methods before overwriting
+ arr._get = arr.get
+ arr._set = arr.set
+
+ // deprecated, will be removed in node 0.13+
+ arr.get = BP.get
+ arr.set = BP.set
+
+ arr.write = BP.write
+ arr.toString = BP.toString
+ arr.toLocaleString = BP.toString
+ arr.toJSON = BP.toJSON
+ arr.equals = BP.equals
+ arr.compare = BP.compare
+ arr.copy = BP.copy
+ arr.slice = BP.slice
+ arr.readUInt8 = BP.readUInt8
+ arr.readUInt16LE = BP.readUInt16LE
+ arr.readUInt16BE = BP.readUInt16BE
+ arr.readUInt32LE = BP.readUInt32LE
+ arr.readUInt32BE = BP.readUInt32BE
+ arr.readInt8 = BP.readInt8
+ arr.readInt16LE = BP.readInt16LE
+ arr.readInt16BE = BP.readInt16BE
+ arr.readInt32LE = BP.readInt32LE
+ arr.readInt32BE = BP.readInt32BE
+ arr.readFloatLE = BP.readFloatLE
+ arr.readFloatBE = BP.readFloatBE
+ arr.readDoubleLE = BP.readDoubleLE
+ arr.readDoubleBE = BP.readDoubleBE
+ arr.writeUInt8 = BP.writeUInt8
+ arr.writeUInt16LE = BP.writeUInt16LE
+ arr.writeUInt16BE = BP.writeUInt16BE
+ arr.writeUInt32LE = BP.writeUInt32LE
+ arr.writeUInt32BE = BP.writeUInt32BE
+ arr.writeInt8 = BP.writeInt8
+ arr.writeInt16LE = BP.writeInt16LE
+ arr.writeInt16BE = BP.writeInt16BE
+ arr.writeInt32LE = BP.writeInt32LE
+ arr.writeInt32BE = BP.writeInt32BE
+ arr.writeFloatLE = BP.writeFloatLE
+ arr.writeFloatBE = BP.writeFloatBE
+ arr.writeDoubleLE = BP.writeDoubleLE
+ arr.writeDoubleBE = BP.writeDoubleBE
+ arr.fill = BP.fill
+ arr.inspect = BP.inspect
+ arr.toArrayBuffer = BP.toArrayBuffer
+
+ return arr
+}
+
+var INVALID_BASE64_RE = /[^+\/0-9A-z]/g
+
+function base64clean (str) {
+ // Node strips out invalid characters like \n and \t from the string, base64-js does not
+ str = stringtrim(str).replace(INVALID_BASE64_RE, '')
+ // Node allows for non-padded base64 strings (missing trailing ===), base64-js does not
+ while (str.length % 4 !== 0) {
+ str = str + '='
+ }
+ return str
+}
+
+function stringtrim (str) {
+ if (str.trim) return str.trim()
+ return str.replace(/^\s+|\s+$/g, '')
+}
+
+function isArrayish (subject) {
+ return isArray(subject) || Buffer.isBuffer(subject) ||
+ subject && typeof subject === 'object' &&
+ typeof subject.length === 'number'
+}
+
+function toHex (n) {
+ if (n < 16) return '0' + n.toString(16)
+ return n.toString(16)
+}
+
+function utf8ToBytes (str) {
+ var byteArray = []
+ for (var i = 0; i < str.length; i++) {
+ var b = str.charCodeAt(i)
+ if (b <= 0x7F) {
+ byteArray.push(b)
+ } else {
+ var start = i
+ if (b >= 0xD800 && b <= 0xDFFF) i++
+ var h = encodeURIComponent(str.slice(start, i+1)).substr(1).split('%')
+ for (var j = 0; j < h.length; j++) {
+ byteArray.push(parseInt(h[j], 16))
+ }
+ }
+ }
+ return byteArray
+}
+
+function asciiToBytes (str) {
+ var byteArray = []
+ for (var i = 0; i < str.length; i++) {
+ // Node's code seems to be doing this and not & 0x7F..
+ byteArray.push(str.charCodeAt(i) & 0xFF)
+ }
+ return byteArray
+}
+
+function utf16leToBytes (str) {
+ var c, hi, lo
+ var byteArray = []
+ for (var i = 0; i < str.length; i++) {
+ c = str.charCodeAt(i)
+ hi = c >> 8
+ lo = c % 256
+ byteArray.push(lo)
+ byteArray.push(hi)
+ }
+
+ return byteArray
+}
+
+function base64ToBytes (str) {
+ return base64.toByteArray(str)
+}
+
+function blitBuffer (src, dst, offset, length) {
+ for (var i = 0; i < length; i++) {
+ if ((i + offset >= dst.length) || (i >= src.length))
+ break
+ dst[i + offset] = src[i]
+ }
+ return i
+}
+
+function decodeUtf8Char (str) {
+ try {
+ return decodeURIComponent(str)
+ } catch (err) {
+ return String.fromCharCode(0xFFFD) // UTF 8 invalid char
+ }
+}
+
+},{"base64-js":4,"ieee754":5,"is-array":6}],4:[function(require,module,exports){
+var lookup = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/';
+
+;(function (exports) {
+ 'use strict';
+
+ var Arr = (typeof Uint8Array !== 'undefined')
+ ? Uint8Array
+ : Array
+
+ var PLUS = '+'.charCodeAt(0)
+ var SLASH = '/'.charCodeAt(0)
+ var NUMBER = '0'.charCodeAt(0)
+ var LOWER = 'a'.charCodeAt(0)
+ var UPPER = 'A'.charCodeAt(0)
+
+ function decode (elt) {
+ var code = elt.charCodeAt(0)
+ if (code === PLUS)
+ return 62 // '+'
+ if (code === SLASH)
+ return 63 // '/'
+ if (code < NUMBER)
+ return -1 //no match
+ if (code < NUMBER + 10)
+ return code - NUMBER + 26 + 26
+ if (code < UPPER + 26)
+ return code - UPPER
+ if (code < LOWER + 26)
+ return code - LOWER + 26
+ }
+
+ function b64ToByteArray (b64) {
+ var i, j, l, tmp, placeHolders, arr
+
+ if (b64.length % 4 > 0) {
+ throw new Error('Invalid string. Length must be a multiple of 4')
+ }
+
+ // the number of equal signs (place holders)
+ // if there are two placeholders, than the two characters before it
+ // represent one byte
+ // if there is only one, then the three characters before it represent 2 bytes
+ // this is just a cheap hack to not do indexOf twice
+ var len = b64.length
+ placeHolders = '=' === b64.charAt(len - 2) ? 2 : '=' === b64.charAt(len - 1) ? 1 : 0
+
+ // base64 is 4/3 + up to two characters of the original data
+ arr = new Arr(b64.length * 3 / 4 - placeHolders)
+
+ // if there are placeholders, only get up to the last complete 4 chars
+ l = placeHolders > 0 ? b64.length - 4 : b64.length
+
+ var L = 0
+
+ function push (v) {
+ arr[L++] = v
+ }
+
+ for (i = 0, j = 0; i < l; i += 4, j += 3) {
+ tmp = (decode(b64.charAt(i)) << 18) | (decode(b64.charAt(i + 1)) << 12) | (decode(b64.charAt(i + 2)) << 6) | decode(b64.charAt(i + 3))
+ push((tmp & 0xFF0000) >> 16)
+ push((tmp & 0xFF00) >> 8)
+ push(tmp & 0xFF)
+ }
+
+ if (placeHolders === 2) {
+ tmp = (decode(b64.charAt(i)) << 2) | (decode(b64.charAt(i + 1)) >> 4)
+ push(tmp & 0xFF)
+ } else if (placeHolders === 1) {
+ tmp = (decode(b64.charAt(i)) << 10) | (decode(b64.charAt(i + 1)) << 4) | (decode(b64.charAt(i + 2)) >> 2)
+ push((tmp >> 8) & 0xFF)
+ push(tmp & 0xFF)
+ }
+
+ return arr
+ }
+
+ function uint8ToBase64 (uint8) {
+ var i,
+ extraBytes = uint8.length % 3, // if we have 1 byte left, pad 2 bytes
+ output = "",
+ temp, length
+
+ function encode (num) {
+ return lookup.charAt(num)
+ }
+
+ function tripletToBase64 (num) {
+ return encode(num >> 18 & 0x3F) + encode(num >> 12 & 0x3F) + encode(num >> 6 & 0x3F) + encode(num & 0x3F)
+ }
+
+ // go through the array every three bytes, we'll deal with trailing stuff later
+ for (i = 0, length = uint8.length - extraBytes; i < length; i += 3) {
+ temp = (uint8[i] << 16) + (uint8[i + 1] << 8) + (uint8[i + 2])
+ output += tripletToBase64(temp)
+ }
+
+ // pad the end with zeros, but make sure to not forget the extra bytes
+ switch (extraBytes) {
+ case 1:
+ temp = uint8[uint8.length - 1]
+ output += encode(temp >> 2)
+ output += encode((temp << 4) & 0x3F)
+ output += '=='
+ break
+ case 2:
+ temp = (uint8[uint8.length - 2] << 8) + (uint8[uint8.length - 1])
+ output += encode(temp >> 10)
+ output += encode((temp >> 4) & 0x3F)
+ output += encode((temp << 2) & 0x3F)
+ output += '='
+ break
+ }
+
+ return output
+ }
+
+ exports.toByteArray = b64ToByteArray
+ exports.fromByteArray = uint8ToBase64
+}(typeof exports === 'undefined' ? (this.base64js = {}) : exports))
+
+},{}],5:[function(require,module,exports){
+exports.read = function(buffer, offset, isLE, mLen, nBytes) {
+ var e, m,
+ eLen = nBytes * 8 - mLen - 1,
+ eMax = (1 << eLen) - 1,
+ eBias = eMax >> 1,
+ nBits = -7,
+ i = isLE ? (nBytes - 1) : 0,
+ d = isLE ? -1 : 1,
+ s = buffer[offset + i];
+
+ i += d;
+
+ e = s & ((1 << (-nBits)) - 1);
+ s >>= (-nBits);
+ nBits += eLen;
+ for (; nBits > 0; e = e * 256 + buffer[offset + i], i += d, nBits -= 8);
+
+ m = e & ((1 << (-nBits)) - 1);
+ e >>= (-nBits);
+ nBits += mLen;
+ for (; nBits > 0; m = m * 256 + buffer[offset + i], i += d, nBits -= 8);
+
+ if (e === 0) {
+ e = 1 - eBias;
+ } else if (e === eMax) {
+ return m ? NaN : ((s ? -1 : 1) * Infinity);
+ } else {
+ m = m + Math.pow(2, mLen);
+ e = e - eBias;
+ }
+ return (s ? -1 : 1) * m * Math.pow(2, e - mLen);
+};
+
+exports.write = function(buffer, value, offset, isLE, mLen, nBytes) {
+ var e, m, c,
+ eLen = nBytes * 8 - mLen - 1,
+ eMax = (1 << eLen) - 1,
+ eBias = eMax >> 1,
+ rt = (mLen === 23 ? Math.pow(2, -24) - Math.pow(2, -77) : 0),
+ i = isLE ? 0 : (nBytes - 1),
+ d = isLE ? 1 : -1,
+ s = value < 0 || (value === 0 && 1 / value < 0) ? 1 : 0;
+
+ value = Math.abs(value);
+
+ if (isNaN(value) || value === Infinity) {
+ m = isNaN(value) ? 1 : 0;
+ e = eMax;
+ } else {
+ e = Math.floor(Math.log(value) / Math.LN2);
+ if (value * (c = Math.pow(2, -e)) < 1) {
+ e--;
+ c *= 2;
+ }
+ if (e + eBias >= 1) {
+ value += rt / c;
+ } else {
+ value += rt * Math.pow(2, 1 - eBias);
+ }
+ if (value * c >= 2) {
+ e++;
+ c /= 2;
+ }
+
+ if (e + eBias >= eMax) {
+ m = 0;
+ e = eMax;
+ } else if (e + eBias >= 1) {
+ m = (value * c - 1) * Math.pow(2, mLen);
+ e = e + eBias;
+ } else {
+ m = value * Math.pow(2, eBias - 1) * Math.pow(2, mLen);
+ e = 0;
+ }
+ }
+
+ for (; mLen >= 8; buffer[offset + i] = m & 0xff, i += d, m /= 256, mLen -= 8);
+
+ e = (e << mLen) | m;
+ eLen += mLen;
+ for (; eLen > 0; buffer[offset + i] = e & 0xff, i += d, e /= 256, eLen -= 8);
+
+ buffer[offset + i - d] |= s * 128;
+};
+
+},{}],6:[function(require,module,exports){
+
+/**
+ * isArray
+ */
+
+var isArray = Array.isArray;
+
+/**
+ * toString
+ */
+
+var str = Object.prototype.toString;
+
+/**
+ * Whether or not the given `val`
+ * is an array.
+ *
+ * example:
+ *
+ * isArray([]);
+ * // > true
+ * isArray(arguments);
+ * // > false
+ * isArray('');
+ * // > false
+ *
+ * @param {mixed} val
+ * @return {bool}
+ */
+
+module.exports = isArray || function (val) {
+ return !! val && '[object Array]' == str.call(val);
+};
+
+},{}],7:[function(require,module,exports){
+// Copyright Joyent, Inc. and other Node contributors.
+//
+// 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.
+
+function EventEmitter() {
+ this._events = this._events || {};
+ this._maxListeners = this._maxListeners || undefined;
+}
+module.exports = EventEmitter;
+
+// Backwards-compat with node 0.10.x
+EventEmitter.EventEmitter = EventEmitter;
+
+EventEmitter.prototype._events = undefined;
+EventEmitter.prototype._maxListeners = undefined;
+
+// By default EventEmitters will print a warning if more than 10 listeners are
+// added to it. This is a useful default which helps finding memory leaks.
+EventEmitter.defaultMaxListeners = 10;
+
+// Obviously not all Emitters should be limited to 10. This function allows
+// that to be increased. Set to zero for unlimited.
+EventEmitter.prototype.setMaxListeners = function(n) {
+ if (!isNumber(n) || n < 0 || isNaN(n))
+ throw TypeError('n must be a positive number');
+ this._maxListeners = n;
+ return this;
+};
+
+EventEmitter.prototype.emit = function(type) {
+ var er, handler, len, args, i, listeners;
+
+ if (!this._events)
+ this._events = {};
+
+ // If there is no 'error' event listener then throw.
+ if (type === 'error') {
+ if (!this._events.error ||
+ (isObject(this._events.error) && !this._events.error.length)) {
+ er = arguments[1];
+ if (er instanceof Error) {
+ throw er; // Unhandled 'error' event
+ }
+ throw TypeError('Uncaught, unspecified "error" event.');
+ }
+ }
+
+ handler = this._events[type];
+
+ if (isUndefined(handler))
+ return false;
+
+ if (isFunction(handler)) {
+ switch (arguments.length) {
+ // fast cases
+ case 1:
+ handler.call(this);
+ break;
+ case 2:
+ handler.call(this, arguments[1]);
+ break;
+ case 3:
+ handler.call(this, arguments[1], arguments[2]);
+ break;
+ // slower
+ default:
+ len = arguments.length;
+ args = new Array(len - 1);
+ for (i = 1; i < len; i++)
+ args[i - 1] = arguments[i];
+ handler.apply(this, args);
+ }
+ } else if (isObject(handler)) {
+ len = arguments.length;
+ args = new Array(len - 1);
+ for (i = 1; i < len; i++)
+ args[i - 1] = arguments[i];
+
+ listeners = handler.slice();
+ len = listeners.length;
+ for (i = 0; i < len; i++)
+ listeners[i].apply(this, args);
+ }
+
+ return true;
+};
+
+EventEmitter.prototype.addListener = function(type, listener) {
+ var m;
+
+ if (!isFunction(listener))
+ throw TypeError('listener must be a function');
+
+ if (!this._events)
+ this._events = {};
+
+ // To avoid recursion in the case that type === "newListener"! Before
+ // adding it to the listeners, first emit "newListener".
+ if (this._events.newListener)
+ this.emit('newListener', type,
+ isFunction(listener.listener) ?
+ listener.listener : listener);
+
+ if (!this._events[type])
+ // Optimize the case of one listener. Don't need the extra array object.
+ this._events[type] = listener;
+ else if (isObject(this._events[type]))
+ // If we've already got an array, just append.
+ this._events[type].push(listener);
+ else
+ // Adding the second element, need to change to array.
+ this._events[type] = [this._events[type], listener];
+
+ // Check for listener leak
+ if (isObject(this._events[type]) && !this._events[type].warned) {
+ var m;
+ if (!isUndefined(this._maxListeners)) {
+ m = this._maxListeners;
+ } else {
+ m = EventEmitter.defaultMaxListeners;
+ }
+
+ if (m && m > 0 && this._events[type].length > m) {
+ this._events[type].warned = true;
+ console.error('(node) warning: possible EventEmitter memory ' +
+ 'leak detected. %d listeners added. ' +
+ 'Use emitter.setMaxListeners() to increase limit.',
+ this._events[type].length);
+ if (typeof console.trace === 'function') {
+ // not supported in IE 10
+ console.trace();
+ }
+ }
+ }
+
+ return this;
+};
+
+EventEmitter.prototype.on = EventEmitter.prototype.addListener;
+
+EventEmitter.prototype.once = function(type, listener) {
+ if (!isFunction(listener))
+ throw TypeError('listener must be a function');
+
+ var fired = false;
+
+ function g() {
+ this.removeListener(type, g);
+
+ if (!fired) {
+ fired = true;
+ listener.apply(this, arguments);
+ }
+ }
+
+ g.listener = listener;
+ this.on(type, g);
+
+ return this;
+};
+
+// emits a 'removeListener' event iff the listener was removed
+EventEmitter.prototype.removeListener = function(type, listener) {
+ var list, position, length, i;
+
+ if (!isFunction(listener))
+ throw TypeError('listener must be a function');
+
+ if (!this._events || !this._events[type])
+ return this;
+
+ list = this._events[type];
+ length = list.length;
+ position = -1;
+
+ if (list === listener ||
+ (isFunction(list.listener) && list.listener === listener)) {
+ delete this._events[type];
+ if (this._events.removeListener)
+ this.emit('removeListener', type, listener);
+
+ } else if (isObject(list)) {
+ for (i = length; i-- > 0;) {
+ if (list[i] === listener ||
+ (list[i].listener && list[i].listener === listener)) {
+ position = i;
+ break;
+ }
+ }
+
+ if (position < 0)
+ return this;
+
+ if (list.length === 1) {
+ list.length = 0;
+ delete this._events[type];
+ } else {
+ list.splice(position, 1);
+ }
+
+ if (this._events.removeListener)
+ this.emit('removeListener', type, listener);
+ }
+
+ return this;
+};
+
+EventEmitter.prototype.removeAllListeners = function(type) {
+ var key, listeners;
+
+ if (!this._events)
+ return this;
+
+ // not listening for removeListener, no need to emit
+ if (!this._events.removeListener) {
+ if (arguments.length === 0)
+ this._events = {};
+ else if (this._events[type])
+ delete this._events[type];
+ return this;
+ }
+
+ // emit removeListener for all listeners on all events
+ if (arguments.length === 0) {
+ for (key in this._events) {
+ if (key === 'removeListener') continue;
+ this.removeAllListeners(key);
+ }
+ this.removeAllListeners('removeListener');
+ this._events = {};
+ return this;
+ }
+
+ listeners = this._events[type];
+
+ if (isFunction(listeners)) {
+ this.removeListener(type, listeners);
+ } else {
+ // LIFO order
+ while (listeners.length)
+ this.removeListener(type, listeners[listeners.length - 1]);
+ }
+ delete this._events[type];
+
+ return this;
+};
+
+EventEmitter.prototype.listeners = function(type) {
+ var ret;
+ if (!this._events || !this._events[type])
+ ret = [];
+ else if (isFunction(this._events[type]))
+ ret = [this._events[type]];
+ else
+ ret = this._events[type].slice();
+ return ret;
+};
+
+EventEmitter.listenerCount = function(emitter, type) {
+ var ret;
+ if (!emitter._events || !emitter._events[type])
+ ret = 0;
+ else if (isFunction(emitter._events[type]))
+ ret = 1;
+ else
+ ret = emitter._events[type].length;
+ return ret;
+};
+
+function isFunction(arg) {
+ return typeof arg === 'function';
+}
+
+function isNumber(arg) {
+ return typeof arg === 'number';
+}
+
+function isObject(arg) {
+ return typeof arg === 'object' && arg !== null;
+}
+
+function isUndefined(arg) {
+ return arg === void 0;
+}
+
+},{}],8:[function(require,module,exports){
+if (typeof Object.create === 'function') {
+ // implementation from standard node.js 'util' module
+ module.exports = function inherits(ctor, superCtor) {
+ ctor.super_ = superCtor
+ ctor.prototype = Object.create(superCtor.prototype, {
+ constructor: {
+ value: ctor,
+ enumerable: false,
+ writable: true,
+ configurable: true
+ }
+ });
+ };
+} else {
+ // old school shim for old browsers
+ module.exports = function inherits(ctor, superCtor) {
+ ctor.super_ = superCtor
+ var TempCtor = function () {}
+ TempCtor.prototype = superCtor.prototype
+ ctor.prototype = new TempCtor()
+ ctor.prototype.constructor = ctor
+ }
+}
+
+},{}],9:[function(require,module,exports){
+module.exports = Array.isArray || function (arr) {
+ return Object.prototype.toString.call(arr) == '[object Array]';
+};
+
+},{}],10:[function(require,module,exports){
+// shim for using process in browser
+
+var process = module.exports = {};
+
+process.nextTick = (function () {
+ var canSetImmediate = typeof window !== 'undefined'
+ && window.setImmediate;
+ var canMutationObserver = typeof window !== 'undefined'
+ && window.MutationObserver;
+ var canPost = typeof window !== 'undefined'
+ && window.postMessage && window.addEventListener
+ ;
+
+ if (canSetImmediate) {
+ return function (f) { return window.setImmediate(f) };
+ }
+
+ var queue = [];
+
+ if (canMutationObserver) {
+ var hiddenDiv = document.createElement("div");
+ var observer = new MutationObserver(function () {
+ var queueList = queue.slice();
+ queue.length = 0;
+ queueList.forEach(function (fn) {
+ fn();
+ });
+ });
+
+ observer.observe(hiddenDiv, { attributes: true });
+
+ return function nextTick(fn) {
+ if (!queue.length) {
+ hiddenDiv.setAttribute('yes', 'no');
+ }
+ queue.push(fn);
+ };
+ }
+
+ if (canPost) {
+ window.addEventListener('message', function (ev) {
+ var source = ev.source;
+ if ((source === window || source === null) && ev.data === 'process-tick') {
+ ev.stopPropagation();
+ if (queue.length > 0) {
+ var fn = queue.shift();
+ fn();
+ }
+ }
+ }, true);
+
+ return function nextTick(fn) {
+ queue.push(fn);
+ window.postMessage('process-tick', '*');
+ };
+ }
+
+ return function nextTick(fn) {
+ setTimeout(fn, 0);
+ };
+})();
+
+process.title = 'browser';
+process.browser = true;
+process.env = {};
+process.argv = [];
+
+function noop() {}
+
+process.on = noop;
+process.addListener = noop;
+process.once = noop;
+process.off = noop;
+process.removeListener = noop;
+process.removeAllListeners = noop;
+process.emit = noop;
+
+process.binding = function (name) {
+ throw new Error('process.binding is not supported');
+};
+
+// TODO(shtylman)
+process.cwd = function () { return '/' };
+process.chdir = function (dir) {
+ throw new Error('process.chdir is not supported');
+};
+
+},{}],11:[function(require,module,exports){
+module.exports = require("./lib/_stream_duplex.js")
+
+},{"./lib/_stream_duplex.js":12}],12:[function(require,module,exports){
+(function (process){
+// Copyright Joyent, Inc. and other Node contributors.
+//
+// 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.
+
+// a duplex stream is just a stream that is both readable and writable.
+// Since JS doesn't have multiple prototypal inheritance, this class
+// prototypally inherits from Readable, and then parasitically from
+// Writable.
+
+module.exports = Duplex;
+
+/*<replacement>*/
+var objectKeys = Object.keys || function (obj) {
+ var keys = [];
+ for (var key in obj) keys.push(key);
+ return keys;
+}
+/*</replacement>*/
+
+
+/*<replacement>*/
+var util = require('core-util-is');
+util.inherits = require('inherits');
+/*</replacement>*/
+
+var Readable = require('./_stream_readable');
+var Writable = require('./_stream_writable');
+
+util.inherits(Duplex, Readable);
+
+forEach(objectKeys(Writable.prototype), function(method) {
+ if (!Duplex.prototype[method])
+ Duplex.prototype[method] = Writable.prototype[method];
+});
+
+function Duplex(options) {
+ if (!(this instanceof Duplex))
+ return new Duplex(options);
+
+ Readable.call(this, options);
+ Writable.call(this, options);
+
+ if (options && options.readable === false)
+ this.readable = false;
+
+ if (options && options.writable === false)
+ this.writable = false;
+
+ this.allowHalfOpen = true;
+ if (options && options.allowHalfOpen === false)
+ this.allowHalfOpen = false;
+
+ this.once('end', onend);
+}
+
+// the no-half-open enforcer
+function onend() {
+ // if we allow half-open state, or if the writable side ended,
+ // then we're ok.
+ if (this.allowHalfOpen || this._writableState.ended)
+ return;
+
+ // no more data can be written.
+ // But allow more writes to happen in this tick.
+ process.nextTick(this.end.bind(this));
+}
+
+function forEach (xs, f) {
+ for (var i = 0, l = xs.length; i < l; i++) {
+ f(xs[i], i);
+ }
+}
+
+}).call(this,require('_process'))
+},{"./_stream_readable":14,"./_stream_writable":16,"_process":10,"core-util-is":17,"inherits":8}],13:[function(require,module,exports){
+// Copyright Joyent, Inc. and other Node contributors.
+//
+// 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.
+
+// a passthrough stream.
+// basically just the most minimal sort of Transform stream.
+// Every written chunk gets output as-is.
+
+module.exports = PassThrough;
+
+var Transform = require('./_stream_transform');
+
+/*<replacement>*/
+var util = require('core-util-is');
+util.inherits = require('inherits');
+/*</replacement>*/
+
+util.inherits(PassThrough, Transform);
+
+function PassThrough(options) {
+ if (!(this instanceof PassThrough))
+ return new PassThrough(options);
+
+ Transform.call(this, options);
+}
+
+PassThrough.prototype._transform = function(chunk, encoding, cb) {
+ cb(null, chunk);
+};
+
+},{"./_stream_transform":15,"core-util-is":17,"inherits":8}],14:[function(require,module,exports){
+(function (process){
+// Copyright Joyent, Inc. and other Node contributors.
+//
+// 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.
+
+module.exports = Readable;
+
+/*<replacement>*/
+var isArray = require('isarray');
+/*</replacement>*/
+
+
+/*<replacement>*/
+var Buffer = require('buffer').Buffer;
+/*</replacement>*/
+
+Readable.ReadableState = ReadableState;
+
+var EE = require('events').EventEmitter;
+
+/*<replacement>*/
+if (!EE.listenerCount) EE.listenerCount = function(emitter, type) {
+ return emitter.listeners(type).length;
+};
+/*</replacement>*/
+
+var Stream = require('stream');
+
+/*<replacement>*/
+var util = require('core-util-is');
+util.inherits = require('inherits');
+/*</replacement>*/
+
+var StringDecoder;
+
+util.inherits(Readable, Stream);
+
+function ReadableState(options, stream) {
+ options = options || {};
+
+ // the point at which it stops calling _read() to fill the buffer
+ // Note: 0 is a valid value, means "don't call _read preemptively ever"
+ var hwm = options.highWaterMark;
+ this.highWaterMark = (hwm || hwm === 0) ? hwm : 16 * 1024;
+
+ // cast to ints.
+ this.highWaterMark = ~~this.highWaterMark;
+
+ this.buffer = [];
+ this.length = 0;
+ this.pipes = null;
+ this.pipesCount = 0;
+ this.flowing = false;
+ this.ended = false;
+ this.endEmitted = false;
+ this.reading = false;
+
+ // In streams that never have any data, and do push(null) right away,
+ // the consumer can miss the 'end' event if they do some I/O before
+ // consuming the stream. So, we don't emit('end') until some reading
+ // happens.
+ this.calledRead = false;
+
+ // a flag to be able to tell if the onwrite cb is called immediately,
+ // or on a later tick. We set this to true at first, becuase any
+ // actions that shouldn't happen until "later" should generally also
+ // not happen before the first write call.
+ this.sync = true;
+
+ // whenever we return null, then we set a flag to say
+ // that we're awaiting a 'readable' event emission.
+ this.needReadable = false;
+ this.emittedReadable = false;
+ this.readableListening = false;
+
+
+ // object stream flag. Used to make read(n) ignore n and to
+ // make all the buffer merging and length checks go away
+ this.objectMode = !!options.objectMode;
+
+ // Crypto is kind of old and crusty. Historically, its default string
+ // encoding is 'binary' so we have to make this configurable.
+ // Everything else in the universe uses 'utf8', though.
+ this.defaultEncoding = options.defaultEncoding || 'utf8';
+
+ // when piping, we only care about 'readable' events that happen
+ // after read()ing all the bytes and not getting any pushback.
+ this.ranOut = false;
+
+ // the number of writers that are awaiting a drain event in .pipe()s
+ this.awaitDrain = 0;
+
+ // if true, a maybeReadMore has been scheduled
+ this.readingMore = false;
+
+ this.decoder = null;
+ this.encoding = null;
+ if (options.encoding) {
+ if (!StringDecoder)
+ StringDecoder = require('string_decoder/').StringDecoder;
+ this.decoder = new StringDecoder(options.encoding);
+ this.encoding = options.encoding;
+ }
+}
+
+function Readable(options) {
+ if (!(this instanceof Readable))
+ return new Readable(options);
+
+ this._readableState = new ReadableState(options, this);
+
+ // legacy
+ this.readable = true;
+
+ Stream.call(this);
+}
+
+// Manually shove something into the read() buffer.
+// This returns true if the highWaterMark has not been hit yet,
+// similar to how Writable.write() returns true if you should
+// write() some more.
+Readable.prototype.push = function(chunk, encoding) {
+ var state = this._readableState;
+
+ if (typeof chunk === 'string' && !state.objectMode) {
+ encoding = encoding || state.defaultEncoding;
+ if (encoding !== state.encoding) {
+ chunk = new Buffer(chunk, encoding);
+ encoding = '';
+ }
+ }
+
+ return readableAddChunk(this, state, chunk, encoding, false);
+};
+
+// Unshift should *always* be something directly out of read()
+Readable.prototype.unshift = function(chunk) {
+ var state = this._readableState;
+ return readableAddChunk(this, state, chunk, '', true);
+};
+
+function readableAddChunk(stream, state, chunk, encoding, addToFront) {
+ var er = chunkInvalid(state, chunk);
+ if (er) {
+ stream.emit('error', er);
+ } else if (chunk === null || chunk === undefined) {
+ state.reading = false;
+ if (!state.ended)
+ onEofChunk(stream, state);
+ } else if (state.objectMode || chunk && chunk.length > 0) {
+ if (state.ended && !addToFront) {
+ var e = new Error('stream.push() after EOF');
+ stream.emit('error', e);
+ } else if (state.endEmitted && addToFront) {
+ var e = new Error('stream.unshift() after end event');
+ stream.emit('error', e);
+ } else {
+ if (state.decoder && !addToFront && !encoding)
+ chunk = state.decoder.write(chunk);
+
+ // update the buffer info.
+ state.length += state.objectMode ? 1 : chunk.length;
+ if (addToFront) {
+ state.buffer.unshift(chunk);
+ } else {
+ state.reading = false;
+ state.buffer.push(chunk);
+ }
+
+ if (state.needReadable)
+ emitReadable(stream);
+
+ maybeReadMore(stream, state);
+ }
+ } else if (!addToFront) {
+ state.reading = false;
+ }
+
+ return needMoreData(state);
+}
+
+
+
+// if it's past the high water mark, we can push in some more.
+// Also, if we have no data yet, we can stand some
+// more bytes. This is to work around cases where hwm=0,
+// such as the repl. Also, if the push() triggered a
+// readable event, and the user called read(largeNumber) such that
+// needReadable was set, then we ought to push more, so that another
+// 'readable' event will be triggered.
+function needMoreData(state) {
+ return !state.ended &&
+ (state.needReadable ||
+ state.length < state.highWaterMark ||
+ state.length === 0);
+}
+
+// backwards compatibility.
+Readable.prototype.setEncoding = function(enc) {
+ if (!StringDecoder)
+ StringDecoder = require('string_decoder/').StringDecoder;
+ this._readableState.decoder = new StringDecoder(enc);
+ this._readableState.encoding = enc;
+};
+
+// Don't raise the hwm > 128MB
+var MAX_HWM = 0x800000;
+function roundUpToNextPowerOf2(n) {
+ if (n >= MAX_HWM) {
+ n = MAX_HWM;
+ } else {
+ // Get the next highest power of 2
+ n--;
+ for (var p = 1; p < 32; p <<= 1) n |= n >> p;
+ n++;
+ }
+ return n;
+}
+
+function howMuchToRead(n, state) {
+ if (state.length === 0 && state.ended)
+ return 0;
+
+ if (state.objectMode)
+ return n === 0 ? 0 : 1;
+
+ if (n === null || isNaN(n)) {
+ // only flow one buffer at a time
+ if (state.flowing && state.buffer.length)
+ return state.buffer[0].length;
+ else
+ return state.length;
+ }
+
+ if (n <= 0)
+ return 0;
+
+ // If we're asking for more than the target buffer level,
+ // then raise the water mark. Bump up to the next highest
+ // power of 2, to prevent increasing it excessively in tiny
+ // amounts.
+ if (n > state.highWaterMark)
+ state.highWaterMark = roundUpToNextPowerOf2(n);
+
+ // don't have that much. return null, unless we've ended.
+ if (n > state.length) {
+ if (!state.ended) {
+ state.needReadable = true;
+ return 0;
+ } else
+ return state.length;
+ }
+
+ return n;
+}
+
+// you can override either this method, or the async _read(n) below.
+Readable.prototype.read = function(n) {
+ var state = this._readableState;
+ state.calledRead = true;
+ var nOrig = n;
+ var ret;
+
+ if (typeof n !== 'number' || n > 0)
+ state.emittedReadable = false;
+
+ // if we're doing read(0) to trigger a readable event, but we
+ // already have a bunch of data in the buffer, then just trigger
+ // the 'readable' event and move on.
+ if (n === 0 &&
+ state.needReadable &&
+ (state.length >= state.highWaterMark || state.ended)) {
+ emitReadable(this);
+ return null;
+ }
+
+ n = howMuchToRead(n, state);
+
+ // if we've ended, and we're now clear, then finish it up.
+ if (n === 0 && state.ended) {
+ ret = null;
+
+ // In cases where the decoder did not receive enough data
+ // to produce a full chunk, then immediately received an
+ // EOF, state.buffer will contain [<Buffer >, <Buffer 00 ...>].
+ // howMuchToRead will see this and coerce the amount to
+ // read to zero (because it's looking at the length of the
+ // first <Buffer > in state.buffer), and we'll end up here.
+ //
+ // This can only happen via state.decoder -- no other venue
+ // exists for pushing a zero-length chunk into state.buffer
+ // and triggering this behavior. In this case, we return our
+ // remaining data and end the stream, if appropriate.
+ if (state.length > 0 && state.decoder) {
+ ret = fromList(n, state);
+ state.length -= ret.length;
+ }
+
+ if (state.length === 0)
+ endReadable(this);
+
+ return ret;
+ }
+
+ // All the actual chunk generation logic needs to be
+ // *below* the call to _read. The reason is that in certain
+ // synthetic stream cases, such as passthrough streams, _read
+ // may be a completely synchronous operation which may change
+ // the state of the read buffer, providing enough data when
+ // before there was *not* enough.
+ //
+ // So, the steps are:
+ // 1. Figure out what the state of things will be after we do
+ // a read from the buffer.
+ //
+ // 2. If that resulting state will trigger a _read, then call _read.
+ // Note that this may be asynchronous, or synchronous. Yes, it is
+ // deeply ugly to write APIs this way, but that still doesn't mean
+ // that the Readable class should behave improperly, as streams are
+ // designed to be sync/async agnostic.
+ // Take note if the _read call is sync or async (ie, if the read call
+ // has returned yet), so that we know whether or not it's safe to emit
+ // 'readable' etc.
+ //
+ // 3. Actually pull the requested chunks out of the buffer and return.
+
+ // if we need a readable event, then we need to do some reading.
+ var doRead = state.needReadable;
+
+ // if we currently have less than the highWaterMark, then also read some
+ if (state.length - n <= state.highWaterMark)
+ doRead = true;
+
+ // however, if we've ended, then there's no point, and if we're already
+ // reading, then it's unnecessary.
+ if (state.ended || state.reading)
+ doRead = false;
+
+ if (doRead) {
+ state.reading = true;
+ state.sync = true;
+ // if the length is currently zero, then we *need* a readable event.
+ if (state.length === 0)
+ state.needReadable = true;
+ // call internal read method
+ this._read(state.highWaterMark);
+ state.sync = false;
+ }
+
+ // If _read called its callback synchronously, then `reading`
+ // will be false, and we need to re-evaluate how much data we
+ // can return to the user.
+ if (doRead && !state.reading)
+ n = howMuchToRead(nOrig, state);
+
+ if (n > 0)
+ ret = fromList(n, state);
+ else
+ ret = null;
+
+ if (ret === null) {
+ state.needReadable = true;
+ n = 0;
+ }
+
+ state.length -= n;
+
+ // If we have nothing in the buffer, then we want to know
+ // as soon as we *do* get something into the buffer.
+ if (state.length === 0 && !state.ended)
+ state.needReadable = true;
+
+ // If we happened to read() exactly the remaining amount in the
+ // buffer, and the EOF has been seen at this point, then make sure
+ // that we emit 'end' on the very next tick.
+ if (state.ended && !state.endEmitted && state.length === 0)
+ endReadable(this);
+
+ return ret;
+};
+
+function chunkInvalid(state, chunk) {
+ var er = null;
+ if (!Buffer.isBuffer(chunk) &&
+ 'string' !== typeof chunk &&
+ chunk !== null &&
+ chunk !== undefined &&
+ !state.objectMode) {
+ er = new TypeError('Invalid non-string/buffer chunk');
+ }
+ return er;
+}
+
+
+function onEofChunk(stream, state) {
+ if (state.decoder && !state.ended) {
+ var chunk = state.decoder.end();
+ if (chunk && chunk.length) {
+ state.buffer.push(chunk);
+ state.length += state.objectMode ? 1 : chunk.length;
+ }
+ }
+ state.ended = true;
+
+ // if we've ended and we have some data left, then emit
+ // 'readable' now to make sure it gets picked up.
+ if (state.length > 0)
+ emitReadable(stream);
+ else
+ endReadable(stream);
+}
+
+// Don't emit readable right away in sync mode, because this can trigger
+// another read() call => stack overflow. This way, it might trigger
+// a nextTick recursion warning, but that's not so bad.
+function emitReadable(stream) {
+ var state = stream._readableState;
+ state.needReadable = false;
+ if (state.emittedReadable)
+ return;
+
+ state.emittedReadable = true;
+ if (state.sync)
+ process.nextTick(function() {
+ emitReadable_(stream);
+ });
+ else
+ emitReadable_(stream);
+}
+
+function emitReadable_(stream) {
+ stream.emit('readable');
+}
+
+
+// at this point, the user has presumably seen the 'readable' event,
+// and called read() to consume some data. that may have triggered
+// in turn another _read(n) call, in which case reading = true if
+// it's in progress.
+// However, if we're not ended, or reading, and the length < hwm,
+// then go ahead and try to read some more preemptively.
+function maybeReadMore(stream, state) {
+ if (!state.readingMore) {
+ state.readingMore = true;
+ process.nextTick(function() {
+ maybeReadMore_(stream, state);
+ });
+ }
+}
+
+function maybeReadMore_(stream, state) {
+ var len = state.length;
+ while (!state.reading && !state.flowing && !state.ended &&
+ state.length < state.highWaterMark) {
+ stream.read(0);
+ if (len === state.length)
+ // didn't get any data, stop spinning.
+ break;
+ else
+ len = state.length;
+ }
+ state.readingMore = false;
+}
+
+// abstract method. to be overridden in specific implementation classes.
+// call cb(er, data) where data is <= n in length.
+// for virtual (non-string, non-buffer) streams, "length" is somewhat
+// arbitrary, and perhaps not very meaningful.
+Readable.prototype._read = function(n) {
+ this.emit('error', new Error('not implemented'));
+};
+
+Readable.prototype.pipe = function(dest, pipeOpts) {
+ var src = this;
+ var state = this._readableState;
+
+ switch (state.pipesCount) {
+ case 0:
+ state.pipes = dest;
+ break;
+ case 1:
+ state.pipes = [state.pipes, dest];
+ break;
+ default:
+ state.pipes.push(dest);
+ break;
+ }
+ state.pipesCount += 1;
+
+ var doEnd = (!pipeOpts || pipeOpts.end !== false) &&
+ dest !== process.stdout &&
+ dest !== process.stderr;
+
+ var endFn = doEnd ? onend : cleanup;
+ if (state.endEmitted)
+ process.nextTick(endFn);
+ else
+ src.once('end', endFn);
+
+ dest.on('unpipe', onunpipe);
+ function onunpipe(readable) {
+ if (readable !== src) return;
+ cleanup();
+ }
+
+ function onend() {
+ dest.end();
+ }
+
+ // when the dest drains, it reduces the awaitDrain counter
+ // on the source. This would be more elegant with a .once()
+ // handler in flow(), but adding and removing repeatedly is
+ // too slow.
+ var ondrain = pipeOnDrain(src);
+ dest.on('drain', ondrain);
+
+ function cleanup() {
+ // cleanup event handlers once the pipe is broken
+ dest.removeListener('close', onclose);
+ dest.removeListener('finish', onfinish);
+ dest.removeListener('drain', ondrain);
+ dest.removeListener('error', onerror);
+ dest.removeListener('unpipe', onunpipe);
+ src.removeListener('end', onend);
+ src.removeListener('end', cleanup);
+
+ // if the reader is waiting for a drain event from this
+ // specific writer, then it would cause it to never start
+ // flowing again.
+ // So, if this is awaiting a drain, then we just call it now.
+ // If we don't know, then assume that we are waiting for one.
+ if (!dest._writableState || dest._writableState.needDrain)
+ ondrain();
+ }
+
+ // if the dest has an error, then stop piping into it.
+ // however, don't suppress the throwing behavior for this.
+ function onerror(er) {
+ unpipe();
+ dest.removeListener('error', onerror);
+ if (EE.listenerCount(dest, 'error') === 0)
+ dest.emit('error', er);
+ }
+ // This is a brutally ugly hack to make sure that our error handler
+ // is attached before any userland ones. NEVER DO THIS.
+ if (!dest._events || !dest._events.error)
+ dest.on('error', onerror);
+ else if (isArray(dest._events.error))
+ dest._events.error.unshift(onerror);
+ else
+ dest._events.error = [onerror, dest._events.error];
+
+
+
+ // Both close and finish should trigger unpipe, but only once.
+ function onclose() {
+ dest.removeListener('finish', onfinish);
+ unpipe();
+ }
+ dest.once('close', onclose);
+ function onfinish() {
+ dest.removeListener('close', onclose);
+ unpipe();
+ }
+ dest.once('finish', onfinish);
+
+ function unpipe() {
+ src.unpipe(dest);
+ }
+
+ // tell the dest that it's being piped to
+ dest.emit('pipe', src);
+
+ // start the flow if it hasn't been started already.
+ if (!state.flowing) {
+ // the handler that waits for readable events after all
+ // the data gets sucked out in flow.
+ // This would be easier to follow with a .once() handler
+ // in flow(), but that is too slow.
+ this.on('readable', pipeOnReadable);
+
+ state.flowing = true;
+ process.nextTick(function() {
+ flow(src);
+ });
+ }
+
+ return dest;
+};
+
+function pipeOnDrain(src) {
+ return function() {
+ var dest = this;
+ var state = src._readableState;
+ state.awaitDrain--;
+ if (state.awaitDrain === 0)
+ flow(src);
+ };
+}
+
+function flow(src) {
+ var state = src._readableState;
+ var chunk;
+ state.awaitDrain = 0;
+
+ function write(dest, i, list) {
+ var written = dest.write(chunk);
+ if (false === written) {
+ state.awaitDrain++;
+ }
+ }
+
+ while (state.pipesCount && null !== (chunk = src.read())) {
+
+ if (state.pipesCount === 1)
+ write(state.pipes, 0, null);
+ else
+ forEach(state.pipes, write);
+
+ src.emit('data', chunk);
+
+ // if anyone needs a drain, then we have to wait for that.
+ if (state.awaitDrain > 0)
+ return;
+ }
+
+ // if every destination was unpiped, either before entering this
+ // function, or in the while loop, then stop flowing.
+ //
+ // NB: This is a pretty rare edge case.
+ if (state.pipesCount === 0) {
+ state.flowing = false;
+
+ // if there were data event listeners added, then switch to old mode.
+ if (EE.listenerCount(src, 'data') > 0)
+ emitDataEvents(src);
+ return;
+ }
+
+ // at this point, no one needed a drain, so we just ran out of data
+ // on the next readable event, start it over again.
+ state.ranOut = true;
+}
+
+function pipeOnReadable() {
+ if (this._readableState.ranOut) {
+ this._readableState.ranOut = false;
+ flow(this);
+ }
+}
+
+
+Readable.prototype.unpipe = function(dest) {
+ var state = this._readableState;
+
+ // if we're not piping anywhere, then do nothing.
+ if (state.pipesCount === 0)
+ return this;
+
+ // just one destination. most common case.
+ if (state.pipesCount === 1) {
+ // passed in one, but it's not the right one.
+ if (dest && dest !== state.pipes)
+ return this;
+
+ if (!dest)
+ dest = state.pipes;
+
+ // got a match.
+ state.pipes = null;
+ state.pipesCount = 0;
+ this.removeListener('readable', pipeOnReadable);
+ state.flowing = false;
+ if (dest)
+ dest.emit('unpipe', this);
+ return this;
+ }
+
+ // slow case. multiple pipe destinations.
+
+ if (!dest) {
+ // remove all.
+ var dests = state.pipes;
+ var len = state.pipesCount;
+ state.pipes = null;
+ state.pipesCount = 0;
+ this.removeListener('readable', pipeOnReadable);
+ state.flowing = false;
+
+ for (var i = 0; i < len; i++)
+ dests[i].emit('unpipe', this);
+ return this;
+ }
+
+ // try to find the right one.
+ var i = indexOf(state.pipes, dest);
+ if (i === -1)
+ return this;
+
+ state.pipes.splice(i, 1);
+ state.pipesCount -= 1;
+ if (state.pipesCount === 1)
+ state.pipes = state.pipes[0];
+
+ dest.emit('unpipe', this);
+
+ return this;
+};
+
+// set up data events if they are asked for
+// Ensure readable listeners eventually get something
+Readable.prototype.on = function(ev, fn) {
+ var res = Stream.prototype.on.call(this, ev, fn);
+
+ if (ev === 'data' && !this._readableState.flowing)
+ emitDataEvents(this);
+
+ if (ev === 'readable' && this.readable) {
+ var state = this._readableState;
+ if (!state.readableListening) {
+ state.readableListening = true;
+ state.emittedReadable = false;
+ state.needReadable = true;
+ if (!state.reading) {
+ this.read(0);
+ } else if (state.length) {
+ emitReadable(this, state);
+ }
+ }
+ }
+
+ return res;
+};
+Readable.prototype.addListener = Readable.prototype.on;
+
+// pause() and resume() are remnants of the legacy readable stream API
+// If the user uses them, then switch into old mode.
+Readable.prototype.resume = function() {
+ emitDataEvents(this);
+ this.read(0);
+ this.emit('resume');
+};
+
+Readable.prototype.pause = function() {
+ emitDataEvents(this, true);
+ this.emit('pause');
+};
+
+function emitDataEvents(stream, startPaused) {
+ var state = stream._readableState;
+
+ if (state.flowing) {
+ // https://github.com/isaacs/readable-stream/issues/16
+ throw new Error('Cannot switch to old mode now.');
+ }
+
+ var paused = startPaused || false;
+ var readable = false;
+
+ // convert to an old-style stream.
+ stream.readable = true;
+ stream.pipe = Stream.prototype.pipe;
+ stream.on = stream.addListener = Stream.prototype.on;
+
+ stream.on('readable', function() {
+ readable = true;
+
+ var c;
+ while (!paused && (null !== (c = stream.read())))
+ stream.emit('data', c);
+
+ if (c === null) {
+ readable = false;
+ stream._readableState.needReadable = true;
+ }
+ });
+
+ stream.pause = function() {
+ paused = true;
+ this.emit('pause');
+ };
+
+ stream.resume = function() {
+ paused = false;
+ if (readable)
+ process.nextTick(function() {
+ stream.emit('readable');
+ });
+ else
+ this.read(0);
+ this.emit('resume');
+ };
+
+ // now make it start, just in case it hadn't already.
+ stream.emit('readable');
+}
+
+// wrap an old-style stream as the async data source.
+// This is *not* part of the readable stream interface.
+// It is an ugly unfortunate mess of history.
+Readable.prototype.wrap = function(stream) {
+ var state = this._readableState;
+ var paused = false;
+
+ var self = this;
+ stream.on('end', function() {
+ if (state.decoder && !state.ended) {
+ var chunk = state.decoder.end();
+ if (chunk && chunk.length)
+ self.push(chunk);
+ }
+
+ self.push(null);
+ });
+
+ stream.on('data', function(chunk) {
+ if (state.decoder)
+ chunk = state.decoder.write(chunk);
+
+ // don't skip over falsy values in objectMode
+ //if (state.objectMode && util.isNullOrUndefined(chunk))
+ if (state.objectMode && (chunk === null || chunk === undefined))
+ return;
+ else if (!state.objectMode && (!chunk || !chunk.length))
+ return;
+
+ var ret = self.push(chunk);
+ if (!ret) {
+ paused = true;
+ stream.pause();
+ }
+ });
+
+ // proxy all the other methods.
+ // important when wrapping filters and duplexes.
+ for (var i in stream) {
+ if (typeof stream[i] === 'function' &&
+ typeof this[i] === 'undefined') {
+ this[i] = function(method) { return function() {
+ return stream[method].apply(stream, arguments);
+ }}(i);
+ }
+ }
+
+ // proxy certain important events.
+ var events = ['error', 'close', 'destroy', 'pause', 'resume'];
+ forEach(events, function(ev) {
+ stream.on(ev, self.emit.bind(self, ev));
+ });
+
+ // when we try to consume some more bytes, simply unpause the
+ // underlying stream.
+ self._read = function(n) {
+ if (paused) {
+ paused = false;
+ stream.resume();
+ }
+ };
+
+ return self;
+};
+
+
+
+// exposed for testing purposes only.
+Readable._fromList = fromList;
+
+// Pluck off n bytes from an array of buffers.
+// Length is the combined lengths of all the buffers in the list.
+function fromList(n, state) {
+ var list = state.buffer;
+ var length = state.length;
+ var stringMode = !!state.decoder;
+ var objectMode = !!state.objectMode;
+ var ret;
+
+ // nothing in the list, definitely empty.
+ if (list.length === 0)
+ return null;
+
+ if (length === 0)
+ ret = null;
+ else if (objectMode)
+ ret = list.shift();
+ else if (!n || n >= length) {
+ // read it all, truncate the array.
+ if (stringMode)
+ ret = list.join('');
+ else
+ ret = Buffer.concat(list, length);
+ list.length = 0;
+ } else {
+ // read just some of it.
+ if (n < list[0].length) {
+ // just take a part of the first list item.
+ // slice is the same for buffers and strings.
+ var buf = list[0];
+ ret = buf.slice(0, n);
+ list[0] = buf.slice(n);
+ } else if (n === list[0].length) {
+ // first list is a perfect match
+ ret = list.shift();
+ } else {
+ // complex case.
+ // we have enough to cover it, but it spans past the first buffer.
+ if (stringMode)
+ ret = '';
+ else
+ ret = new Buffer(n);
+
+ var c = 0;
+ for (var i = 0, l = list.length; i < l && c < n; i++) {
+ var buf = list[0];
+ var cpy = Math.min(n - c, buf.length);
+
+ if (stringMode)
+ ret += buf.slice(0, cpy);
+ else
+ buf.copy(ret, c, 0, cpy);
+
+ if (cpy < buf.length)
+ list[0] = buf.slice(cpy);
+ else
+ list.shift();
+
+ c += cpy;
+ }
+ }
+ }
+
+ return ret;
+}
+
+function endReadable(stream) {
+ var state = stream._readableState;
+
+ // If we get here before consuming all the bytes, then that is a
+ // bug in node. Should never happen.
+ if (state.length > 0)
+ throw new Error('endReadable called on non-empty stream');
+
+ if (!state.endEmitted && state.calledRead) {
+ state.ended = true;
+ process.nextTick(function() {
+ // Check that we didn't get one last unshift.
+ if (!state.endEmitted && state.length === 0) {
+ state.endEmitted = true;
+ stream.readable = false;
+ stream.emit('end');
+ }
+ });
+ }
+}
+
+function forEach (xs, f) {
+ for (var i = 0, l = xs.length; i < l; i++) {
+ f(xs[i], i);
+ }
+}
+
+function indexOf (xs, x) {
+ for (var i = 0, l = xs.length; i < l; i++) {
+ if (xs[i] === x) return i;
+ }
+ return -1;
+}
+
+}).call(this,require('_process'))
+},{"_process":10,"buffer":3,"core-util-is":17,"events":7,"inherits":8,"isarray":9,"stream":22,"string_decoder/":23}],15:[function(require,module,exports){
+// Copyright Joyent, Inc. and other Node contributors.
+//
+// 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.
+
+
+// a transform stream is a readable/writable stream where you do
+// something with the data. Sometimes it's called a "filter",
+// but that's not a great name for it, since that implies a thing where
+// some bits pass through, and others are simply ignored. (That would
+// be a valid example of a transform, of course.)
+//
+// While the output is causally related to the input, it's not a
+// necessarily symmetric or synchronous transformation. For example,
+// a zlib stream might take multiple plain-text writes(), and then
+// emit a single compressed chunk some time in the future.
+//
+// Here's how this works:
+//
+// The Transform stream has all the aspects of the readable and writable
+// stream classes. When you write(chunk), that calls _write(chunk,cb)
+// internally, and returns false if there's a lot of pending writes
+// buffered up. When you call read(), that calls _read(n) until
+// there's enough pending readable data buffered up.
+//
+// In a transform stream, the written data is placed in a buffer. When
+// _read(n) is called, it transforms the queued up data, calling the
+// buffered _write cb's as it consumes chunks. If consuming a single
+// written chunk would result in multiple output chunks, then the first
+// outputted bit calls the readcb, and subsequent chunks just go into
+// the read buffer, and will cause it to emit 'readable' if necessary.
+//
+// This way, back-pressure is actually determined by the reading side,
+// since _read has to be called to start processing a new chunk. However,
+// a pathological inflate type of transform can cause excessive buffering
+// here. For example, imagine a stream where every byte of input is
+// interpreted as an integer from 0-255, and then results in that many
+// bytes of output. Writing the 4 bytes {ff,ff,ff,ff} would result in
+// 1kb of data being output. In this case, you could write a very small
+// amount of input, and end up with a very large amount of output. In
+// such a pathological inflating mechanism, there'd be no way to tell
+// the system to stop doing the transform. A single 4MB write could
+// cause the system to run out of memory.
+//
+// However, even in such a pathological case, only a single written chunk
+// would be consumed, and then the rest would wait (un-transformed) until
+// the results of the previous transformed chunk were consumed.
+
+module.exports = Transform;
+
+var Duplex = require('./_stream_duplex');
+
+/*<replacement>*/
+var util = require('core-util-is');
+util.inherits = require('inherits');
+/*</replacement>*/
+
+util.inherits(Transform, Duplex);
+
+
+function TransformState(options, stream) {
+ this.afterTransform = function(er, data) {
+ return afterTransform(stream, er, data);
+ };
+
+ this.needTransform = false;
+ this.transforming = false;
+ this.writecb = null;
+ this.writechunk = null;
+}
+
+function afterTransform(stream, er, data) {
+ var ts = stream._transformState;
+ ts.transforming = false;
+
+ var cb = ts.writecb;
+
+ if (!cb)
+ return stream.emit('error', new Error('no writecb in Transform class'));
+
+ ts.writechunk = null;
+ ts.writecb = null;
+
+ if (data !== null && data !== undefined)
+ stream.push(data);
+
+ if (cb)
+ cb(er);
+
+ var rs = stream._readableState;
+ rs.reading = false;
+ if (rs.needReadable || rs.length < rs.highWaterMark) {
+ stream._read(rs.highWaterMark);
+ }
+}
+
+
+function Transform(options) {
+ if (!(this instanceof Transform))
+ return new Transform(options);
+
+ Duplex.call(this, options);
+
+ var ts = this._transformState = new TransformState(options, this);
+
+ // when the writable side finishes, then flush out anything remaining.
+ var stream = this;
+
+ // start out asking for a readable event once data is transformed.
+ this._readableState.needReadable = true;
+
+ // we have implemented the _read method, and done the other things
+ // that Readable wants before the first _read call, so unset the
+ // sync guard flag.
+ this._readableState.sync = false;
+
+ this.once('finish', function() {
+ if ('function' === typeof this._flush)
+ this._flush(function(er) {
+ done(stream, er);
+ });
+ else
+ done(stream);
+ });
+}
+
+Transform.prototype.push = function(chunk, encoding) {
+ this._transformState.needTransform = false;
+ return Duplex.prototype.push.call(this, chunk, encoding);
+};
+
+// This is the part where you do stuff!
+// override this function in implementation classes.
+// 'chunk' is an input chunk.
+//
+// Call `push(newChunk)` to pass along transformed output
+// to the readable side. You may call 'push' zero or more times.
+//
+// Call `cb(err)` when you are done with this chunk. If you pass
+// an error, then that'll put the hurt on the whole operation. If you
+// never call cb(), then you'll never get another chunk.
+Transform.prototype._transform = function(chunk, encoding, cb) {
+ throw new Error('not implemented');
+};
+
+Transform.prototype._write = function(chunk, encoding, cb) {
+ var ts = this._transformState;
+ ts.writecb = cb;
+ ts.writechunk = chunk;
+ ts.writeencoding = encoding;
+ if (!ts.transforming) {
+ var rs = this._readableState;
+ if (ts.needTransform ||
+ rs.needReadable ||
+ rs.length < rs.highWaterMark)
+ this._read(rs.highWaterMark);
+ }
+};
+
+// Doesn't matter what the args are here.
+// _transform does all the work.
+// That we got here means that the readable side wants more data.
+Transform.prototype._read = function(n) {
+ var ts = this._transformState;
+
+ if (ts.writechunk !== null && ts.writecb && !ts.transforming) {
+ ts.transforming = true;
+ this._transform(ts.writechunk, ts.writeencoding, ts.afterTransform);
+ } else {
+ // mark that we need a transform, so that any data that comes in
+ // will get processed, now that we've asked for it.
+ ts.needTransform = true;
+ }
+};
+
+
+function done(stream, er) {
+ if (er)
+ return stream.emit('error', er);
+
+ // if there's nothing in the write buffer, then that means
+ // that nothing more will ever be provided
+ var ws = stream._writableState;
+ var rs = stream._readableState;
+ var ts = stream._transformState;
+
+ if (ws.length)
+ throw new Error('calling transform done when ws.length != 0');
+
+ if (ts.transforming)
+ throw new Error('calling transform done when still transforming');
+
+ return stream.push(null);
+}
+
+},{"./_stream_duplex":12,"core-util-is":17,"inherits":8}],16:[function(require,module,exports){
+(function (process){
+// Copyright Joyent, Inc. and other Node contributors.
+//
+// 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.
+
+// A bit simpler than readable streams.
+// Implement an async ._write(chunk, cb), and it'll handle all
+// the drain event emission and buffering.
+
+module.exports = Writable;
+
+/*<replacement>*/
+var Buffer = require('buffer').Buffer;
+/*</replacement>*/
+
+Writable.WritableState = WritableState;
+
+
+/*<replacement>*/
+var util = require('core-util-is');
+util.inherits = require('inherits');
+/*</replacement>*/
+
+var Stream = require('stream');
+
+util.inherits(Writable, Stream);
+
+function WriteReq(chunk, encoding, cb) {
+ this.chunk = chunk;
+ this.encoding = encoding;
+ this.callback = cb;
+}
+
+function WritableState(options, stream) {
+ options = options || {};
+
+ // the point at which write() starts returning false
+ // Note: 0 is a valid value, means that we always return false if
+ // the entire buffer is not flushed immediately on write()
+ var hwm = options.highWaterMark;
+ this.highWaterMark = (hwm || hwm === 0) ? hwm : 16 * 1024;
+
+ // object stream flag to indicate whether or not this stream
+ // contains buffers or objects.
+ this.objectMode = !!options.objectMode;
+
+ // cast to ints.
+ this.highWaterMark = ~~this.highWaterMark;
+
+ this.needDrain = false;
+ // at the start of calling end()
+ this.ending = false;
+ // when end() has been called, and returned
+ this.ended = false;
+ // when 'finish' is emitted
+ this.finished = false;
+
+ // should we decode strings into buffers before passing to _write?
+ // this is here so that some node-core streams can optimize string
+ // handling at a lower level.
+ var noDecode = options.decodeStrings === false;
+ this.decodeStrings = !noDecode;
+
+ // Crypto is kind of old and crusty. Historically, its default string
+ // encoding is 'binary' so we have to make this configurable.
+ // Everything else in the universe uses 'utf8', though.
+ this.defaultEncoding = options.defaultEncoding || 'utf8';
+
+ // not an actual buffer we keep track of, but a measurement
+ // of how much we're waiting to get pushed to some underlying
+ // socket or file.
+ this.length = 0;
+
+ // a flag to see when we're in the middle of a write.
+ this.writing = false;
+
+ // a flag to be able to tell if the onwrite cb is called immediately,
+ // or on a later tick. We set this to true at first, becuase any
+ // actions that shouldn't happen until "later" should generally also
+ // not happen before the first write call.
+ this.sync = true;
+
+ // a flag to know if we're processing previously buffered items, which
+ // may call the _write() callback in the same tick, so that we don't
+ // end up in an overlapped onwrite situation.
+ this.bufferProcessing = false;
+
+ // the callback that's passed to _write(chunk,cb)
+ this.onwrite = function(er) {
+ onwrite(stream, er);
+ };
+
+ // the callback that the user supplies to write(chunk,encoding,cb)
+ this.writecb = null;
+
+ // the amount that is being written when _write is called.
+ this.writelen = 0;
+
+ this.buffer = [];
+
+ // True if the error was already emitted and should not be thrown again
+ this.errorEmitted = false;
+}
+
+function Writable(options) {
+ var Duplex = require('./_stream_duplex');
+
+ // Writable ctor is applied to Duplexes, though they're not
+ // instanceof Writable, they're instanceof Readable.
+ if (!(this instanceof Writable) && !(this instanceof Duplex))
+ return new Writable(options);
+
+ this._writableState = new WritableState(options, this);
+
+ // legacy.
+ this.writable = true;
+
+ Stream.call(this);
+}
+
+// Otherwise people can pipe Writable streams, which is just wrong.
+Writable.prototype.pipe = function() {
+ this.emit('error', new Error('Cannot pipe. Not readable.'));
+};
+
+
+function writeAfterEnd(stream, state, cb) {
+ var er = new Error('write after end');
+ // TODO: defer error events consistently everywhere, not just the cb
+ stream.emit('error', er);
+ process.nextTick(function() {
+ cb(er);
+ });
+}
+
+// If we get something that is not a buffer, string, null, or undefined,
+// and we're not in objectMode, then that's an error.
+// Otherwise stream chunks are all considered to be of length=1, and the
+// watermarks determine how many objects to keep in the buffer, rather than
+// how many bytes or characters.
+function validChunk(stream, state, chunk, cb) {
+ var valid = true;
+ if (!Buffer.isBuffer(chunk) &&
+ 'string' !== typeof chunk &&
+ chunk !== null &&
+ chunk !== undefined &&
+ !state.objectMode) {
+ var er = new TypeError('Invalid non-string/buffer chunk');
+ stream.emit('error', er);
+ process.nextTick(function() {
+ cb(er);
+ });
+ valid = false;
+ }
+ return valid;
+}
+
+Writable.prototype.write = function(chunk, encoding, cb) {
+ var state = this._writableState;
+ var ret = false;
+
+ if (typeof encoding === 'function') {
+ cb = encoding;
+ encoding = null;
+ }
+
+ if (Buffer.isBuffer(chunk))
+ encoding = 'buffer';
+ else if (!encoding)
+ encoding = state.defaultEncoding;
+
+ if (typeof cb !== 'function')
+ cb = function() {};
+
+ if (state.ended)
+ writeAfterEnd(this, state, cb);
+ else if (validChunk(this, state, chunk, cb))
+ ret = writeOrBuffer(this, state, chunk, encoding, cb);
+
+ return ret;
+};
+
+function decodeChunk(state, chunk, encoding) {
+ if (!state.objectMode &&
+ state.decodeStrings !== false &&
+ typeof chunk === 'string') {
+ chunk = new Buffer(chunk, encoding);
+ }
+ return chunk;
+}
+
+// if we're already writing something, then just put this
+// in the queue, and wait our turn. Otherwise, call _write
+// If we return false, then we need a drain event, so set that flag.
+function writeOrBuffer(stream, state, chunk, encoding, cb) {
+ chunk = decodeChunk(state, chunk, encoding);
+ if (Buffer.isBuffer(chunk))
+ encoding = 'buffer';
+ var len = state.objectMode ? 1 : chunk.length;
+
+ state.length += len;
+
+ var ret = state.length < state.highWaterMark;
+ // we must ensure that previous needDrain will not be reset to false.
+ if (!ret)
+ state.needDrain = true;
+
+ if (state.writing)
+ state.buffer.push(new WriteReq(chunk, encoding, cb));
+ else
+ doWrite(stream, state, len, chunk, encoding, cb);
+
+ return ret;
+}
+
+function doWrite(stream, state, len, chunk, encoding, cb) {
+ state.writelen = len;
+ state.writecb = cb;
+ state.writing = true;
+ state.sync = true;
+ stream._write(chunk, encoding, state.onwrite);
+ state.sync = false;
+}
+
+function onwriteError(stream, state, sync, er, cb) {
+ if (sync)
+ process.nextTick(function() {
+ cb(er);
+ });
+ else
+ cb(er);
+
+ stream._writableState.errorEmitted = true;
+ stream.emit('error', er);
+}
+
+function onwriteStateUpdate(state) {
+ state.writing = false;
+ state.writecb = null;
+ state.length -= state.writelen;
+ state.writelen = 0;
+}
+
+function onwrite(stream, er) {
+ var state = stream._writableState;
+ var sync = state.sync;
+ var cb = state.writecb;
+
+ onwriteStateUpdate(state);
+
+ if (er)
+ onwriteError(stream, state, sync, er, cb);
+ else {
+ // Check if we're actually ready to finish, but don't emit yet
+ var finished = needFinish(stream, state);
+
+ if (!finished && !state.bufferProcessing && state.buffer.length)
+ clearBuffer(stream, state);
+
+ if (sync) {
+ process.nextTick(function() {
+ afterWrite(stream, state, finished, cb);
+ });
+ } else {
+ afterWrite(stream, state, finished, cb);
+ }
+ }
+}
+
+function afterWrite(stream, state, finished, cb) {
+ if (!finished)
+ onwriteDrain(stream, state);
+ cb();
+ if (finished)
+ finishMaybe(stream, state);
+}
+
+// Must force callback to be called on nextTick, so that we don't
+// emit 'drain' before the write() consumer gets the 'false' return
+// value, and has a chance to attach a 'drain' listener.
+function onwriteDrain(stream, state) {
+ if (state.length === 0 && state.needDrain) {
+ state.needDrain = false;
+ stream.emit('drain');
+ }
+}
+
+
+// if there's something in the buffer waiting, then process it
+function clearBuffer(stream, state) {
+ state.bufferProcessing = true;
+
+ for (var c = 0; c < state.buffer.length; c++) {
+ var entry = state.buffer[c];
+ var chunk = entry.chunk;
+ var encoding = entry.encoding;
+ var cb = entry.callback;
+ var len = state.objectMode ? 1 : chunk.length;
+
+ doWrite(stream, state, len, chunk, encoding, cb);
+
+ // if we didn't call the onwrite immediately, then
+ // it means that we need to wait until it does.
+ // also, that means that the chunk and cb are currently
+ // being processed, so move the buffer counter past them.
+ if (state.writing) {
+ c++;
+ break;
+ }
+ }
+
+ state.bufferProcessing = false;
+ if (c < state.buffer.length)
+ state.buffer = state.buffer.slice(c);
+ else
+ state.buffer.length = 0;
+}
+
+Writable.prototype._write = function(chunk, encoding, cb) {
+ cb(new Error('not implemented'));
+};
+
+Writable.prototype.end = function(chunk, encoding, cb) {
+ var state = this._writableState;
+
+ if (typeof chunk === 'function') {
+ cb = chunk;
+ chunk = null;
+ encoding = null;
+ } else if (typeof encoding === 'function') {
+ cb = encoding;
+ encoding = null;
+ }
+
+ if (typeof chunk !== 'undefined' && chunk !== null)
+ this.write(chunk, encoding);
+
+ // ignore unnecessary end() calls.
+ if (!state.ending && !state.finished)
+ endWritable(this, state, cb);
+};
+
+
+function needFinish(stream, state) {
+ return (state.ending &&
+ state.length === 0 &&
+ !state.finished &&
+ !state.writing);
+}
+
+function finishMaybe(stream, state) {
+ var need = needFinish(stream, state);
+ if (need) {
+ state.finished = true;
+ stream.emit('finish');
+ }
+ return need;
+}
+
+function endWritable(stream, state, cb) {
+ state.ending = true;
+ finishMaybe(stream, state);
+ if (cb) {
+ if (state.finished)
+ process.nextTick(cb);
+ else
+ stream.once('finish', cb);
+ }
+ state.ended = true;
+}
+
+}).call(this,require('_process'))
+},{"./_stream_duplex":12,"_process":10,"buffer":3,"core-util-is":17,"inherits":8,"stream":22}],17:[function(require,module,exports){
+(function (Buffer){
+// Copyright Joyent, Inc. and other Node contributors.
+//
+// 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.
+
+// NOTE: These type checking functions intentionally don't use `instanceof`
+// because it is fragile and can be easily faked with `Object.create()`.
+function isArray(ar) {
+ return Array.isArray(ar);
+}
+exports.isArray = isArray;
+
+function isBoolean(arg) {
+ return typeof arg === 'boolean';
+}
+exports.isBoolean = isBoolean;
+
+function isNull(arg) {
+ return arg === null;
+}
+exports.isNull = isNull;
+
+function isNullOrUndefined(arg) {
+ return arg == null;
+}
+exports.isNullOrUndefined = isNullOrUndefined;
+
+function isNumber(arg) {
+ return typeof arg === 'number';
+}
+exports.isNumber = isNumber;
+
+function isString(arg) {
+ return typeof arg === 'string';
+}
+exports.isString = isString;
+
+function isSymbol(arg) {
+ return typeof arg === 'symbol';
+}
+exports.isSymbol = isSymbol;
+
+function isUndefined(arg) {
+ return arg === void 0;
+}
+exports.isUndefined = isUndefined;
+
+function isRegExp(re) {
+ return isObject(re) && objectToString(re) === '[object RegExp]';
+}
+exports.isRegExp = isRegExp;
+
+function isObject(arg) {
+ return typeof arg === 'object' && arg !== null;
+}
+exports.isObject = isObject;
+
+function isDate(d) {
+ return isObject(d) && objectToString(d) === '[object Date]';
+}
+exports.isDate = isDate;
+
+function isError(e) {
+ return isObject(e) &&
+ (objectToString(e) === '[object Error]' || e instanceof Error);
+}
+exports.isError = isError;
+
+function isFunction(arg) {
+ return typeof arg === 'function';
+}
+exports.isFunction = isFunction;
+
+function isPrimitive(arg) {
+ return arg === null ||
+ typeof arg === 'boolean' ||
+ typeof arg === 'number' ||
+ typeof arg === 'string' ||
+ typeof arg === 'symbol' || // ES6 symbol
+ typeof arg === 'undefined';
+}
+exports.isPrimitive = isPrimitive;
+
+function isBuffer(arg) {
+ return Buffer.isBuffer(arg);
+}
+exports.isBuffer = isBuffer;
+
+function objectToString(o) {
+ return Object.prototype.toString.call(o);
+}
+}).call(this,require("buffer").Buffer)
+},{"buffer":3}],18:[function(require,module,exports){
+module.exports = require("./lib/_stream_passthrough.js")
+
+},{"./lib/_stream_passthrough.js":13}],19:[function(require,module,exports){
+var Stream = require('stream'); // hack to fix a circular dependency issue when used with browserify
+exports = module.exports = require('./lib/_stream_readable.js');
+exports.Stream = Stream;
+exports.Readable = exports;
+exports.Writable = require('./lib/_stream_writable.js');
+exports.Duplex = require('./lib/_stream_duplex.js');
+exports.Transform = require('./lib/_stream_transform.js');
+exports.PassThrough = require('./lib/_stream_passthrough.js');
+
+},{"./lib/_stream_duplex.js":12,"./lib/_stream_passthrough.js":13,"./lib/_stream_readable.js":14,"./lib/_stream_transform.js":15,"./lib/_stream_writable.js":16,"stream":22}],20:[function(require,module,exports){
+module.exports = require("./lib/_stream_transform.js")
+
+},{"./lib/_stream_transform.js":15}],21:[function(require,module,exports){
+module.exports = require("./lib/_stream_writable.js")
+
+},{"./lib/_stream_writable.js":16}],22:[function(require,module,exports){
+// Copyright Joyent, Inc. and other Node contributors.
+//
+// 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.
+
+module.exports = Stream;
+
+var EE = require('events').EventEmitter;
+var inherits = require('inherits');
+
+inherits(Stream, EE);
+Stream.Readable = require('readable-stream/readable.js');
+Stream.Writable = require('readable-stream/writable.js');
+Stream.Duplex = require('readable-stream/duplex.js');
+Stream.Transform = require('readable-stream/transform.js');
+Stream.PassThrough = require('readable-stream/passthrough.js');
+
+// Backwards-compat with node 0.4.x
+Stream.Stream = Stream;
+
+
+
+// old-style streams. Note that the pipe method (the only relevant
+// part of this class) is overridden in the Readable class.
+
+function Stream() {
+ EE.call(this);
+}
+
+Stream.prototype.pipe = function(dest, options) {
+ var source = this;
+
+ function ondata(chunk) {
+ if (dest.writable) {
+ if (false === dest.write(chunk) && source.pause) {
+ source.pause();
+ }
+ }
+ }
+
+ source.on('data', ondata);
+
+ function ondrain() {
+ if (source.readable && source.resume) {
+ source.resume();
+ }
+ }
+
+ dest.on('drain', ondrain);
+
+ // If the 'end' option is not supplied, dest.end() will be called when
+ // source gets the 'end' or 'close' events. Only dest.end() once.
+ if (!dest._isStdio && (!options || options.end !== false)) {
+ source.on('end', onend);
+ source.on('close', onclose);
+ }
+
+ var didOnEnd = false;
+ function onend() {
+ if (didOnEnd) return;
+ didOnEnd = true;
+
+ dest.end();
+ }
+
+
+ function onclose() {
+ if (didOnEnd) return;
+ didOnEnd = true;
+
+ if (typeof dest.destroy === 'function') dest.destroy();
+ }
+
+ // don't leave dangling pipes when there are errors.
+ function onerror(er) {
+ cleanup();
+ if (EE.listenerCount(this, 'error') === 0) {
+ throw er; // Unhandled stream error in pipe.
+ }
+ }
+
+ source.on('error', onerror);
+ dest.on('error', onerror);
+
+ // remove all the event listeners that were added.
+ function cleanup() {
+ source.removeListener('data', ondata);
+ dest.removeListener('drain', ondrain);
+
+ source.removeListener('end', onend);
+ source.removeListener('close', onclose);
+
+ source.removeListener('error', onerror);
+ dest.removeListener('error', onerror);
+
+ source.removeListener('end', cleanup);
+ source.removeListener('close', cleanup);
+
+ dest.removeListener('close', cleanup);
+ }
+
+ source.on('end', cleanup);
+ source.on('close', cleanup);
+
+ dest.on('close', cleanup);
+
+ dest.emit('pipe', source);
+
+ // Allow for unix-like usage: A.pipe(B).pipe(C)
+ return dest;
+};
+
+},{"events":7,"inherits":8,"readable-stream/duplex.js":11,"readable-stream/passthrough.js":18,"readable-stream/readable.js":19,"readable-stream/transform.js":20,"readable-stream/writable.js":21}],23:[function(require,module,exports){
+// Copyright Joyent, Inc. and other Node contributors.
+//
+// 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.
+
+var Buffer = require('buffer').Buffer;
+
+var isBufferEncoding = Buffer.isEncoding
+ || function(encoding) {
+ switch (encoding && encoding.toLowerCase()) {
+ case 'hex': case 'utf8': case 'utf-8': case 'ascii': case 'binary': case 'base64': case 'ucs2': case 'ucs-2': case 'utf16le': case 'utf-16le': case 'raw': return true;
+ default: return false;
+ }
+ }
+
+
+function assertEncoding(encoding) {
+ if (encoding && !isBufferEncoding(encoding)) {
+ throw new Error('Unknown encoding: ' + encoding);
+ }
+}
+
+// StringDecoder provides an interface for efficiently splitting a series of
+// buffers into a series of JS strings without breaking apart multi-byte
+// characters. CESU-8 is handled as part of the UTF-8 encoding.
+//
+// @TODO Handling all encodings inside a single object makes it very difficult
+// to reason about this code, so it should be split up in the future.
+// @TODO There should be a utf8-strict encoding that rejects invalid UTF-8 code
+// points as used by CESU-8.
+var StringDecoder = exports.StringDecoder = function(encoding) {
+ this.encoding = (encoding || 'utf8').toLowerCase().replace(/[-_]/, '');
+ assertEncoding(encoding);
+ switch (this.encoding) {
+ case 'utf8':
+ // CESU-8 represents each of Surrogate Pair by 3-bytes
+ this.surrogateSize = 3;
+ break;
+ case 'ucs2':
+ case 'utf16le':
+ // UTF-16 represents each of Surrogate Pair by 2-bytes
+ this.surrogateSize = 2;
+ this.detectIncompleteChar = utf16DetectIncompleteChar;
+ break;
+ case 'base64':
+ // Base-64 stores 3 bytes in 4 chars, and pads the remainder.
+ this.surrogateSize = 3;
+ this.detectIncompleteChar = base64DetectIncompleteChar;
+ break;
+ default:
+ this.write = passThroughWrite;
+ return;
+ }
+
+ // Enough space to store all bytes of a single character. UTF-8 needs 4
+ // bytes, but CESU-8 may require up to 6 (3 bytes per surrogate).
+ this.charBuffer = new Buffer(6);
+ // Number of bytes received for the current incomplete multi-byte character.
+ this.charReceived = 0;
+ // Number of bytes expected for the current incomplete multi-byte character.
+ this.charLength = 0;
+};
+
+
+// write decodes the given buffer and returns it as JS string that is
+// guaranteed to not contain any partial multi-byte characters. Any partial
+// character found at the end of the buffer is buffered up, and will be
+// returned when calling write again with the remaining bytes.
+//
+// Note: Converting a Buffer containing an orphan surrogate to a String
+// currently works, but converting a String to a Buffer (via `new Buffer`, or
+// Buffer#write) will replace incomplete surrogates with the unicode
+// replacement character. See https://codereview.chromium.org/121173009/ .
+StringDecoder.prototype.write = function(buffer) {
+ var charStr = '';
+ // if our last write ended with an incomplete multibyte character
+ while (this.charLength) {
+ // determine how many remaining bytes this buffer has to offer for this char
+ var available = (buffer.length >= this.charLength - this.charReceived) ?
+ this.charLength - this.charReceived :
+ buffer.length;
+
+ // add the new bytes to the char buffer
+ buffer.copy(this.charBuffer, this.charReceived, 0, available);
+ this.charReceived += available;
+
+ if (this.charReceived < this.charLength) {
+ // still not enough chars in this buffer? wait for more ...
+ return '';
+ }
+
+ // remove bytes belonging to the current character from the buffer
+ buffer = buffer.slice(available, buffer.length);
+
+ // get the character that was split
+ charStr = this.charBuffer.slice(0, this.charLength).toString(this.encoding);
+
+ // CESU-8: lead surrogate (D800-DBFF) is also the incomplete character
+ var charCode = charStr.charCodeAt(charStr.length - 1);
+ if (charCode >= 0xD800 && charCode <= 0xDBFF) {
+ this.charLength += this.surrogateSize;
+ charStr = '';
+ continue;
+ }
+ this.charReceived = this.charLength = 0;
+
+ // if there are no more bytes in this buffer, just emit our char
+ if (buffer.length === 0) {
+ return charStr;
+ }
+ break;
+ }
+
+ // determine and set charLength / charReceived
+ this.detectIncompleteChar(buffer);
+
+ var end = buffer.length;
+ if (this.charLength) {
+ // buffer the incomplete character bytes we got
+ buffer.copy(this.charBuffer, 0, buffer.length - this.charReceived, end);
+ end -= this.charReceived;
+ }
+
+ charStr += buffer.toString(this.encoding, 0, end);
+
+ var end = charStr.length - 1;
+ var charCode = charStr.charCodeAt(end);
+ // CESU-8: lead surrogate (D800-DBFF) is also the incomplete character
+ if (charCode >= 0xD800 && charCode <= 0xDBFF) {
+ var size = this.surrogateSize;
+ this.charLength += size;
+ this.charReceived += size;
+ this.charBuffer.copy(this.charBuffer, size, 0, size);
+ buffer.copy(this.charBuffer, 0, 0, size);
+ return charStr.substring(0, end);
+ }
+
+ // or just emit the charStr
+ return charStr;
+};
+
+// detectIncompleteChar determines if there is an incomplete UTF-8 character at
+// the end of the given buffer. If so, it sets this.charLength to the byte
+// length that character, and sets this.charReceived to the number of bytes
+// that are available for this character.
+StringD
[Diff truncated. Use the raw view or local clone for the full content.]