patx/makeitpdf

Move conversion into the browser, drop the backend

Commit 029793f · patx · 2026-08-09T01:08:07-04:00

Changeset
029793f652fadd5ccdfad4d17f503635494daa5e
Parents
c0733669b8b13d67df70ec6bcc00605d07c70cd5

View source at this commit

Move conversion into the browser, drop the backend

Images are decoded, downscaled, and written to a PDF with jsPDF on the
user's device. No upload, no server: the whole thing is a static folder.

- pages.js holds page geometry, free of DOM and jsPDF so node can test it
- EXIF orientation via createImageBitmap imageOrientation "from-image";
  a PDF page has no orientation metadata, so it must be baked into pixels
- TIFF has no browser decoder, so UTIF unpacks it; pako loads first because
  UTIF reads self.pako at load time to inflate deflate-compressed TIFFs
- resize happens in createImageBitmap, not drawImage, which aliases badly
  when downscaling 24MP in one step
- dependencies vendored, so no CDN and no third-party runtime requests

The FastAPI + Pillow implementation is in c073366.

Co-Authored-By: Claude Opus 5 <[email protected]>

Comments

No comments yet.

Log in to comment

Diff

diff --git a/.gitignore b/.gitignore
index 814c4ab..b28497c 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1,7 +1,3 @@
-.venv/
-__pycache__/
-*.py[cod]
-.pytest_cache/
-.ruff_cache/
+node_modules/
 *.pdf
 .DS_Store
diff --git a/.python-version b/.python-version
deleted file mode 100644
index 6324d40..0000000
--- a/.python-version
+++ /dev/null
@@ -1 +0,0 @@
-3.14
diff --git a/README.md b/README.md
index fe0f3e5..bf6ab00 100644
--- a/README.md
+++ b/README.md
@@ -1,112 +1,121 @@
 # MakeItPDF
 
-Upload up to 10 images, get back a single PDF. FastAPI on the back, Jinja2 and
-plain JavaScript on the front.
+Combine up to 10 images into a single PDF. Everything runs in the browser —
+no server, no upload, no account.
 
 ## Run it
 
+It's a static site. Any file server will do:
+
 ```bash
-uv sync
-uv run uvicorn app.main:app --reload
+npm run serve       # python3 -m http.server 8000
 ```
 
 Then open http://127.0.0.1:8000.
 
+Deploying is copying the folder to GitHub Pages, Netlify, S3, or any CDN.
+There is nothing to install and nothing to keep running.
+
 ## Tests
 
 ```bash
-uv run pytest
+npm test            # node --test "tests/*.test.js"
 ```
 
+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.
+
 ## How it works
 
-`GET /` renders the composer. `POST /convert` takes a multipart body of
-`images` (repeated) and responds with `application/pdf` as an attachment.
+`app.js` walks the images in order and, for each one:
 
-```bash
-curl -X POST http://127.0.0.1:8000/convert \
-  -F "[email protected]" \
-  -F "[email protected]" \
-  -o out.pdf
-```
-
-Page order follows the order the parts arrive in.
+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.
 
-### Page size
+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.
 
-Pages are **A4**. The interface offers no choice — change
-`DEFAULT_PAGE_SIZE` in `app/config.py` to `letter` for US paper.
+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.
 
-Each page takes the orientation of its own image, so a batch can mix portrait
-and landscape. The image is centred inside a 24 pt margin.
+### Formats
 
-The other sizes stay reachable through the API with an optional `page_size`
-field (`a4`, `letter`, `fit`):
+JPEG, PNG, WebP, GIF, and BMP go through the browser's own decoder.
 
-```bash
-curl -X POST http://127.0.0.1:8000/convert \
-  -F "page_size=fit" -F "[email protected]" -o out.pdf
-```
+**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.
 
-`fit` gives every page the size of its own image. It reads the image's DPI when
-the file records a plausible one (36–1200, which is what scanners write), and
-otherwise assumes 150 DPI — a 4032 px phone photo becomes a 27 in page rather
-than the 56 in you would get from the 72 DPI PDF default.
+`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.
 
-Every page is rasterised at 150 DPI (`RENDER_DPI`) so one resolution describes
-the whole document. Pillow's PDF writer applies a single resolution to all
-frames, so mixing per-image densities would silently distort pages.
+TIFF thumbnails show a blank sheet outline, since `<img>` can't render TIFF
+either. The PDF page is correct regardless.
 
 ### Limits
 
-Set in `app/config.py`; the template feeds the same numbers to the browser so
-client and server can't drift apart.
+In `index.html` as `data-` attributes on the form, so the markup and `app.js`
+can't drift apart.
 
 - 10 images per PDF
-- 25 MB per image, 80 MB per batch
-- 80 megapixels per image (decompression-bomb guard)
-- JPEG, PNG, WebP, GIF, BMP, TIFF
+- 25 MB per image
 
-Uploads are read in 1 MB chunks and abandoned the moment they pass their
-budget, so an oversized file is rejected without being buffered whole.
+There is no batch total limit — that existed to bound an HTTP upload, and
+there is no upload any more.
 
-### Handling notes
+## Verified behaviour
 
-- Format is determined by decoding the bytes, not by the browser's
-  `Content-Type` header.
-- EXIF orientation is baked in, so sideways phone photos come out upright.
-- Transparency is composited onto white; PDF has no alpha here, so a
-  transparent PNG would otherwise render on black.
-- Multi-frame files (animated GIF/WebP) contribute their first frame.
+Checked by running it and inspecting the resulting PDF, not just the success
+screen:
 
-Nothing is written to disk and nothing is stored — the PDF is built in memory
-and streamed back.
+- 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.
 
-## Front end
+## Layout
 
-The form works without JavaScript: pick files with the native input, submit,
-get a PDF. Errors re-render the page with a message.
+```
+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
+```
 
-With JavaScript, `app/static/composer.js` takes over and adds a thumbnail grid,
-drag-to-reorder (plus arrow buttons, since dragging isn't reachable by keyboard
-and doesn't work on touch), per-image removal, client-side validation against
-the same limits, and a "your PDF is ready" step that keeps you on the page
-instead of firing a download at you. It posts with `Accept: application/json`
-so the server answers errors as JSON instead of HTML.
+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 design is one typeface (Figtree), a white ground, and a single red used in
-exactly two places: the logo and the button you press.
+The only external request on the page is the Google Fonts stylesheet for
+Figtree. Drop that `<link>` and the system sans-serif takes over.
 
-## Layout
+## History
 
-```
-app/
-  main.py        routes, upload limits, error negotiation
-  converter.py   images -> PDF, no web imports, directly testable
-  config.py      limits and page geometry
-  templates/     base.html, index.html
-  static/        style.css, composer.js
-tests/
-  test_converter.py   geometry, formats, EXIF, rejections
-  test_api.py         endpoints, limits, content negotiation
-```
+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.
diff --git a/app/static/composer.js b/app.js
similarity index 56%
rename from app/static/composer.js
rename to app.js
index ec5d918..35cc1ee 100644
--- a/app/static/composer.js
+++ b/app.js
@@ -1,17 +1,19 @@
-/* Progressive enhancement.
-   Without this file the form still posts natively: pick files, press the
-   button, get a PDF. With it you get thumbnails, reordering, and a
-   download step that keeps you on the page. */
+/* 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. */
 
 (() => {
   "use strict";
 
+  const { jsPDF } = window.jspdf;
+
   const form = document.getElementById("composer");
   const dropzone = document.getElementById("dropzone");
   const tray = document.getElementById("tray");
   const trayCount = document.getElementById("tray-count");
   const sheetsEl = document.getElementById("sheets");
   const picker = document.getElementById("picker");
+  const formats = document.getElementById("formats");
   const template = document.getElementById("sheet-template");
   const buildBtn = document.getElementById("build");
   const clearBtn = document.getElementById("clear");
@@ -21,12 +23,9 @@
   const downloadLink = document.getElementById("download");
   const restartBtn = document.getElementById("restart");
 
-  if (!form || !template) return;
-
-  // Limits come from the server so the two layers can't drift apart.
   const MAX_FILES = Number(form.dataset.maxFiles);
   const MAX_FILE_BYTES = Number(form.dataset.maxFileBytes);
-  const MAX_TOTAL_BYTES = Number(form.dataset.maxTotalBytes);
+  const JPEG_QUALITY = 0.9;
 
   /** @type {{id:number,file:File,url:string}[]} */
   let pages = [];
@@ -35,16 +34,11 @@
   let busy = false;
   let lastUrl = null;
 
-  const formatBytes = (bytes) => {
-    if (bytes < 1024) return `${bytes} B`;
-    if (bytes < 1024 * 1024) return `${Math.round(bytes / 1024)} KB`;
-    return `${(bytes / (1024 * 1024)).toFixed(1)} MB`;
-  };
-
+  const { formatBytes } = Pages;
   const totalBytes = () => pages.reduce((sum, page) => sum + page.file.size, 0);
 
   function setAlert(message) {
-    let alert = document.querySelector(".alert");
+    let alert = document.querySelector("main > .alert");
     if (!message) {
       if (alert) alert.remove();
       return;
@@ -58,33 +52,141 @@
     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 budget = MAX_TOTAL_BYTES - totalBytes();
 
     for (const file of Array.from(fileList)) {
       if (room <= 0) {
         problems.push(`${MAX_FILES} images is the limit — skipped the rest.`);
         break;
       }
-      if (!file.type.startsWith("image/")) {
+      // 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.`);
         continue;
       }
       if (file.size > MAX_FILE_BYTES) {
-        problems.push(`${file.name} is ${formatBytes(file.size)}, over the ${formatBytes(MAX_FILE_BYTES)} limit.`);
-        continue;
-      }
-      if (file.size > budget) {
-        problems.push(`${file.name} pushes the batch past ${formatBytes(MAX_TOTAL_BYTES)}.`);
+        problems.push(
+          `${file.name} is ${formatBytes(file.size)}, over the ${formatBytes(MAX_FILE_BYTES)} limit.`
+        );
         continue;
       }
       pages.push({ id: nextId++, file, url: URL.createObjectURL(file) });
       room -= 1;
-      budget -= file.size;
     }
 
     setAlert(problems.length ? problems[0] : "");
@@ -129,7 +231,15 @@
       node.dataset.id = String(page.id);
       node.querySelector(".card__num").textContent = String(index + 1);
       node.querySelector(".card__name").textContent = page.file.name;
-      node.querySelector(".card__img").src = page.url;
+
+      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", () => {
+        img.remove();
+        node.querySelector(".card__frame").classList.add("card__frame--blank");
+      }, { once: true });
 
       const up = node.querySelector(".card__arrow--up");
       const down = node.querySelector(".card__arrow--down");
@@ -175,7 +285,8 @@
     const count = pages.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 ? "image" : "images"} · ${formatBytes(totalBytes())}`;
     buildBtn.disabled = count === 0 || busy;
     buildBtn.hidden = count === 0;
   }
@@ -187,11 +298,12 @@
     form.hidden = false;
   }
 
-  function showDone(blob, filename) {
+  function showDone(blob) {
     if (lastUrl) URL.revokeObjectURL(lastUrl);
     lastUrl = URL.createObjectURL(blob);
+    const stamp = new Date().toISOString().slice(0, 19).replace(/[-:]/g, "").replace("T", "-");
     downloadLink.href = lastUrl;
-    downloadLink.download = filename;
+    downloadLink.download = `makeitpdf-${stamp}.pdf`;
     doneMeta.textContent =
       `${pages.length} ${pages.length === 1 ? "page" : "pages"} · ${formatBytes(blob.size)}`;
     form.hidden = true;
@@ -201,6 +313,9 @@
 
   /* ---- wiring ---------------------------------------------------------- */
 
+  formats.textContent =
+    `JPG · PNG · WEBP · GIF · BMP · TIFF · up to ${formatBytes(MAX_FILE_BYTES)} each`;
+
   picker.addEventListener("change", () => {
     accept(picker.files);
     picker.value = "";
@@ -211,7 +326,7 @@
   restartBtn.addEventListener("click", () => {
     clearAll();
     showComposer();
-    document.getElementById("picker").focus();
+    picker.focus();
   });
 
   ["dragenter", "dragover"].forEach((type) => {
@@ -235,39 +350,24 @@
     accept(event.dataTransfer.files);
   });
 
-  /* ---- submit ---------------------------------------------------------- */
+  /* ---- convert --------------------------------------------------------- */
 
   form.addEventListener("submit", async (event) => {
-    if (!pages.length) return;
     event.preventDefault();
-    if (busy) return;
+    if (busy || !pages.length) return;
 
     busy = true;
     setAlert("");
     progress.classList.add("is-running");
     buildBtn.disabled = true;
-    buildBtn.textContent = "Converting…";
-
-    const payload = new FormData();
-    pages.forEach((page) => payload.append("images", page.file, page.file.name));
 
     try {
-      const response = await fetch(form.action, {
-        method: "POST",
-        body: payload,
-        headers: { Accept: "application/json" },
+      const blob = await buildPdf(pages, (current, total) => {
+        buildBtn.textContent = `Converting ${current} of ${total}…`;
       });
-
-      if (!response.ok) {
-        const body = await response.json().catch(() => ({}));
-        throw new Error(body.error || `The server returned ${response.status}.`);
-      }
-
-      const blob = await response.blob();
-      const match = (response.headers.get("Content-Disposition") || "").match(/filename="([^"]+)"/);
-      showDone(blob, match ? match[1] : "makeitpdf.pdf");
+      showDone(blob);
     } catch (error) {
-      setAlert(error.message);
+      setAlert(error.message || "Something went wrong building the PDF.");
     } finally {
       busy = false;
       progress.classList.remove("is-running");
diff --git a/app/config.py b/app/config.py
deleted file mode 100644
index 42430a2..0000000
--- a/app/config.py
+++ /dev/null
@@ -1,43 +0,0 @@
-"""Limits and tunables for the upload/convert pipeline."""
-
-from __future__ import annotations
-
-MAX_FILES = 10
-"""Hard cap on images per PDF."""
-
-MAX_FILE_BYTES = 25 * 1024 * 1024
-"""Largest single upload accepted (25 MB)."""
-
-MAX_TOTAL_BYTES = 80 * 1024 * 1024
-"""Largest combined upload accepted (80 MB)."""
-
-MAX_PIXELS = 80_000_000
-"""Decompression-bomb guard: reject images above 80 megapixels."""
-
-RENDER_DPI = 150
-"""Every page is rasterised at this density, so one resolution describes the
-whole document and page geometry stays exact."""
-
-FIT_FALLBACK_DPI = 150
-"""Assumed density for 'fit' pages whose file carries no sane DPI metadata.
-A 4032px phone photo becomes a ~27in page at 150 DPI rather than 56in at 72."""
-
-MARGIN_PT = 24
-"""Margin for fixed page sizes, in PostScript points (1/72in)."""
-
-ACCEPTED_FORMATS = {"JPEG", "PNG", "WEBP", "GIF", "BMP", "TIFF", "MPO"}
-"""Pillow format names we accept. MPO is what iPhone burst/HDR JPEGs decode as."""
-
-ACCEPT_ATTRIBUTE = "image/jpeg,image/png,image/webp,image/gif,image/bmp,image/tiff"
-"""Value for the file input's accept attribute."""
-
-# Page sizes in points, portrait orientation.
-PAGE_SIZES: dict[str, tuple[float, float] | None] = {
-    "fit": None,
-    "a4": (595.28, 841.89),
-    "letter": (612.0, 792.0),
-}
-
-DEFAULT_PAGE_SIZE = "a4"
-"""What the web form produces. The others stay reachable through the API;
-switch this to 'letter' for US paper."""
diff --git a/app/converter.py b/app/converter.py
deleted file mode 100644
index 4107b94..0000000
--- a/app/converter.py
+++ /dev/null
@@ -1,166 +0,0 @@
-"""Turn a batch of image bytes into a single PDF.
-
-Kept free of web-framework imports so it can be exercised directly in tests.
-
-Every page follows the same path: work out the page box in points, rasterise
-the image into a canvas sized for that box at ``RENDER_DPI``, then hand the
-whole run to Pillow with one resolution. A single global DPI keeps page
-geometry exact -- Pillow's PDF writer applies one resolution to every frame,
-so per-image DPI would otherwise silently distort pages.
-"""
-
-from __future__ import annotations
-
-import io
-from dataclasses import dataclass
-
-from PIL import Image, ImageOps, UnidentifiedImageError
-
-from app.config import (
-    ACCEPTED_FORMATS,
-    FIT_FALLBACK_DPI,
-    MARGIN_PT,
-    MAX_PIXELS,
-    PAGE_SIZES,
-    RENDER_DPI,
-)
-
-
-class ConversionError(Exception):
-    """Raised when a batch cannot be turned into a PDF."""
-
-
-@dataclass(frozen=True)
-class SourceImage:
-    """One uploaded image, already read into memory."""
-
-    filename: str
-    data: bytes
-
-
-def _sane_dpi(image: Image.Image) -> float | None:
-    """Return the file's own DPI when it looks trustworthy.
-
-    Scanners write real values here; phone cameras and screenshots often write
-    nothing, a placeholder 1, or a bogus 72 alongside 4000px of detail.
-    """
-    dpi = image.info.get("dpi")
-    if not dpi:
-        return None
-    try:
-        horizontal = float(dpi[0])
-    except (TypeError, ValueError, IndexError):
-        return None
-    if 36.0 <= horizontal <= 1200.0:
-        return horizontal
-    return None
-
-
-def _flatten(image: Image.Image) -> Image.Image:
-    """Drop to RGB, compositing any transparency onto white.
-
-    PDF has no alpha channel here, so a transparent PNG would otherwise render
-    with a black background.
-    """
-    if image.mode in ("RGBA", "LA") or (
-        image.mode == "P" and "transparency" in image.info
-    ):
-        rgba = image.convert("RGBA")
-        canvas = Image.new("RGB", rgba.size, "white")
-        canvas.paste(rgba, mask=rgba.split()[-1])
-        return canvas
-    if image.mode != "RGB":
-        return image.convert("RGB")
-    return image
-
-
-def _decode(source: SourceImage) -> Image.Image:
-    """Decode bytes into a flattened, upright RGB image.
-
-    The format is read from the decoded bytes rather than the browser's
-    content-type header, which is attacker-controlled.
-    """
-    try:
-        with Image.open(io.BytesIO(source.data)) as opened:
-            if opened.format not in ACCEPTED_FORMATS:
-                raise ConversionError(
-                    f"{source.filename}: {opened.format or 'unknown'} files aren't supported."
-                )
-            width, height = opened.size
-            if width * height > MAX_PIXELS:
-                raise ConversionError(
-                    f"{source.filename}: {width}x{height} is past the "
-                    f"{MAX_PIXELS // 1_000_000} megapixel limit."
-                )
-            opened.load()
-            # exif_transpose bakes in the orientation tag; phone photos are
-            # otherwise stored sideways and would come out rotated.
-            upright = ImageOps.exif_transpose(opened) or opened
-            flattened = _flatten(upright)
-            if flattened is upright and upright is opened:
-                flattened = opened.copy()
-            flattened.info = dict(opened.info)
-            return flattened
-    except ConversionError:
-        raise
-    except UnidentifiedImageError:
-        raise ConversionError(f"{source.filename} isn't an image we can read.") from None
-    except OSError as exc:
-        raise ConversionError(f"{source.filename} is damaged or incomplete.") from exc
-
-
-def _page_box(image: Image.Image, page_size: tuple[float, float] | None) -> tuple[float, float]:
-    """Page dimensions in points, oriented to match the image."""
-    width, height = image.size
-    if page_size is None:
-        dpi = _sane_dpi(image) or FIT_FALLBACK_DPI
-        return (width * 72.0 / dpi, height * 72.0 / dpi)
-    short, long = page_size
-    return (long, short) if width > height else (short, long)
-
-
-def _render(image: Image.Image, page_size: tuple[float, float] | None) -> Image.Image:
-    """Rasterise one image into its page canvas at RENDER_DPI."""
-    page_w_pt, page_h_pt = _page_box(image, page_size)
-    canvas_w = max(1, round(page_w_pt * RENDER_DPI / 72.0))
-    canvas_h = max(1, round(page_h_pt * RENDER_DPI / 72.0))
-
-    if page_size is None:
-        # The page *is* the image; only resample when the DPI assumption
-        # actually implies a different pixel count.
-        if (canvas_w, canvas_h) == image.size:
-            return image
-        return image.resize((canvas_w, canvas_h), Image.LANCZOS)
-
-    margin = round(MARGIN_PT * RENDER_DPI / 72.0)
-    box_w = max(1, canvas_w - margin * 2)
-    box_h = max(1, canvas_h - margin * 2)
-    scale = min(box_w / image.width, box_h / image.height)
-    target = (max(1, round(image.width * scale)), max(1, round(image.height * scale)))
-
-    fitted = image.resize(target, Image.LANCZOS) if target != image.size else image
-    page = Image.new("RGB", (canvas_w, canvas_h), "white")
-    page.paste(fitted, ((canvas_w - target[0]) // 2, (canvas_h - target[1]) // 2))
-    return page
-
-
-def images_to_pdf(sources: list[SourceImage], page_size: str = "fit") -> bytes:
-    """Convert images to a single PDF, one page per image, in list order."""
-    if not sources:
-        raise ConversionError("Add at least one image.")
-    if page_size not in PAGE_SIZES:
-        raise ConversionError(f"{page_size!r} isn't a page size we offer.")
-
-    box = PAGE_SIZES[page_size]
-    pages = [_render(_decode(source), box) for source in sources]
-
-    buffer = io.BytesIO()
-    first, rest = pages[0], pages[1:]
-    first.save(
-        buffer,
-        format="PDF",
-        save_all=True,
-        append_images=rest,
-        resolution=float(RENDER_DPI),
-    )
-    return buffer.getvalue()
diff --git a/app/main.py b/app/main.py
deleted file mode 100644
index cdd76f4..0000000
--- a/app/main.py
+++ /dev/null
@@ -1,125 +0,0 @@
-"""MakeItPDF -- stack up to ten images into one PDF."""
-
-from __future__ import annotations
-
-from datetime import datetime, timezone
-from pathlib import Path
-
-from fastapi import FastAPI, File, Form, Request, UploadFile
-from fastapi.responses import HTMLResponse, JSONResponse, Response
-from fastapi.staticfiles import StaticFiles
-from fastapi.templating import Jinja2Templates
-from starlette.concurrency import run_in_threadpool
-
-from app.config import (
-    ACCEPT_ATTRIBUTE,
-    DEFAULT_PAGE_SIZE,
-    MAX_FILE_BYTES,
-    MAX_FILES,
-    MAX_TOTAL_BYTES,
-    PAGE_SIZES,
-)
-from app.converter import ConversionError, SourceImage, images_to_pdf
-
-BASE_DIR = Path(__file__).resolve().parent
-
-app = FastAPI(title="MakeItPDF", docs_url=None, redoc_url=None)
-app.mount("/static", StaticFiles(directory=BASE_DIR / "static"), name="static")
-templates = Jinja2Templates(directory=BASE_DIR / "templates")
-
-CHUNK_BYTES = 1024 * 1024
-
-
-def _template_context(request: Request) -> dict:
-    return {
-        "request": request,
-        "max_files": MAX_FILES,
-        "max_file_mb": MAX_FILE_BYTES // (1024 * 1024),
-        "max_total_mb": MAX_TOTAL_BYTES // (1024 * 1024),
-        "accept": ACCEPT_ATTRIBUTE,
-    }
-
-
-def _wants_json(request: Request) -> bool:
-    """The JS layer asks for JSON; a plain form post gets HTML back."""
-    return "application/json" in request.headers.get("accept", "")
-
-
-def _fail(request: Request, message: str, status: int = 400) -> Response:
-    if _wants_json(request):
-        return JSONResponse({"error": message}, status_code=status)
-    context = _template_context(request) | {"error": message}
-    return templates.TemplateResponse(request, "index.html", context, status_code=status)
-
-
-async def _read_capped(upload: UploadFile, budget: int) -> bytes | None:
-    """Read an upload, stopping as soon as it exceeds ``budget`` bytes.
-
-    Uploads are streamed rather than read whole so an oversized file is
-    rejected without first pulling all of it into memory.
-    """
-    chunks: list[bytes] = []
-    total = 0
-    while chunk := await upload.read(CHUNK_BYTES):
-        total += len(chunk)
-        if total > budget:
-            return None
-        chunks.append(chunk)
-    return b"".join(chunks)
-
-
[email protected]("/", response_class=HTMLResponse)
-async def index(request: Request) -> Response:
-    return templates.TemplateResponse(request, "index.html", _template_context(request))
-
-
[email protected]("/healthz")
-async def healthz() -> dict[str, str]:
-    return {"status": "ok"}
-
-
[email protected]("/convert")
-async def convert(
-    request: Request,
-    images: list[UploadFile] = File(default=[]),
-    page_size: str = Form(default=DEFAULT_PAGE_SIZE),
-) -> Response:
-    named = [f for f in images if f.filename]
-    if not named:
-        return _fail(request, "Choose at least one image.")
-    if len(named) > MAX_FILES:
-        return _fail(request, f"That's {len(named)} images. The limit is {MAX_FILES}.")
-    if page_size not in PAGE_SIZES:
-        return _fail(request, "Pick a page size from the list.")
-
-    sources: list[SourceImage] = []
-    total = 0
-    for upload in named:
-        remaining = min(MAX_FILE_BYTES, MAX_TOTAL_BYTES - total)
-        data = await _read_capped(upload, remaining)
-        if data is None:
-            limit_mb = MAX_FILE_BYTES // (1024 * 1024)
-            total_mb = MAX_TOTAL_BYTES // (1024 * 1024)
-            return _fail(
-                request,
-                f"{upload.filename} pushes the upload past its limit "
-                f"({limit_mb} MB per image, {total_mb} MB total).",
-                status=413,
-            )
-        total += len(data)
-        sources.append(SourceImage(filename=upload.filename or "image", data=data))
-
-    try:
-        pdf = await run_in_threadpool(images_to_pdf, sources, page_size)
-    except ConversionError as exc:
-        return _fail(request, str(exc))
-
-    stamp = datetime.now(timezone.utc).strftime("%Y%m%d-%H%M%S")
-    return Response(
-        content=pdf,
-        media_type="application/pdf",
-        headers={
-            "Content-Disposition": f'attachment; filename="makeitpdf-{stamp}.pdf"',
-            "X-Page-Count": str(len(sources)),
-        },
-    )
diff --git a/app/templates/base.html b/app/templates/base.html
deleted file mode 100644
index 96fa7b0..0000000
--- a/app/templates/base.html
+++ /dev/null
@@ -1,19 +0,0 @@
-<!doctype html>
-<html lang="en">
-<head>
-<meta charset="utf-8">
-<meta name="viewport" content="width=device-width, initial-scale=1">
-<title>{% block title %}MakeItPDF{% endblock %}</title>
-<meta name="description" content="Combine up to 10 images into a single PDF. Free, no account.">
-<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">
-<link rel="stylesheet" href="{{ url_for('static', path='/style.css') }}">
-<script>document.documentElement.classList.add("js");</script>
-<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>
-<body>
-{% block body %}{% endblock %}
-{% block scripts %}{% endblock %}
-</body>
-</html>
diff --git a/app/templates/index.html b/index.html
similarity index 61%
rename from app/templates/index.html
rename to index.html
index 6f4f6f4..a03c147 100644
--- a/app/templates/index.html
+++ b/index.html
@@ -1,6 +1,18 @@
-{% extends "base.html" %}
+<!doctype html>
+<html lang="en">
+<head>
+<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">
+<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>
+<body>
 
-{% block body %}
 <div class="progress" id="progress" aria-hidden="true"></div>
 
 <main class="page">
@@ -16,26 +28,25 @@
       </svg>
       <span class="logo__text">MakeIt<span class="logo__accent">PDF</span></span>
     </h1>
-    <p class="intro__sub">Combine up to {{ max_files }} images into a single PDF.
-      Free, and nothing is stored.</p>
+    <p class="intro__sub">Combine up to 10 images into a single PDF.
+      Nothing leaves your device.</p>
   </div>
 
-  {% if error %}
-  <p class="alert" role="alert">{{ error }}</p>
-  {% endif %}
+  <noscript>
+    <p class="alert">This tool builds the PDF in your browser, so it needs
+      JavaScript switched on.</p>
+  </noscript>
 
-  <form class="composer" id="composer" method="post" action="/convert" enctype="multipart/form-data"
-        data-max-files="{{ max_files }}"
-        data-max-file-bytes="{{ max_file_mb * 1024 * 1024 }}"
-        data-max-total-bytes="{{ max_total_mb * 1024 * 1024 }}">
+  <!-- The limits live here so app.js and the visible text can't drift apart. -->
+  <form class="composer" id="composer" data-max-files="10" data-max-file-bytes="26214400">
 
-    <input class="picker" id="picker" type="file" name="images" multiple accept="{{ accept }}">
+    <input class="picker" id="picker" type="file" multiple
+           accept="image/jpeg,image/png,image/webp,image/gif,image/bmp,image/tiff">
 
     <div class="dropzone" id="dropzone">
       <label class="btn btn--accent btn--lg" for="picker">Select images</label>
       <p class="dropzone__hint">or drop them here</p>
-      <p class="dropzone__formats">JPG &middot; PNG &middot; WEBP &middot; GIF &middot; BMP &middot; TIFF
-        &middot; up to {{ max_file_mb }} MB each</p>
+      <p class="dropzone__formats" id="formats"></p>
     </div>
 
     <section class="tray" id="tray" hidden>
@@ -46,7 +57,7 @@
       <ol class="grid" id="sheets"></ol>
     </section>
 
-    <button class="btn btn--accent btn--block" id="build" type="submit">Convert to PDF</button>
+    <button class="btn btn--accent btn--block" id="build" type="submit" hidden>Convert to PDF</button>
   </form>
 
   <section class="done" id="done" hidden>
@@ -61,8 +72,8 @@
     <button class="link" id="restart" type="button">Convert more images</button>
   </section>
 
-  <p class="footnote">Images are converted on the server and discarded the moment
-    the PDF is sent.</p>
+  <p class="footnote">Your images are never uploaded. The PDF is built on this
+    device and stays here.</p>
 </main>
 
 <template id="sheet-template">
@@ -95,8 +106,13 @@
     <span class="card__name"></span>
   </li>
 </template>
-{% endblock %}
 
-{% block scripts %}
-<script src="{{ url_for('static', path='/composer.js') }}" defer></script>
-{% endblock %}
+<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/pako_inflate.min.js"></script>
+<script src="vendor/UTIF.js"></script>
+<script src="pages.js"></script>
+<script src="app.js"></script>
+</body>
+</html>
diff --git a/package.json b/package.json
new file mode 100644
index 0000000..81fa59b
--- /dev/null
+++ b/package.json
@@ -0,0 +1,10 @@
+{
+  "name": "makeitpdf",
+  "version": "2.0.0",
+  "private": true,
+  "description": "Combine up to 10 images into a single PDF, entirely in the browser",
+  "scripts": {
+    "test": "node --test \"tests/*.test.js\"",
+    "serve": "python3 -m http.server 8000"
+  }
+}
diff --git a/pages.js b/pages.js
new file mode 100644
index 0000000..42966e1
--- /dev/null
+++ b/pages.js
@@ -0,0 +1,97 @@
+/* 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. */
+
+(function (root, factory) {
+  if (typeof module === "object" && module.exports) module.exports = factory();
+  else root.Pages = factory();
+})(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;
+
+  /**
+   * 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;
+    return {
+      width: landscape ? long : short,
+      height: landscape ? short : long,
+      orientation: landscape ? "l" : "p",
+    };
+  }
+
+  /**
+   * 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);
+    const boxHeight = Math.max(1, pageHeight - inset * 2);
+    const scale = Math.min(boxWidth / imageWidth, boxHeight / imageHeight);
+    const width = imageWidth * scale;
+    const height = imageHeight * scale;
+    return {
+      x: (pageWidth - width) / 2,
+      y: (pageHeight - height) / 2,
+      width,
+      height,
+    };
+  }
+
+  /**
+   * 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 {
+      width: Math.max(1, Math.round(placement.width * density)),
+      height: Math.max(1, Math.round(placement.height * density)),
+    };
+  }
+
+  function formatBytes(bytes) {
+    if (bytes < 1024) return bytes + " B";
+    if (bytes < 1024 * 1024) return Math.round(bytes / 1024) + " KB";
+    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 {
+    A4_PORTRAIT,
+    MARGIN_PT,
+    RENDER_DPI,
+    pageSize,
+    placeImage,
+    rasterSize,
+    formatBytes,
+    isTiff,
+  };
+});
diff --git a/pyproject.toml b/pyproject.toml
deleted file mode 100644
index 05e4a4b..0000000
--- a/pyproject.toml
+++ /dev/null
@@ -1,23 +0,0 @@
-[project]
-name = "makeitpdf"
-version = "0.1.0"
-description = "Stack up to ten images into a single PDF"
-readme = "README.md"
-requires-python = ">=3.14"
-dependencies = [
-    "fastapi>=0.115",
-    "jinja2>=3.1",
-    "pillow>=11.0",
-    "python-multipart>=0.0.12",
-    "uvicorn[standard]>=0.32",
-]
-
-[dependency-groups]
-dev = [
-    "httpx>=0.28.1",
-    "pytest>=9.1.1",
-]
-
-[tool.pytest.ini_options]
-pythonpath = ["."]
-testpaths = ["tests"]
diff --git a/app/static/style.css b/style.css
similarity index 96%
rename from app/static/style.css
rename to style.css
index 79f5b51..5811adf 100644
--- a/app/static/style.css
+++ b/style.css
@@ -165,15 +165,13 @@ ol { padding: 0; list-style: none; }
 
 /* ---- Dropzone ----------------------------------------------------------- */
 
-/* Visible only to assistive tech and the no-JS fallback below. */
-.js .picker {
+/* Kept in the DOM and focusable, but the label is what you see. */
+.picker {
   position: absolute;
   width: 1px; height: 1px;
   opacity: 0;
 }
 
-.picker { display: block; margin: 1.5rem auto 0; }
-
 .dropzone {
   display: grid;
   justify-items: center;
@@ -257,6 +255,16 @@ 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;
+  background: #fff;
+}
+
 .card__num {
   position: absolute;
   top: 0.4rem;
diff --git a/tests/pages.test.js b/tests/pages.test.js
new file mode 100644
index 0000000..f542b32
--- /dev/null
+++ b/tests/pages.test.js
@@ -0,0 +1,113 @@
+/* Run with: node --test */
+
+const test = require("node:test");
+const assert = require("node:assert/strict");
+const Pages = require("../pages.js");
+
+const round = (n) => Math.round(n);
+
+test("portrait images get a portrait A4 page", () => {
+  const page = Pages.pageSize(1200, 1800);
+  assert.equal(round(page.width), 595);
+  assert.equal(round(page.height), 842);
+  assert.equal(page.orientation, "p");
+});
+
+test("landscape images get a landscape A4 page", () => {
+  const page = Pages.pageSize(1800, 1200);
+  assert.equal(round(page.width), 842);
+  assert.equal(round(page.height), 595);
+  assert.equal(page.orientation, "l");
+});
+
+test("square images stay portrait", () => {
+  assert.equal(Pages.pageSize(1000, 1000).orientation, "p");
+});
+
+test("a batch can mix orientations", () => {
+  const orientations = [[400, 800], [800, 400], [600, 900]].map(
+    ([w, h]) => Pages.pageSize(w, h).orientation
+  );
+  assert.deepEqual(orientations, ["p", "l", "p"]);
+});
+
+test("the image is centred on the page", () => {
+  const page = Pages.pageSize(1000, 1000);
+  const spot = Pages.placeImage(1000, 1000, page.width, page.height);
+  const rightGap = page.width - (spot.x + spot.width);
+  const bottomGap = page.height - (spot.y + spot.height);
+  assert.ok(Math.abs(spot.x - rightGap) < 0.001, "equal side margins");
+  assert.ok(Math.abs(spot.y - bottomGap) < 0.001, "equal top and bottom margins");
+});
+
+test("the image never crosses the margin", () => {
+  const page = Pages.pageSize(1200, 1800);
+  const spot = Pages.placeImage(1200, 1800, page.width, page.height);
+  assert.ok(spot.x >= Pages.MARGIN_PT - 0.001);
+  assert.ok(spot.y >= Pages.MARGIN_PT - 0.001);
+  assert.ok(spot.width <= page.width - Pages.MARGIN_PT * 2 + 0.001);
+  assert.ok(spot.height <= page.height - Pages.MARGIN_PT * 2 + 0.001);
+});
+
+test("aspect ratio survives placement", () => {
+  const page = Pages.pageSize(1600, 900);
+  const spot = Pages.placeImage(1600, 900, page.width, page.height);
+  assert.ok(Math.abs(spot.width / spot.height - 1600 / 900) < 0.001);
+});
+
+test("a very wide image is limited by width, not height", () => {
+  const page = Pages.pageSize(4000, 500);
+  const spot = Pages.placeImage(4000, 500, page.width, page.height);
+  assert.ok(Math.abs(spot.width - (page.width - Pages.MARGIN_PT * 2)) < 0.001);
+  assert.ok(spot.height < page.height - Pages.MARGIN_PT * 2);
+});
+
+test("a very tall image is limited by height, not width", () => {
+  const page = Pages.pageSize(500, 4000);
+  const spot = Pages.placeImage(500, 4000, page.width, page.height);
+  assert.ok(Math.abs(spot.height - (page.height - Pages.MARGIN_PT * 2)) < 0.001);
+  assert.ok(spot.width < page.width - Pages.MARGIN_PT * 2);
+});
+
+test("a small image is scaled up to fill the page", () => {
+  const page = Pages.pageSize(100, 150);
+  const spot = Pages.placeImage(100, 150, page.width, page.height);
+  assert.ok(spot.height > 700, "a thumbnail still fills an A4 page");
+});
+
+test("raster size follows the render density", () => {
+  const spot = { width: 288, height: 144 };
+  // 288pt = 4in; at 150 DPI that is 600px.
+  assert.deepEqual(Pages.rasterSize(spot), { width: 600, height: 300 });
+  assert.deepEqual(Pages.rasterSize(spot, 72), { width: 288, height: 144 });
+});
+
+test("raster size never collapses to zero", () => {
+  const raster = Pages.rasterSize({ width: 0.01, height: 0.01 });
+  assert.ok(raster.width >= 1 && raster.height >= 1);
+});
+
+test("an A4 content area stays a sane pixel size", () => {
+  const page = Pages.pageSize(6000, 8000);
+  const raster = Pages.rasterSize(Pages.placeImage(6000, 8000, page.width, page.height));
+  assert.ok(raster.width < 1300, `expected a downscale, got ${raster.width}px`);
+});
+
+test("byte formatting", () => {
+  assert.equal(Pages.formatBytes(512), "512 B");
+  assert.equal(Pages.formatBytes(2048), "2 KB");
+  assert.equal(Pages.formatBytes(5 * 1024 * 1024), "5.0 MB");
+});
+
+test("TIFF is detected from magic bytes in both byte orders", () => {
+  assert.equal(Pages.isTiff(new Uint8Array([0x49, 0x49, 0x2a, 0x00])), true, "II*\\0");
+  assert.equal(Pages.isTiff(new Uint8Array([0x4d, 0x4d, 0x00, 0x2a])), true, "MM\\0*");
+});
+
+test("other formats are not mistaken for TIFF", () => {
+  assert.equal(Pages.isTiff(new Uint8Array([0xff, 0xd8, 0xff, 0xe0])), false, "JPEG");
+  assert.equal(Pages.isTiff(new Uint8Array([0x89, 0x50, 0x4e, 0x47])), false, "PNG");
+  assert.equal(Pages.isTiff(new Uint8Array([0x52, 0x49, 0x46, 0x46])), false, "WebP");
+  assert.equal(Pages.isTiff(new Uint8Array([0x49, 0x49])), false, "truncated");
+  assert.equal(Pages.isTiff(null), false, "nothing");
+});
diff --git a/tests/test_api.py b/tests/test_api.py
deleted file mode 100644
index 4716aa1..0000000
--- a/tests/test_api.py
+++ /dev/null
@@ -1,130 +0,0 @@
-import io
-import re
-
-import pytest
-from fastapi.testclient import TestClient
-from PIL import Image
-
-from app.config import MAX_FILES
-from app.main import app
-
-
[email protected]
-def client():
-    with TestClient(app) as test_client:
-        yield test_client
-
-
-def image_bytes(size=(120, 160), fmt="PNG") -> bytes:
-    buffer = io.BytesIO()
-    Image.new("RGB", size, "teal").save(buffer, format=fmt)
-    return buffer.getvalue()
-
-
-def upload(name="page.png", data=None, content_type="image/png"):
-    return ("images", (name, data or image_bytes(), content_type))
-
-
-def test_index_renders_the_composer(client):
-    response = client.get("/")
-    assert response.status_code == 200
-    assert 'enctype="multipart/form-data"' in response.text
-    assert 'name="images"' in response.text
-    assert f"Combine up to {MAX_FILES} images" in response.text
-
-
-def test_index_offers_no_page_size_control(client):
-    """Page size is fixed; the form must not ship a picker for it."""
-    assert 'name="page_size"' not in client.get("/").text
-
-
-def test_healthz(client):
-    assert client.get("/healthz").json() == {"status": "ok"}
-
-
-def test_convert_returns_a_pdf_attachment(client):
-    response = client.post("/convert", files=[upload(), upload("second.png")])
-    assert response.status_code == 200
-    assert response.headers["content-type"] == "application/pdf"
-    assert response.headers["x-page-count"] == "2"
-    assert 'attachment; filename="makeitpdf-' in response.headers["content-disposition"]
-    assert response.content.startswith(b"%PDF-")
-
-
-def test_convert_accepts_the_full_batch(client):
-    files = [upload(f"page-{n}.png") for n in range(MAX_FILES)]
-    response = client.post("/convert", files=files)
-    assert response.status_code == 200
-    assert response.headers["x-page-count"] == str(MAX_FILES)
-
-
-def test_convert_rejects_more_than_the_limit(client):
-    files = [upload(f"page-{n}.png") for n in range(MAX_FILES + 1)]
-    response = client.post("/convert", files=files, headers={"accept": "application/json"})
-    assert response.status_code == 400
-    assert str(MAX_FILES) in response.json()["error"]
-
-
-def test_convert_without_files_is_rejected(client):
-    response = client.post("/convert", headers={"accept": "application/json"})
-    assert response.status_code == 400
-    assert "at least one image" in response.json()["error"]
-
-
-def test_convert_rejects_unknown_page_size(client):
-    response = client.post(
-        "/convert",
-        files=[upload()],
-        data={"page_size": "billboard"},
-        headers={"accept": "application/json"},
-    )
-    assert response.status_code == 400
-
-
-def media_box(pdf: bytes) -> tuple[int, int]:
-    box = re.search(rb"/MediaBox \[ 0 0 ([\d.]+) ([\d.]+) \]", pdf)
-    assert box is not None
-    return round(float(box[1])), round(float(box[2]))
-
-
-def test_default_page_size_is_a4(client):
-    """The form sends no page_size, so the server's default decides."""
-    response = client.post("/convert", files=[upload()])
-    assert response.status_code == 200
-    assert media_box(response.content) == (595, 842)
-
-
-def test_page_size_stays_available_over_the_api(client):
-    response = client.post("/convert", files=[upload()], data={"page_size": "letter"})
-    assert response.status_code == 200
-    assert media_box(response.content) == (612, 792)
-
-
-def test_lying_content_type_is_caught_by_decoding(client):
-    """A .png content-type on non-image bytes must not get through."""
-    response = client.post(
-        "/convert",
-        files=[upload("trojan.png", b"#!/bin/sh\necho hi")],
-        headers={"accept": "application/json"},
-    )
-    assert response.status_code == 400
-    assert "trojan.png" in response.json()["error"]
-
-
-def test_oversized_upload_is_rejected(client, monkeypatch):
-    monkeypatch.setattr("app.main.MAX_FILE_BYTES", 1024)
-    response = client.post(
-        "/convert",
-        files=[upload("big.png", image_bytes(size=(900, 900)))],
-        headers={"accept": "application/json"},
-    )
-    assert response.status_code == 413
-    assert "big.png" in response.json()["error"]
-
-
-def test_browser_form_post_gets_html_error_back(client):
-    """No-JS clients re-render the page with the message instead of JSON."""
-    response = client.post("/convert", files=[upload("notes.txt", b"nope")])
-    assert response.status_code == 400
-    assert "text/html" in response.headers["content-type"]
-    assert "notes.txt" in response.text
diff --git a/tests/test_converter.py b/tests/test_converter.py
deleted file mode 100644
index 6394325..0000000
--- a/tests/test_converter.py
+++ /dev/null
@@ -1,147 +0,0 @@
-import io
-
-import pytest
-from PIL import Image
-
-from app.config import MAX_PIXELS
-from app.converter import ConversionError, SourceImage, images_to_pdf
-
-
-def make_image(
-    size: tuple[int, int] = (200, 300),
-    fmt: str = "PNG",
-    mode: str = "RGB",
-    color: str | tuple = "red",
-    **save_kwargs,
-) -> bytes:
-    buffer = io.BytesIO()
-    Image.new(mode, size, color).save(buffer, format=fmt, **save_kwargs)
-    return buffer.getvalue()
-
-
-def source(name: str = "page.png", **kwargs) -> SourceImage:
-    return SourceImage(filename=name, data=make_image(**kwargs))
-
-
-def page_boxes(pdf: bytes) -> list[tuple[float, float]]:
-    """Pull MediaBox dimensions out of the generated PDF."""
-    boxes = []
-    for chunk in pdf.split(b"/MediaBox [ ")[1:]:
-        numbers = chunk.split(b"]")[0].split()
-        boxes.append((float(numbers[2]), float(numbers[3])))
-    return boxes
-
-
-def test_single_image_makes_one_page():
-    pdf = images_to_pdf([source()])
-    assert pdf.startswith(b"%PDF-")
-    assert len(page_boxes(pdf)) == 1
-
-
-def test_page_count_matches_input_order():
-    sources = [source(f"page-{n}.png") for n in range(5)]
-    assert len(page_boxes(images_to_pdf(sources))) == 5
-
-
-def test_fit_uses_fallback_density_for_dpi_less_images():
-    # 300x600px at the 150 DPI fallback is a 2x4in page = 144x288pt.
-    pdf = images_to_pdf([source(size=(300, 600))])
-    width, height = page_boxes(pdf)[0]
-    assert round(width) == 144
-    assert round(height) == 288
-
-
-def test_fit_honours_embedded_dpi():
-    data = make_image(size=(300, 600), dpi=(300, 300))
-    pdf = images_to_pdf([SourceImage("scan.png", data)])
-    width, height = page_boxes(pdf)[0]
-    assert round(width) == 72  # 300px at 300 DPI = 1in
-    assert round(height) == 144
-
-
-def test_fit_ignores_implausible_dpi():
-    data = make_image(size=(300, 600), dpi=(1, 1))
-    pdf = images_to_pdf([SourceImage("odd.png", data)])
-    assert round(page_boxes(pdf)[0][0]) == 144  # falls back to 150 DPI
-
-
[email protected](
-    ("page_size", "expected"),
-    [("a4", (595, 842)), ("letter", (612, 792))],
-)
-def test_fixed_page_sizes(page_size, expected):
-    pdf = images_to_pdf([source(size=(200, 400))], page_size=page_size)
-    width, height = page_boxes(pdf)[0]
-    assert (round(width), round(height)) == expected
-
-
-def test_landscape_image_gets_landscape_page():
-    pdf = images_to_pdf([source(size=(800, 400))], page_size="a4")
-    width, height = page_boxes(pdf)[0]
-    assert width > height
-    assert (round(width), round(height)) == (842, 595)
-
-
-def test_mixed_orientations_keep_their_own_pages():
-    sources = [source("tall.png", size=(400, 800)), source("wide.png", size=(800, 400))]
-    boxes = page_boxes(images_to_pdf(sources, page_size="letter"))
-    assert (round(boxes[0][0]), round(boxes[0][1])) == (612, 792)
-    assert (round(boxes[1][0]), round(boxes[1][1])) == (792, 612)
-
-
-def test_transparency_is_flattened_onto_white():
-    data = make_image(mode="RGBA", color=(0, 0, 0, 0))
-    pdf = images_to_pdf([SourceImage("clear.png", data)])
-    assert pdf.startswith(b"%PDF-")
-
-
[email protected]("fmt", ["PNG", "JPEG", "WEBP", "GIF", "BMP", "TIFF"])
-def test_accepted_formats(fmt):
-    mode = "P" if fmt == "GIF" else "RGB"
-    pdf = images_to_pdf([SourceImage(f"a.{fmt.lower()}", make_image(fmt=fmt, mode=mode))])
-    assert pdf.startswith(b"%PDF-")
-
-
-def test_empty_batch_is_rejected():
-    with pytest.raises(ConversionError, match="at least one image"):
-        images_to_pdf([])
-
-
-def test_unknown_page_size_is_rejected():
-    with pytest.raises(ConversionError, match="page size"):
-        images_to_pdf([source()], page_size="a3")
-
-
-def test_non_image_bytes_are_rejected_by_name():
-    with pytest.raises(ConversionError, match="notes.txt"):
-        images_to_pdf([SourceImage("notes.txt", b"just some text, not an image")])
-
-
-def test_truncated_image_is_rejected():
-    broken = make_image()[:60]
-    with pytest.raises(ConversionError):
-        images_to_pdf([SourceImage("cut.png", broken)])
-
-
-def test_oversized_image_is_rejected(monkeypatch):
-    monkeypatch.setattr("app.converter.MAX_PIXELS", 1000)
-    with pytest.raises(ConversionError, match="megapixel"):
-        images_to_pdf([source(size=(100, 100))])
-
-
-def test_pixel_guard_matches_config():
-    assert MAX_PIXELS > 0
-
-
-def test_exif_orientation_is_applied():
-    # Orientation 6 means "rotate 90° CW to display": a 400x200 stored image
-    # should come out 200x400 on the page.
-    buffer = io.BytesIO()
-    image = Image.new("RGB", (400, 200), "blue")
-    exif = image.getexif()
-    exif[274] = 6
-    image.save(buffer, format="JPEG", exif=exif)
-
-    pdf = images_to_pdf([SourceImage("rotated.jpg", buffer.getvalue())])
-    width, height = page_boxes(pdf)[0]
-    assert height > width
diff --git a/uv.lock b/uv.lock
deleted file mode 100644
index 7d56652..0000000
--- a/uv.lock
+++ /dev/null
@@ -1,596 +0,0 @@
-version = 1
-revision = 1
-requires-python = ">=3.14"
-
-[[package]]
-name = "annotated-doc"
-version = "0.0.5"
-source = { registry = "https://pypi.org/simple" }
-sdist = { url = "https://files.pythonhosted.org/packages/5a/8e/38aa427ed5402449e226975b649c5dc73ccadfefeb95e6aecb8f8ea4b6b6/annotated_doc-0.0.5.tar.gz", hash = "sha256:c7e58ce09192557605d8bbd92836d7e1d520ac9580096042c0bfd197efacf1bb", size = 10758 }
-wheels = [
-    { url = "https://files.pythonhosted.org/packages/3e/30/e900b21425a860e195f32e37657aa1f7c7f2b1bfb26f03ca209b90933c06/annotated_doc-0.0.5-py3-none-any.whl", hash = "sha256:117bac03a25ede5df5440e855b32d556049ca169ead221505badf432fed4b101", size = 5302 },
-]
-
-[[package]]
-name = "annotated-types"
-version = "0.8.0"
-source = { registry = "https://pypi.org/simple" }
-sdist = { url = "https://files.pythonhosted.org/packages/5f/56/a8120250d128bed162cd73c76d45f6ef9991f3e068f62a8ee060afa3104a/annotated_types-0.8.0.tar.gz", hash = "sha256:13b2beaad985e05e2d6407ee4c4f35590b11f8d693a258a561055cac8f64cab7", size = 15893 }
-wheels = [
-    { url = "https://files.pythonhosted.org/packages/99/91/8acff4f5e50511b911bbccb72b8628a49c68ce14148cd9f6431094859a90/annotated_types-0.8.0-py3-none-any.whl", hash = "sha256:f072f4d804ea359e4eaf198b1af7a8b0943881a87f31bb764f8bf219bb9419e0", size = 13427 },
-]
-
-[[package]]
-name = "anyio"
-version = "4.14.2"
-source = { registry = "https://pypi.org/simple" }
-dependencies = [
-    { name = "idna" },
-]
-sdist = { url = "https://files.pythonhosted.org/packages/61/cc/a381afa6efea9f496eff839d4a6a1aed3bfafc7b3ab4b0d1b243a12573dd/anyio-4.14.2.tar.gz", hash = "sha256:cfa139f3ed1a23ee8f88a145ddb5ac7605b8bbfd8592baacd7ce3d8bb4313c7f", size = 260176 }
-wheels = [
-    { url = "https://files.pythonhosted.org/packages/da/35/f2287558c17e29fafc8ef3daf819bb9834061cfa43bff8014f7df7f63bdc/anyio-4.14.2-py3-none-any.whl", hash = "sha256:9f505dda5ac9f0c8309b5e8bd445a8c2bf7246f3ce950121e45ea15bc41d1494", size = 125813 },
-]
-
-[[package]]
-name = "certifi"
-version = "2026.7.22"
-source = { registry = "https://pypi.org/simple" }
-sdist = { url = "https://files.pythonhosted.org/packages/a3/c2/24167ea9858356b47a87a50d39908bfdb72ceeefe0041586e704e5376b3a/certifi-2026.7.22.tar.gz", hash = "sha256:741e2c3b351ddf169a738da9f2c048608ff7f2c5cc02f1ebc6b118bb090d5d55", size = 138112 }
-wheels = [
-    { url = "https://files.pythonhosted.org/packages/0b/a7/71ac2cff56fec219ed242bb11b8efb69fcc4bec75db06fb7bfe35de520e6/certifi-2026.7.22-py3-none-any.whl", hash = "sha256:62f22742b58a1a33014a2b6b706588a8d7e2a88ae7bd1a6ebe8c992928483775", size = 136983 },
-]
-
-[[package]]
-name = "click"
-version = "8.4.2"
-source = { registry = "https://pypi.org/simple" }
-dependencies = [
-    { name = "colorama", marker = "sys_platform == 'win32'" },
-]
-sdist = { url = "https://files.pythonhosted.org/packages/76/d4/81420972a676e8ffea40450d8c8c92943e7218a78fe9b64359836cc9876b/click-8.4.2.tar.gz", hash = "sha256:9a6cea6e60b17ebe0a44c5cc636d94f09bd66142c1cd7d8b4cd731c4917a15f6", size = 338000 }
-wheels = [
-    { url = "https://files.pythonhosted.org/packages/fb/e2/79c688af8b210d232694e31e59da9f6ec747bae31c3f5946e4e9b98860d5/click-8.4.2-py3-none-any.whl", hash = "sha256:e6f9f66136c816745b9d65817da91d61d957fb16e02e4dcd0552553c5a197b76", size = 119243 },
-]
-
-[[package]]
-name = "colorama"
-version = "0.4.6"
-source = { registry = "https://pypi.org/simple" }
-sdist = { url = "https://files.pythonhosted.org/packages/d8/53/6f443c9a4a8358a93a6792e2acffb9d9d5cb0a5cfd8802644b7b1c9a02e4/colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44", size = 27697 }
-wheels = [
-    { url = "https://files.pythonhosted.org/packages/d1/d6/3965ed04c63042e047cb6a3e6ed1a63a35087b6a609aa3a15ed8ac56c221/colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6", size = 25335 },
-]
-
-[[package]]
-name = "fastapi"
-version = "0.141.1"
-source = { registry = "https://pypi.org/simple" }
-dependencies = [
-    { name = "annotated-doc" },
-    { name = "pydantic" },
-    { name = "starlette" },
-    { name = "typing-extensions" },
-    { name = "typing-inspection" },
-]
-sdist = { url = "https://files.pythonhosted.org/packages/8a/02/91e3416a8fdd715abb903a952a6bec7cdd8d14eed55d415fc8595524c319/fastapi-0.141.1.tar.gz", hash = "sha256:e8822fc40db1e1858054d7a949a888695bc9bdce70139178e33bd2871a453ca1", size = 425799 }
-wheels = [
-    { url = "https://files.pythonhosted.org/packages/cb/03/10388a42375ee7e4ac9b94eb2c5c569c8b5795e377e701c9ac3ad63de890/fastapi-0.141.1-py3-none-any.whl", hash = "sha256:bfb91aa2d334c61cb35ba9a116fc123b3d3df31640b801cf57a7a78ec3f603b3", size = 131954 },
-]
-
-[[package]]
-name = "h11"
-version = "0.16.0"
-source = { registry = "https://pypi.org/simple" }
-sdist = { url = "https://files.pythonhosted.org/packages/01/ee/02a2c011bdab74c6fb3c75474d40b3052059d95df7e73351460c8588d963/h11-0.16.0.tar.gz", hash = "sha256:4e35b956cf45792e4caa5885e69fba00bdbc6ffafbfa020300e549b208ee5ff1", size = 101250 }
-wheels = [
-    { url = "https://files.pythonhosted.org/packages/04/4b/29cac41a4d98d144bf5f6d33995617b185d14b22401f75ca86f384e87ff1/h11-0.16.0-py3-none-any.whl", hash = "sha256:63cf8bbe7522de3bf65932fda1d9c2772064ffb3dae62d55932da54b31cb6c86", size = 37515 },
-]
-
-[[package]]
-name = "httpcore"
-version = "1.0.9"
-source = { registry = "https://pypi.org/simple" }
-dependencies = [
-    { name = "certifi" },
-    { name = "h11" },
-]
-sdist = { url = "https://files.pythonhosted.org/packages/06/94/82699a10bca87a5556c9c59b5963f2d039dbd239f25bc2a63907a05a14cb/httpcore-1.0.9.tar.gz", hash = "sha256:6e34463af53fd2ab5d807f399a9b45ea31c3dfa2276f15a2c3f00afff6e176e8", size = 85484 }
-wheels = [
-    { url = "https://files.pythonhosted.org/packages/7e/f5/f66802a942d491edb555dd61e3a9961140fd64c90bce1eafd741609d334d/httpcore-1.0.9-py3-none-any.whl", hash = "sha256:2d400746a40668fc9dec9810239072b40b4484b640a8c38fd654a024c7a1bf55", size = 78784 },
-]
-
-[[package]]
-name = "httptools"
-version = "0.8.0"
-source = { registry = "https://pypi.org/simple" }
-sdist = { url = "https://files.pythonhosted.org/packages/43/e5/d471fcb0e14523fe1c3f4ba58ca52480e7bd70ad7109a3846bc75892f7fb/httptools-0.8.0.tar.gz", hash = "sha256:6b2a32f18d97e16e90827d7a819ffa8dbd8cc245fc4e1fa9d1095b54ef4bd999", size = 271342 }
-wheels = [
-    { url = "https://files.pythonhosted.org/packages/1a/12/fa3fbf5f9517b273edea2dc982aa82a8c634091e67c590792b729017bc6f/httptools-0.8.0-cp314-cp314-macosx_10_13_universal2.whl", hash = "sha256:de242a49b5d18e0a8776e654e9f6bf6d89f3875a5c35b425a0e7ce940feb3fd6", size = 206183 },
-    { url = "https://files.pythonhosted.org/packages/30/fc/5e7c4cb443370f2090a3aba0453a07384d29ff66b7435bb90e77e1037599/httptools-0.8.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:159e9ab5f701ccd42e555a12f1ad8ff69702910fc1c996cf2bb66e5fcb7a231b", size = 112079 },
-    { url = "https://files.pythonhosted.org/packages/ba/53/771bd891eb0f236f32145d6a1775777ec85745f3cc983a1f23d1a3b8ddfe/httptools-0.8.0-cp314-cp314-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:c4a9f1707e4823d54dfec6c33fa3697d302aed536ed352a7ebb5a061ddb869d0", size = 481596 },
-    { url = "https://files.pythonhosted.org/packages/62/42/94e15bc68ce3d423243c45d7f1b0c7561f13844f97dc52ae23182fb65628/httptools-0.8.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:d76ad7b951387e3632c8716a9bb03ac5b45c5f16119aa409db0459520887944e", size = 480865 },
-    { url = "https://files.pythonhosted.org/packages/1c/7c/fe2980fc03723272e30f135b62360b075f513dfe7cc73aef36c7f04012bd/httptools-0.8.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:a3b7387147361c3fd47a0bde763c5c91b5b4cd4dc9989b8ece84ff436c99843b", size = 463189 },
-    { url = "https://files.pythonhosted.org/packages/15/1b/47fc5fff68acd1bfa20b4734059c9a06cadb88119dcd5258b5b0d21d91c8/httptools-0.8.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:f256d6ce930c52ca1cb2a960b7da03548c454e7d28b06059ad41bfe789036ce0", size = 466610 },
-    { url = "https://files.pythonhosted.org/packages/60/bd/07b13c93ffd9bec9546e0d43f8e19378dd696dbd278511406bc07371ef1f/httptools-0.8.0-cp314-cp314-win_amd64.whl", hash = "sha256:19d1ee275bb59ba2643ba9a3a1e51cc0c788caf2b8df506368e03f56fdd08527", size = 92705 },
-    { url = "https://files.pythonhosted.org/packages/fd/c4/121648f68ce066d7bd762d6b6d97e620847642d38d54f3d90ff11d947629/httptools-0.8.0-cp314-cp314t-macosx_10_13_universal2.whl", hash = "sha256:de1ed58a974e75d56560acc7e7fed01a454994429456f65209789992e41f2568", size = 215023 },
-    { url = "https://files.pythonhosted.org/packages/b9/b0/312a062ae741ae3e8baa8c8bf20be81b2e67337b259ab4349bebc7b6142e/httptools-0.8.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:e93c227b595c6926c1acee96891dd9da4be338cfbe82e5cd3bb9d8dd7dc4ac0b", size = 117405 },
-    { url = "https://files.pythonhosted.org/packages/fc/37/fccd705f795386bb05bf413012fecff2a33e5aa8c2f069096de3e9fd8702/httptools-0.8.0-cp314-cp314t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:2a021c3a8e65cc125390d72f59b968afca3bdcaff25bd67965e0a055a14946ca", size = 558497 },
-    { url = "https://files.pythonhosted.org/packages/bd/39/f172e8003576de35f5ba77ff417cf0e34429d35dc014deef15afa337a72c/httptools-0.8.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:48774d39cbb70e2b1f71f88852a3087ae1d3a1eb80482bb48c13067ab080c14f", size = 571585 },
-    { url = "https://files.pythonhosted.org/packages/3e/b9/f5564760af99f3dbbf3f9104dc00e5da27e96cf433c6bdcf77617f70bf3f/httptools-0.8.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:88eead8ec8680a9f146c655bc88445a325bd7921cfd8194c7337e9467282427d", size = 543297 },
-    { url = "https://files.pythonhosted.org/packages/99/67/8d9f2c313618e161b82f3873188e7196126da1d6e29688df40eb3997c77a/httptools-0.8.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:2c032fa028f46871ec7e1fc59fc15e8023eab3e6bbe6ece786a1611719a5d081", size = 539535 },
-    { url = "https://files.pythonhosted.org/packages/48/63/b906c01e53f50d432c0defe43ce52764a111dc1bdd028bafbeb54dcfd008/httptools-0.8.0-cp314-cp314t-win_amd64.whl", hash = "sha256:384c17174464c8e873398b7af24f0b1f44d992c820328413951a625323155d77", size = 108209 },
-]
-
-[[package]]
-name = "httpx"
-version = "0.28.1"
-source = { registry = "https://pypi.org/simple" }
-dependencies = [
-    { name = "anyio" },
-    { name = "certifi" },
-    { name = "httpcore" },
-    { name = "idna" },
-]
-sdist = { url = "https://files.pythonhosted.org/packages/b1/df/48c586a5fe32a0f01324ee087459e112ebb7224f646c0b5023f5e79e9956/httpx-0.28.1.tar.gz", hash = "sha256:75e98c5f16b0f35b567856f597f06ff2270a374470a5c2392242528e3e3e42fc", size = 141406 }
-wheels = [
-    { url = "https://files.pythonhosted.org/packages/2a/39/e50c7c3a983047577ee07d2a9e53faf5a69493943ec3f6a384bdc792deb2/httpx-0.28.1-py3-none-any.whl", hash = "sha256:d909fcccc110f8c7faf814ca82a9a4d816bc5a6dbfea25d6591d6985b8ba59ad", size = 73517 },
-]
-
-[[package]]
-name = "idna"
-version = "3.18"
-source = { registry = "https://pypi.org/simple" }
-sdist = { url = "https://files.pythonhosted.org/packages/cd/63/9496c57188a2ee585e0f1db071d75089a11e98aa86eb99d9d7618fc1edce/idna-3.18.tar.gz", hash = "sha256:ffb385a7e039654cef1ab9ef32c6fafe283c0c0467bba1d9029738ce4a14a848", size = 196711 }
-wheels = [
-    { url = "https://files.pythonhosted.org/packages/1e/5e/d4e9f1a599fb8e573b7b87160658329fbf28d19eac2718f51fc3def3aa5a/idna-3.18-py3-none-any.whl", hash = "sha256:7f952cbe720b688055e3f87de14f5c3e5fdaa8bc3928985c4077ca689de849a2", size = 65455 },
-]
-
-[[package]]
-name = "iniconfig"
-version = "2.3.0"
-source = { registry = "https://pypi.org/simple" }
-sdist = { url = "https://files.pythonhosted.org/packages/72/34/14ca021ce8e5dfedc35312d08ba8bf51fdd999c576889fc2c24cb97f4f10/iniconfig-2.3.0.tar.gz", hash = "sha256:c76315c77db068650d49c5b56314774a7804df16fee4402c1f19d6d15d8c4730", size = 20503 }
-wheels = [
-    { url = "https://files.pythonhosted.org/packages/cb/b1/3846dd7f199d53cb17f49cba7e651e9ce294d8497c8c150530ed11865bb8/iniconfig-2.3.0-py3-none-any.whl", hash = "sha256:f631c04d2c48c52b84d0d0549c99ff3859c98df65b3101406327ecc7d53fbf12", size = 7484 },
-]
-
-[[package]]
-name = "jinja2"
-version = "3.1.6"
-source = { registry = "https://pypi.org/simple" }
-dependencies = [
-    { name = "markupsafe" },
-]
-sdist = { url = "https://files.pythonhosted.org/packages/df/bf/f7da0350254c0ed7c72f3e33cef02e048281fec7ecec5f032d4aac52226b/jinja2-3.1.6.tar.gz", hash = "sha256:0137fb05990d35f1275a587e9aee6d56da821fc83491a0fb838183be43f66d6d", size = 245115 }
-wheels = [
-    { url = "https://files.pythonhosted.org/packages/62/a1/3d680cbfd5f4b8f15abc1d571870c5fc3e594bb582bc3b64ea099db13e56/jinja2-3.1.6-py3-none-any.whl", hash = "sha256:85ece4451f492d0c13c5dd7c13a64681a86afae63a5f347908daf103ce6d2f67", size = 134899 },
-]
-
-[[package]]
-name = "makeitpdf"
-version = "0.1.0"
-source = { virtual = "." }
-dependencies = [
-    { name = "fastapi" },
-    { name = "jinja2" },
-    { name = "pillow" },
-    { name = "python-multipart" },
-    { name = "uvicorn", extra = ["standard"] },
-]
-
-[package.dev-dependencies]
-dev = [
-    { name = "httpx" },
-    { name = "pytest" },
-]
-
-[package.metadata]
-requires-dist = [
-    { name = "fastapi", specifier = ">=0.115" },
-    { name = "jinja2", specifier = ">=3.1" },
-    { name = "pillow", specifier = ">=11.0" },
-    { name = "python-multipart", specifier = ">=0.0.12" },
-    { name = "uvicorn", extras = ["standard"], specifier = ">=0.32" },
-]
-
-[package.metadata.requires-dev]
-dev = [
-    { name = "httpx", specifier = ">=0.28.1" },
-    { name = "pytest", specifier = ">=9.1.1" },
-]
-
-[[package]]
-name = "markupsafe"
-version = "3.0.3"
-source = { registry = "https://pypi.org/simple" }
-sdist = { url = "https://files.pythonhosted.org/packages/7e/99/7690b6d4034fffd95959cbe0c02de8deb3098cc577c67bb6a24fe5d7caa7/markupsafe-3.0.3.tar.gz", hash = "sha256:722695808f4b6457b320fdc131280796bdceb04ab50fe1795cd540799ebe1698", size = 80313 }
-wheels = [
-    { url = "https://files.pythonhosted.org/packages/33/8a/8e42d4838cd89b7dde187011e97fe6c3af66d8c044997d2183fbd6d31352/markupsafe-3.0.3-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:eaa9599de571d72e2daf60164784109f19978b327a3910d3e9de8c97b5b70cfe", size = 11619 },
-    { url = "https://files.pythonhosted.org/packages/b5/64/7660f8a4a8e53c924d0fa05dc3a55c9cee10bbd82b11c5afb27d44b096ce/markupsafe-3.0.3-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:c47a551199eb8eb2121d4f0f15ae0f923d31350ab9280078d1e5f12b249e0026", size = 12029 },
-    { url = "https://files.pythonhosted.org/packages/da/ef/e648bfd021127bef5fa12e1720ffed0c6cbb8310c8d9bea7266337ff06de/markupsafe-3.0.3-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f34c41761022dd093b4b6896d4810782ffbabe30f2d443ff5f083e0cbbb8c737", size = 24408 },
-    { url = "https://files.pythonhosted.org/packages/41/3c/a36c2450754618e62008bf7435ccb0f88053e07592e6028a34776213d877/markupsafe-3.0.3-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:457a69a9577064c05a97c41f4e65148652db078a3a509039e64d3467b9e7ef97", size = 23005 },
-    { url = "https://files.pythonhosted.org/packages/bc/20/b7fdf89a8456b099837cd1dc21974632a02a999ec9bf7ca3e490aacd98e7/markupsafe-3.0.3-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:e8afc3f2ccfa24215f8cb28dcf43f0113ac3c37c2f0f0806d8c70e4228c5cf4d", size = 22048 },
-    { url = "https://files.pythonhosted.org/packages/9a/a7/591f592afdc734f47db08a75793a55d7fbcc6902a723ae4cfbab61010cc5/markupsafe-3.0.3-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:ec15a59cf5af7be74194f7ab02d0f59a62bdcf1a537677ce67a2537c9b87fcda", size = 23821 },
-    { url = "https://files.pythonhosted.org/packages/7d/33/45b24e4f44195b26521bc6f1a82197118f74df348556594bd2262bda1038/markupsafe-3.0.3-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:0eb9ff8191e8498cca014656ae6b8d61f39da5f95b488805da4bb029cccbfbaf", size = 21606 },
-    { url = "https://files.pythonhosted.org/packages/ff/0e/53dfaca23a69fbfbbf17a4b64072090e70717344c52eaaaa9c5ddff1e5f0/markupsafe-3.0.3-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:2713baf880df847f2bece4230d4d094280f4e67b1e813eec43b4c0e144a34ffe", size = 23043 },
-    { url = "https://files.pythonhosted.org/packages/46/11/f333a06fc16236d5238bfe74daccbca41459dcd8d1fa952e8fbd5dccfb70/markupsafe-3.0.3-cp314-cp314-win32.whl", hash = "sha256:729586769a26dbceff69f7a7dbbf59ab6572b99d94576a5592625d5b411576b9", size = 14747 },
-    { url = "https://files.pythonhosted.org/packages/28/52/182836104b33b444e400b14f797212f720cbc9ed6ba34c800639d154e821/markupsafe-3.0.3-cp314-cp314-win_amd64.whl", hash = "sha256:bdc919ead48f234740ad807933cdf545180bfbe9342c2bb451556db2ed958581", size = 15341 },
-    { url = "https://files.pythonhosted.org/packages/6f/18/acf23e91bd94fd7b3031558b1f013adfa21a8e407a3fdb32745538730382/markupsafe-3.0.3-cp314-cp314-win_arm64.whl", hash = "sha256:5a7d5dc5140555cf21a6fefbdbf8723f06fcd2f63ef108f2854de715e4422cb4", size = 14073 },
-    { url = "https://files.pythonhosted.org/packages/3c/f0/57689aa4076e1b43b15fdfa646b04653969d50cf30c32a102762be2485da/markupsafe-3.0.3-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:1353ef0c1b138e1907ae78e2f6c63ff67501122006b0f9abad68fda5f4ffc6ab", size = 11661 },
-    { url = "https://files.pythonhosted.org/packages/89/c3/2e67a7ca217c6912985ec766c6393b636fb0c2344443ff9d91404dc4c79f/markupsafe-3.0.3-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:1085e7fbddd3be5f89cc898938f42c0b3c711fdcb37d75221de2666af647c175", size = 12069 },
-    { url = "https://files.pythonhosted.org/packages/f0/00/be561dce4e6ca66b15276e184ce4b8aec61fe83662cce2f7d72bd3249d28/markupsafe-3.0.3-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1b52b4fb9df4eb9ae465f8d0c228a00624de2334f216f178a995ccdcf82c4634", size = 25670 },
-    { url = "https://files.pythonhosted.org/packages/50/09/c419f6f5a92e5fadde27efd190eca90f05e1261b10dbd8cbcb39cd8ea1dc/markupsafe-3.0.3-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:fed51ac40f757d41b7c48425901843666a6677e3e8eb0abcff09e4ba6e664f50", size = 23598 },
-    { url = "https://files.pythonhosted.org/packages/22/44/a0681611106e0b2921b3033fc19bc53323e0b50bc70cffdd19f7d679bb66/markupsafe-3.0.3-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:f190daf01f13c72eac4efd5c430a8de82489d9cff23c364c3ea822545032993e", size = 23261 },
-    { url = "https://files.pythonhosted.org/packages/5f/57/1b0b3f100259dc9fffe780cfb60d4be71375510e435efec3d116b6436d43/markupsafe-3.0.3-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:e56b7d45a839a697b5eb268c82a71bd8c7f6c94d6fd50c3d577fa39a9f1409f5", size = 24835 },
-    { url = "https://files.pythonhosted.org/packages/26/6a/4bf6d0c97c4920f1597cc14dd720705eca0bf7c787aebc6bb4d1bead5388/markupsafe-3.0.3-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:f3e98bb3798ead92273dc0e5fd0f31ade220f59a266ffd8a4f6065e0a3ce0523", size = 22733 },
-    { url = "https://files.pythonhosted.org/packages/14/c7/ca723101509b518797fedc2fdf79ba57f886b4aca8a7d31857ba3ee8281f/markupsafe-3.0.3-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:5678211cb9333a6468fb8d8be0305520aa073f50d17f089b5b4b477ea6e67fdc", size = 23672 },
-    { url = "https://files.pythonhosted.org/packages/fb/df/5bd7a48c256faecd1d36edc13133e51397e41b73bb77e1a69deab746ebac/markupsafe-3.0.3-cp314-cp314t-win32.whl", hash = "sha256:915c04ba3851909ce68ccc2b8e2cd691618c4dc4c4232fb7982bca3f41fd8c3d", size = 14819 },
-    { url = "https://files.pythonhosted.org/packages/1a/8a/0402ba61a2f16038b48b39bccca271134be00c5c9f0f623208399333c448/markupsafe-3.0.3-cp314-cp314t-win_amd64.whl", hash = "sha256:4faffd047e07c38848ce017e8725090413cd80cbc23d86e55c587bf979e579c9", size = 15426 },
-    { url = "https://files.pythonhosted.org/packages/70/bc/6f1c2f612465f5fa89b95bead1f44dcb607670fd42891d8fdcd5d039f4f4/markupsafe-3.0.3-cp314-cp314t-win_arm64.whl", hash = "sha256:32001d6a8fc98c8cb5c947787c5d08b0a50663d139f1305bac5885d98d9b40fa", size = 14146 },
-]
-
-[[package]]
-name = "packaging"
-version = "26.3"
-source = { registry = "https://pypi.org/simple" }
-sdist = { url = "https://files.pythonhosted.org/packages/7d/fa/3944b40b07da9ce895c0e6303a5ab7d53da063554f534556b134a54d6093/packaging-26.3.tar.gz", hash = "sha256:94edc256424af38762eb31306eed28beb9f0efc50a8837492c9d6fd6004aed79", size = 313412 }
-wheels = [
-    { url = "https://files.pythonhosted.org/packages/63/34/ba1c580383c9eada3711951fef0795c80b829a078d72188184bcab9dd527/packaging-26.3-py3-none-any.whl", hash = "sha256:d7193f7c8e4e93f444fde0262bf90af30e16fa0ad0ad44cb553c87339b23cd1c", size = 129956 },
-]
-
-[[package]]
-name = "pillow"
-version = "12.3.0"
-source = { registry = "https://pypi.org/simple" }
-sdist = { url = "https://files.pythonhosted.org/packages/1c/3d/bb7fca845737cf9d7dbde16ed1843984665ff2e0a518f5db43e77ec540b9/pillow-12.3.0.tar.gz", hash = "sha256:3b8182a766685eaa002637e28b4ec8d6b18819a0c71f579bf0dbaa5830297cce", size = 47025035 }
-wheels = [
-    { url = "https://files.pythonhosted.org/packages/dc/01/001f65b68192f0228cc1dbbc8d2530ab5d58b61037ba0587f946fea607cd/pillow-12.3.0-cp314-cp314-ios_13_0_arm64_iphoneos.whl", hash = "sha256:9cf95fe4d0f84c82d282745d9bb08ad9f926efa00be4697e767b814ce40d4330", size = 4161736 },
-    { url = "https://files.pythonhosted.org/packages/1a/d2/0219746d0fd16fc8a84498e79452375be3797d3ce4044596ce565164b84f/pillow-12.3.0-cp314-cp314-ios_13_0_arm64_iphonesimulator.whl", hash = "sha256:8728f216dcdb6e6d555cf971cb34076139ad74b31fc2c14da4fafc741c5f6217", size = 4255435 },
-    { url = "https://files.pythonhosted.org/packages/c8/02/8d0bc62ef0302318c46ff2a512822d2610e81c7aa46c9b3abe6cbaca5ad0/pillow-12.3.0-cp314-cp314-ios_13_0_x86_64_iphonesimulator.whl", hash = "sha256:a45650e8ce7fafffd731db8550230db6b0d306d181a90b67d3e6bca2f1990930", size = 3696262 },
-    { url = "https://files.pythonhosted.org/packages/85/e2/73c77d218410b14f5f2d565e8a998d5317b7b9c75368d29985139f7a46f0/pillow-12.3.0-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:ba54cfebe86920a559a7c4d6b9050791c20513650a1952ebe3368c7dc70306f8", size = 5350344 },
-    { url = "https://files.pythonhosted.org/packages/c7/da/32c752228ae345f489e3a42499d817b6c3996da7e8a3bc7a04fc806b243b/pillow-12.3.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:e158cb00350dc278f3b91551101aa7d12415a66ebf2c91d8d5ac14e56ddd3ad0", size = 4780131 },
-    { url = "https://files.pythonhosted.org/packages/b1/9d/8b2c807dbef61a5197c047afe99823787eb66f63daf9fb2432f91d6f0462/pillow-12.3.0-cp314-cp314-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e9aeb04d6aef139de265b29683e119b638208f88cf73cdd1658aa07221165321", size = 6263757 },
-    { url = "https://files.pythonhosted.org/packages/5c/44/c85361f65dbe00eea8576ee467c768d25129989efb76e94f205e9ca9bb46/pillow-12.3.0-cp314-cp314-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:251bf95b67017e27b13d82f5b326234ca62d70f9cf4c2b9032de2358a3b12c7b", size = 6936962 },
-    { url = "https://files.pythonhosted.org/packages/18/7e/e483414b35800b86b6f08dbbc7803fb5cd52c4d6f897f47d53ea2c7e6f65/pillow-12.3.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:fe3cca2e4e8a592be0f269a1ca4835c25199d9f3ce815c8491048f785b0a0198", size = 6339171 },
-    { url = "https://files.pythonhosted.org/packages/f0/f4/68c491844841ede6bed70189546b3ee9731cf9f2cbad396faff5e1ccba45/pillow-12.3.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:23aceaa007d6172b02c277f0cd359c79492bbb14f7072b4ede9fbcaf20648130", size = 7048116 },
-    { url = "https://files.pythonhosted.org/packages/a3/34/77f3f793fed8efc7d243f21b33c5a3f0d1c97ee70346d3db855587e155ff/pillow-12.3.0-cp314-cp314-win32.whl", hash = "sha256:af8d94b0db561cf68b88a267c5c44b49e134f525d0dc2cb7ed413a66bc23559a", size = 6467209 },
-    { url = "https://files.pythonhosted.org/packages/f1/e0/492879f69d94f91f60fc8cd05ba03650e9520afebb2fb7aa12777d7c7f38/pillow-12.3.0-cp314-cp314-win_amd64.whl", hash = "sha256:fdafc9cce40277e0f7a0feabce0ee50dd2fa1800f3b38015e51296b5e814048d", size = 7237707 },
-    { url = "https://files.pythonhosted.org/packages/c9/ac/6b11f2875f1c2ac040d84e1bbf9cf22a88038f901ca1037898b280b38365/pillow-12.3.0-cp314-cp314-win_arm64.whl", hash = "sha256:e91206ee562682b51b98ef4b26a6ef48fd84e15fd4c4bc5ec768eb641d206838", size = 2565995 },
-    { url = "https://files.pythonhosted.org/packages/52/69/c2208e56af9bfc1913afb24020297a691eb1d4ef688474c8a04913f65e04/pillow-12.3.0-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:164b31cd1a0490ab6efae01aa5df49da7061be0af1b30e035b6e9a1bfe34ee6e", size = 5352503 },
-    { url = "https://files.pythonhosted.org/packages/07/70/e5686d753e898a45d778ff1718dba8516ead6ab6b95d85fc8c4b70650cf2/pillow-12.3.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:5afb51d599ea772b8365ae807ae557f18bccfe46ab261fd1c2a9ed700fc6eb17", size = 4782956 },
-    { url = "https://files.pythonhosted.org/packages/d5/37/25c6692f06927ee973ff18c8d9ee98ad0b4d84ee67a09610c2dd1447958e/pillow-12.3.0-cp314-cp314t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:3edce1d53195db527e0191f84b71d02022de0540bf43a16ed734ed7537b07385", size = 6322855 },
-    { url = "https://files.pythonhosted.org/packages/cc/91/420637fcb8f1bc11029e403b4538e6694744428d8246118e45719f944556/pillow-12.3.0-cp314-cp314t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:bf16ba1b4d0b6b7c8e534936632270cf70eb00dbe09005bc345b2677b726855c", size = 6989642 },
-    { url = "https://files.pythonhosted.org/packages/10/08/b94d7811281ccf0d143a1cf768d1c49e1e54af63e7b708ab2ee3eb87face/pillow-12.3.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:24870b09b224f7ae3c39ed07d10e819d06f8720bc551847b1d623832b5b0e28d", size = 6391281 },
-    { url = "https://files.pythonhosted.org/packages/d2/87/24233f785f55474dc02ce3e739c5528a77e3a862e9333d1dd7a25cc31f70/pillow-12.3.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:30f2aa603c41533cc25c05acd0da21636e84a315768feb631c937177db558931", size = 7096716 },
-    { url = "https://files.pythonhosted.org/packages/23/26/fcb2f6e37175b04f53570b59937867e2b80ee1685e744023153028fc14f9/pillow-12.3.0-cp314-cp314t-win32.whl", hash = "sha256:4b0a7fe987b14c31ebda6083f74f22b561fd3739bc0ac51e019622e3d72668c7", size = 6474125 },
-    { url = "https://files.pythonhosted.org/packages/90/de/3634abee5f1c9e13c56787b7d5517b0ba8d6de51700b95578cf338349c9f/pillow-12.3.0-cp314-cp314t-win_amd64.whl", hash = "sha256:962864dc93511324d51ddbb5b9f8731bf71675b93ca612a07441896f4688fb8c", size = 7242939 },
-    { url = "https://files.pythonhosted.org/packages/ce/2a/fd13f8eb24de5714a6eb444a3d67e2842c6c576e159a43793adf23051351/pillow-12.3.0-cp314-cp314t-win_arm64.whl", hash = "sha256:0740a512dc522224c77d9aa5a8d70d8b7d73fb91f2c21125d8d025d3b8990e45", size = 2567506 },
-    { url = "https://files.pythonhosted.org/packages/5d/dc/8fdce34ec725a33c81c6ba122b904d6b9024e50ea9ac7bede62fab54506c/pillow-12.3.0-cp315-cp315-ios_13_0_arm64_iphoneos.whl", hash = "sha256:0feb2e9d6ad6c9e3c06effe9d00f3f1e618a6643273576b016f591e9315a7139", size = 4162063 },
-    { url = "https://files.pythonhosted.org/packages/76/66/2044b9a63d3b84ff048228dfcb7cd9bf0df983e8470971bf7d4c57b693de/pillow-12.3.0-cp315-cp315-ios_13_0_arm64_iphonesimulator.whl", hash = "sha256:9e881fca225083806662a5c43d627d215f258ff43c890f831966c7d7ba9c7402", size = 4255549 },
-    { url = "https://files.pythonhosted.org/packages/52/7e/1f67e6f4ece6b582ee4b539decbcc9f848dc245a93ed8cd7338bafef72f1/pillow-12.3.0-cp315-cp315-ios_13_0_x86_64_iphonesimulator.whl", hash = "sha256:4998562bf62a445225f22e07c896bb04b35b1b1f2eb6d760584c9c51d7a5f78c", size = 3696331 },
-    { url = "https://files.pythonhosted.org/packages/12/40/d306fc2c8e4d45d7f175c77edca7063be7b86fe7fe6e68f4353bf71d808c/pillow-12.3.0-cp315-cp315-macosx_10_15_x86_64.whl", hash = "sha256:dc624f6bc473dacdf7ef7eb8678d0d08edf15cd94fad6ae5c7d6cc67a4e4902f", size = 5350370 },
-    { url = "https://files.pythonhosted.org/packages/dd/44/668fb1437e8ce420f62d6106eb66e44a5971602a4d794615bdf79315d82d/pillow-12.3.0-cp315-cp315-macosx_11_0_arm64.whl", hash = "sha256:71d6097b330eea8fd15097780c8e89cb1a8ce7838669f48c5bacd6f663dd4701", size = 4780147 },
-    { url = "https://files.pythonhosted.org/packages/0c/08/93fa2e70e30a2d81547e481b6ee2bb9522117221fb1e0ce4b5df70967677/pillow-12.3.0-cp315-cp315-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:28ce87c5ab450a9dd970b52e5aca5fe63ed432d18a2eaddd1979a00a1ba24ace", size = 6273659 },
-    { url = "https://files.pythonhosted.org/packages/f8/6d/043e96ff814fc31a33077e4cba86082167db520c93632afdf2042febbb0c/pillow-12.3.0-cp315-cp315-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:6b02afb9b97f65fbca5f31db6a2a3ba21aa93030225f150fa3f249717e938fb4", size = 6947439 },
-    { url = "https://files.pythonhosted.org/packages/af/92/ba71d2ee2ac0edf3fa33bd9d5ee9ee080da70b1766f3ca3934f9938ddac9/pillow-12.3.0-cp315-cp315-musllinux_1_2_aarch64.whl", hash = "sha256:1182d52bc2d5e5d7d0949503aa7e36d12f42205dc287e4883f407b1988820d39", size = 6353577 },
-    { url = "https://files.pythonhosted.org/packages/0f/ce/e63064e2122923ff687c8ad792d0d736a7b3920a56a46982e81a7fdd25d6/pillow-12.3.0-cp315-cp315-musllinux_1_2_x86_64.whl", hash = "sha256:e795b7eb908249c4e43c7c99fac7c2c75dab0c43566e37db472a355f63693d71", size = 7060394 },
-    { url = "https://files.pythonhosted.org/packages/54/76/a09cc3ccc8d773a7283d34c38bec1708f9e3cc932093cbc4c5e71ac4060b/pillow-12.3.0-cp315-cp315-win32.whl", hash = "sha256:57b3d78c95ba9059768b10e28b813002261d3f3dfc55cc48b0c988f625175827", size = 6467375 },
-    { url = "https://files.pythonhosted.org/packages/3e/03/1846c49ba3b1d5550392a4bbd06d6fb4578e1cd91a803198b5c90f5f7d53/pillow-12.3.0-cp315-cp315-win_amd64.whl", hash = "sha256:fa4ecea169a355be7a3ade2c783e2ed12f0e40d2c5621cda8b3297faf7fbb9f5", size = 7237048 },
-    { url = "https://files.pythonhosted.org/packages/fb/bb/89f35dcc79610423f9f195504d7def7f0d1416a711541b42867e25fe3412/pillow-12.3.0-cp315-cp315-win_arm64.whl", hash = "sha256:877c3f311ff35410f690861c4409e7ccbf0cd2f878e50628a28e5a0bb689e658", size = 2566006 },
-    { url = "https://files.pythonhosted.org/packages/30/88/707027ba09942dfa2c28759b5c222d769290a41c6d20ea60ec250801941f/pillow-12.3.0-cp315-cp315t-macosx_10_15_x86_64.whl", hash = "sha256:e9871b1ffbfa9656b60aeee92ed5136a5742696006fa322b29ea3d8da0ecc9cf", size = 5352509 },
-    { url = "https://files.pythonhosted.org/packages/b0/6d/00352fa25332c2569cd387851f568cc5a4b75a9adbfb37ac4fbce4c02eec/pillow-12.3.0-cp315-cp315t-macosx_11_0_arm64.whl", hash = "sha256:53aa02d20d10c3d814d536aa4e5ac9b84ca0ff5a88377963b085ad6822f93e64", size = 4783167 },
-    { url = "https://files.pythonhosted.org/packages/13/4f/9e049dfa21af7c22427275720e2490267ba8138120add5c4c574deb69782/pillow-12.3.0-cp315-cp315t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:446c34dcc4324b084a53b705127dc15717b22c5e140ae0a3c38349d4efec071e", size = 6329237 },
-    { url = "https://files.pythonhosted.org/packages/36/16/cf6eeaae8d0fce8dd390a33437cf68c5d5bd73834a2bc6e2f14efda0ab45/pillow-12.3.0-cp315-cp315t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:cf1845d02ad822a369a49f2bb9345b1614744267682e7a03527dc3bf6eea1777", size = 6997047 },
-    { url = "https://files.pythonhosted.org/packages/1e/69/dbf769bdd55f48bf5733cac28edc6364ffaa072ec9ba336266e4fe66be55/pillow-12.3.0-cp315-cp315t-musllinux_1_2_aarch64.whl", hash = "sha256:186941b6aef820ad110fb01fb06eb925374dc3a21b17e37ec9a53b250c6fe2d1", size = 6400440 },
-    { url = "https://files.pythonhosted.org/packages/a0/e1/ffc9cfc2eea0d178da8018e18e959301ad9d6bc9f3edb7181e748a474b97/pillow-12.3.0-cp315-cp315t-musllinux_1_2_x86_64.whl", hash = "sha256:f13c32a3abd6079a66d9526e18dad9b6d280384d49d7c54040cd57b6424041d9", size = 7105895 },
-    { url = "https://files.pythonhosted.org/packages/18/f0/a5595c1e8c3ae44b9828cb2f0fa8155e5095ef04d6327b8f61cf44a3df85/pillow-12.3.0-cp315-cp315t-win32.whl", hash = "sha256:1657923d2d45afb66526e5b933e5b3052e6bdea196c90d3abb2424e18c77dae8", size = 6474384 },
-    { url = "https://files.pythonhosted.org/packages/e4/04/62bcd9f844984c5938d3b05264a61d797a29d3e0812341a8204af70bbdee/pillow-12.3.0-cp315-cp315t-win_amd64.whl", hash = "sha256:8cd2f7bdda092d99c9fc2fb7391354f306d01443d22785d0cbfafa2e2c8bb418", size = 7243537 },
-    { url = "https://files.pythonhosted.org/packages/3d/68/1f3066acedf37673694a7141381d8f811ae97f30d34413d236abe7d489f1/pillow-12.3.0-cp315-cp315t-win_arm64.whl", hash = "sha256:06ff022112bc9cbf83b60f8e028d94ad87b60621706487e65f673de61610ab59", size = 2567491 },
-]
-
-[[package]]
-name = "pluggy"
-version = "1.6.0"
-source = { registry = "https://pypi.org/simple" }
-sdist = { url = "https://files.pythonhosted.org/packages/f9/e2/3e91f31a7d2b083fe6ef3fa267035b518369d9511ffab804f839851d2779/pluggy-1.6.0.tar.gz", hash = "sha256:7dcc130b76258d33b90f61b658791dede3486c3e6bfb003ee5c9bfb396dd22f3", size = 69412 }
-wheels = [
-    { url = "https://files.pythonhosted.org/packages/54/20/4d324d65cc6d9205fabedc306948156824eb9f0ee1633355a8f7ec5c66bf/pluggy-1.6.0-py3-none-any.whl", hash = "sha256:e920276dd6813095e9377c0bc5566d94c932c33b27a3e3945d8389c374dd4746", size = 20538 },
-]
-
-[[package]]
-name = "pydantic"
-version = "2.13.4"
-source = { registry = "https://pypi.org/simple" }
-dependencies = [
-    { name = "annotated-types" },
-    { name = "pydantic-core" },
-    { name = "typing-extensions" },
-    { name = "typing-inspection" },
-]
-sdist = { url = "https://files.pythonhosted.org/packages/18/a5/b60d21ac674192f8ab0ba4e9fd860690f9b4a6e51ca5df118733b487d8d6/pydantic-2.13.4.tar.gz", hash = "sha256:c40756b57adaa8b1efeeced5c196f3f3b7c435f90e84ea7f443901bec8099ef6", size = 844775 }
-wheels = [
-    { url = "https://files.pythonhosted.org/packages/fd/7b/122376b1fd3c62c1ed9dc80c931ace4844b3c55407b6fb2d199377c9736f/pydantic-2.13.4-py3-none-any.whl", hash = "sha256:45a282cde31d808236fd7ea9d919b128653c8b38b393d1c4ab335c62924d9aba", size = 472262 },
-]
-
-[[package]]
-name = "pydantic-core"
-version = "2.46.4"
-source = { registry = "https://pypi.org/simple" }
-dependencies = [
-    { name = "typing-extensions" },
-]
-sdist = { url = "https://files.pythonhosted.org/packages/9d/56/921726b776ace8d8f5db44c4ef961006580d91dc52b803c489fafd1aa249/pydantic_core-2.46.4.tar.gz", hash = "sha256:62f875393d7f270851f20523dd2e29f082bcc82292d66db2b64ea71f64b6e1c1", size = 471464 }
-wheels = [
-    { url = "https://files.pythonhosted.org/packages/8d/74/228a26ddad29c6672b805d9fd78e8d251cd04004fa7eed0e622096cd0250/pydantic_core-2.46.4-cp314-cp314-macosx_10_12_x86_64.whl", hash = "sha256:428e04521a40150c85216fc8b85e8d39fece235a9cf5e383761238c7fa9b96fb", size = 2102079 },
-    { url = "https://files.pythonhosted.org/packages/ad/1f/8970b150a4b4365623ae00fc88603491f763c627311ae8031e3111356d6e/pydantic_core-2.46.4-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:23ace664830ee0bfe014a0c7bc248b1f7f25ed7ad103852c317624a1083af462", size = 1952179 },
-    { url = "https://files.pythonhosted.org/packages/95/30/5211a831ae054928054b2f79731661087a2bc5c01e825c672b3a4a8f1b3e/pydantic_core-2.46.4-cp314-cp314-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ce5c1d2a8b27468f433ca974829c44060b8097eedc39933e3c206a90ee49c4a9", size = 1978926 },
-    { url = "https://files.pythonhosted.org/packages/57/e9/689668733b1eb67adeef047db3c2e8788fcf65a7fd9c9e2b46b7744fe245/pydantic_core-2.46.4-cp314-cp314-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:7283d57845ecf5a163403eb0702dfc220cc4fbdd18919cb5ccea4f95ee1cdab4", size = 2046785 },
-    { url = "https://files.pythonhosted.org/packages/60/d9/6715260422ff50a2109878fd24d948a6c3446bb2664f34ee78cd972b3acd/pydantic_core-2.46.4-cp314-cp314-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:8daafc69c93ee8a0204506a3b6b30f586ef54028f52aeeeb5c4cfc5184fd5914", size = 2228733 },
-    { url = "https://files.pythonhosted.org/packages/18/ae/fdb2f64316afca925640f8e70bb1a564b0ec2721c1389e25b8eb4bf9a299/pydantic_core-2.46.4-cp314-cp314-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:cd2213145bcc2ba85884d0ac63d222fece9209678f77b9b4d76f054c561adb28", size = 2307534 },
-    { url = "https://files.pythonhosted.org/packages/89/1d/8eff589b45bb8190a9d12c49cfad0f176a5cbd1534908a6b5125e2886239/pydantic_core-2.46.4-cp314-cp314-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7a5f930472650a82629163023e630d160863fce524c616f4e5186e5de9d9a49b", size = 2099732 },
-    { url = "https://files.pythonhosted.org/packages/06/d5/ee5a3366637fee41dee51a1fc91562dcf12ddbc68fda34e6b253da2324bb/pydantic_core-2.46.4-cp314-cp314-manylinux_2_31_riscv64.whl", hash = "sha256:c1b3f518abeca3aa13c712fd202306e145abf59a18b094a6bafb2d2bbf59192c", size = 2129627 },
-    { url = "https://files.pythonhosted.org/packages/94/33/2414be571d2c6a6c4d08be21f9292b6d3fdb08949a97b6dfe985017821db/pydantic_core-2.46.4-cp314-cp314-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:1a7dd0b3ee80d90150e3495a3a13ac34dbcbfd4f012996a6a1d8900e91b5c0fb", size = 2179141 },
-    { url = "https://files.pythonhosted.org/packages/7b/79/7daa95be995be0eecc4cf75064cb33f9bbbfe3fe0158caf2f0d4a996a5c7/pydantic_core-2.46.4-cp314-cp314-musllinux_1_1_aarch64.whl", hash = "sha256:3fb702cd90b0446a3a1c5e470bfa0dd23c0233b676a9099ddcc964fa6ca13898", size = 2184325 },
-    { url = "https://files.pythonhosted.org/packages/9f/cb/d0a382f5c0de8a222dc61c65348e0ce831b1f68e0a018450d31c2cace3a5/pydantic_core-2.46.4-cp314-cp314-musllinux_1_1_armv7l.whl", hash = "sha256:b8458003118a712e66286df6a707db01c52c0f52f7db8e4a38f0da1d3b94fc4e", size = 2323990 },
-    { url = "https://files.pythonhosted.org/packages/05/db/d9ba624cc4a5aced1598e88c04fdbd8310c8a69b9d38b9a3d39ce3a61ed7/pydantic_core-2.46.4-cp314-cp314-musllinux_1_1_x86_64.whl", hash = "sha256:372429a130e469c9cd698925ce5fc50940b7a1336b0d82038e63d5bbc4edc519", size = 2369978 },
-    { url = "https://files.pythonhosted.org/packages/f2/20/d15df15ba918c423461905802bfd2981c3af0bfa0e40d05e13edbfa48bc3/pydantic_core-2.46.4-cp314-cp314-win32.whl", hash = "sha256:85bb3611ff1802f3ee7fdd7dbff26b56f343fb432d57a4728fdd49b6ef35e2f4", size = 1966354 },
-    { url = "https://files.pythonhosted.org/packages/fc/b6/6b8de4c0a7d7ab3004c439c80c5c1e0a3e8d78bbae19379b01960383d9e5/pydantic_core-2.46.4-cp314-cp314-win_amd64.whl", hash = "sha256:811ff8e9c313ab425368bcbb36e5c4ebd7108c2bbf4e4089cfbb0b01eff63fac", size = 2072238 },
-    { url = "https://files.pythonhosted.org/packages/32/36/51eb763beec1f4cf59b1db243a7dcc39cbb41230f050a09b9d69faaf0a48/pydantic_core-2.46.4-cp314-cp314-win_arm64.whl", hash = "sha256:bfec22eab3c8cc2ceec0248aec886624116dc079afa027ecc8ad4a7e62010f8a", size = 2018251 },
-    { url = "https://files.pythonhosted.org/packages/e8/91/855af51d625b23aa987116a19e231d2aaef9c4a415273ddc189b79a45fee/pydantic_core-2.46.4-cp314-cp314t-macosx_10_12_x86_64.whl", hash = "sha256:af8244b2bef6aaad6d92cda81372de7f8c8d36c9f0c3ea36e827c60e7d9467a0", size = 2099593 },
-    { url = "https://files.pythonhosted.org/packages/fb/1b/8784a54c65edb5f49f0a14d6977cf1b209bba85a4c77445b255c2de58ab3/pydantic_core-2.46.4-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:5a4330cdbc57162e4b3aa303f588ba752257694c9c9be3e7ebb11b4aca659b5d", size = 1935226 },
-    { url = "https://files.pythonhosted.org/packages/e8/e7/1955d28d1afc56dd4b3ad7cc0cf39df1b9852964cf16e5d13912756d6d6b/pydantic_core-2.46.4-cp314-cp314t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:29c61fc04a3d840155ff08e475a04809278972fe6aef51e2720554e96367e34b", size = 1974605 },
-    { url = "https://files.pythonhosted.org/packages/93/e2/3fedbf0ba7a22850e6e9fd78117f1c0f10f950182344d8a6c535d468fdd8/pydantic_core-2.46.4-cp314-cp314t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:c50f2528cf200c5eed56faf3f4e22fcd5f38c157a8b78576e6ba3168ec35f000", size = 2030777 },
-    { url = "https://files.pythonhosted.org/packages/f8/61/46be275fcaaba0b4f5b9669dd852267ce1ff616592dccf7a7845588df091/pydantic_core-2.46.4-cp314-cp314t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:0cbe8b01f948de4286c74cdd6c667aceb38f5c1e26f0693b3983d9d74887c65e", size = 2236641 },
-    { url = "https://files.pythonhosted.org/packages/60/db/12e93e46a8bac9988be3c016860f83293daea8c716c029c9ace279036f2f/pydantic_core-2.46.4-cp314-cp314t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:617d7e2ca7dcb8c5cf6bcb8c59b8832c94b36196bbf1cbd1bfb56ed341905edd", size = 2286404 },
-    { url = "https://files.pythonhosted.org/packages/e2/4a/4d8b19008f38d31c53b8219cfedc2e3d5de5fe99d90076b7e767de29274f/pydantic_core-2.46.4-cp314-cp314t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7027560ee92211647d0d34e3f7cd6f50da56399d26a9c8ad0da286d3869a53f3", size = 2109219 },
-    { url = "https://files.pythonhosted.org/packages/88/70/3cbc40978fefb7bb09c6708d40d4ad1a5d70fd7213c3d17f971de868ec1f/pydantic_core-2.46.4-cp314-cp314t-manylinux_2_31_riscv64.whl", hash = "sha256:f99626688942fb746e545232e7726926f3be91b5975f8b55327665fafda991c7", size = 2110594 },
-    { url = "https://files.pythonhosted.org/packages/9d/20/b8d36736216e29491125531685b2f9e61aa5b4b2599893f8268551da3338/pydantic_core-2.46.4-cp314-cp314t-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:fc3e9034a63de20e15e8ade85358bc6efc614008cab72898b4b4952bea0509ff", size = 2159542 },
-    { url = "https://files.pythonhosted.org/packages/1d/a2/367df868eb584dacf6bf82a389272406d7178e301c4ac82545ab98bc2dd9/pydantic_core-2.46.4-cp314-cp314t-musllinux_1_1_aarch64.whl", hash = "sha256:97e7cf2be5c77b7d1a9713a05605d49460d02c6078d38d8bef3cbe323c548424", size = 2168146 },
-    { url = "https://files.pythonhosted.org/packages/c1/b8/4460f77f7e201893f649a29ab355dddd3beee8a97bcb1a320db414f9a06e/pydantic_core-2.46.4-cp314-cp314t-musllinux_1_1_armv7l.whl", hash = "sha256:3bf92c5d0e00fefaab325a4d27828fe6b6e2a21848686b5b60d2d9eeb09d76c6", size = 2306309 },
-    { url = "https://files.pythonhosted.org/packages/64/c4/be2639293acd87dc8ddbcec41a73cee9b2ebf996fe6d892a1a74e88ad3f7/pydantic_core-2.46.4-cp314-cp314t-musllinux_1_1_x86_64.whl", hash = "sha256:3ecbc122d18468d06ca279dc26a8c2e2d5acb10943bb35e36ae92096dc3b5565", size = 2369736 },
-    { url = "https://files.pythonhosted.org/packages/30/a6/9f9f380dbb301f67023bf8f707aaa75daadf84f7152d95c410fd7e81d994/pydantic_core-2.46.4-cp314-cp314t-win32.whl", hash = "sha256:e846ae7835bf0703ae43f534ab79a867146dadd59dc9ca5c8b53d5c8f7c9ef02", size = 1955575 },
-    { url = "https://files.pythonhosted.org/packages/40/1f/f1eb9eb350e795d1af8586289746f5c5677d16043040d63710e22abc43c9/pydantic_core-2.46.4-cp314-cp314t-win_amd64.whl", hash = "sha256:2108ba5c1c1eca18030634489dc544844144ee36357f2f9f780b93e7ddbb44b5", size = 2051624 },
-    { url = "https://files.pythonhosted.org/packages/f6/d2/42dd53d0a85c27606f316d3aa5d2869c4e8470a5ed6dec30e4a1abe19192/pydantic_core-2.46.4-cp314-cp314t-win_arm64.whl", hash = "sha256:4fcbe087dbc2068af7eda3aa87634eba216dbda64d1ae73c8684b621d33f6596", size = 2017325 },
-]
-
-[[package]]
-name = "pygments"
-version = "2.20.0"
-source = { registry = "https://pypi.org/simple" }
-sdist = { url = "https://files.pythonhosted.org/packages/c3/b2/bc9c9196916376152d655522fdcebac55e66de6603a76a02bca1b6414f6c/pygments-2.20.0.tar.gz", hash = "sha256:6757cd03768053ff99f3039c1a36d6c0aa0b263438fcab17520b30a303a82b5f", size = 4955991 }
-wheels = [
-    { url = "https://files.pythonhosted.org/packages/f4/7e/a72dd26f3b0f4f2bf1dd8923c85f7ceb43172af56d63c7383eb62b332364/pygments-2.20.0-py3-none-any.whl", hash = "sha256:81a9e26dd42fd28a23a2d169d86d7ac03b46e2f8b59ed4698fb4785f946d0176", size = 1231151 },
-]
-
-[[package]]
-name = "pytest"
-version = "9.1.1"
-source = { registry = "https://pypi.org/simple" }
-dependencies = [
-    { name = "colorama", marker = "sys_platform == 'win32'" },
-    { name = "iniconfig" },
-    { name = "packaging" },
-    { name = "pluggy" },
-    { name = "pygments" },
-]
-sdist = { url = "https://files.pythonhosted.org/packages/e4/47/b9efed96c114afcfa3c9d3fe98a76a1d14c74a9e266d397cf6eb64be5e01/pytest-9.1.1.tar.gz", hash = "sha256:1088fbde8f2b49d95a549a195707afa7a76a3ce9bcadc26b6d71f0ffda5fe313", size = 1636369 }
-wheels = [
-    { url = "https://files.pythonhosted.org/packages/24/25/1de2678b631f5a49215c6c96fff41ba892b0a34df68d6d80292b1b48aa7f/pytest-9.1.1-py3-none-any.whl", hash = "sha256:37a86b45efb9a47a61a36449063e8e18d0cab3161329fc099eb21783169c4f0c", size = 386536 },
-]
-
-[[package]]
-name = "python-dotenv"
-version = "1.2.2"
-source = { registry = "https://pypi.org/simple" }
-sdist = { url = "https://files.pythonhosted.org/packages/82/ed/0301aeeac3e5353ef3d94b6ec08bbcabd04a72018415dcb29e588514bba8/python_dotenv-1.2.2.tar.gz", hash = "sha256:2c371a91fbd7ba082c2c1dc1f8bf89ca22564a087c2c287cd9b662adde799cf3", size = 50135 }
-wheels = [
-    { url = "https://files.pythonhosted.org/packages/0b/d7/1959b9648791274998a9c3526f6d0ec8fd2233e4d4acce81bbae76b44b2a/python_dotenv-1.2.2-py3-none-any.whl", hash = "sha256:1d8214789a24de455a8b8bd8ae6fe3c6b69a5e3d64aa8a8e5d68e694bbcb285a", size = 22101 },
-]
-
-[[package]]
-name = "python-multipart"
-version = "0.0.32"
-source = { registry = "https://pypi.org/simple" }
-sdist = { url = "https://files.pythonhosted.org/packages/5b/42/55c32bb9b12693c092ad250a0e82edb5b31ddeda6eb772de5f308b3804ad/python_multipart-0.0.32.tar.gz", hash = "sha256:be54b7f3fa167bb83e4fcd936b887b708f4e57fe75911c02aebf53efaf8d938e", size = 46881 }
-wheels = [
-    { url = "https://files.pythonhosted.org/packages/e1/04/e8135ebd1ad02c56ec633277529b2602ff99ff634be76cdba5744cf554fd/python_multipart-0.0.32-py3-none-any.whl", hash = "sha256:ff6d3f776f16878c894e52e107296ffc890e913c611b1a4ec6c44e2821fe2e23", size = 30042 },
-]
-
-[[package]]
-name = "pyyaml"
-version = "6.0.3"
-source = { registry = "https://pypi.org/simple" }
-sdist = { url = "https://files.pythonhosted.org/packages/05/8e/961c0007c59b8dd7729d542c61a4d537767a59645b82a0b521206e1e25c2/pyyaml-6.0.3.tar.gz", hash = "sha256:d76623373421df22fb4cf8817020cbb7ef15c725b9d5e45f17e189bfc384190f", size = 130960 }
-wheels = [
-    { url = "https://files.pythonhosted.org/packages/9d/8c/f4bd7f6465179953d3ac9bc44ac1a8a3e6122cf8ada906b4f96c60172d43/pyyaml-6.0.3-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:8d1fab6bb153a416f9aeb4b8763bc0f22a5586065f86f7664fc23339fc1c1fac", size = 181814 },
-    { url = "https://files.pythonhosted.org/packages/bd/9c/4d95bb87eb2063d20db7b60faa3840c1b18025517ae857371c4dd55a6b3a/pyyaml-6.0.3-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:34d5fcd24b8445fadc33f9cf348c1047101756fd760b4dacb5c3e99755703310", size = 173809 },
-    { url = "https://files.pythonhosted.org/packages/92/b5/47e807c2623074914e29dabd16cbbdd4bf5e9b2db9f8090fa64411fc5382/pyyaml-6.0.3-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:501a031947e3a9025ed4405a168e6ef5ae3126c59f90ce0cd6f2bfc477be31b7", size = 766454 },
-    { url = "https://files.pythonhosted.org/packages/02/9e/e5e9b168be58564121efb3de6859c452fccde0ab093d8438905899a3a483/pyyaml-6.0.3-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:b3bc83488de33889877a0f2543ade9f70c67d66d9ebb4ac959502e12de895788", size = 836355 },
-    { url = "https://files.pythonhosted.org/packages/88/f9/16491d7ed2a919954993e48aa941b200f38040928474c9e85ea9e64222c3/pyyaml-6.0.3-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c458b6d084f9b935061bc36216e8a69a7e293a2f1e68bf956dcd9e6cbcd143f5", size = 794175 },
-    { url = "https://files.pythonhosted.org/packages/dd/3f/5989debef34dc6397317802b527dbbafb2b4760878a53d4166579111411e/pyyaml-6.0.3-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:7c6610def4f163542a622a73fb39f534f8c101d690126992300bf3207eab9764", size = 755228 },
-    { url = "https://files.pythonhosted.org/packages/d7/ce/af88a49043cd2e265be63d083fc75b27b6ed062f5f9fd6cdc223ad62f03e/pyyaml-6.0.3-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:5190d403f121660ce8d1d2c1bb2ef1bd05b5f68533fc5c2ea899bd15f4399b35", size = 789194 },
-    { url = "https://files.pythonhosted.org/packages/23/20/bb6982b26a40bb43951265ba29d4c246ef0ff59c9fdcdf0ed04e0687de4d/pyyaml-6.0.3-cp314-cp314-win_amd64.whl", hash = "sha256:4a2e8cebe2ff6ab7d1050ecd59c25d4c8bd7e6f400f5f82b96557ac0abafd0ac", size = 156429 },
-    { url = "https://files.pythonhosted.org/packages/f4/f4/a4541072bb9422c8a883ab55255f918fa378ecf083f5b85e87fc2b4eda1b/pyyaml-6.0.3-cp314-cp314-win_arm64.whl", hash = "sha256:93dda82c9c22deb0a405ea4dc5f2d0cda384168e466364dec6255b293923b2f3", size = 143912 },
-    { url = "https://files.pythonhosted.org/packages/7c/f9/07dd09ae774e4616edf6cda684ee78f97777bdd15847253637a6f052a62f/pyyaml-6.0.3-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:02893d100e99e03eda1c8fd5c441d8c60103fd175728e23e431db1b589cf5ab3", size = 189108 },
-    { url = "https://files.pythonhosted.org/packages/4e/78/8d08c9fb7ce09ad8c38ad533c1191cf27f7ae1effe5bb9400a46d9437fcf/pyyaml-6.0.3-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:c1ff362665ae507275af2853520967820d9124984e0f7466736aea23d8611fba", size = 183641 },
-    { url = "https://files.pythonhosted.org/packages/7b/5b/3babb19104a46945cf816d047db2788bcaf8c94527a805610b0289a01c6b/pyyaml-6.0.3-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6adc77889b628398debc7b65c073bcb99c4a0237b248cacaf3fe8a557563ef6c", size = 831901 },
-    { url = "https://files.pythonhosted.org/packages/8b/cc/dff0684d8dc44da4d22a13f35f073d558c268780ce3c6ba1b87055bb0b87/pyyaml-6.0.3-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:a80cb027f6b349846a3bf6d73b5e95e782175e52f22108cfa17876aaeff93702", size = 861132 },
-    { url = "https://files.pythonhosted.org/packages/b1/5e/f77dc6b9036943e285ba76b49e118d9ea929885becb0a29ba8a7c75e29fe/pyyaml-6.0.3-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:00c4bdeba853cc34e7dd471f16b4114f4162dc03e6b7afcc2128711f0eca823c", size = 839261 },
-    { url = "https://files.pythonhosted.org/packages/ce/88/a9db1376aa2a228197c58b37302f284b5617f56a5d959fd1763fb1675ce6/pyyaml-6.0.3-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:66e1674c3ef6f541c35191caae2d429b967b99e02040f5ba928632d9a7f0f065", size = 805272 },
-    { url = "https://files.pythonhosted.org/packages/da/92/1446574745d74df0c92e6aa4a7b0b3130706a4142b2d1a5869f2eaa423c6/pyyaml-6.0.3-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:16249ee61e95f858e83976573de0f5b2893b3677ba71c9dd36b9cf8be9ac6d65", size = 829923 },
-    { url = "https://files.pythonhosted.org/packages/f0/7a/1c7270340330e575b92f397352af856a8c06f230aa3e76f86b39d01b416a/pyyaml-6.0.3-cp314-cp314t-win_amd64.whl", hash = "sha256:4ad1906908f2f5ae4e5a8ddfce73c320c2a1429ec52eafd27138b7f1cbe341c9", size = 174062 },
-    { url = "https://files.pythonhosted.org/packages/f1/12/de94a39c2ef588c7e6455cfbe7343d3b2dc9d6b6b2f40c4c6565744c873d/pyyaml-6.0.3-cp314-cp314t-win_arm64.whl", hash = "sha256:ebc55a14a21cb14062aa4162f906cd962b28e2e9ea38f9b4391244cd8de4ae0b", size = 149341 },
-]
-
-[[package]]
-name = "starlette"
-version = "1.6.0"
-source = { registry = "https://pypi.org/simple" }
-dependencies = [
-    { name = "anyio" },
-]
-sdist = { url = "https://files.pythonhosted.org/packages/b5/b4/205b0d5241d934e8add0c38aa924c4f9fb7330834ff11e5444db964ec3f9/starlette-1.6.0.tar.gz", hash = "sha256:d4e3ac5e546444960c710297a3c9fc3f7ebae1b7e963f3d36173b49da535be9b", size = 2716969 }
-wheels = [
-    { url = "https://files.pythonhosted.org/packages/c8/cb/6a6a47d5b464bd08695d254f3da6e7986cc70c9fa5d778eda57538edfe56/starlette-1.6.0-py3-none-any.whl", hash = "sha256:a86dd39d14bb45f85a3d18525215a9ef0cfd1f192ac793220e72598c90335f0c", size = 75969 },
-]
-
-[[package]]
-name = "typing-extensions"
-version = "4.16.0"
-source = { registry = "https://pypi.org/simple" }
-sdist = { url = "https://files.pythonhosted.org/packages/f6/cc/6253133b5bb138fc3306cebfbda2c520f545d36b5be2c7255cc528bb45d6/typing_extensions-4.16.0.tar.gz", hash = "sha256:dc983d19a509c94dba722ee6abd33940f7c05a89e243c47e907eb4db6f1a43e5", size = 113555 }
-wheels = [
-    { url = "https://files.pythonhosted.org/packages/49/d3/b8441a820a491ddfc024b0b0cf0393375b75ea13866d9c66727e54c2fc80/typing_extensions-4.16.0-py3-none-any.whl", hash = "sha256:481caa481374e813c1b176ada14e97f1f67a4539ce9cfeb3f350d78d6370c2e8", size = 45571 },
-]
-
-[[package]]
-name = "typing-inspection"
-version = "0.4.2"
-source = { registry = "https://pypi.org/simple" }
-dependencies = [
-    { name = "typing-extensions" },
-]
-sdist = { url = "https://files.pythonhosted.org/packages/55/e3/70399cb7dd41c10ac53367ae42139cf4b1ca5f36bb3dc6c9d33acdb43655/typing_inspection-0.4.2.tar.gz", hash = "sha256:ba561c48a67c5958007083d386c3295464928b01faa735ab8547c5692e87f464", size = 75949 }
-wheels = [
-    { url = "https://files.pythonhosted.org/packages/dc/9b/47798a6c91d8bdb567fe2698fe81e0c6b7cb7ef4d13da4114b41d239f65d/typing_inspection-0.4.2-py3-none-any.whl", hash = "sha256:4ed1cacbdc298c220f1bd249ed5287caa16f34d44ef4e9c3d0cbad5b521545e7", size = 14611 },
-]
-
-[[package]]
-name = "uvicorn"
-version = "0.52.1"
-source = { registry = "https://pypi.org/simple" }
-dependencies = [
-    { name = "click" },
-    { name = "h11" },
-]
-sdist = { url = "https://files.pythonhosted.org/packages/03/18/ccce41535dee1be77735592bd19965f3972c82e07ee703d324709496b716/uvicorn-0.52.1.tar.gz", hash = "sha256:112ec661814189acbccd3f7b86460147cc065fc92c0821afa78918780e4354dd", size = 100571 }
-wheels = [
-    { url = "https://files.pythonhosted.org/packages/c7/d5/68e6e9bca63c0badf67002890a46d3784c958de45b65e1275ec583ca1f06/uvicorn-0.52.1-py3-none-any.whl", hash = "sha256:e4403f9d93188cf9d1088e9f40e3acd12630e2df8675316704379a7fc20fff6a", size = 79859 },
-]
-
-[package.optional-dependencies]
-standard = [
-    { name = "httptools" },
-    { name = "python-dotenv" },
-    { name = "pyyaml" },
-    { name = "uvloop", marker = "platform_python_implementation != 'PyPy' and sys_platform != 'cygwin' and sys_platform != 'win32'" },
-    { name = "watchfiles" },
-    { name = "websockets" },
-]
-
-[[package]]
-name = "uvloop"
-version = "0.22.1"
-source = { registry = "https://pypi.org/simple" }
-sdist = { url = "https://files.pythonhosted.org/packages/06/f0/18d39dbd1971d6d62c4629cc7fa67f74821b0dc1f5a77af43719de7936a7/uvloop-0.22.1.tar.gz", hash = "sha256:6c84bae345b9147082b17371e3dd5d42775bddce91f885499017f4607fdaf39f", size = 2443250 }
-wheels = [
-    { url = "https://files.pythonhosted.org/packages/90/cd/b62bdeaa429758aee8de8b00ac0dd26593a9de93d302bff3d21439e9791d/uvloop-0.22.1-cp314-cp314-macosx_10_13_universal2.whl", hash = "sha256:3879b88423ec7e97cd4eba2a443aa26ed4e59b45e6b76aabf13fe2f27023a142", size = 1362067 },
-    { url = "https://files.pythonhosted.org/packages/0d/f8/a132124dfda0777e489ca86732e85e69afcd1ff7686647000050ba670689/uvloop-0.22.1-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:4baa86acedf1d62115c1dc6ad1e17134476688f08c6efd8a2ab076e815665c74", size = 752423 },
-    { url = "https://files.pythonhosted.org/packages/a3/94/94af78c156f88da4b3a733773ad5ba0b164393e357cc4bd0ab2e2677a7d6/uvloop-0.22.1-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:297c27d8003520596236bdb2335e6b3f649480bd09e00d1e3a99144b691d2a35", size = 4272437 },
-    { url = "https://files.pythonhosted.org/packages/b5/35/60249e9fd07b32c665192cec7af29e06c7cd96fa1d08b84f012a56a0b38e/uvloop-0.22.1-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c1955d5a1dd43198244d47664a5858082a3239766a839b2102a269aaff7a4e25", size = 4292101 },
-    { url = "https://files.pythonhosted.org/packages/02/62/67d382dfcb25d0a98ce73c11ed1a6fba5037a1a1d533dcbb7cab033a2636/uvloop-0.22.1-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:b31dc2fccbd42adc73bc4e7cdbae4fc5086cf378979e53ca5d0301838c5682c6", size = 4114158 },
-    { url = "https://files.pythonhosted.org/packages/f0/7a/f1171b4a882a5d13c8b7576f348acfe6074d72eaf52cccef752f748d4a9f/uvloop-0.22.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:93f617675b2d03af4e72a5333ef89450dfaa5321303ede6e67ba9c9d26878079", size = 4177360 },
-    { url = "https://files.pythonhosted.org/packages/79/7b/b01414f31546caf0919da80ad57cbfe24c56b151d12af68cee1b04922ca8/uvloop-0.22.1-cp314-cp314t-macosx_10_13_universal2.whl", hash = "sha256:37554f70528f60cad66945b885eb01f1bb514f132d92b6eeed1c90fd54ed6289", size = 1454790 },
-    { url = "https://files.pythonhosted.org/packages/d4/31/0bb232318dd838cad3fa8fb0c68c8b40e1145b32025581975e18b11fab40/uvloop-0.22.1-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:b76324e2dc033a0b2f435f33eb88ff9913c156ef78e153fb210e03c13da746b3", size = 796783 },
-    { url = "https://files.pythonhosted.org/packages/42/38/c9b09f3271a7a723a5de69f8e237ab8e7803183131bc57c890db0b6bb872/uvloop-0.22.1-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:badb4d8e58ee08dad957002027830d5c3b06aea446a6a3744483c2b3b745345c", size = 4647548 },
-    { url = "https://files.pythonhosted.org/packages/c1/37/945b4ca0ac27e3dc4952642d4c900edd030b3da6c9634875af6e13ae80e5/uvloop-0.22.1-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:b91328c72635f6f9e0282e4a57da7470c7350ab1c9f48546c0f2866205349d21", size = 4467065 },
-    { url = "https://files.pythonhosted.org/packages/97/cc/48d232f33d60e2e2e0b42f4e73455b146b76ebe216487e862700457fbf3c/uvloop-0.22.1-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:daf620c2995d193449393d6c62131b3fbd40a63bf7b307a1527856ace637fe88", size = 4328384 },
-    { url = "https://files.pythonhosted.org/packages/e4/16/c1fd27e9549f3c4baf1dc9c20c456cd2f822dbf8de9f463824b0c0357e06/uvloop-0.22.1-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:6cde23eeda1a25c75b2e07d39970f3374105d5eafbaab2a4482be82f272d5a5e", size = 4296730 },
-]
-
-[[package]]
-name = "watchfiles"
-version = "1.2.0"
-source = { registry = "https://pypi.org/simple" }
-dependencies = [
-    { name = "anyio" },
-]
-sdist = { url = "https://files.pythonhosted.org/packages/cd/41/5e1a4bb12aac5f1493fa1bdc11154eca3b258ca4eba65d39c473fe19d8e9/watchfiles-1.2.0.tar.gz", hash = "sha256:c995fba777f1ea992f090f9236e9284cf7a5d1a0130dd5a3d82c598cacd76838", size = 108252 }
-wheels = [
-    { url = "https://files.pythonhosted.org/packages/e7/54/a9c7ea9a82a4ac65e7004c0a03920b5cdd2f9c3b678757d9cd425aa51d53/watchfiles-1.2.0-cp314-cp314-macosx_10_12_x86_64.whl", hash = "sha256:b8c8358484d5fa12ef34f05b7f4168eaf1932f408725ff6d023c33ec17bd79d4", size = 400205 },
-    { url = "https://files.pythonhosted.org/packages/aa/5d/c9ab3534374a4a67450696905d6ef16a04405448b8dc52bd752ae50423d4/watchfiles-1.2.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:9f04b092229ad2c50126dd3c922c8822e51e605993764a33058d4a791ab42281", size = 392508 },
-    { url = "https://files.pythonhosted.org/packages/26/ca/1ad30103535cf0cecd7b993e8d50edc5351b1820e38f2d22e3df58962feb/watchfiles-1.2.0-cp314-cp314-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7a7ce236284f002a156f70add88efe5c70879cccbb658be0822c54b1306fc09d", size = 452448 },
-    { url = "https://files.pythonhosted.org/packages/37/a1/ceee2cdf2afbd715fa07758d39c9859513eae411b23196f7fd039e5feedd/watchfiles-1.2.0-cp314-cp314-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:b9909cc2b48468b575eefa944919e1fe8a36c5849d5c7c168f80a8c1db69398e", size = 459605 },
-    { url = "https://files.pythonhosted.org/packages/e8/f6/421e30fd1cb3907a84ed92ab3f1983e37ba2dca015e9a894a048418417a2/watchfiles-1.2.0-cp314-cp314-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:0a37faaed405c67e28e6be45a1fa4f206ef5a2860f27c237db9fa30704c38242", size = 490757 },
-    { url = "https://files.pythonhosted.org/packages/41/b0/55ed1b97ed08be7bba6f9a541cac15f2a858e1d74d2b07b6da70a82aab00/watchfiles-1.2.0-cp314-cp314-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:9649193aa27bd9ff2e80ff29bfaa93085496c7a3a377592823cc58b77ee88add", size = 568672 },
-    { url = "https://files.pythonhosted.org/packages/d1/cf/d8ae8a80dd7bafab395ea7681c10237311bbf34d37704a8c744e7cf31fc7/watchfiles-1.2.0-cp314-cp314-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:4e4ff8e37f99cf1da89e255e07c9c4b37c214038c4283707bdec308cb1b0ea1f", size = 464197 },
-    { url = "https://files.pythonhosted.org/packages/7c/8a/3076c496ca8dafe0e8cd03fcebdfc47be4b1174b4e5b24ff6e396e6b3af2/watchfiles-1.2.0-cp314-cp314-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:054dc20fd2e3132b4c3883b4a00d72fd6e1f56fdaf89fccd12e8057d74cd74d7", size = 453181 },
-    { url = "https://files.pythonhosted.org/packages/e5/10/9745e17c98e7b8a86454df0a3c7b5686bd650383f1e9f26e4ebcbd6cc0c0/watchfiles-1.2.0-cp314-cp314-manylinux_2_31_riscv64.whl", hash = "sha256:e140ed30ebde76796b686e67c182cff10ea2fbab186fafd1560f74bb5a473a6e", size = 465109 },
-    { url = "https://files.pythonhosted.org/packages/8f/95/8ef4a95481d3e0cb52d62a06fa6e972e81424be2d9698b91a2fecca9904c/watchfiles-1.2.0-cp314-cp314-musllinux_1_1_aarch64.whl", hash = "sha256:bb7e52ecf68ba46d22df23467b87cffeb2146908aa523ebfe803019618cfda06", size = 630653 },
-    { url = "https://files.pythonhosted.org/packages/fd/e4/3b3bf36b0f829b50c6ebcb8d031583863c59f923d6a6af3d485e470d0fac/watchfiles-1.2.0-cp314-cp314-musllinux_1_1_x86_64.whl", hash = "sha256:23282a321c8baf9b3a3c4afff673f9fe65eb7fdc2338d765ccad9d3d1916a5ba", size = 657838 },
-    { url = "https://files.pythonhosted.org/packages/21/b1/6cbbb50c1f3002ab568777d44aa21206dfb8807a840990c4037523b51812/watchfiles-1.2.0-cp314-cp314-win32.whl", hash = "sha256:c0db965c5f79aa49fe672d297cf1febc5ad149b658594944f49a54a2b96270a7", size = 275108 },
-    { url = "https://files.pythonhosted.org/packages/92/45/190ce6db8dcb4536682cf75d3889ff1a27182a58cb519d343cb6d9ea63d8/watchfiles-1.2.0-cp314-cp314-win_amd64.whl", hash = "sha256:71283b39fd17e5408eb123bd37aeecfd9d54c81fc184421943208aadb879d103", size = 288441 },
-    { url = "https://files.pythonhosted.org/packages/74/0d/3eae1c2313ab08378431d907c3f8095ecca00f3eda33111cf4f0f2591799/watchfiles-1.2.0-cp314-cp314-win_arm64.whl", hash = "sha256:c5c19526f4e54a00f2666a6c0e9e40d582c09e865055ea7378bf0009aab857b3", size = 280684 },
-    { url = "https://files.pythonhosted.org/packages/b1/75/fb64e6c25d6b5ca636d03df34ffb1c6e9873303e76d27967e045f8df088f/watchfiles-1.2.0-cp314-cp314t-macosx_10_12_x86_64.whl", hash = "sha256:d73a585accffa5ae39c17264c36ec3166d2fad7000c780f5ef83b2722afb9dd2", size = 398857 },
-    { url = "https://files.pythonhosted.org/packages/73/4e/9f7adf01754cbf81843722ccfec169d8f26c69778281a302855cecd2ee08/watchfiles-1.2.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:ae99b14c5f21e026e0e9d96f40e07d8570ebee6cafd9d8fc318354606daa7a28", size = 392413 },
-    { url = "https://files.pythonhosted.org/packages/47/c8/bec626bcc2d69f44b9acb24ce7d60ed7b16b73628eea747fcbd169d8edda/watchfiles-1.2.0-cp314-cp314t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4429f3b105524a10b72c3a819b091c495d2811d419c1e1e8df773a5a5974f831", size = 452409 },
-    { url = "https://files.pythonhosted.org/packages/00/b7/b6362068e81e7c556d155a34c35d40ac3ef42d747b06d7f6e5bf58e359c2/watchfiles-1.2.0-cp314-cp314t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:43d818978d06062d9b22c4fab2ebe44cf5213d42dc8e62bda8c2760cfa2eeb33", size = 458827 },
-    { url = "https://files.pythonhosted.org/packages/67/f8/9a813fa42afb1e0b4625e75f0479826644d3ee8dc287e093799bc01f390c/watchfiles-1.2.0-cp314-cp314t-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:b9f732dc58b2dbe69e464ccf8fff7a03b0dd0be439da4c0720d3558527d3d6b4", size = 490104 },
-    { url = "https://files.pythonhosted.org/packages/2f/bf/27dfb6094ca4c9aad21298b5525b6c53cb36121ee454331d05161e58d130/watchfiles-1.2.0-cp314-cp314t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:8f200104103feb097de4cab8fe4f5dd18a2026934c7dea98c55a2f5fd6d5a33b", size = 571360 },
-    { url = "https://files.pythonhosted.org/packages/fb/39/44a096d67270ea93df91d33877dbe91fbda3aa4f8ec2edf799d93eda8736/watchfiles-1.2.0-cp314-cp314t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:63ac26eefbf4af1741247d6fb68b11c49a25b2f7413fbd318a83a12aaa9cf666", size = 464644 },
-    { url = "https://files.pythonhosted.org/packages/0e/80/c7472203bad6268e3ef1ad260739704847898938ad7ea8b63a5131f46b50/watchfiles-1.2.0-cp314-cp314t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0c4997d4e4a55f0d02b6cde327322daf3a0400e5df6c6b15948994bf72497925", size = 454771 },
-    { url = "https://files.pythonhosted.org/packages/51/cf/3b10b268b4b7f0fc26e9debb5eef1998b515887840f444cd3ec80c688755/watchfiles-1.2.0-cp314-cp314t-manylinux_2_31_riscv64.whl", hash = "sha256:4c887eba18b7945ac73067a8b4a66f21cd46c2539b2bc68588f7be6c7eb6d26b", size = 463494 },
-    { url = "https://files.pythonhosted.org/packages/3d/3e/a4302545cd589262a0dc7d140e86f7688eba3f9c72776c27f7e23b8864c4/watchfiles-1.2.0-cp314-cp314t-musllinux_1_1_aarch64.whl", hash = "sha256:3416ff151bb6b5a8d8d11664974fbef4d9305b9b2957839ab5a270468fd8df30", size = 629383 },
-    { url = "https://files.pythonhosted.org/packages/db/99/d5649df0a9a410d45b7c882304d0b790903ac9b6e8f2cfd12114e0c6b9f2/watchfiles-1.2.0-cp314-cp314t-musllinux_1_1_x86_64.whl", hash = "sha256:0e831a271c035d89789cffc386b6aa1375f39f1cd25eb7ca0997e4970d152fc5", size = 656093 },
-    { url = "https://files.pythonhosted.org/packages/92/b9/362702539275019a54dd2e94511b31a9b89c5f9e6a21966de7eb692549fc/watchfiles-1.2.0-cp315-cp315-macosx_10_12_x86_64.whl", hash = "sha256:37a6721cdf3f65dbb13aa9503510ccb4451603ac837e44d265d7992a597e1374", size = 400109 },
-    { url = "https://files.pythonhosted.org/packages/8f/75/71d5ba62db781e5587bded1d944c675374bc4aa37ff33d5018d98e8b6538/watchfiles-1.2.0-cp315-cp315-macosx_11_0_arm64.whl", hash = "sha256:2b37d10b5a63bd4d87e18472d80fa525bd670586fae62e5dd580452764879b65", size = 392167 },
-    { url = "https://files.pythonhosted.org/packages/3c/01/c66dd95d0423fe30d31820e2d1d5bda773764131bbb6ac0cb1cf303ac328/watchfiles-1.2.0-cp315-cp315-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0a105bc2283f67e8fbec74253ec2d94925de92ed72c0393f1206bf326b7b7b69", size = 452372 },
-    { url = "https://files.pythonhosted.org/packages/91/15/2fe99557e72f85627c6a8eed50d889e8d101623e060a22ad75b875cb932d/watchfiles-1.2.0-cp315-cp315-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:5327989a465505f05cfe06f04fa9d0c2fd5432bb243e10e6f012b1bdca3c8579", size = 459596 },
-    { url = "https://files.pythonhosted.org/packages/ed/23/d4acfa0023367428ed48351b3b9b267893037b6cadae55620c61c24bcfd4/watchfiles-1.2.0-cp315-cp315-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ecb47f183a8025b2aa18b546725c3657e542112ae9c0613a2af79b4fa8d04ad7", size = 490869 },
-    { url = "https://files.pythonhosted.org/packages/a4/5f/3164cbdce06c9fb95c4f7b9e2f9760b5e2797af43a9ecc317ef42a23a278/watchfiles-1.2.0-cp315-cp315-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:8520a4ab0e37f770afc34459c4f8f7019e153f9124dc101c15538365875d1ab2", size = 571641 },
-    { url = "https://files.pythonhosted.org/packages/41/e6/85d3731c55e65cd7690f3f803d24c139588aaf863e4bf2148fe7a7fa1a19/watchfiles-1.2.0-cp315-cp315-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:71cd71740ed2c15211ebb237ced4e39a1cdf6f80566e5fe95428da1626f4fde6", size = 464444 },
-    { url = "https://files.pythonhosted.org/packages/f4/7d/562641012b8b09872742c3b8adf9629ec479fd78f8d68ae4a0c13da8add6/watchfiles-1.2.0-cp315-cp315-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f88af53d6ddaf72179ef613ddc905e6f4785f712b49b80b3bef9f3525e6194b4", size = 453593 },
-    { url = "https://files.pythonhosted.org/packages/56/fe/cb8ef3d6f929d14158fdaaad9925985b7310abc9384dcd4d82dd0016fb59/watchfiles-1.2.0-cp315-cp315-manylinux_2_31_riscv64.whl", hash = "sha256:cee9d5efd929efdac5f7e58f72b3376f676b64050a91c5b99a7094c5b2317488", size = 465096 },
-    { url = "https://files.pythonhosted.org/packages/25/91/80908e835e100527a9267147b08c0eee1fa6ab0ffec15edc04d1d44885f7/watchfiles-1.2.0-cp315-cp315-musllinux_1_1_aarch64.whl", hash = "sha256:b718bf356bbc15e559bd8ef41782b573b8ae0e3f177ab244b440568d7ea02cfb", size = 630638 },
-    { url = "https://files.pythonhosted.org/packages/46/4b/95ab2f256bb4af3cb2eb23b9317bda984ee6e0f11733a5c004a6c95b06e3/watchfiles-1.2.0-cp315-cp315-musllinux_1_1_x86_64.whl", hash = "sha256:922c0e019fe68b3ae392965a766b02a71ba1168c932cebc3733cd52c5fe5b377", size = 657684 },
-]
-
-[[package]]
-name = "websockets"
-version = "17.0.1"
-source = { registry = "https://pypi.org/simple" }
-sdist = { url = "https://files.pythonhosted.org/packages/f7/96/e01084f83a64bcb3a27994bd0cb0db68ff29d9c6707fae37ec19b18ba990/websockets-17.0.1.tar.gz", hash = "sha256:5baa9bc0dfbae8c507e51c8cf1b6d4628086f7a87bbd3a9952bd5f035451f1cc", size = 183298 }
-wheels = [
-    { url = "https://files.pythonhosted.org/packages/98/dc/cadab608924ac605647031472fb1f8792d7d4ea07565ba1899ec42028e0d/websockets-17.0.1-cp314-cp314-macosx_10_15_universal2.whl", hash = "sha256:53b90c00bc6201ab6695c7ff51a04d0e425514c37515e9eeecd2c1b978ac6c0e", size = 212640 },
-    { url = "https://files.pythonhosted.org/packages/16/7a/b034d13ca181211bbd58bb50835cb196a7784cd505b5a2079d4d03374f9f/websockets-17.0.1-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:5f33a649bfcb8312524173cc4bbafa7dbb236e18eee9aa31a1d324ca0ddda28c", size = 210332 },
-    { url = "https://files.pythonhosted.org/packages/2f/4d/943ede39b53744768edf1ed84a3f9401527388228a3d6c1249c02c3d6bd7/websockets-17.0.1-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:cddc675ec31bca65473321f9a9794e488b43b3b8de5d02c8ef4810c5d5792163", size = 210546 },
-    { url = "https://files.pythonhosted.org/packages/cf/e3/88dc159d2ae66743c669443246243f28d873b0c5e58271b8cc1ca0440334/websockets-17.0.1-cp314-cp314-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:b3ff0ad440ad52dda64138f16895f66403f40192365e39b1010e889f289746b0", size = 219928 },
-    { url = "https://files.pythonhosted.org/packages/fe/f2/ff27eaefa15851a5cf7f004ab827a022bf2d6632cb520f89cb100db7e84b/websockets-17.0.1-cp314-cp314-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:72d7f2a5aeb4e82daa4ee18f125b4277f427033359be5c745ad709608446cc2c", size = 220279 },
-    { url = "https://files.pythonhosted.org/packages/19/2e/a5166149f363d2449c1cb2dde6486a245521979509d53b87a09f3e79662b/websockets-17.0.1-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6fd88365da261c53d3e943fb37e0d0721b9cde119f6b2e3fc84369b6ab234d63", size = 221525 },
-    { url = "https://files.pythonhosted.org/packages/9b/b5/f46931269b3ff3bde65d27c65ddb22f9bb8ce92ac2c6c4df0910128f6219/websockets-17.0.1-cp314-cp314-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:ab9f962a5b64a5c3c845d556b7dc4e6fb683f7b67179f8205e814bb2e0213ffe", size = 222897 },
-    { url = "https://files.pythonhosted.org/packages/42/f4/deccf3439f35df953ec35e13fe07986821c5f1ab5785d69614283bdb9034/websockets-17.0.1-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:8c07f145d0b9e90cbd96035f31fb79199aef4da1872854e36ebeb258e3d57594", size = 222129 },
-    { url = "https://files.pythonhosted.org/packages/35/a5/e1b57a59da92ade37fd021567a17b518ea8267b28e5530075844cdb525fe/websockets-17.0.1-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:9f7747d3daa41a11f25f7cca5dc988fc51da97b311bed4c9d843860f79779283", size = 220875 },
-    { url = "https://files.pythonhosted.org/packages/f0/30/e7d0889c790a854156de424575fd67af79ddbaed9ff3157ae863dfd1c1dc/websockets-17.0.1-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:2abb1ba0a5133b7d2ef3c1c9f4b0c1e8a101012dce0b594ab2b2888d9a64820e", size = 218160 },
-    { url = "https://files.pythonhosted.org/packages/09/2e/43db785d6ed9ae7594fae7b62bbc9cb4dfee2b015e06a1005f1e5ce283b6/websockets-17.0.1-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:f3fd9a1f87f8f0f3f8e9f9bd0195f7516562d13f5b178db8c5784d1f60b60bed", size = 220951 },
-    { url = "https://files.pythonhosted.org/packages/eb/f5/4ac3cab3d5e8a830657a822f64a8910e3803229c6783e59c3fd9a3487427/websockets-17.0.1-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:2bc14b481e05e331811108daa1aeb41a5e237a5564ef2f02ec5a356a0f102f78", size = 219460 },
-    { url = "https://files.pythonhosted.org/packages/03/0e/c3a4020673ffc17c82cf1a467835038a196a555d3b4f2a50f0f063cf8ccc/websockets-17.0.1-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:57d2ee9b24b404ce75f3814f92073c0ed88106c950148d2427fe8d25ca254d1f", size = 220248 },
-    { url = "https://files.pythonhosted.org/packages/7d/87/e47a6a278cc1dfade38444c893ce18322943c25d4b780a74450d9d164be1/websockets-17.0.1-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:1b363bfd72a52c0658a3154a4cff219f15a474b35a235057d38853bf151acce7", size = 221421 },
-    { url = "https://files.pythonhosted.org/packages/da/8f/473d5fc4e3836e375b0233c6ef26777e6e5e3f7bfc84ccd524eae4090ed5/websockets-17.0.1-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:10b1587c599fa0f2c89154587c80e0fda98ade6c9fa8c0260a2823fb1800b685", size = 218975 },
-    { url = "https://files.pythonhosted.org/packages/d4/b9/819ec2dcdf69031d7e9cab11247f3a6ff9bbc8c7c53ada1dbcb9055b227b/websockets-17.0.1-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:d7d72843691f50b91127c50688df10cb72ec6f4c4b1d7e2c11ab33b16acf8e51", size = 219925 },
-    { url = "https://files.pythonhosted.org/packages/85/b9/6c0da301f6118502e079cf92f4e864adf28e56b3f8c0f6085076ced7b876/websockets-17.0.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:90973a3a00f23afdfd1c9b06fb84289bf0220f247ef8a62501a1967c7af54f7b", size = 220218 },
-    { url = "https://files.pythonhosted.org/packages/55/f9/cba32dc9dd856565263d6272f594255bd0e2781deb8cd982c026a54760ad/websockets-17.0.1-cp314-cp314-win32.whl", hash = "sha256:599b03beb77633bffc095334338fad79cafc2b01fbd58953838130a9ae967d7b", size = 212626 },
-    { url = "https://files.pythonhosted.org/packages/fa/95/91cdd8c192287d7ea741f37cf7d64fdc1a14410f06f73805e428a1a590af/websockets-17.0.1-cp314-cp314-win_amd64.whl", hash = "sha256:81ce19c6046ace11da7001781be7317bb1dc389f399af4b2ed962190f76f9add", size = 212969 },
-    { url = "https://files.pythonhosted.org/packages/8e/fd/8c98a1e431960661c5769ab1a4dd66494e87ab02d791cc79e51e0d9a289f/websockets-17.0.1-cp314-cp314-win_arm64.whl", hash = "sha256:efe0ae052a8d023b87198921e8a7ce1dc7768816bcd2fbc20df171ac73a04891", size = 212850 },
-    { url = "https://files.pythonhosted.org/packages/13/c1/142f5186ee7dc3beee0426b998a79e223e067b7689afcaa95890d64aa800/websockets-17.0.1-cp314-cp314t-macosx_10_15_universal2.whl", hash = "sha256:ab56439c9f74c52770690c7b2f616b3bf775cb3920453ee355ac765c032d8bbf", size = 212967 },
-    { url = "https://files.pythonhosted.org/packages/02/de/4b03ed316c9dee180365286c298219809ff247be39beeaaf9958b21167ab/websockets-17.0.1-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:20a92f78ac8250984ed459faa9ca48c285adbfc0038ddc3fdac6046990a9c9ed", size = 210504 },
-    { url = "https://files.pythonhosted.org/packages/cd/d8/ad2b3e8f867e1e8cac3077e2f33ffb60b71bd763d6cfc71bd916f113c3bf/websockets-17.0.1-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:6a434e59962a4fb9016bea327e1d14d6cd67670ecfb8942b4f4a0c24036634ce", size = 210702 },
-    { url = "https://files.pythonhosted.org/packages/48/18/7a77a82ce9d6f831c07b176da3942f7e71acd0f115f3ecdb1d00a040eb01/websockets-17.0.1-cp314-cp314t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:2503c7e2a5049a12d5dac917a46d5d52591283a766165b8176bb167560421b38", size = 220290 },
-    { url = "https://files.pythonhosted.org/packages/f5/af/43c3e3c3ea7ba4693c2181743f3221957df28bededadcbd9fc8a0661bde0/websockets-17.0.1-cp314-cp314t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:28012a54510fe8301bb893ef143cec30a2780a2d3bc20b7bbdf4379d7a63945d", size = 220573 },
-    { url = "https://files.pythonhosted.org/packages/1f/bd/ed48eca15725743ee7e2dc172e15c61de29e85ec98dace7b14257f366836/websockets-17.0.1-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:22bd00f8bae2bccdb5dbe41e20f58ba44ca9fff0b4b561aaf39099c35da762ed", size = 221747 },
-    { url = "https://files.pythonhosted.org/packages/99/50/838deb7937a8225c4925dd4a977eafea473fabf444178a99de0bc7e92bb0/websockets-17.0.1-cp314-cp314t-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:e98ec9ec61cce5bc4b8b218322ad090b0994eb060bb04da704c62ef0a3d864e6", size = 223891 },
-    { url = "https://files.pythonhosted.org/packages/f8/e9/657fb70c6eb6bcd01adfa5d2b06496e9911e1c1a8813d353b8c00f7591cd/websockets-17.0.1-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:8e387adb0c692c6b5571bdeafc8ac9d1901ea30f10309134780b16ecd35e6605", size = 222317 },
-    { url = "https://files.pythonhosted.org/packages/07/4c/82cb722afa5428fed981331210c4c07600570db01bb1620579f655b5adaf/websockets-17.0.1-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:bd1470d2c53fe53269bf5619da7725d30dd9b9693f1689f7a85eab8dea734442", size = 221047 },
-    { url = "https://files.pythonhosted.org/packages/90/84/bd6d67d6bc65f0de0cb50de55dab42f256a9876a351c4736522eb168fda0/websockets-17.0.1-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:884af729b8ab50486acd94d9768c2b60914bf39b579ebba0a5cb73bfdfd61fd2", size = 218626 },
-    { url = "https://files.pythonhosted.org/packages/96/cb/6a372c8553976f0d8f97f5115826ed47e34b3be6b8bf0d0249af249a7416/websockets-17.0.1-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:a60fa1a25cca1bcc2bf87b8d6be37a741f0a3239fb5e9cfb7a37173b68ffcf87", size = 221299 },
-    { url = "https://files.pythonhosted.org/packages/bc/31/f966e8472337974f74d788b3ef6c6f3b8b9a5f201efd16a843c91d269fa5/websockets-17.0.1-cp314-cp314t-musllinux_1_2_armv7l.whl", hash = "sha256:e8208f2729cba030ff872a92064c97584eeb9502f53d32a05a0f05d5a17ca6c6", size = 219789 },
-    { url = "https://files.pythonhosted.org/packages/57/34/404e83a6cc7b0efcac810b7041bffd72ff76900e6fd0aa45a26c92fb2ffe/websockets-17.0.1-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:54cdcaa56f5d3eafd57058f0fa4a3de93a310b43a3c4699f06efc4c0bd054a5a", size = 220678 },
-    { url = "https://files.pythonhosted.org/packages/e5/70/8946188c2a68d67251859b589a3634918cf7867bf0b891347a5ecaa43d30/websockets-17.0.1-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:4d41c0a1d47a478bc432b3b9068097bee1ce0c5b19327ea6f75c2ab34ab1f2fb", size = 221697 },
-    { url = "https://files.pythonhosted.org/packages/04/16/ee73fc2083a2938ac6209f4ec804960496835b20a0068dbcfe8424957c04/websockets-17.0.1-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:f991247276797d0c61ab7770bc9791eadc16f683b4d83517f624932adc1a8bab", size = 219390 },
-    { url = "https://files.pythonhosted.org/packages/94/6a/d5f88033c69932af6cdaa72da62516ade47c257e3bf69f4c0ba5f40e12a2/websockets-17.0.1-cp314-cp314t-musllinux_1_2_s390x.whl", hash = "sha256:733e3cc7171fa1b899edbe725ef9382d0e960657dc1fd933f3281ae910c01dab", size = 220161 },
-    { url = "https://files.pythonhosted.org/packages/e1/2e/6183dd2c0370287ecf4afe0bb33aca364208e5e7b0e1a286adcaecc0c78b/websockets-17.0.1-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:810cb3fb5fa6e447216f4e82d9a85cb8aed0929ae3538153ddfe8a6e3121a58d", size = 220591 },
-    { url = "https://files.pythonhosted.org/packages/26/93/70f6516d85b9744f7eac224c4b1b9ef4e84133f80b53be02080cb1c3e663/websockets-17.0.1-cp314-cp314t-win32.whl", hash = "sha256:17ac37716c0244e82c9e384c41653c090b1864c6610224ca3857e7f7b58fce10", size = 212755 },
-    { url = "https://files.pythonhosted.org/packages/f1/2c/9d9c1da5a7ea9af307b386d25f64d1dead4729644198d2b92e36db5dfd41/websockets-17.0.1-cp314-cp314t-win_amd64.whl", hash = "sha256:bb31f42ea095ea826463c770829aa188a86c9a5c976b1467cbbf583c811de833", size = 213094 },
-    { url = "https://files.pythonhosted.org/packages/5b/24/a585e7573e128070605d003b5544729bcd58d9756c7e99d550818ca4b916/websockets-17.0.1-cp314-cp314t-win_arm64.whl", hash = "sha256:dbfae8e75b342e31fc6fd1a8bbb393b7cbb91d6cfd581650300a94381e7b7e2b", size = 213009 },
-    { url = "https://files.pythonhosted.org/packages/09/ce/3929538b2b9918f5eee623fbf3346893973191f6df93f19bbda097bd7bb7/websockets-17.0.1-py3-none-any.whl", hash = "sha256:c6be9cba65c65cc76dfa3d4619e359ff02a4476c74e179b215236c11a0b32345", size = 206718 },
-]
diff --git a/vendor/UTIF.LICENSE b/vendor/UTIF.LICENSE
new file mode 100644
index 0000000..491e239
--- /dev/null
+++ b/vendor/UTIF.LICENSE
@@ -0,0 +1,21 @@
+MIT License
+
+Copyright (c) 2017 Photopea
+
+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.
diff --git a/vendor/UTIF.js b/vendor/UTIF.js
new file mode 100644
index 0000000..781ef1f
--- /dev/null
+++ b/vendor/UTIF.js
@@ -0,0 +1,1160 @@
+
+
+
+
+;(function(){
+var UTIF = {};
+
+// Make available for import by `require()`
+if (typeof module == "object") {module.exports = UTIF;}
+else {self.UTIF = UTIF;}
+
+var pako;
+if (typeof require == "function") {pako = require("pako");}
+else {pako = self.pako;}
+
+function log() { if (typeof process=="undefined" || process.env.NODE_ENV=="development") console.log.apply(console, arguments);  }
+
+(function(UTIF, pako){
+	
+// Following lines add a JPEG decoder  to UTIF.JpegDecoder
+(function(){var V="function"===typeof Symbol&&"symbol"===typeof Symbol.iterator?function(g){return typeof g}:function(g){return g&&"function"===typeof Symbol&&g.constructor===Symbol&&g!==Symbol.prototype?"symbol":typeof g},D=function(){function g(g){this.message="JPEG error: "+g}g.prototype=Error();g.prototype.name="JpegError";return g.constructor=g}(),P=function(){function g(g,D){this.message=g;this.g=D}g.prototype=Error();g.prototype.name="DNLMarkerError";return g.constructor=g}();(function(){function g(){this.M=
+null;this.B=-1}function W(a,d){for(var f=0,e=[],b,B,k=16;0<k&&!a[k-1];)k--;e.push({children:[],index:0});var l=e[0],r;for(b=0;b<k;b++){for(B=0;B<a[b];B++){l=e.pop();for(l.children[l.index]=d[f];0<l.index;)l=e.pop();l.index++;for(e.push(l);e.length<=b;)e.push(r={children:[],index:0}),l.children[l.index]=r.children,l=r;f++}b+1<k&&(e.push(r={children:[],index:0}),l.children[l.index]=r.children,l=r)}return e[0].children}function X(a,d,f,e,b,B,k,l,r){function n(){if(0<x)return x--,z>>x&1;z=a[d++];if(255===
+z){var c=a[d++];if(c){if(220===c&&g){d+=2;var b=a[d++]<<8|a[d++];if(0<b&&b!==f.g)throw new P("Found DNL marker (0xFFDC) while parsing scan data",b);}throw new D("unexpected marker "+(z<<8|c).toString(16));}}x=7;return z>>>7}function q(a){for(;;){a=a[n()];if("number"===typeof a)return a;if("object"!==("undefined"===typeof a?"undefined":V(a)))throw new D("invalid huffman sequence");}}function h(a){for(var c=0;0<a;)c=c<<1|n(),a--;return c}function c(a){if(1===a)return 1===n()?1:-1;var c=h(a);return c>=
+1<<a-1?c:c+(-1<<a)+1}function C(a,b){var d=q(a.D);d=0===d?0:c(d);a.a[b]=a.m+=d;for(d=1;64>d;){var h=q(a.o),k=h&15;h>>=4;if(0===k){if(15>h)break;d+=16}else d+=h,a.a[b+J[d]]=c(k),d++}}function w(a,d){var b=q(a.D);b=0===b?0:c(b)<<r;a.a[d]=a.m+=b}function p(a,c){a.a[c]|=n()<<r}function m(a,b){if(0<A)A--;else for(var d=B;d<=k;){var e=q(a.o),f=e&15;e>>=4;if(0===f){if(15>e){A=h(e)+(1<<e)-1;break}d+=16}else d+=e,a.a[b+J[d]]=c(f)*(1<<r),d++}}function t(a,d){for(var b=B,e=0,f;b<=k;){f=d+J[b];var l=0>a.a[f]?
+-1:1;switch(E){case 0:e=q(a.o);f=e&15;e>>=4;if(0===f)15>e?(A=h(e)+(1<<e),E=4):(e=16,E=1);else{if(1!==f)throw new D("invalid ACn encoding");Q=c(f);E=e?2:3}continue;case 1:case 2:a.a[f]?a.a[f]+=l*(n()<<r):(e--,0===e&&(E=2===E?3:0));break;case 3:a.a[f]?a.a[f]+=l*(n()<<r):(a.a[f]=Q<<r,E=0);break;case 4:a.a[f]&&(a.a[f]+=l*(n()<<r))}b++}4===E&&(A--,0===A&&(E=0))}var g=9<arguments.length&&void 0!==arguments[9]?arguments[9]:!1,u=f.P,v=d,z=0,x=0,A=0,E=0,Q,K=e.length,F,L,M,I;var R=f.S?0===B?0===l?w:p:0===l?
+m:t:C;var G=0;var O=1===K?e[0].c*e[0].l:u*f.O;for(var S,T;G<O;){var U=b?Math.min(O-G,b):O;for(F=0;F<K;F++)e[F].m=0;A=0;if(1===K){var y=e[0];for(I=0;I<U;I++)R(y,64*((y.c+1)*(G/y.c|0)+G%y.c)),G++}else for(I=0;I<U;I++){for(F=0;F<K;F++)for(y=e[F],S=y.h,T=y.j,L=0;L<T;L++)for(M=0;M<S;M++)R(y,64*((y.c+1)*((G/u|0)*y.j+L)+(G%u*y.h+M)));G++}x=0;(y=N(a,d))&&y.f&&((0,_util.warn)("decodeScan - unexpected MCU data, current marker is: "+y.f),d=y.offset);y=y&&y.F;if(!y||65280>=y)throw new D("marker was not found");
+if(65488<=y&&65495>=y)d+=2;else break}(y=N(a,d))&&y.f&&((0,_util.warn)("decodeScan - unexpected Scan data, current marker is: "+y.f),d=y.offset);return d-v}function Y(a,d){for(var f=d.c,e=d.l,b=new Int16Array(64),B=0;B<e;B++)for(var k=0;k<f;k++){var l=64*((d.c+1)*B+k),r=b,n=d.G,q=d.a;if(!n)throw new D("missing required Quantization Table.");for(var h=0;64>h;h+=8){var c=q[l+h];var C=q[l+h+1];var w=q[l+h+2];var p=q[l+h+3];var m=q[l+h+4];var t=q[l+h+5];var g=q[l+h+6];var u=q[l+h+7];c*=n[h];if(0===(C|
+w|p|m|t|g|u))c=5793*c+512>>10,r[h]=c,r[h+1]=c,r[h+2]=c,r[h+3]=c,r[h+4]=c,r[h+5]=c,r[h+6]=c,r[h+7]=c;else{C*=n[h+1];w*=n[h+2];p*=n[h+3];m*=n[h+4];t*=n[h+5];g*=n[h+6];u*=n[h+7];var v=5793*c+128>>8;var z=5793*m+128>>8;var x=w;var A=g;m=2896*(C-u)+128>>8;u=2896*(C+u)+128>>8;p<<=4;t<<=4;v=v+z+1>>1;z=v-z;c=3784*x+1567*A+128>>8;x=1567*x-3784*A+128>>8;A=c;m=m+t+1>>1;t=m-t;u=u+p+1>>1;p=u-p;v=v+A+1>>1;A=v-A;z=z+x+1>>1;x=z-x;c=2276*m+3406*u+2048>>12;m=3406*m-2276*u+2048>>12;u=c;c=799*p+4017*t+2048>>12;p=4017*
+p-799*t+2048>>12;t=c;r[h]=v+u;r[h+7]=v-u;r[h+1]=z+t;r[h+6]=z-t;r[h+2]=x+p;r[h+5]=x-p;r[h+3]=A+m;r[h+4]=A-m}}for(n=0;8>n;++n)c=r[n],C=r[n+8],w=r[n+16],p=r[n+24],m=r[n+32],t=r[n+40],g=r[n+48],u=r[n+56],0===(C|w|p|m|t|g|u)?(c=5793*c+8192>>14,c=-2040>c?0:2024<=c?255:c+2056>>4,q[l+n]=c,q[l+n+8]=c,q[l+n+16]=c,q[l+n+24]=c,q[l+n+32]=c,q[l+n+40]=c,q[l+n+48]=c,q[l+n+56]=c):(v=5793*c+2048>>12,z=5793*m+2048>>12,x=w,A=g,m=2896*(C-u)+2048>>12,u=2896*(C+u)+2048>>12,v=(v+z+1>>1)+4112,z=v-z,c=3784*x+1567*A+2048>>
+12,x=1567*x-3784*A+2048>>12,A=c,m=m+t+1>>1,t=m-t,u=u+p+1>>1,p=u-p,v=v+A+1>>1,A=v-A,z=z+x+1>>1,x=z-x,c=2276*m+3406*u+2048>>12,m=3406*m-2276*u+2048>>12,u=c,c=799*p+4017*t+2048>>12,p=4017*p-799*t+2048>>12,t=c,c=v+u,u=v-u,C=z+t,g=z-t,w=x+p,t=x-p,p=A+m,m=A-m,c=16>c?0:4080<=c?255:c>>4,C=16>C?0:4080<=C?255:C>>4,w=16>w?0:4080<=w?255:w>>4,p=16>p?0:4080<=p?255:p>>4,m=16>m?0:4080<=m?255:m>>4,t=16>t?0:4080<=t?255:t>>4,g=16>g?0:4080<=g?255:g>>4,u=16>u?0:4080<=u?255:u>>4,q[l+n]=c,q[l+n+8]=C,q[l+n+16]=w,q[l+n+24]=
+p,q[l+n+32]=m,q[l+n+40]=t,q[l+n+48]=g,q[l+n+56]=u)}return d.a}function N(a,d){var f=2<arguments.length&&void 0!==arguments[2]?arguments[2]:d,e=a.length-1;f=f<d?f:d;if(d>=e)return null;var b=a[d]<<8|a[d+1];if(65472<=b&&65534>=b)return{f:null,F:b,offset:d};for(var B=a[f]<<8|a[f+1];!(65472<=B&&65534>=B);){if(++f>=e)return null;B=a[f]<<8|a[f+1]}return{f:b.toString(16),F:B,offset:f}}var J=new Uint8Array([0,1,8,16,9,2,3,10,17,24,32,25,18,11,4,5,12,19,26,33,40,48,41,34,27,20,13,6,7,14,21,28,35,42,49,56,
+57,50,43,36,29,22,15,23,30,37,44,51,58,59,52,45,38,31,39,46,53,60,61,54,47,55,62,63]);g.prototype={parse:function(a){function d(){var d=a[k]<<8|a[k+1];k+=2;return d}function f(){var b=d();b=k+b-2;var c=N(a,b,k);c&&c.f&&((0,_util.warn)("readDataBlock - incorrect length, current marker is: "+c.f),b=c.offset);b=a.subarray(k,b);k+=b.length;return b}function e(a){for(var b=Math.ceil(a.v/8/a.s),c=Math.ceil(a.g/8/a.u),d=0;d<a.b.length;d++){v=a.b[d];var e=Math.ceil(Math.ceil(a.v/8)*v.h/a.s),f=Math.ceil(Math.ceil(a.g/
+8)*v.j/a.u);v.a=new Int16Array(64*c*v.j*(b*v.h+1));v.c=e;v.l=f}a.P=b;a.O=c}var b=(1<arguments.length&&void 0!==arguments[1]?arguments[1]:{}).N,B=void 0===b?null:b,k=0,l=null,r=0;b=[];var n=[],q=[],h=d();if(65496!==h)throw new D("SOI not found");for(h=d();65497!==h;){switch(h){case 65504:case 65505:case 65506:case 65507:case 65508:case 65509:case 65510:case 65511:case 65512:case 65513:case 65514:case 65515:case 65516:case 65517:case 65518:case 65519:case 65534:var c=f();65518===h&&65===c[0]&&100===
+c[1]&&111===c[2]&&98===c[3]&&101===c[4]&&(l={version:c[5]<<8|c[6],Y:c[7]<<8|c[8],Z:c[9]<<8|c[10],W:c[11]});break;case 65499:h=d()+k-2;for(var g;k<h;){var w=a[k++],p=new Uint16Array(64);if(0===w>>4)for(c=0;64>c;c++)g=J[c],p[g]=a[k++];else if(1===w>>4)for(c=0;64>c;c++)g=J[c],p[g]=d();else throw new D("DQT - invalid table spec");b[w&15]=p}break;case 65472:case 65473:case 65474:if(m)throw new D("Only single frame JPEGs supported");d();var m={};m.X=65473===h;m.S=65474===h;m.precision=a[k++];h=d();m.g=
+B||h;m.v=d();m.b=[];m.C={};c=a[k++];for(h=p=w=0;h<c;h++){g=a[k];var t=a[k+1]>>4;var H=a[k+1]&15;w<t&&(w=t);p<H&&(p=H);t=m.b.push({h:t,j:H,T:a[k+2],G:null});m.C[g]=t-1;k+=3}m.s=w;m.u=p;e(m);break;case 65476:g=d();for(h=2;h<g;){w=a[k++];p=new Uint8Array(16);for(c=t=0;16>c;c++,k++)t+=p[c]=a[k];H=new Uint8Array(t);for(c=0;c<t;c++,k++)H[c]=a[k];h+=17+t;(0===w>>4?q:n)[w&15]=W(p,H)}break;case 65501:d();var u=d();break;case 65498:c=1===++r&&!B;d();w=a[k++];g=[];for(h=0;h<w;h++){p=m.C[a[k++]];var v=m.b[p];
+p=a[k++];v.D=q[p>>4];v.o=n[p&15];g.push(v)}h=a[k++];w=a[k++];p=a[k++];try{var z=X(a,k,m,g,u,h,w,p>>4,p&15,c);k+=z}catch(x){if(x instanceof P)return(0,_util.warn)('Attempting to re-parse JPEG image using "scanLines" parameter found in DNL marker (0xFFDC) segment.'),this.parse(a,{N:x.g});throw x;}break;case 65500:k+=4;break;case 65535:255!==a[k]&&k--;break;default:if(255===a[k-3]&&192<=a[k-2]&&254>=a[k-2])k-=3;else if((c=N(a,k-2))&&c.f)(0,_util.warn)("JpegImage.parse - unexpected data, current marker is: "+
+c.f),k=c.offset;else throw new D("unknown marker "+h.toString(16));}h=d()}this.width=m.v;this.height=m.g;this.A=l;this.b=[];for(h=0;h<m.b.length;h++){v=m.b[h];if(u=b[v.T])v.G=u;this.b.push({R:Y(m,v),U:v.h/m.s,V:v.j/m.u,c:v.c,l:v.l})}this.i=this.b.length},L:function(a,d){var f=this.width/a,e=this.height/d,b,g,k=this.b.length,l=a*d*k,r=new Uint8ClampedArray(l),n=new Uint32Array(a);for(g=0;g<k;g++){var q=this.b[g];var h=q.U*f;var c=q.V*e;var C=g;var w=q.R;var p=q.c+1<<3;for(b=0;b<a;b++)q=0|b*h,n[b]=
+(q&4294967288)<<3|q&7;for(h=0;h<d;h++)for(q=0|h*c,q=p*(q&4294967288)|(q&7)<<3,b=0;b<a;b++)r[C]=w[q+n[b]],C+=k}if(e=this.M)for(g=0;g<l;)for(f=q=0;q<k;q++,g++,f+=2)r[g]=(r[g]*e[f]>>8)+e[f+1];return r},w:function(){return this.A?!!this.A.W:3===this.i?0===this.B?!1:!0:1===this.B?!0:!1},I:function(a){for(var d,f,e,b=0,g=a.length;b<g;b+=3)d=a[b],f=a[b+1],e=a[b+2],a[b]=d-179.456+1.402*e,a[b+1]=d+135.459-.344*f-.714*e,a[b+2]=d-226.816+1.772*f;return a},K:function(a){for(var d,f,e,b,g=0,k=0,l=a.length;k<l;k+=
+4)d=a[k],f=a[k+1],e=a[k+2],b=a[k+3],a[g++]=-122.67195406894+f*(-6.60635669420364E-5*f+4.37130475926232E-4*e-5.4080610064599E-5*d+4.8449797120281E-4*b-.154362151871126)+e*(-9.57964378445773E-4*e+8.17076911346625E-4*d-.00477271405408747*b+1.53380253221734)+d*(9.61250184130688E-4*d-.00266257332283933*b+.48357088451265)+b*(-3.36197177618394E-4*b+.484791561490776),a[g++]=107.268039397724+f*(2.19927104525741E-5*f-6.40992018297945E-4*e+6.59397001245577E-4*d+4.26105652938837E-4*b-.176491792462875)+e*(-7.78269941513683E-4*
+e+.00130872261408275*d+7.70482631801132E-4*b-.151051492775562)+d*(.00126935368114843*d-.00265090189010898*b+.25802910206845)+b*(-3.18913117588328E-4*b-.213742400323665),a[g++]=-20.810012546947+f*(-5.70115196973677E-4*f-2.63409051004589E-5*e+.0020741088115012*d-.00288260236853442*b+.814272968359295)+e*(-1.53496057440975E-5*e-1.32689043961446E-4*d+5.60833691242812E-4*b-.195152027534049)+d*(.00174418132927582*d-.00255243321439347*b+.116935020465145)+b*(-3.43531996510555E-4*b+.24165260232407);return a.subarray(0,
+g)},J:function(a){for(var d,f,e,b=0,g=a.length;b<g;b+=4)d=a[b],f=a[b+1],e=a[b+2],a[b]=434.456-d-1.402*e,a[b+1]=119.541-d+.344*f+.714*e,a[b+2]=481.816-d-1.772*f;return a},H:function(a){for(var d,f,e,b,g=0,k=1/255,l=0,r=a.length;l<r;l+=4)d=a[l]*k,f=a[l+1]*k,e=a[l+2]*k,b=a[l+3]*k,a[g++]=255+d*(-4.387332384609988*d+54.48615194189176*f+18.82290502165302*e+212.25662451639585*b-285.2331026137004)+f*(1.7149763477362134*f-5.6096736904047315*e-17.873870861415444*b-5.497006427196366)+e*(-2.5217340131683033*
+e-21.248923337353073*b+17.5119270841813)-b*(21.86122147463605*b+189.48180835922747),a[g++]=255+d*(8.841041422036149*d+60.118027045597366*f+6.871425592049007*e+31.159100130055922*b-79.2970844816548)+f*(-15.310361306967817*f+17.575251261109482*e+131.35250912493976*b-190.9453302588951)+e*(4.444339102852739*e+9.8632861493405*b-24.86741582555878)-b*(20.737325471181034*b+187.80453709719578),a[g++]=255+d*(.8842522430003296*d+8.078677503112928*f+30.89978309703729*e-.23883238689178934*b-14.183576799673286)+
+f*(10.49593273432072*f+63.02378494754052*e+50.606957656360734*b-112.23884253719248)+e*(.03296041114873217*e+115.60384449646641*b-193.58209356861505)-b*(22.33816807309886*b+180.12613974708367);return a.subarray(0,g)},getData:function(a,d,f){if(4<this.i)throw new D("Unsupported color mode");a=this.L(a,d);if(1===this.i&&f){f=a.length;d=new Uint8ClampedArray(3*f);for(var e=0,b=0;b<f;b++){var g=a[b];d[e++]=g;d[e++]=g;d[e++]=g}return d}if(3===this.i&&this.w())return this.I(a);if(4===this.i){if(this.w())return f?
+this.K(a):this.J(a);if(f)return this.H(a)}return a}}; UTIF.JpegDecoder=g})()})();
+
+//UTIF.JpegDecoder = PDFJS.JpegImage;
+
+
+UTIF.encodeImage = function(rgba, w, h, metadata)
+{
+	var idf = { "t256":[w], "t257":[h], "t258":[8,8,8,8], "t259":[1], "t262":[2], "t273":[1000], // strips offset
+				"t277":[4], "t278":[h], /* rows per strip */          "t279":[w*h*4], // strip byte counts
+				"t282":[1], "t283":[1], "t284":[1], "t286":[0], "t287":[0], "t296":[1], "t305": ["Photopea (UTIF.js)"], "t338":[1]
+		};
+	if (metadata) for (var i in metadata) idf[i] = metadata[i];
+	
+	var prfx = new Uint8Array(UTIF.encode([idf]));
+	var img = new Uint8Array(rgba);
+	var data = new Uint8Array(1000+w*h*4);
+	for(var i=0; i<prfx.length; i++) data[i] = prfx[i];
+	for(var i=0; i<img .length; i++) data[1000+i] = img[i];
+	return data.buffer;
+}
+
+UTIF.encode = function(ifds)
+{
+	var data = new Uint8Array(20000), offset = 4, bin = UTIF._binBE;
+	data[0]=77;  data[1]=77;  data[3]=42;
+
+	var ifdo = 8;
+	bin.writeUint(data, offset, ifdo);  offset+=4;
+	for(var i=0; i<ifds.length; i++)
+	{
+		var noffs = UTIF._writeIFD(bin, data, ifdo, ifds[i]);
+		ifdo = noffs[1];
+		if(i<ifds.length-1) bin.writeUint(data, noffs[0], ifdo);
+	}
+	return data.slice(0, ifdo).buffer;
+}
+//UTIF.encode._writeIFD
+
+UTIF.decode = function(buff)
+{
+	UTIF.decode._decodeG3.allow2D = null;
+	var data = new Uint8Array(buff), offset = 0;
+
+	var id = UTIF._binBE.readASCII(data, offset, 2);  offset+=2;
+	var bin = id=="II" ? UTIF._binLE : UTIF._binBE;
+	var num = bin.readUshort(data, offset);  offset+=2;
+
+	var ifdo = bin.readUint(data, offset);  offset+=4;
+	var ifds = [];
+	while(true) {
+		var noff = UTIF._readIFD(bin, data, ifdo, ifds, 0, false);
+		ifdo = bin.readUint(data, noff);
+		if(ifdo==0) break;
+	}
+	return ifds;
+}
+
+UTIF.decodeImage = function(buff, img, ifds)
+{
+	var data = new Uint8Array(buff);
+	var id = UTIF._binBE.readASCII(data, 0, 2);
+
+	if(img["t256"]==null) return;	// No width => probably not an image
+	img.isLE = id=="II";
+	img.width  = img["t256"][0];  //delete img["t256"];
+	img.height = img["t257"][0];  //delete img["t257"];
+
+	var cmpr   = img["t259"] ? img["t259"][0] : 1;  //delete img["t259"];
+	var fo = img["t266"] ? img["t266"][0] : 1;  //delete img["t266"];
+	if(img["t284"] && img["t284"][0]==2) log("PlanarConfiguration 2 should not be used!");
+
+	var bipp;  // bits per pixel
+	if(img["t258"]) bipp = Math.min(32,img["t258"][0])*img["t258"].length;
+	else            bipp = (img["t277"]?img["t277"][0]:1);  
+	// Some .NEF files have t258==14, even though they use 16 bits per pixel
+	if(cmpr==1 && img["t279"]!=null && img["t278"] && img["t262"][0]==32803)  {
+		bipp = Math.round((img["t279"][0]*8)/(img.width*img["t278"][0]));
+	}
+	var bipl = Math.ceil(img.width*bipp/8)*8;
+	var soff = img["t273"];  if(soff==null) soff = img["t324"];
+	var bcnt = img["t279"];  if(cmpr==1 && soff.length==1) bcnt = [img.height*(bipl>>>3)];  if(bcnt==null) bcnt = img["t325"];
+	var bytes = new Uint8Array(img.height*(bipl>>>3)), bilen = 0;
+
+	if(img["t322"]!=null) // tiled
+	{
+		var tw = img["t322"][0], th = img["t323"][0];
+		var tx = Math.floor((img.width  + tw - 1) / tw);
+		var ty = Math.floor((img.height + th - 1) / th);
+		var tbuff = new Uint8Array(Math.ceil(tw*th*bipp/8)|0);
+		for(var y=0; y<ty; y++)
+			for(var x=0; x<tx; x++)
+			{
+				var i = y*tx+x;  for(var j=0; j<tbuff.length; j++) tbuff[j]=0;
+				UTIF.decode._decompress(img,ifds, data, soff[i], bcnt[i], cmpr, tbuff, 0, fo);
+				// Might be required for 7 too. Need to check
+				if (cmpr==6) bytes = tbuff;
+				else UTIF._copyTile(tbuff, Math.ceil(tw*bipp/8)|0, th, bytes, Math.ceil(img.width*bipp/8)|0, img.height, Math.ceil(x*tw*bipp/8)|0, y*th);
+			}
+		bilen = bytes.length*8;
+	}
+	else	// stripped
+	{
+		var rps = img["t278"] ? img["t278"][0] : img.height;   rps = Math.min(rps, img.height);
+		for(var i=0; i<soff.length; i++)
+		{
+			UTIF.decode._decompress(img,ifds, data, soff[i], bcnt[i], cmpr, bytes, Math.ceil(bilen/8)|0, fo);
+			bilen += bipl * rps;
+		}
+		bilen = Math.min(bilen, bytes.length*8);
+	}
+	img.data = new Uint8Array(bytes.buffer, 0, Math.ceil(bilen/8)|0);
+}
+
+UTIF.decode._decompress = function(img,ifds, data, off, len, cmpr, tgt, toff, fo)  // fill order
+{
+	//console.log("compression", cmpr);
+	//var time = Date.now();
+	if(false) {}
+	else if(cmpr==1 || (len==tgt.length && cmpr!=32767)) for(var j=0; j<len; j++) tgt[toff+j] = data[off+j];
+	else if(cmpr==3) UTIF.decode._decodeG3 (data, off, len, tgt, toff, img.width, fo);
+	else if(cmpr==4) UTIF.decode._decodeG4 (data, off, len, tgt, toff, img.width, fo);
+	else if(cmpr==5) UTIF.decode._decodeLZW(data, off, tgt, toff);
+	else if(cmpr==6) UTIF.decode._decodeOldJPEG(img, data, off, len, tgt, toff);
+	else if(cmpr==7) UTIF.decode._decodeNewJPEG(img, data, off, len, tgt, toff);
+	else if(cmpr==8) {  var src = new Uint8Array(data.buffer,off,len);  var bin = pako["inflate"](src);  for(var i=0; i<bin.length; i++) tgt[toff+i]=bin[i];  }
+	else if(cmpr==32767) UTIF.decode._decodeARW(img, data, off, len, tgt, toff);
+	else if(cmpr==32773) UTIF.decode._decodePackBits(data, off, len, tgt, toff);
+	else if(cmpr==32809) UTIF.decode._decodeThunder (data, off, len, tgt, toff);
+	else if(cmpr==34713) //for(var j=0; j<len; j++) tgt[toff+j] = data[off+j];
+		UTIF.decode._decodeNikon   (img,ifds, data, off, len, tgt, toff);
+	else log("Unknown compression", cmpr);
+	
+	//console.log(Date.now()-time);
+	
+	var bps = (img["t258"]?Math.min(32,img["t258"][0]):1);
+	var noc = (img["t277"]?img["t277"][0]:1), bpp=(bps*noc)>>>3, h = (img["t278"] ? img["t278"][0] : img.height), bpl = Math.ceil(bps*noc*img.width/8);
+	
+	// convert to Little Endian  /*
+	if(bps==16 && !img.isLE && img["t33422"]==null)  // not DNG
+		for(var y=0; y<h; y++) {
+			//console.log("fixing endianity");
+			var roff = toff+y*bpl;
+			for(var x=1; x<bpl; x+=2) {  var t=tgt[roff+x];  tgt[roff+x]=tgt[roff+x-1];  tgt[roff+x-1]=t;  }
+		}  //*/
+
+	if(img["t317"] && img["t317"][0]==2)
+	{
+		for(var y=0; y<h; y++)
+		{
+			var ntoff = toff+y*bpl;
+			if(bps==16) for(var j=bpp; j<bpl; j+=2) {
+				var nv = ((tgt[ntoff+j+1]<<8)|tgt[ntoff+j])  +  ((tgt[ntoff+j-bpp+1]<<8)|tgt[ntoff+j-bpp]);
+				tgt[ntoff+j] = nv&255;  tgt[ntoff+j+1] = (nv>>>8)&255;  
+			}
+			else if(noc==3) for(var j=  3; j<bpl; j+=3)
+			{
+				tgt[ntoff+j  ] = (tgt[ntoff+j  ] + tgt[ntoff+j-3])&255;
+				tgt[ntoff+j+1] = (tgt[ntoff+j+1] + tgt[ntoff+j-2])&255;
+				tgt[ntoff+j+2] = (tgt[ntoff+j+2] + tgt[ntoff+j-1])&255;
+			}
+			else for(var j=bpp; j<bpl; j++) tgt[ntoff+j] = (tgt[ntoff+j] + tgt[ntoff+j-bpp])&255;
+		}
+	}
+}
+
+UTIF.decode._ljpeg_diff = function(data, prm, huff) {
+	var getbithuff   = UTIF.decode._getbithuff;
+	var len, diff;
+	len  = getbithuff(data, prm, huff[0], huff);
+	diff = getbithuff(data, prm, len, 0);
+	if ((diff & (1 << (len-1))) == 0)  diff -= (1 << len) - 1;
+	return diff;
+}
+UTIF.decode._decodeARW = function(img, inp, off, src_length, tgt, toff) {
+	var raw_width = img["t256"][0], height=img["t257"][0], tiff_bps=img["t258"][0];
+	var bin=(img.isLE ? UTIF._binLE : UTIF._binBE);
+	//console.log(raw_width, height, tiff_bps, raw_width*height, src_length);
+	var arw2 = (raw_width*height == src_length) || (raw_width*height*1.5 == src_length);
+	//arw2 = true;
+	//console.log("ARW2: ", arw2, raw_width*height, src_length, tgt.length);
+	if(!arw2) {  //"sony_arw_load_raw"; // not arw2
+		height+=8;
+		var prm = [off,0,0,0];
+		var huff = new Uint16Array(32770);
+		var tab = [ 0xf11,0xf10,0xe0f,0xd0e,0xc0d,0xb0c,0xa0b,0x90a,0x809,
+			0x708,0x607,0x506,0x405,0x304,0x303,0x300,0x202,0x201 ];
+		var i, c, n, col, row, sum=0;
+		var ljpeg_diff = UTIF.decode._ljpeg_diff;
+
+		huff[0] = 15;
+		for (n=i=0; i < 18; i++) {
+			var lim = 32768 >>> (tab[i] >>> 8);
+			for(var c=0; c<lim; c++) huff[++n] = tab[i];
+		}
+		for (col = raw_width; col--; )
+			for (row=0; row < height+1; row+=2) {
+				if (row == height) row = 1;
+				sum += ljpeg_diff(inp, prm, huff);
+				if (row < height) {
+					var clr =  (sum)&4095;
+					UTIF.decode._putsF(tgt, (row*raw_width+col)*tiff_bps, clr<<(16-tiff_bps));
+				}
+			}
+		return;
+	}
+	if(raw_width*height*1.5==src_length) {
+		//console.log("weird compression");
+		for(var i=0; i<src_length; i+=3) {  var b0=inp[off+i+0], b1=inp[off+i+1], b2=inp[off+i+2];  
+			tgt[toff+i]=(b1<<4)|(b0>>>4);  tgt[toff+i+1]=(b0<<4)|(b2>>>4);  tgt[toff+i+2]=(b2<<4)|(b1>>>4);  }
+		return;
+	}
+	
+	var pix = new Uint16Array(16);
+	var row, col, val, max, min, imax, imin, sh, bit, i,    dp;
+	
+	var data = new Uint8Array(raw_width+1);
+	for (row=0; row < height; row++) {
+		//fread (data, 1, raw_width, ifp);
+		for(var j=0; j<raw_width; j++) data[j]=inp[off++];
+		for (dp=0, col=0; col < raw_width-30; dp+=16) {
+			max  = 0x7ff & (val = bin.readUint(data,dp));
+			min  = 0x7ff & (val >>> 11);
+			imax = 0x0f & (val >>> 22);
+			imin = 0x0f & (val >>> 26);
+			for (sh=0; sh < 4 && 0x80 << sh <= max-min; sh++);
+			for (bit=30, i=0; i < 16; i++)
+				if      (i == imax) pix[i] = max;
+				else if (i == imin) pix[i] = min;
+				else {
+					pix[i] = ((bin.readUshort(data, dp+(bit >> 3)) >>> (bit & 7) & 0x7f) << sh) + min;
+					if (pix[i] > 0x7ff) pix[i] = 0x7ff;
+					bit += 7;
+				}
+			for (i=0; i < 16; i++, col+=2) {
+				//RAW(row,col) = curve[pix[i] << 1] >> 2;
+				var clr =  pix[i]<<1;   //clr = 0xffff;
+				UTIF.decode._putsF(tgt, (row*raw_width+col)*tiff_bps, clr<<(16-tiff_bps));
+			}
+			col -= col & 1 ? 1:31;
+		}
+	}
+}
+
+UTIF.decode._decodeNikon = function(img,imgs, data, off, src_length, tgt, toff)
+{
+	var nikon_tree = [
+    [ 0, 0,1,5,1,1,1,1,1,1,2,0,0,0,0,0,0,	/* 12-bit lossy */
+      5,4,3,6,2,7,1,0,8,9,11,10,12 ],
+    [ 0, 0,1,5,1,1,1,1,1,1,2,0,0,0,0,0,0,	/* 12-bit lossy after split */
+      0x39,0x5a,0x38,0x27,0x16,5,4,3,2,1,0,11,12,12 ],
+    [ 0, 0,1,4,2,3,1,2,0,0,0,0,0,0,0,0,0,  /* 12-bit lossless */
+      5,4,6,3,7,2,8,1,9,0,10,11,12 ],
+    [ 0, 0,1,4,3,1,1,1,1,1,2,0,0,0,0,0,0,	/* 14-bit lossy */
+      5,6,4,7,8,3,9,2,1,0,10,11,12,13,14 ],
+    [ 0, 0,1,5,1,1,1,1,1,1,1,2,0,0,0,0,0,	/* 14-bit lossy after split */
+      8,0x5c,0x4b,0x3a,0x29,7,6,5,4,3,2,1,0,13,14 ],
+    [ 0, 0,1,4,2,2,3,1,2,0,0,0,0,0,0,0,0,	/* 14-bit lossless */
+      7,6,8,5,9,4,10,3,11,12,2,0,1,13,14 ] ];
+	  
+	var raw_width = img["t256"][0], height=img["t257"][0], tiff_bps=img["t258"][0];
+	
+	var tree = 0, split = 0;
+	var make_decoder = UTIF.decode._make_decoder;
+	var getbithuff   = UTIF.decode._getbithuff;
+	
+	var mn = imgs[0].exifIFD.makerNote, md = mn["t150"]?mn["t150"]:mn["t140"], mdo=0;  //console.log(mn,md);
+	//console.log(md[0].toString(16), md[1].toString(16), tiff_bps);
+	var ver0 = md[mdo++], ver1 = md[mdo++];
+	if (ver0 == 0x49 || ver1 == 0x58)  mdo+=2110;
+	if (ver0 == 0x46) tree = 2;
+	if (tiff_bps == 14) tree += 3;
+	
+	var vpred = [[0,0],[0,0]], bin=(img.isLE ? UTIF._binLE : UTIF._binBE);
+	for(var i=0; i<2; i++) for(var j=0; j<2; j++) {  vpred[i][j] = bin.readShort(md,mdo);  mdo+=2;   }  // not sure here ... [i][j] or [j][i]
+	//console.log(vpred);
+	
+	
+	var max = 1 << tiff_bps & 0x7fff, step=0;
+	var csize = bin.readShort(md,mdo);  mdo+=2;
+	if (csize > 1) step = Math.floor(max / (csize-1));
+	if (ver0 == 0x44 && ver1 == 0x20 && step > 0)  split = bin.readShort(md,562);
+	
+	
+	var i;
+	var row, col;
+	var len, shl, diff;
+	var min_v = 0;
+	var hpred = [0,0];
+	var huff = make_decoder(nikon_tree[tree]);
+	
+	//var g_input_offset=0, bitbuf=0, vbits=0, reset=0;
+	var prm = [off,0,0,0];
+	//console.log(split);  split = 170;
+	
+	for (min_v=row=0; row < height; row++) {
+		if (split && row == split) {
+			//free (huff);
+			huff = make_decoder (nikon_tree[tree+1]);
+			//max_v += (min_v = 16) << 1;
+		}
+		for (col=0; col < raw_width; col++) {
+			i = getbithuff(data,prm,huff[0],huff);
+			len = i  & 15;
+			shl = i >>> 4;
+			diff = (((getbithuff(data,prm,len-shl,0) << 1) + 1) << shl) >>> 1;
+			if ((diff & (1 << (len-1))) == 0)
+				diff -= (1 << len) - (shl==0?1:0);
+			if (col < 2) hpred[col] = vpred[row & 1][col] += diff;
+			else         hpred[col & 1] += diff;
+			
+			var clr = Math.min(Math.max(hpred[col & 1],0),(1<<tiff_bps)-1);
+			var bti = (row*raw_width+col)*tiff_bps;  
+			UTIF.decode._putsF(tgt, bti, clr<<(16-tiff_bps));
+		}
+	}
+}
+// put 16 bits
+UTIF.decode._putsF= function(dt, pos, val) {  val = val<<(8-(pos&7));  var o=(pos>>>3);  dt[o]|=val>>>16;  dt[o+1]|=val>>>8;  dt[o+2]|=val;  }
+
+
+UTIF.decode._getbithuff = function(data,prm,nbits, huff) {
+	var zero_after_ff = 0;
+	var get_byte = UTIF.decode._get_byte;
+	var c;
+  
+	var off=prm[0], bitbuf=prm[1], vbits=prm[2], reset=prm[3];
+
+	//if (nbits > 25) return 0;
+	//if (nbits <  0) return bitbuf = vbits = reset = 0;
+	if (nbits == 0 || vbits < 0) return 0; 
+	while (!reset && vbits < nbits && (c = data[off++]) != -1 &&
+		!(reset = zero_after_ff && c == 0xff && data[off++])) {
+		//console.log("byte read into c");
+		bitbuf = (bitbuf << 8) + c;
+		vbits += 8;
+	} 
+	c = (bitbuf << (32-vbits)) >>> (32-nbits);
+	if (huff) {
+		vbits -= huff[c+1] >>> 8;  //console.log(c, huff[c]>>8);
+		c =  huff[c+1]&255;
+	} else
+		vbits -= nbits;
+	if (vbits < 0) throw "e";
+  
+	prm[0]=off;  prm[1]=bitbuf;  prm[2]=vbits;  prm[3]=reset;
+  
+	return c;
+}
+
+UTIF.decode._make_decoder = function(source) {
+	var max, len, h, i, j;
+	var huff = [];
+
+	for (max=16; max!=0 && !source[max]; max--);
+	var si=17;
+	
+	huff[0] = max;
+	for (h=len=1; len <= max; len++)
+		for (i=0; i < source[len]; i++, ++si)
+			for (j=0; j < 1 << (max-len); j++)
+				if (h <= 1 << max)
+					huff[h++] = (len << 8) | source[si];
+	return huff;
+}
+
+UTIF.decode._decodeNewJPEG = function(img, data, off, len, tgt, toff)
+{
+	var tables = img["t347"], tlen = tables ? tables.length : 0, buff = new Uint8Array(tlen + len);
+	
+	if (tables) {
+		var SOI = 216, EOI = 217, boff = 0;
+		for (var i=0; i<(tlen-1); i++)
+		{
+			// Skip EOI marker from JPEGTables
+			if (tables[i]==255 && tables[i+1]==EOI) break;
+			buff[boff++] = tables[i];
+		}
+
+		// Skip SOI marker from data
+		var byte1 = data[off], byte2 = data[off + 1];
+		if (byte1!=255 || byte2!=SOI)
+		{
+			buff[boff++] = byte1;
+			buff[boff++] = byte2;
+		}
+		for (var i=2; i<len; i++) buff[boff++] = data[off+i];
+	}
+	else for (var i=0; i<len; i++) buff[i] = data[off+i];
+
+	if(img["t262"][0]==32803 || img["t262"][0]==34892) // lossless JPEG and lossy JPEG (used in DNG files)
+	{
+		var bps = img["t258"][0];//, dcdr = new LosslessJpegDecoder();
+		var out = UTIF.LosslessJpegDecode(buff), olen=out.length;  //console.log(olen);
+		
+		if(false) {}
+		else if(bps==16) {
+			if(img.isLE) for(var i=0; i<olen; i++ ) {  tgt[toff+(i<<1)] = (out[i]&255);  tgt[toff+(i<<1)+1] = (out[i]>>>8);  }
+			else         for(var i=0; i<olen; i++ ) {  tgt[toff+(i<<1)] = (out[i]>>>8);  tgt[toff+(i<<1)+1] = (out[i]&255);  }
+		}
+		else if(bps==14 || bps==12) {  // 4 * 14 == 56 == 7 * 8
+			var rst = 16-bps;
+			for(var i=0; i<olen; i++) UTIF.decode._putsF(tgt, i*bps, out[i]<<rst);
+		}
+		else throw new Error("unsupported bit depth "+bps);
+	}
+	else
+	{
+		var parser = new UTIF.JpegDecoder();  parser.parse(buff);
+		var decoded = parser.getData(parser.width, parser.height);
+		for (var i=0; i<decoded.length; i++) tgt[toff + i] = decoded[i];
+	}
+
+	// PhotometricInterpretation is 6 (YCbCr) for JPEG, but after decoding we populate data in
+	// RGB format, so updating the tag value
+	if(img["t262"][0] == 6)  img["t262"][0] = 2;
+}
+
+UTIF.decode._decodeOldJPEGInit = function(img, data, off, len)
+{
+	var SOI = 216, EOI = 217, DQT = 219, DHT = 196, DRI = 221, SOF0 = 192, SOS = 218;
+	var joff = 0, soff = 0, tables, sosMarker, isTiled = false, i, j, k;
+	var jpgIchgFmt    = img["t513"], jifoff = jpgIchgFmt ? jpgIchgFmt[0] : 0;
+	var jpgIchgFmtLen = img["t514"], jiflen = jpgIchgFmtLen ? jpgIchgFmtLen[0] : 0;
+	var soffTag       = img["t324"] || img["t273"] || jpgIchgFmt;
+	var ycbcrss       = img["t530"], ssx = 0, ssy = 0;
+	var spp           = img["t277"]?img["t277"][0]:1;
+	var jpgresint     = img["t515"];
+
+	if(soffTag)
+	{
+		soff = soffTag[0];
+		isTiled = (soffTag.length > 1);
+	}
+
+	if(!isTiled)
+	{
+		if(data[off]==255 && data[off+1]==SOI) return { jpegOffset: off };
+		if(jpgIchgFmt!=null)
+		{
+			if(data[off+jifoff]==255 && data[off+jifoff+1]==SOI) joff = off+jifoff;
+			else log("JPEGInterchangeFormat does not point to SOI");
+
+			if(jpgIchgFmtLen==null) log("JPEGInterchangeFormatLength field is missing");
+			else if(jifoff >= soff || (jifoff+jiflen) <= soff) log("JPEGInterchangeFormatLength field value is invalid");
+
+			if(joff != null) return { jpegOffset: joff };
+		}
+	}
+
+	if(ycbcrss!=null) {  ssx = ycbcrss[0];  ssy = ycbcrss[1];  }
+
+	if(jpgIchgFmt!=null)
+		if(jpgIchgFmtLen!=null)
+			if(jiflen >= 2 && (jifoff+jiflen) <= soff)
+			{
+				if(data[off+jifoff+jiflen-2]==255 && data[off+jifoff+jiflen-1]==SOI) tables = new Uint8Array(jiflen-2);
+				else tables = new Uint8Array(jiflen);
+
+				for(i=0; i<tables.length; i++) tables[i] = data[off+jifoff+i];
+				log("Incorrect JPEG interchange format: using JPEGInterchangeFormat offset to derive tables");
+			}
+			else log("JPEGInterchangeFormat+JPEGInterchangeFormatLength > offset to first strip or tile");
+
+	if(tables == null)
+	{
+		var ooff = 0, out = [];
+		out[ooff++] = 255; out[ooff++] = SOI;
+
+		var qtables = img["t519"];
+		if(qtables==null) throw new Error("JPEGQTables tag is missing");
+		for(i=0; i<qtables.length; i++)
+		{
+			out[ooff++] = 255; out[ooff++] = DQT; out[ooff++] = 0; out[ooff++] = 67; out[ooff++] = i;
+			for(j=0; j<64; j++) out[ooff++] = data[off+qtables[i]+j];
+		}
+
+		for(k=0; k<2; k++)
+		{
+			var htables = img[(k == 0) ? "t520" : "t521"];
+			if(htables==null) throw new Error(((k == 0) ? "JPEGDCTables" : "JPEGACTables") + " tag is missing");
+			for(i=0; i<htables.length; i++)
+			{
+				out[ooff++] = 255; out[ooff++] = DHT;
+				//out[ooff++] = 0; out[ooff++] = 67; out[ooff++] = i;
+				var nc = 19;
+				for(j=0; j<16; j++) nc += data[off+htables[i]+j];
+
+				out[ooff++] = (nc >>> 8); out[ooff++] = nc & 255;
+				out[ooff++] = (i | (k << 4));
+				for(j=0; j<16; j++) out[ooff++] = data[off+htables[i]+j];
+				for(j=0; j<nc; j++) out[ooff++] = data[off+htables[i]+16+j];
+			}
+		}
+
+		out[ooff++] = 255; out[ooff++] = SOF0;
+		out[ooff++] = 0;  out[ooff++] = 8 + 3*spp;  out[ooff++] = 8;
+		out[ooff++] = (img.height >>> 8) & 255;  out[ooff++] = img.height & 255;
+		out[ooff++] = (img.width  >>> 8) & 255;  out[ooff++] = img.width  & 255;
+		out[ooff++] = spp;
+		if(spp==1) {  out[ooff++] = 1;  out[ooff++] = 17;  out[ooff++] = 0;  }
+		else for(i=0; i<3; i++)
+		{
+			out[ooff++] = i + 1;
+			out[ooff++] = (i != 0) ? 17 : (((ssx & 15) << 4) | (ssy & 15));
+			out[ooff++] = i;
+		}
+
+		if(jpgresint!=null && jpgresint[0]!=0)
+		{
+			out[ooff++] = 255;  out[ooff++] = DRI;  out[ooff++] = 0;  out[ooff++] = 4;
+			out[ooff++] = (jpgresint[0] >>> 8) & 255;
+			out[ooff++] = jpgresint[0] & 255;
+		}
+
+		tables = new Uint8Array(out);
+	}
+
+	var sofpos = -1;
+	i = 0;
+	while(i < (tables.length - 1)) {
+		if(tables[i]==255 && tables[i+1]==SOF0) {  sofpos = i; break;  }
+		i++;
+	}
+
+	if(sofpos == -1)
+	{
+		var tmptab = new Uint8Array(tables.length + 10 + 3*spp);
+		tmptab.set(tables);
+		var tmpoff = tables.length;
+		sofpos = tables.length;
+		tables = tmptab;
+
+		tables[tmpoff++] = 255; tables[tmpoff++] = SOF0;
+		tables[tmpoff++] = 0;  tables[tmpoff++] = 8 + 3*spp;  tables[tmpoff++] = 8;
+		tables[tmpoff++] = (img.height >>> 8) & 255;  tables[tmpoff++] = img.height & 255;
+		tables[tmpoff++] = (img.width  >>> 8) & 255;  tables[tmpoff++] = img.width  & 255;
+		tables[tmpoff++] = spp;
+		if(spp==1) {  tables[tmpoff++] = 1;  tables[tmpoff++] = 17;  tables[tmpoff++] = 0;  }
+		else for(i=0; i<3; i++)
+		{
+			tables[tmpoff++] = i + 1;
+			tables[tmpoff++] = (i != 0) ? 17 : (((ssx & 15) << 4) | (ssy & 15));
+			tables[tmpoff++] = i;
+		}
+	}
+
+	if(data[soff]==255 && data[soff+1]==SOS)
+	{
+		var soslen = (data[soff+2]<<8) | data[soff+3];
+		sosMarker = new Uint8Array(soslen+2);
+		sosMarker[0] = data[soff];  sosMarker[1] = data[soff+1]; sosMarker[2] = data[soff+2];  sosMarker[3] = data[soff+3];
+		for(i=0; i<(soslen-2); i++) sosMarker[i+4] = data[soff+i+4];
+	}
+	else
+	{
+		sosMarker = new Uint8Array(2 + 6 + 2*spp);
+		var sosoff = 0;
+		sosMarker[sosoff++] = 255;  sosMarker[sosoff++] = SOS;
+		sosMarker[sosoff++] = 0;  sosMarker[sosoff++] = 6 + 2*spp;  sosMarker[sosoff++] = spp;
+		if(spp==1) {  sosMarker[sosoff++] = 1;  sosMarker[sosoff++] = 0;  }
+		else for(i=0; i<3; i++)
+		{
+			sosMarker[sosoff++] = i+1;  sosMarker[sosoff++] = (i << 4) | i;
+		}
+		sosMarker[sosoff++] = 0;  sosMarker[sosoff++] = 63;  sosMarker[sosoff++] = 0;
+	}
+
+	return { jpegOffset: off, tables: tables, sosMarker: sosMarker, sofPosition: sofpos };
+}
+
+UTIF.decode._decodeOldJPEG = function(img, data, off, len, tgt, toff)
+{
+	var i, dlen, tlen, buff, buffoff;
+	var jpegData = UTIF.decode._decodeOldJPEGInit(img, data, off, len);
+
+	if(jpegData.jpegOffset!=null)
+	{
+		dlen = off+len-jpegData.jpegOffset;
+		buff = new Uint8Array(dlen);
+		for(i=0; i<dlen; i++) buff[i] = data[jpegData.jpegOffset+i];
+	}
+	else
+	{
+		tlen = jpegData.tables.length;
+		buff = new Uint8Array(tlen + jpegData.sosMarker.length + len + 2);
+		buff.set(jpegData.tables);
+		buffoff = tlen;
+
+		buff[jpegData.sofPosition+5] = (img.height >>> 8) & 255;  buff[jpegData.sofPosition+6] = img.height & 255;
+		buff[jpegData.sofPosition+7] = (img.width  >>> 8) & 255;  buff[jpegData.sofPosition+8] = img.width  & 255;
+
+		if(data[off]!=255 || data[off+1]!=SOS)
+		{
+			buff.set(jpegData.sosMarker, buffoff);
+			buffoff += sosMarker.length;
+		}
+		for(i=0; i<len; i++) buff[buffoff++] = data[off+i];
+		buff[buffoff++] = 255;  buff[buffoff++] = EOI;
+	}
+
+	var parser = new UTIF.JpegDecoder();  parser.parse(buff);
+	var decoded = parser.getData(parser.width, parser.height);
+	for (var i=0; i<decoded.length; i++) tgt[toff + i] = decoded[i];
+
+	// PhotometricInterpretation is 6 (YCbCr) for JPEG, but after decoding we populate data in
+	// RGB format, so updating the tag value
+	if(img["t262"] && img["t262"][0] == 6)  img["t262"][0] = 2;
+}
+
+UTIF.decode._decodePackBits = function(data, off, len, tgt, toff)
+{
+	var sa = new Int8Array(data.buffer), ta = new Int8Array(tgt.buffer), lim = off+len;
+	while(off<lim)
+	{
+		var n = sa[off];  off++;
+		if(n>=0  && n<128)    for(var i=0; i< n+1; i++) {  ta[toff]=sa[off];  toff++;  off++;   }
+		if(n>=-127 && n<0) {  for(var i=0; i<-n+1; i++) {  ta[toff]=sa[off];  toff++;           }  off++;  }
+	}
+}
+
+UTIF.decode._decodeThunder = function(data, off, len, tgt, toff)
+{
+	var d2 = [ 0, 1, 0, -1 ],  d3 = [ 0, 1, 2, 3, 0, -3, -2, -1 ];
+	var lim = off+len, qoff = toff*2, px = 0;
+	while(off<lim)
+	{
+		var b = data[off], msk = (b>>>6), n = (b&63);  off++;
+		if(msk==3) { px=(n&15);  tgt[qoff>>>1] |= (px<<(4*(1-qoff&1)));  qoff++;   }
+		if(msk==0) for(var i=0; i<n; i++) {  tgt[qoff>>>1] |= (px<<(4*(1-qoff&1)));  qoff++;   }
+		if(msk==2) for(var i=0; i<2; i++) {  var d=(n>>>(3*(1-i)))&7;  if(d!=4) { px+=d3[d];  tgt[qoff>>>1] |= (px<<(4*(1-qoff&1)));  qoff++; }  }
+		if(msk==1) for(var i=0; i<3; i++) {  var d=(n>>>(2*(2-i)))&3;  if(d!=2) { px+=d2[d];  tgt[qoff>>>1] |= (px<<(4*(1-qoff&1)));  qoff++; }  }
+	}
+}
+
+UTIF.decode._dmap = { "1":0,"011":1,"000011":2,"0000011":3, "010":-1,"000010":-2,"0000010":-3  };
+UTIF.decode._lens = ( function()
+{
+	var addKeys = function(lens, arr, i0, inc) {  for(var i=0; i<arr.length; i++) lens[arr[i]] = i0 + i*inc;  }
+
+	var termW = "00110101,000111,0111,1000,1011,1100,1110,1111,10011,10100,00111,01000,001000,000011,110100,110101," // 15
+	+ "101010,101011,0100111,0001100,0001000,0010111,0000011,0000100,0101000,0101011,0010011,0100100,0011000,00000010,00000011,00011010," // 31
+	+ "00011011,00010010,00010011,00010100,00010101,00010110,00010111,00101000,00101001,00101010,00101011,00101100,00101101,00000100,00000101,00001010," // 47
+	+ "00001011,01010010,01010011,01010100,01010101,00100100,00100101,01011000,01011001,01011010,01011011,01001010,01001011,00110010,00110011,00110100";
+
+	var termB = "0000110111,010,11,10,011,0011,0010,00011,000101,000100,0000100,0000101,0000111,00000100,00000111,000011000," // 15
+	+ "0000010111,0000011000,0000001000,00001100111,00001101000,00001101100,00000110111,00000101000,00000010111,00000011000,000011001010,000011001011,000011001100,000011001101,000001101000,000001101001," // 31
+	+ "000001101010,000001101011,000011010010,000011010011,000011010100,000011010101,000011010110,000011010111,000001101100,000001101101,000011011010,000011011011,000001010100,000001010101,000001010110,000001010111," // 47
+	+ "000001100100,000001100101,000001010010,000001010011,000000100100,000000110111,000000111000,000000100111,000000101000,000001011000,000001011001,000000101011,000000101100,000001011010,000001100110,000001100111";
+
+	var makeW = "11011,10010,010111,0110111,00110110,00110111,01100100,01100101,01101000,01100111,011001100,011001101,011010010,011010011,011010100,011010101,011010110,"
+	+ "011010111,011011000,011011001,011011010,011011011,010011000,010011001,010011010,011000,010011011";
+
+	var makeB = "0000001111,000011001000,000011001001,000001011011,000000110011,000000110100,000000110101,0000001101100,0000001101101,0000001001010,0000001001011,0000001001100,"
+	+ "0000001001101,0000001110010,0000001110011,0000001110100,0000001110101,0000001110110,0000001110111,0000001010010,0000001010011,0000001010100,0000001010101,0000001011010,"
+	+ "0000001011011,0000001100100,0000001100101";
+
+	var makeA = "00000001000,00000001100,00000001101,000000010010,000000010011,000000010100,000000010101,000000010110,000000010111,000000011100,000000011101,000000011110,000000011111";
+
+	termW = termW.split(",");  termB = termB.split(",");  makeW = makeW.split(",");  makeB = makeB.split(",");  makeA = makeA.split(",");
+
+	var lensW = {}, lensB = {};
+	addKeys(lensW, termW, 0, 1);  addKeys(lensW, makeW, 64,64);  addKeys(lensW, makeA, 1792,64);
+	addKeys(lensB, termB, 0, 1);  addKeys(lensB, makeB, 64,64);  addKeys(lensB, makeA, 1792,64);
+	return [lensW, lensB];
+} )();
+
+UTIF.decode._decodeG4 = function(data, off, slen, tgt, toff, w, fo)
+{
+	var U = UTIF.decode, boff=off<<3, len=0, wrd="";	// previous starts with 1
+	var line=[], pline=[];  for(var i=0; i<w; i++) pline.push(0);  pline=U._makeDiff(pline);
+	var a0=0, a1=0, a2=0, b1=0, b2=0, clr=0;
+	var y=0, mode="", toRead=0;
+	var bipl = Math.ceil(w/8)*8;
+
+	while((boff>>>3)<off+slen)
+	{
+		b1 = U._findDiff(pline, a0+(a0==0?0:1), 1-clr), b2 = U._findDiff(pline, b1, clr);	// could be precomputed
+		var bit =0;
+		if(fo==1) bit = (data[boff>>>3]>>>(7-(boff&7)))&1;
+		if(fo==2) bit = (data[boff>>>3]>>>(  (boff&7)))&1;
+		boff++;  wrd+=bit;
+		if(mode=="H")
+		{
+			if(U._lens[clr][wrd]!=null)
+			{
+				var dl=U._lens[clr][wrd];  wrd="";  len+=dl;
+				if(dl<64) {  U._addNtimes(line,len,clr);  a0+=len;  clr=1-clr;  len=0;  toRead--;  if(toRead==0) mode="";  }
+			}
+		}
+		else
+		{
+			if(wrd=="0001")  {  wrd="";  U._addNtimes(line,b2-a0,clr);  a0=b2;   }
+			if(wrd=="001" )  {  wrd="";  mode="H";  toRead=2;  }
+			if(U._dmap[wrd]!=null) {  a1 = b1+U._dmap[wrd];  U._addNtimes(line, a1-a0, clr);  a0=a1;  wrd="";  clr=1-clr;  }
+		}
+		if(line.length==w && mode=="")
+		{
+			U._writeBits(line, tgt, toff*8+y*bipl);
+			clr=0;  y++;  a0=0;
+			pline=U._makeDiff(line);  line=[];
+		}
+		//if(wrd.length>150) {  log(wrd);  break;  throw "e";  }
+	}
+}
+
+UTIF.decode._findDiff = function(line, x, clr) {  for(var i=0; i<line.length; i+=2) if(line[i]>=x && line[i+1]==clr)  return line[i];  }
+
+UTIF.decode._makeDiff = function(line)
+{
+	var out = [];  if(line[0]==1) out.push(0,1);
+	for(var i=1; i<line.length; i++) if(line[i-1]!=line[i]) out.push(i, line[i]);
+	out.push(line.length,0,line.length,1);  return out;
+}
+
+UTIF.decode._decodeG3 = function(data, off, slen, tgt, toff, w, fo)
+{
+	var U = UTIF.decode, boff=off<<3, len=0, wrd="";
+	var line=[], pline=[];  for(var i=0; i<w; i++) line.push(0);
+	var a0=0, a1=0, a2=0, b1=0, b2=0, clr=0;
+	var y=-1, mode="", toRead=0, is1D=false;
+	var bipl = Math.ceil(w/8)*8;
+	while((boff>>>3)<off+slen)
+	{
+		b1 = U._findDiff(pline, a0+(a0==0?0:1), 1-clr), b2 = U._findDiff(pline, b1, clr);	// could be precomputed
+		var bit =0;
+		if(fo==1) bit = (data[boff>>>3]>>>(7-(boff&7)))&1;
+		if(fo==2) bit = (data[boff>>>3]>>>(  (boff&7)))&1;
+		boff++;  wrd+=bit;
+
+		if(is1D)
+		{
+			if(U._lens[clr][wrd]!=null)
+			{
+				var dl=U._lens[clr][wrd];  wrd="";  len+=dl;
+				if(dl<64) {  U._addNtimes(line,len,clr);  clr=1-clr;  len=0;  }
+			}
+		}
+		else
+		{
+			if(mode=="H")
+			{
+				if(U._lens[clr][wrd]!=null)
+				{
+					var dl=U._lens[clr][wrd];  wrd="";  len+=dl;
+					if(dl<64) {  U._addNtimes(line,len,clr);  a0+=len;  clr=1-clr;  len=0;  toRead--;  if(toRead==0) mode="";  }
+				}
+			}
+			else
+			{
+				if(wrd=="0001")  {  wrd="";  U._addNtimes(line,b2-a0,clr);  a0=b2;   }
+				if(wrd=="001" )  {  wrd="";  mode="H";  toRead=2;  }
+				if(U._dmap[wrd]!=null) {  a1 = b1+U._dmap[wrd];  U._addNtimes(line, a1-a0, clr);  a0=a1;  wrd="";  clr=1-clr;  }
+			}
+		}
+		if(wrd.endsWith("000000000001")) // needed for some files
+		{
+			if(y>=0) U._writeBits(line, tgt, toff*8+y*bipl);
+			if(fo==1) is1D = ((data[boff>>>3]>>>(7-(boff&7)))&1)==1;
+			if(fo==2) is1D = ((data[boff>>>3]>>>(  (boff&7)))&1)==1;
+			boff++;
+			if(U._decodeG3.allow2D==null) U._decodeG3.allow2D=is1D;
+			if(!U._decodeG3.allow2D) {  is1D = true;  boff--;  }
+			//log("EOL",y, "next 1D:", is1D);
+			wrd="";  clr=0;  y++;  a0=0;
+			pline=U._makeDiff(line);  line=[];
+		}
+	}
+	if(line.length==w) U._writeBits(line, tgt, toff*8+y*bipl);
+}
+
+UTIF.decode._addNtimes = function(arr, n, val) {  for(var i=0; i<n; i++) arr.push(val);  }
+
+UTIF.decode._writeBits = function(bits, tgt, boff)
+{
+	for(var i=0; i<bits.length; i++) tgt[(boff+i)>>>3] |= (bits[i]<<(7-((boff+i)&7)));
+}
+
+UTIF.decode._decodeLZW = function(data, off, tgt, toff)
+{
+	if(UTIF.decode._lzwTab==null)
+	{
+		var tb=new Uint32Array(0xffff), tn=new Uint16Array(0xffff), chr=new Uint8Array(2e6);
+		for(var i=0; i<256; i++) { chr[i<<2]=i;  tb[i]=i<<2;  tn[i]=1;  }
+		UTIF.decode._lzwTab = [tb,tn,chr];
+	}
+	var copy = UTIF.decode._copyData;
+	var tab = UTIF.decode._lzwTab[0], tln=UTIF.decode._lzwTab[1], chr=UTIF.decode._lzwTab[2], totl = 258, chrl = 258<<2;
+	var bits = 9, boff = off<<3;  // offset in bits
+
+	var ClearCode = 256, EoiCode = 257;
+	var v = 0, Code = 0, OldCode = 0;
+	while(true)
+	{
+		v = (data[boff>>>3]<<16) | (data[(boff+8)>>>3]<<8) | data[(boff+16)>>>3];
+		Code = ( v>>(24-(boff&7)-bits) )    &   ((1<<bits)-1);  boff+=bits;
+
+		if(Code==EoiCode) break;
+		if(Code==ClearCode)
+		{
+			bits=9;  totl = 258;  chrl = 258<<2;
+
+			v = (data[boff>>>3]<<16) | (data[(boff+8)>>>3]<<8) | data[(boff+16)>>>3];
+			Code = ( v>>(24-(boff&7)-bits) )    &   ((1<<bits)-1);  boff+=bits;
+			if(Code==EoiCode) break;
+			tgt[toff]=Code;  toff++;
+		}
+		else if(Code<totl)
+		{
+			var cd = tab[Code], cl = tln[Code];
+			copy(chr,cd,tgt,toff,cl);  toff += cl;
+
+			if(OldCode>=totl) {  tab[totl] = chrl;  chr[tab[totl]] = cd[0];  tln[totl]=1;  chrl=(chrl+1+3)&~0x03;  totl++;  }
+			else
+			{
+				tab[totl] = chrl;
+				var nit = tab[OldCode], nil = tln[OldCode];
+				copy(chr,nit,chr,chrl,nil);
+				chr[chrl+nil]=chr[cd];  nil++;
+				tln[totl]=nil;  totl++;
+
+				chrl=(chrl+nil+3)&~0x03;
+			}
+			if(totl+1==(1<<bits)) bits++;
+		}
+		else
+		{
+			if(OldCode>=totl) {  tab[totl] = chrl;  tln[totl]=0;  totl++;  }
+			else
+			{
+				tab[totl] = chrl;
+				var nit = tab[OldCode], nil = tln[OldCode];
+				copy(chr,nit,chr,chrl,nil);
+				chr[chrl+nil]=chr[chrl];  nil++;
+				tln[totl]=nil;  totl++;
+
+				copy(chr,chrl,tgt,toff,nil);  toff += nil;
+				chrl=(chrl+nil+3)&~0x03;
+			}
+			if(totl+1==(1<<bits)) bits++;
+		}
+		OldCode = Code;
+	}
+}
+
+UTIF.decode._copyData = function(s,so,t,to,l) {  for(var i=0;i<l;i+=4) {  t[to+i]=s[so+i];  t[to+i+1]=s[so+i+1];  t[to+i+2]=s[so+i+2];  t[to+i+3]=s[so+i+3];  }  }
+
+UTIF.tags = {};
+UTIF.ttypes = {  256:3,257:3,258:3,   259:3, 262:3,  273:4,  274:3, 277:3,278:4,279:4, 282:5, 283:5, 284:3, 286:5,287:5, 296:3, 305:2, 306:2, 338:3, 513:4, 514:4, 34665:4  };
+
+UTIF._readIFD = function(bin, data, offset, ifds, depth, debug)
+{
+	var cnt = bin.readUshort(data, offset);  offset+=2;
+	var ifd = {};  ifds.push(ifd);
+
+	if(debug) log("   ".repeat(depth),ifds.length-1,">>>----------------");
+	for(var i=0; i<cnt; i++)
+	{
+		var tag  = bin.readUshort(data, offset);    offset+=2;
+		var type = bin.readUshort(data, offset);    offset+=2;
+		var num  = bin.readUint  (data, offset);    offset+=4;
+		var voff = bin.readUint  (data, offset);    offset+=4;
+		//if(tag==33723) {type=1; num*=4;}//console.log(type,num,voff);//type = 1;  // IPTC/NAA
+
+		var arr = [];
+		//ifd["t"+tag+"-"+UTIF.tags[tag]] = arr;
+		if(type== 1 || type==7) {  arr = new Uint8Array(data.buffer, (num<5 ? offset-4 : voff), num);  }
+		if(type== 2) {  var o0 = (num<5 ? offset-4 : voff), c=data[o0];  
+						if(c<128) arr.push( bin.readASCII(data, o0, num-1) );
+						else      arr = new Uint8Array(data.buffer, o0, num-1);  }
+		if(type== 3) {  for(var j=0; j<num; j++) arr.push(bin.readUshort(data, (num<3 ? offset-4 : voff)+2*j));  }
+		if(type== 4) {  for(var j=0; j<num; j++) arr.push(bin.readUint  (data, (num<2 ? offset-4 : voff)+4*j));  }
+		if(type== 5) {  for(var j=0; j<num; j++) arr.push(bin.readUint  (data, voff+j*8) / bin.readUint(data,voff+j*8+4));  }
+		if(type== 8) {  for(var j=0; j<num; j++) arr.push(bin.readShort (data, (num<3 ? offset-4 : voff)+2*j));  }
+		if(type== 9) {  for(var j=0; j<num; j++) arr.push(bin.readInt   (data, (num<2 ? offset-4 : voff)+4*j));  }
+		if(type==10) {  for(var j=0; j<num; j++) arr.push(bin.readInt   (data, voff+j*8) / bin.readInt (data,voff+j*8+4));  }
+		if(type==11) {  for(var j=0; j<num; j++) arr.push(bin.readFloat (data, voff+j*4));  }
+		if(type==12) {  for(var j=0; j<num; j++) arr.push(bin.readDouble(data, voff+j*8));  }
+		
+		ifd["t"+tag] = arr;
+		
+		if(num!=0 && arr.length==0) {  log("unknown TIFF tag type: ", type, "num:",num);  }
+		if(debug) log("   ".repeat(depth), tag, type, UTIF.tags[tag], arr);
+		
+		if(tag==330 && ifd["t272"] && ifd["t272"][0]=="DSLR-A100") {  } 
+		// ifd["t258"]=[12];  ifd["t259"]=[32767];  ifd["t273"]=[offset+arr[0]];  ifd["t277"]=[1];  ifd["t279"]=[1];  ifd["t33421"]=[2,2];  ifd["t33422"]=[0,1,1,2];
+		else if(tag==330 || tag==34665 || (tag==50740 && bin.readUshort(data,bin.readUint(arr,0))<300  )) {
+			var oarr = tag==50740 ? [bin.readUint(arr,0)] : arr;
+			var subfd = [];
+			for(var j=0; j<oarr.length; j++) UTIF._readIFD(bin, data, oarr[j], subfd, depth+1, debug);
+			if(tag==  330) ifd.subIFD = subfd;
+			if(tag==34665) ifd.exifIFD = subfd[0];
+			if(tag==50740) ifd.dngPrvt = subfd[0];
+		}
+		if(tag==37500) {
+			var mn = arr;
+			//console.log(bin.readASCII(mn,0,mn.length), mn);
+			if(bin.readASCII(mn,0,5)=="Nikon")  ifd.makerNote = UTIF["decode"](mn.slice(10).buffer)[0];
+			else if(bin.readUshort(data,voff)<300){
+				var subsub=[];  UTIF._readIFD(bin, data, voff, subsub, depth+1, debug);
+				ifd.makerNote = subsub[0];
+			}
+		}
+	}
+	if(debug) log("   ".repeat(depth),"<<<---------------");
+	return offset;
+}
+
+UTIF._writeIFD = function(bin, data, offset, ifd)
+{
+	var keys = Object.keys(ifd);
+	bin.writeUshort(data, offset, keys.length);  offset+=2;
+
+	var eoff = offset + keys.length*12 + 4;
+
+	for(var ki=0; ki<keys.length; ki++)
+	{
+		var key = keys[ki];
+		var tag = parseInt(key.slice(1)), type = UTIF.ttypes[tag];  if(type==null) throw new Error("unknown type of tag: "+tag);
+		var val = ifd[key];  if(type==2) val=val[0]+"\u0000";  var num = val.length;
+		bin.writeUshort(data, offset, tag );  offset+=2;
+		bin.writeUshort(data, offset, type);  offset+=2;
+		bin.writeUint  (data, offset, num );  offset+=4;
+
+		var dlen = [-1, 1, 1, 2, 4, 8, 0, 0, 0, 0, 0, 0, 8][type] * num;
+		var toff = offset;
+		if(dlen>4) {  bin.writeUint(data, offset, eoff);  toff=eoff;  }
+
+		if(type==2) {  bin.writeASCII(data, toff, val);   }
+		if(type==3) {  for(var i=0; i<num; i++) bin.writeUshort(data, toff+2*i, val[i]);    }
+		if(type==4) {  for(var i=0; i<num; i++) bin.writeUint  (data, toff+4*i, val[i]);    }
+		if(type==5) {  for(var i=0; i<num; i++) {  bin.writeUint(data, toff+8*i, Math.round(val[i]*10000));  bin.writeUint(data, toff+8*i+4, 10000);  }   }
+		if (type == 12) {  for (var i = 0; i < num; i++) bin.writeDouble(data, toff + 8 * i, val[i]); }
+
+		if(dlen>4) {  dlen += (dlen&1);  eoff += dlen;  }
+		offset += 4;
+	}
+	return [offset, eoff];
+}
+
+UTIF.toRGBA8 = function(out)
+{
+	var w = out.width, h = out.height, area = w*h, qarea = area*4, data = out.data;
+	var img = new Uint8Array(area*4);
+	//console.log(out);
+	// 0: WhiteIsZero, 1: BlackIsZero, 2: RGB, 3: Palette color, 4: Transparency mask, 5: CMYK
+	var intp = (out["t262"] ? out["t262"][0]: 2), bps = (out["t258"]?Math.min(32,out["t258"][0]):1);
+	//log("interpretation: ", intp, "bps", bps, out);
+	if(false) {}
+	else if(intp==0)
+	{
+		var bpl = Math.ceil(bps*w/8);
+		for(var y=0; y<h; y++) {
+			var off = y*bpl, io = y*w;
+			if(bps== 1) for(var i=0; i<w; i++) {  var qi=(io+i)<<2, px=((data[off+(i>>3)])>>(7-  (i&7)))& 1;  img[qi]=img[qi+1]=img[qi+2]=( 1-px)*255;  img[qi+3]=255;    }
+			if(bps== 4) for(var i=0; i<w; i++) {  var qi=(io+i)<<2, px=((data[off+(i>>1)])>>(4-4*(i&1)))&15;  img[qi]=img[qi+1]=img[qi+2]=(15-px)* 17;  img[qi+3]=255;    }
+			if(bps== 8) for(var i=0; i<w; i++) {  var qi=(io+i)<<2, px=data[off+i];  img[qi]=img[qi+1]=img[qi+2]=255-px;  img[qi+3]=255;    }
+		}
+	}
+	else if(intp==1)
+	{
+		var bpl = Math.ceil(bps*w/8);
+		for(var y=0; y<h; y++) {
+			var off = y*bpl, io = y*w;
+			if(bps== 1) for(var i=0; i<w; i++) {  var qi=(io+i)<<2, px=((data[off+(i>>3)])>>(7-  (i&7)))&1;   img[qi]=img[qi+1]=img[qi+2]=(px)*255;  img[qi+3]=255;    }
+			if(bps== 2) for(var i=0; i<w; i++) {  var qi=(io+i)<<2, px=((data[off+(i>>2)])>>(6-2*(i&3)))&3;   img[qi]=img[qi+1]=img[qi+2]=(px)* 85;  img[qi+3]=255;    }
+			if(bps== 8) for(var i=0; i<w; i++) {  var qi=(io+i)<<2, px=data[off+i];  img[qi]=img[qi+1]=img[qi+2]=    px;  img[qi+3]=255;    }
+			if(bps==16) for(var i=0; i<w; i++) {  var qi=(io+i)<<2, px=data[off+(2*i+1)];  img[qi]=img[qi+1]=img[qi+2]= Math.min(255,px);  img[qi+3]=255;    } // ladoga.tif
+		}
+	}
+	else if(intp==2)
+	{
+		var smpls = out["t258"]?out["t258"].length : 3;
+		
+		if(bps== 8) 
+		{
+			if(smpls==4) for(var i=0; i<qarea; i++) img[i] = data[i];
+			if(smpls==3) for(var i=0; i< area; i++) {  var qi=i<<2, ti=i*3;  img[qi]=data[ti];  img[qi+1]=data[ti+1];  img[qi+2]=data[ti+2];  img[qi+3]=255;    }
+		}
+		else{  // 3x 16-bit channel
+			if(smpls==4) for(var i=0; i<area; i++) {  var qi=i<<2, ti=i*8+1;  img[qi]=data[ti];  img[qi+1]=data[ti+2];  img[qi+2]=data[ti+4];  img[qi+3]=data[ti+6];    }
+			if(smpls==3) for(var i=0; i<area; i++) {  var qi=i<<2, ti=i*6+1;  img[qi]=data[ti];  img[qi+1]=data[ti+2];  img[qi+2]=data[ti+4];  img[qi+3]=255;           }
+		}
+	}
+	else if(intp==3)
+	{
+		var map = out["t320"];
+		for(var i=0; i<area; i++) {  var qi=i<<2, mi=data[i];  img[qi]=(map[mi]>>8);  img[qi+1]=(map[256+mi]>>8);  img[qi+2]=(map[512+mi]>>8);  img[qi+3]=255;    }
+	}
+	else if(intp==5) 
+	{
+		var smpls = out["t258"]?out["t258"].length : 4;
+		var gotAlpha = smpls>4 ? 1 : 0;
+		for(var i=0; i<area; i++) {
+			var qi=i<<2, si=i*smpls;  var C=255-data[si], M=255-data[si+1], Y=255-data[si+2], K=(255-data[si+3])*(1/255);
+			img[qi]=~~(C*K+0.5);  img[qi+1]=~~(M*K+0.5);  img[qi+2]=~~(Y*K+0.5);  img[qi+3]=255*(1-gotAlpha)+data[si+4]*gotAlpha;
+		}
+	}
+	else log("Unknown Photometric interpretation: "+intp);
+	return img;
+}
+
+UTIF.replaceIMG = function(imgs)
+{
+	if(imgs==null) imgs = document.getElementsByTagName("img");
+	var sufs = ["tif","tiff","dng","cr2","nef"]
+	for (var i=0; i<imgs.length; i++)
+	{
+		var img=imgs[i], src=img.getAttribute("src");  if(src==null) continue;
+		var suff=src.split(".").pop().toLowerCase();
+		if(sufs.indexOf(suff)==-1) continue;
+		var xhr = new XMLHttpRequest();  UTIF._xhrs.push(xhr);  UTIF._imgs.push(img);
+		xhr.open("GET", src);  xhr.responseType = "arraybuffer";
+		xhr.onload = UTIF._imgLoaded;   xhr.send();
+	}
+}
+
+UTIF._xhrs = [];  UTIF._imgs = [];
+UTIF._imgLoaded = function(e)
+{
+	var buff = e.target.response;
+	var ifds = UTIF.decode(buff);  //console.log(ifds);
+	var vsns = ifds, ma=0, page=vsns[0];  if(ifds[0].subIFD) vsns = vsns.concat(ifds[0].subIFD);
+	for(var i=0; i<vsns.length; i++) {
+		var img = vsns[i];
+		if(img["t258"]==null || img["t258"].length<3) continue;
+		var ar = img["t256"]*img["t257"];
+		if(ar>ma) {  ma=ar;  page=img;  }
+	}
+	UTIF.decodeImage(buff, page, ifds);
+	var rgba = UTIF.toRGBA8(page), w=page.width, h=page.height;
+	var ind = UTIF._xhrs.indexOf(e.target), img = UTIF._imgs[ind];
+	UTIF._xhrs.splice(ind,1);  UTIF._imgs.splice(ind,1);
+	var cnv = document.createElement("canvas");  cnv.width=w;  cnv.height=h;
+	var ctx = cnv.getContext("2d"), imgd = ctx.createImageData(w,h);
+	for(var i=0; i<rgba.length; i++) imgd.data[i]=rgba[i];       ctx.putImageData(imgd,0,0);
+	img.setAttribute("src",cnv.toDataURL());
+}
+
+
+UTIF._binBE =
+{
+	nextZero   : function(data, o) {  while(data[o]!=0) o++;  return o;  },
+	readUshort : function(buff, p) {  return (buff[p]<< 8) |  buff[p+1];  },
+	readShort  : function(buff, p) {  var a=UTIF._binBE.ui8;  a[0]=buff[p+1];  a[1]=buff[p+0];                                    return UTIF._binBE. i16[0];  },
+	readInt    : function(buff, p) {  var a=UTIF._binBE.ui8;  a[0]=buff[p+3];  a[1]=buff[p+2];  a[2]=buff[p+1];  a[3]=buff[p+0];  return UTIF._binBE. i32[0];  },
+	readUint   : function(buff, p) {  var a=UTIF._binBE.ui8;  a[0]=buff[p+3];  a[1]=buff[p+2];  a[2]=buff[p+1];  a[3]=buff[p+0];  return UTIF._binBE.ui32[0];  },
+	readASCII  : function(buff, p, l) {  var s = "";   for(var i=0; i<l; i++) s += String.fromCharCode(buff[p+i]);   return s; },
+	readFloat  : function(buff, p) {  var a=UTIF._binBE.ui8;  for(var i=0;i<4;i++) a[i]=buff[p+3-i];  return UTIF._binBE.fl32[0];  },
+	readDouble : function(buff, p) {  var a=UTIF._binBE.ui8;  for(var i=0;i<8;i++) a[i]=buff[p+7-i];  return UTIF._binBE.fl64[0];  },
+
+	writeUshort: function(buff, p, n) {  buff[p] = (n>> 8)&255;  buff[p+1] =  n&255;  },
+	writeUint  : function(buff, p, n) {  buff[p] = (n>>24)&255;  buff[p+1] = (n>>16)&255;  buff[p+2] = (n>>8)&255;  buff[p+3] = (n>>0)&255;  },
+	writeASCII : function(buff, p, s) {  for(var i = 0; i < s.length; i++)  buff[p+i] = s.charCodeAt(i);  },
+	writeDouble: function(buff, p, n)
+	{
+		UTIF._binBE.fl64[0] = n;
+		for (var i = 0; i < 8; i++) buff[p + i] = UTIF._binBE.ui8[7 - i];
+	}
+}
+UTIF._binBE.ui8  = new Uint8Array  (8);
+UTIF._binBE.i16  = new Int16Array  (UTIF._binBE.ui8.buffer);
+UTIF._binBE.i32  = new Int32Array  (UTIF._binBE.ui8.buffer);
+UTIF._binBE.ui32 = new Uint32Array (UTIF._binBE.ui8.buffer);
+UTIF._binBE.fl32 = new Float32Array(UTIF._binBE.ui8.buffer);
+UTIF._binBE.fl64 = new Float64Array(UTIF._binBE.ui8.buffer);
+
+UTIF._binLE =
+{
+	nextZero   : UTIF._binBE.nextZero,
+	readUshort : function(buff, p) {  return (buff[p+1]<< 8) |  buff[p];  },
+	readShort  : function(buff, p) {  var a=UTIF._binBE.ui8;  a[0]=buff[p+0];  a[1]=buff[p+1];                                    return UTIF._binBE. i16[0];  },
+	readInt    : function(buff, p) {  var a=UTIF._binBE.ui8;  a[0]=buff[p+0];  a[1]=buff[p+1];  a[2]=buff[p+2];  a[3]=buff[p+3];  return UTIF._binBE. i32[0];  },
+	readUint   : function(buff, p) {  var a=UTIF._binBE.ui8;  a[0]=buff[p+0];  a[1]=buff[p+1];  a[2]=buff[p+2];  a[3]=buff[p+3];  return UTIF._binBE.ui32[0];  },
+	readASCII  : UTIF._binBE.readASCII,
+	readFloat  : function(buff, p) {  var a=UTIF._binBE.ui8;  for(var i=0;i<4;i++) a[i]=buff[p+  i];  return UTIF._binBE.fl32[0];  },
+	readDouble : function(buff, p) {  var a=UTIF._binBE.ui8;  for(var i=0;i<8;i++) a[i]=buff[p+  i];  return UTIF._binBE.fl64[0];  }
+}
+UTIF._copyTile = function(tb, tw, th, b, w, h, xoff, yoff)
+{
+	//log("copyTile", tw, th,  w, h, xoff, yoff);
+	var xlim = Math.min(tw, w-xoff);
+	var ylim = Math.min(th, h-yoff);
+	for(var y=0; y<ylim; y++)
+	{
+		var tof = (yoff+y)*w+xoff;
+		var sof = y*tw;
+		for(var x=0; x<xlim; x++) b[tof+x] = tb[sof+x];
+	}
+}
+
+UTIF.LosslessJpegDecode = (function(){function t(Z){this.w=Z;this.N=0;this._=0;this.G=0}t.prototype={t:function(Z){this.N=Math.max(0,Math.min(this.w.length,Z))},i:function(){return this.w[this.N++]},l:function(){var Z=this.N;
+this.N+=2;return this.w[Z]<<8|this.w[Z+1]},J:function(){if(this._==0){this.G=this.w[this.N];this.N+=1+(this.G+1>>>8);
+this._=8}return this.G>>>--this._&1},Z:function(Z){var X=this._,s=this.G,E=Math.min(X,Z);Z-=E;X-=E;var Y=s>>>X&(1<<E)-1;
+while(Z>0){s=this.w[this.N];this.N+=1+(s+1>>>8);E=Math.min(8,Z);Z-=E;X=8-E;Y<<=E;Y|=s>>>X&(1<<E)-1}this._=X;
+this.G=s;return Y}};var i={};i.X=function(){return[0,0,-1]};i.s=function(Z,X,s){Z[i.Y(Z,0,s)+2]=X};i.Y=function(Z,X,s){if(Z[X+2]!=-1)return 0;
+if(s==0)return X;for(var E=0;E<2;E++){if(Z[X+E]==0){Z[X+E]=Z.length;Z.push(0);Z.push(0);Z.push(-1)}var Y=i.Y(Z,Z[X+E],s-1);
+if(Y!=0)return Y}return 0};i.B=function(Z,X){var s=0,E=0,Y=0,B=X._,$=X.G,e=X.N;while(!0){if(B==0){$=X.w[e];
+e+=1+($+1>>>8);B=8}Y=$>>>--B&1;s=Z[s+Y];E=Z[s+2];if(E!=-1){X._=B;X.G=$;X.N=e;return E}}return-1};function l(Z){this.z=new t(Z);
+this.D(this.z)}l.prototype={$:function(Z,X){this.Q=Z.i();this.F=Z.l();this.o=Z.l();var s=this.O=Z.i();
+this.L=[];for(var E=0;E<s;E++){var Y=Z.i(),B=Z.i();Z.i();this.L[Y]=E}Z.t(Z.N+X-(6+s*3))},e:function(){var Z=0,X=this.z.i();
+if(this.H==null)this.H={};var s=this.H[X]=i.X(),E=[];for(var Y=0;Y<16;Y++){E[Y]=this.z.i();Z+=E[Y]}for(var Y=0;
+Y<16;Y++)for(var B=0;B<E[Y];B++)i.s(s,this.z.i(),Y+1);return Z+17},W:function(Z){while(Z>0)Z-=this.e()},p:function(Z,X){var s=Z.i();
+if(!this.U){this.U=[]}for(var E=0;E<s;E++){var Y=Z.i(),B=Z.i();this.U[this.L[Y]]=this.H[B>>>4]}this.g=Z.i();
+Z.t(Z.N+X-(2+s*2))},D:function(Z){var X=!1,s=Z.l();if(s!==l.q)return;do{var s=Z.l(),E=Z.l()-2;switch(s){case l.m:this.$(Z,E);
+break;case l.K:this.W(E);break;case l.V:this.p(Z,E);X=!0;break;default:Z.t(Z.N+E);break}}while(!X)},I:function(Z,X){var s=i.B(X,Z);
+if(s==16)return-32768;var E=Z.Z(s);if((E&1<<s-1)==0)E-=(1<<s)-1;return E},B:function(Z,X){var s=this.z,E=this.O,Y=this.F,B=this.I,$=this.g,e=this.o*E,W=this.U;
+for(var p=0;p<E;p++){Z[p]=B(s,W[p])+(1<<this.Q-1)}for(var D=E;D<e;D+=E){for(var p=0;p<E;p++)Z[D+p]=B(s,W[p])+Z[D+p-E]}var I=X;
+for(var m=1;m<Y;m++){for(var p=0;p<E;p++){Z[I+p]=B(s,W[p])+Z[I+p-X]}for(var D=E;D<e;D+=E){for(var p=0;
+p<E;p++){var K=I+D+p,q=Z[K-E];if($==6)q=Z[K-X]+(q-Z[K-E-X]>>>1);Z[K]=q+B(s,W[p])}}I+=X}}};l.m=65475;
+l.K=65476;l.q=65496;l.V=65498;function J(Z){var X=new l(Z),s=X.Q>8?Uint16Array:Uint8Array,E=new s(X.o*X.F*X.O),Y=X.o*X.O;
+X.B(E,Y);return E}return J}())
+
+
+
+
+})(UTIF, pako);
+})();
\ No newline at end of file
diff --git a/vendor/jspdf.LICENSE b/vendor/jspdf.LICENSE
new file mode 100644
index 0000000..dc7d3a9
--- /dev/null
+++ b/vendor/jspdf.LICENSE
@@ -0,0 +1,22 @@
+Copyright
+(c) 2010-2025 James Hall, https://github.com/MrRio/jsPDF
+(c) 2015-2025 yWorks GmbH, https://www.yworks.com/
+
+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.
diff --git a/vendor/jspdf.umd.min.js b/vendor/jspdf.umd.min.js
new file mode 100644
index 0000000..0e600d4
--- /dev/null
+++ b/vendor/jspdf.umd.min.js
@@ -0,0 +1,373 @@
+/** @license
+ *
+ * jsPDF - PDF Document creation from JavaScript
+ * Version 4.2.1 Built on 2026-03-17T11:11:27.056Z
+ *                      CommitID 00000000
+ *
+ * Copyright (c) 2010-2025 James Hall <[email protected]>, https://github.com/MrRio/jsPDF
+ *               2015-2025 yWorks GmbH, http://www.yworks.com
+ *               2015-2025 Lukas Holländer <[email protected]>, https://github.com/HackbrettXXX
+ *               2016-2018 Aras Abbasi <[email protected]>
+ *               2010 Aaron Spike, https://github.com/acspike
+ *               2012 Willow Systems Corporation, https://github.com/willowsystems
+ *               2012 Pablo Hess, https://github.com/pablohess
+ *               2012 Florian Jenett, https://github.com/fjenett
+ *               2013 Warren Weckesser, https://github.com/warrenweckesser
+ *               2013 Youssef Beddad, https://github.com/lifof
+ *               2013 Lee Driscoll, https://github.com/lsdriscoll
+ *               2013 Stefan Slonevskiy, https://github.com/stefslon
+ *               2013 Jeremy Morel, https://github.com/jmorel
+ *               2013 Christoph Hartmann, https://github.com/chris-rock
+ *               2014 Juan Pablo Gaviria, https://github.com/juanpgaviria
+ *               2014 James Makes, https://github.com/dollaruw
+ *               2014 Diego Casorran, https://github.com/diegocr
+ *               2014 Steven Spungin, https://github.com/Flamenco
+ *               2014 Kenneth Glassey, https://github.com/Gavvers
+ *
+ * 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.
+ *
+ * Contributor(s):
+ *    siefkenj, ahwolf, rickygu, Midnith, saintclair, eaparango,
+ *    kim3er, mfo, alnorth, Flamenco
+ */
+
+!function(t,e){"object"==typeof exports&&"undefined"!=typeof module?e(exports):"function"==typeof define&&define.amd?define(["exports"],e):e((t="undefined"!=typeof globalThis?globalThis:t||self).jspdf={})}(this,function(t){function e(t,e){(null==e||e>t.length)&&(e=t.length);for(var n=0,r=Array(e);n<e;n++)r[n]=t[n];return r}function n(t,n){return function(t){if(Array.isArray(t))return t}(t)||function(t,e){var n=null==t?null:"undefined"!=typeof Symbol&&t[Symbol.iterator]||t["@@iterator"];if(null!=n){var r,i,a,s,o=[],h=!0,l=!1;try{if(a=(n=n.call(t)).next,0===e){if(Object(n)!==n)return;h=!1}else for(;!(h=(r=a.call(n)).done)&&(o.push(r.value),o.length!==e);h=!0);}catch(t){l=!0,i=t}finally{try{if(!h&&null!=n.return&&(s=n.return(),Object(s)!==s))return}finally{if(l)throw i}}return o}}(t,n)||function(t,n){if(t){if("string"==typeof t)return e(t,n);var r={}.toString.call(t).slice(8,-1);return"Object"===r&&t.constructor&&(r=t.constructor.name),"Map"===r||"Set"===r?Array.from(t):"Arguments"===r||/^(?:Ui|I)nt(?:8|16|32)(?:Clamped)?Array$/.test(r)?e(t,n):void 0}}(t,n)||function(){throw new TypeError("Invalid attempt to destructure non-iterable instance.\nIn order to be iterable, non-array objects must have a [Symbol.iterator]() method.")}()}function r(t){return r="function"==typeof Symbol&&"symbol"==typeof Symbol.iterator?function(t){return typeof t}:function(t){return t&&"function"==typeof Symbol&&t.constructor===Symbol&&t!==Symbol.prototype?"symbol":typeof t},r(t)}var i=function(){return"undefined"!=typeof window?window:"undefined"!=typeof global?global:"undefined"!=typeof self?self:this}();function a(){i.console&&"function"==typeof i.console.log&&i.console.log.apply(i.console,arguments)}var s={log:a,warn:function(t){i.console&&("function"==typeof i.console.warn?i.console.warn.apply(i.console,arguments):a.call(null,arguments))},error:function(t){i.console&&("function"==typeof i.console.error?i.console.error.apply(i.console,arguments):a(t))}};function o(t,e,n){var r=new XMLHttpRequest;r.open("GET",t),r.responseType="blob",r.onload=function(){c(r.response,e,n)},r.onerror=function(){s.error("could not download file")},r.send()}function h(t){var e=new XMLHttpRequest;e.open("HEAD",t,!1);try{e.send()}catch(n){}return e.status>=200&&e.status<=299}function l(t){try{t.dispatchEvent(new MouseEvent("click"))}catch(n){var e=document.createEvent("MouseEvents");e.initMouseEvent("click",!0,!0,window,0,0,0,80,20,!1,!1,!1,!1,0,null),t.dispatchEvent(e)}}var c=i.saveAs||("object"!==("undefined"==typeof window?"undefined":r(window))||window!==i?function(){}:"undefined"!=typeof HTMLAnchorElement&&"download"in HTMLAnchorElement.prototype?function(t,e,n){var r=i.URL||i.webkitURL,a=document.createElement("a");e=e||t.name||"download",a.download=e,a.rel="noopener","string"==typeof t?(a.href=t,a.origin!==location.origin?h(a.href)?o(t,e,n):l(a,a.target="_blank"):l(a)):(a.href=r.createObjectURL(t),setTimeout(function(){r.revokeObjectURL(a.href)},4e4),setTimeout(function(){l(a)},0))}:"msSaveOrOpenBlob"in navigator?function(t,e,n){if(e=e||t.name||"download","string"==typeof t)if(h(t))o(t,e,n);else{var i=document.createElement("a");i.href=t,i.target="_blank",setTimeout(function(){l(i)})}else navigator.msSaveOrOpenBlob(function(t,e){return void 0===e?e={autoBom:!1}:"object"!==r(e)&&(s.warn("Deprecated: Expected third argument to be a object"),e={autoBom:!e}),e.autoBom&&/^\s*(?:text\/\S*|application\/xml|\S*\/\S*\+xml)\s*;.*charset\s*=\s*utf-8/i.test(t.type)?new Blob([String.fromCharCode(65279),t],{type:t.type}):t}(t,n),e)}:function(t,e,n,a){if((a=a||open("","_blank"))&&(a.document.title=a.document.body.innerText="downloading..."),"string"==typeof t)return o(t,e,n);var s="application/octet-stream"===t.type,h=/constructor/i.test(i.HTMLElement)||i.safari,l=/CriOS\/[\d]+/.test(navigator.userAgent);if((l||s&&h)&&"object"===("undefined"==typeof FileReader?"undefined":r(FileReader))){var c=new FileReader;c.onloadend=function(){var t=c.result;t=l?t:t.replace(/^data:[^;]*;/,"data:attachment/file;"),a?a.location.href=t:location=t,a=null},c.readAsDataURL(t)}else{var u=i.URL||i.webkitURL,f=u.createObjectURL(t);a?a.location=f:location.href=f,a=null,setTimeout(function(){u.revokeObjectURL(f)},4e4)}});
+/**
+   * A class to parse color values
+   * @author Stoyan Stefanov <[email protected]>
+   * {@link   http://www.phpied.com/rgb-color-parser-in-javascript/}
+   * @license Use it if you like it
+   */function u(t){var e;t=t||"",this.ok=!1,"#"==t.charAt(0)&&(t=t.substr(1,6)),t={aliceblue:"f0f8ff",antiquewhite:"faebd7",aqua:"00ffff",aquamarine:"7fffd4",azure:"f0ffff",beige:"f5f5dc",bisque:"ffe4c4",black:"000000",blanchedalmond:"ffebcd",blue:"0000ff",blueviolet:"8a2be2",brown:"a52a2a",burlywood:"deb887",cadetblue:"5f9ea0",chartreuse:"7fff00",chocolate:"d2691e",coral:"ff7f50",cornflowerblue:"6495ed",cornsilk:"fff8dc",crimson:"dc143c",cyan:"00ffff",darkblue:"00008b",darkcyan:"008b8b",darkgoldenrod:"b8860b",darkgray:"a9a9a9",darkgreen:"006400",darkkhaki:"bdb76b",darkmagenta:"8b008b",darkolivegreen:"556b2f",darkorange:"ff8c00",darkorchid:"9932cc",darkred:"8b0000",darksalmon:"e9967a",darkseagreen:"8fbc8f",darkslateblue:"483d8b",darkslategray:"2f4f4f",darkturquoise:"00ced1",darkviolet:"9400d3",deeppink:"ff1493",deepskyblue:"00bfff",dimgray:"696969",dodgerblue:"1e90ff",feldspar:"d19275",firebrick:"b22222",floralwhite:"fffaf0",forestgreen:"228b22",fuchsia:"ff00ff",gainsboro:"dcdcdc",ghostwhite:"f8f8ff",gold:"ffd700",goldenrod:"daa520",gray:"808080",green:"008000",greenyellow:"adff2f",honeydew:"f0fff0",hotpink:"ff69b4",indianred:"cd5c5c",indigo:"4b0082",ivory:"fffff0",khaki:"f0e68c",lavender:"e6e6fa",lavenderblush:"fff0f5",lawngreen:"7cfc00",lemonchiffon:"fffacd",lightblue:"add8e6",lightcoral:"f08080",lightcyan:"e0ffff",lightgoldenrodyellow:"fafad2",lightgrey:"d3d3d3",lightgreen:"90ee90",lightpink:"ffb6c1",lightsalmon:"ffa07a",lightseagreen:"20b2aa",lightskyblue:"87cefa",lightslateblue:"8470ff",lightslategray:"778899",lightsteelblue:"b0c4de",lightyellow:"ffffe0",lime:"00ff00",limegreen:"32cd32",linen:"faf0e6",magenta:"ff00ff",maroon:"800000",mediumaquamarine:"66cdaa",mediumblue:"0000cd",mediumorchid:"ba55d3",mediumpurple:"9370d8",mediumseagreen:"3cb371",mediumslateblue:"7b68ee",mediumspringgreen:"00fa9a",mediumturquoise:"48d1cc",mediumvioletred:"c71585",midnightblue:"191970",mintcream:"f5fffa",mistyrose:"ffe4e1",moccasin:"ffe4b5",navajowhite:"ffdead",navy:"000080",oldlace:"fdf5e6",olive:"808000",olivedrab:"6b8e23",orange:"ffa500",orangered:"ff4500",orchid:"da70d6",palegoldenrod:"eee8aa",palegreen:"98fb98",paleturquoise:"afeeee",palevioletred:"d87093",papayawhip:"ffefd5",peachpuff:"ffdab9",peru:"cd853f",pink:"ffc0cb",plum:"dda0dd",powderblue:"b0e0e6",purple:"800080",red:"ff0000",rosybrown:"bc8f8f",royalblue:"4169e1",saddlebrown:"8b4513",salmon:"fa8072",sandybrown:"f4a460",seagreen:"2e8b57",seashell:"fff5ee",sienna:"a0522d",silver:"c0c0c0",skyblue:"87ceeb",slateblue:"6a5acd",slategray:"708090",snow:"fffafa",springgreen:"00ff7f",steelblue:"4682b4",tan:"d2b48c",teal:"008080",thistle:"d8bfd8",tomato:"ff6347",turquoise:"40e0d0",violet:"ee82ee",violetred:"d02090",wheat:"f5deb3",white:"ffffff",whitesmoke:"f5f5f5",yellow:"ffff00",yellowgreen:"9acd32"}[t=(t=t.replace(/ /g,"")).toLowerCase()]||t;for(var n=[{re:/^rgb\((\d{1,3}),\s*(\d{1,3}),\s*(\d{1,3})\)$/,example:["rgb(123, 234, 45)","rgb(255,234,245)"],process:function(t){return[parseInt(t[1]),parseInt(t[2]),parseInt(t[3])]}},{re:/^(\w{2})(\w{2})(\w{2})$/,example:["#00ff00","336699"],process:function(t){return[parseInt(t[1],16),parseInt(t[2],16),parseInt(t[3],16)]}},{re:/^(\w{1})(\w{1})(\w{1})$/,example:["#fb0","f0f"],process:function(t){return[parseInt(t[1]+t[1],16),parseInt(t[2]+t[2],16),parseInt(t[3]+t[3],16)]}}],r=0;r<n.length;r++){var i=n[r].re,a=n[r].process,s=i.exec(t);s&&(e=a(s),this.r=e[0],this.g=e[1],this.b=e[2],this.ok=!0)}this.r=this.r<0||isNaN(this.r)?0:this.r>255?255:this.r,this.g=this.g<0||isNaN(this.g)?0:this.g>255?255:this.g,this.b=this.b<0||isNaN(this.b)?0:this.b>255?255:this.b,this.toRGB=function(){return"rgb("+this.r+", "+this.g+", "+this.b+")"},this.toHex=function(){var t=this.r.toString(16),e=this.g.toString(16),n=this.b.toString(16);return 1==t.length&&(t="0"+t),1==e.length&&(e="0"+e),1==n.length&&(n="0"+n),"#"+t+e+n}}var f=i.atob.bind(i),d=i.btoa.bind(i);
+/**
+   * @license
+   * Joseph Myers does not specify a particular license for his work.
+   *
+   * Author: Joseph Myers
+   * Accessed from: http://www.myersdaily.org/joseph/javascript/md5.js
+   *
+   * Modified by: Owen Leong
+   */
+function p(t,e){var n=t[0],r=t[1],i=t[2],a=t[3];n=m(n,r,i,a,e[0],7,-680876936),a=m(a,n,r,i,e[1],12,-389564586),i=m(i,a,n,r,e[2],17,606105819),r=m(r,i,a,n,e[3],22,-1044525330),n=m(n,r,i,a,e[4],7,-176418897),a=m(a,n,r,i,e[5],12,1200080426),i=m(i,a,n,r,e[6],17,-1473231341),r=m(r,i,a,n,e[7],22,-45705983),n=m(n,r,i,a,e[8],7,1770035416),a=m(a,n,r,i,e[9],12,-1958414417),i=m(i,a,n,r,e[10],17,-42063),r=m(r,i,a,n,e[11],22,-1990404162),n=m(n,r,i,a,e[12],7,1804603682),a=m(a,n,r,i,e[13],12,-40341101),i=m(i,a,n,r,e[14],17,-1502002290),n=b(n,r=m(r,i,a,n,e[15],22,1236535329),i,a,e[1],5,-165796510),a=b(a,n,r,i,e[6],9,-1069501632),i=b(i,a,n,r,e[11],14,643717713),r=b(r,i,a,n,e[0],20,-373897302),n=b(n,r,i,a,e[5],5,-701558691),a=b(a,n,r,i,e[10],9,38016083),i=b(i,a,n,r,e[15],14,-660478335),r=b(r,i,a,n,e[4],20,-405537848),n=b(n,r,i,a,e[9],5,568446438),a=b(a,n,r,i,e[14],9,-1019803690),i=b(i,a,n,r,e[3],14,-187363961),r=b(r,i,a,n,e[8],20,1163531501),n=b(n,r,i,a,e[13],5,-1444681467),a=b(a,n,r,i,e[2],9,-51403784),i=b(i,a,n,r,e[7],14,1735328473),n=v(n,r=b(r,i,a,n,e[12],20,-1926607734),i,a,e[5],4,-378558),a=v(a,n,r,i,e[8],11,-2022574463),i=v(i,a,n,r,e[11],16,1839030562),r=v(r,i,a,n,e[14],23,-35309556),n=v(n,r,i,a,e[1],4,-1530992060),a=v(a,n,r,i,e[4],11,1272893353),i=v(i,a,n,r,e[7],16,-155497632),r=v(r,i,a,n,e[10],23,-1094730640),n=v(n,r,i,a,e[13],4,681279174),a=v(a,n,r,i,e[0],11,-358537222),i=v(i,a,n,r,e[3],16,-722521979),r=v(r,i,a,n,e[6],23,76029189),n=v(n,r,i,a,e[9],4,-640364487),a=v(a,n,r,i,e[12],11,-421815835),i=v(i,a,n,r,e[15],16,530742520),n=w(n,r=v(r,i,a,n,e[2],23,-995338651),i,a,e[0],6,-198630844),a=w(a,n,r,i,e[7],10,1126891415),i=w(i,a,n,r,e[14],15,-1416354905),r=w(r,i,a,n,e[5],21,-57434055),n=w(n,r,i,a,e[12],6,1700485571),a=w(a,n,r,i,e[3],10,-1894986606),i=w(i,a,n,r,e[10],15,-1051523),r=w(r,i,a,n,e[1],21,-2054922799),n=w(n,r,i,a,e[8],6,1873313359),a=w(a,n,r,i,e[15],10,-30611744),i=w(i,a,n,r,e[6],15,-1560198380),r=w(r,i,a,n,e[13],21,1309151649),n=w(n,r,i,a,e[4],6,-145523070),a=w(a,n,r,i,e[11],10,-1120210379),i=w(i,a,n,r,e[2],15,718787259),r=w(r,i,a,n,e[9],21,-343485551),t[0]=k(n,t[0]),t[1]=k(r,t[1]),t[2]=k(i,t[2]),t[3]=k(a,t[3])}function g(t,e,n,r,i,a){return e=k(k(e,t),k(r,a)),k(e<<i|e>>>32-i,n)}function m(t,e,n,r,i,a,s){return g(e&n|~e&r,t,e,i,a,s)}function b(t,e,n,r,i,a,s){return g(e&r|n&~r,t,e,i,a,s)}function v(t,e,n,r,i,a,s){return g(e^n^r,t,e,i,a,s)}function w(t,e,n,r,i,a,s){return g(n^(e|~r),t,e,i,a,s)}function y(t){var e,n=t.length,r=[1732584193,-271733879,-1732584194,271733878];for(e=64;e<=t.length;e+=64)p(r,_(t.substring(e-64,e)));t=t.substring(e-64);var i=[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0];for(e=0;e<t.length;e++)i[e>>2]|=t.charCodeAt(e)<<(e%4<<3);if(i[e>>2]|=128<<(e%4<<3),e>55)for(p(r,i),e=0;e<16;e++)i[e]=0;return i[14]=8*n,p(r,i),r}function _(t){var e,n=[];for(e=0;e<64;e+=4)n[e>>2]=t.charCodeAt(e)+(t.charCodeAt(e+1)<<8)+(t.charCodeAt(e+2)<<16)+(t.charCodeAt(e+3)<<24);return n}var x="0123456789abcdef".split("");function A(t){for(var e="",n=0;n<4;n++)e+=x[t>>8*n+4&15]+x[t>>8*n&15];return e}function L(t){return String.fromCharCode(255&t,(65280&t)>>8,(16711680&t)>>16,(4278190080&t)>>24)}function N(t){return function(t){return t.map(L).join("")}(y(t))}var S="5d41402abc4b2a76b9719d911017c592"!=function(t){for(var e=0;e<t.length;e++)t[e]=A(t[e]);return t.join("")}(y("hello"));function k(t,e){if(S){var n=(65535&t)+(65535&e);return(t>>16)+(e>>16)+(n>>16)<<16|65535&n}return t+e&4294967295}
+/**
+   * @license
+   * FPDF is released under a permissive license: there is no usage restriction.
+   * You may embed it freely in your application (commercial or not), with or
+   * without modifications.
+   *
+   * Reference: http://www.fpdf.org/en/script/script37.php
+   */function P(t,e){var n,r,i,a;if(t!==n){for(var s=(i=t,a=1+(256/t.length|0),new Array(a+1).join(i)),o=[],h=0;h<256;h++)o[h]=h;var l=0;for(h=0;h<256;h++){var c=o[h];l=(l+c+s.charCodeAt(h))%256,o[h]=o[l],o[l]=c}n=t,r=o}else o=r;var u=e.length,f=0,d=0,p="";for(h=0;h<u;h++)d=(d+(c=o[f=(f+1)%256]))%256,o[f]=o[d],o[d]=c,s=o[(o[f]+o[d])%256],p+=String.fromCharCode(e.charCodeAt(h)^s);return p}
+/**
+   * @license
+   * Licensed under the MIT License.
+   * http://opensource.org/licenses/mit-license
+   * Author: Owen Leong (@owenl131)
+   * Date: 15 Oct 2020
+   * References:
+   * https://www.cs.cmu.edu/~dst/Adobe/Gallery/anon21jul01-pdf-encryption.txt
+   * https://github.com/foliojs/pdfkit/blob/master/lib/security.js
+   * http://www.fpdf.org/en/script/script37.php
+   */var F={print:4,modify:8,copy:16,"annot-forms":32};function I(t,e,n,r){this.v=1,this.r=2;var i=192;t.forEach(function(t){if(void 0!==F.perm)throw new Error("Invalid permission: "+t);i+=F[t]}),this.padding="(¿N^NuŠAd\0NVÿú\b..\0¶Ðh>€/\f©þdSiz";var a=(e+this.padding).substr(0,32),s=(n+this.padding).substr(0,32);this.O=this.processOwnerPassword(a,s),this.P=-(1+(255^i)),this.encryptionKey=N(a+this.O+this.lsbFirstWord(this.P)+this.hexToBytes(r)).substr(0,5),this.U=P(this.encryptionKey,this.padding)}function C(t){if(/[^\u0000-\u00ff]/.test(t))throw new Error("Invalid PDF Name Object: "+t+", Only accept ASCII characters.");for(var e="",n=t.length,r=0;r<n;r++){var i=t.charCodeAt(r);e+=i<33||35===i||37===i||40===i||41===i||47===i||60===i||62===i||91===i||93===i||123===i||125===i||i>126?"#"+("0"+i.toString(16)).slice(-2):t[r]}return e}function j(t){if("object"!==r(t))throw new Error("Invalid Context passed to initialize PubSub (jsPDF-module)");var e={};this.subscribe=function(t,n,r){if(r=r||!1,"string"!=typeof t||"function"!=typeof n||"boolean"!=typeof r)throw new Error("Invalid arguments passed to PubSub.subscribe (jsPDF-module)");e.hasOwnProperty(t)||(e[t]={});var i=Math.random().toString(35);return e[t][i]=[n,!!r],i},this.unsubscribe=function(t){for(var n in e)if(e[n][t])return delete e[n][t],0===Object.keys(e[n]).length&&delete e[n],!0;return!1},this.publish=function(n){if(e.hasOwnProperty(n)){var r=Array.prototype.slice.call(arguments,1),a=[];for(var o in e[n]){var h=e[n][o];try{h[0].apply(t,r)}catch(l){i.console&&s.error("jsPDF PubSub Error",l.message,l)}h[1]&&a.push(o)}a.length&&a.forEach(this.unsubscribe)}},this.getTopics=function(){return e}}function E(t){if(!(this instanceof E))return new E(t);var e="opacity,stroke-opacity".split(",");for(var n in t)t.hasOwnProperty(n)&&e.indexOf(n)>=0&&(this[n]=t[n]);this.id="",this.objectNumber=-1}function O(t,e){this.gState=t,this.matrix=e,this.id="",this.objectNumber=-1}function B(t,e,n,r,i){if(!(this instanceof B))return new B(t,e,n,r,i);this.type="axial"===t?2:3,this.coords=e,this.colors=n,O.call(this,r,i)}function M(t,e,n,r,i){if(!(this instanceof M))return new M(t,e,n,r,i);this.boundingBox=t,this.xStep=e,this.yStep=n,this.stream="",this.cloneIndex=0,O.call(this,r,i)}function R(t){var e,n="string"==typeof arguments[0]?arguments[0]:"p",a=arguments[1],o=arguments[2],h=arguments[3],l=[],f=1,p=16,g="S",m=null;"object"===r(t=t||{})&&(n=t.orientation,a=t.unit||a,o=t.format||o,h=t.compress||t.compressPdf||h,null!==(m=t.encryption||null)&&(m.userPassword=m.userPassword||"",m.ownerPassword=m.ownerPassword||"",m.userPermissions=m.userPermissions||[]),f="number"==typeof t.userUnit?Math.abs(t.userUnit):1,void 0!==t.precision&&(e=t.precision),void 0!==t.floatPrecision&&(p=t.floatPrecision),g=t.defaultPathOperation||"S"),l=t.filters||(!0===h?["FlateEncode"]:l),a=a||"mm",n=(""+(n||"P")).toLowerCase();var b=t.putOnlyUsedFonts||!1,v={},w={internal:{},__private__:{}};w.__private__.PubSub=j;var y="1.3",_=w.__private__.getPdfVersion=function(){return y};w.__private__.setPdfVersion=function(t){y=t};var x={a0:[2383.94,3370.39],a1:[1683.78,2383.94],a2:[1190.55,1683.78],a3:[841.89,1190.55],a4:[595.28,841.89],a5:[419.53,595.28],a6:[297.64,419.53],a7:[209.76,297.64],a8:[147.4,209.76],a9:[104.88,147.4],a10:[73.7,104.88],b0:[2834.65,4008.19],b1:[2004.09,2834.65],b2:[1417.32,2004.09],b3:[1000.63,1417.32],b4:[708.66,1000.63],b5:[498.9,708.66],b6:[354.33,498.9],b7:[249.45,354.33],b8:[175.75,249.45],b9:[124.72,175.75],b10:[87.87,124.72],c0:[2599.37,3676.54],c1:[1836.85,2599.37],c2:[1298.27,1836.85],c3:[918.43,1298.27],c4:[649.13,918.43],c5:[459.21,649.13],c6:[323.15,459.21],c7:[229.61,323.15],c8:[161.57,229.61],c9:[113.39,161.57],c10:[79.37,113.39],dl:[311.81,623.62],letter:[612,792],"government-letter":[576,756],legal:[612,1008],"junior-legal":[576,360],ledger:[1224,792],tabloid:[792,1224],"credit-card":[153,243]};w.__private__.getPageFormats=function(){return x};var A=w.__private__.getPageFormat=function(t){return x[t]};o=o||"a4";var L="compat",N="advanced",S=L;function k(){this.saveGraphicsState(),ct(new Wt(Nt,0,0,-Nt,0,Pn()*Nt).toString()+" cm"),this.setFontSize(this.getFontSize()/Nt),g="n",S=N}function P(){this.restoreGraphicsState(),g="S",S=L}var F=w.__private__.combineFontStyleAndFontWeight=function(t,e){if("bold"==t&&"normal"==e||"bold"==t&&400==e||"normal"==t&&"italic"==e||"bold"==t&&"italic"==e)throw new Error("Invalid Combination of fontweight and fontstyle");return e&&(t=400==e||"normal"===e?"italic"===t?"italic":"normal":700!=e&&"bold"!==e||"normal"!==t?(700==e?"bold":e)+""+t:"bold"),t};w.advancedAPI=function(t){var e=S===L;return e&&k.call(this),"function"!=typeof t||(t(this),e&&P.call(this)),this},w.compatAPI=function(t){var e=S===N;return e&&P.call(this),"function"!=typeof t||(t(this),e&&k.call(this)),this},w.isAdvancedAPI=function(){return S===N};var O,T=function(t){if(S!==N)throw new Error(t+" is only available in 'advanced' API mode. You need to call advancedAPI() first.")},D=w.roundToPrecision=w.__private__.roundToPrecision=function(t,n){var r=e||n;if(isNaN(t)||isNaN(r))throw new Error("Invalid argument passed to jsPDF.roundToPrecision");return t.toFixed(r).replace(/0+$/,"")};O=w.hpf=w.__private__.hpf="number"==typeof p?function(t){if(isNaN(t))throw new Error("Invalid argument passed to jsPDF.hpf");return D(t,p)}:"smart"===p?function(t){if(isNaN(t))throw new Error("Invalid argument passed to jsPDF.hpf");return D(t,t>-1&&t<1?16:5)}:function(t){if(isNaN(t))throw new Error("Invalid argument passed to jsPDF.hpf");return D(t,16)};var q=w.f2=w.__private__.f2=function(t){if(isNaN(t))throw new Error("Invalid argument passed to jsPDF.f2");return D(t,2)},z=w.__private__.f3=function(t){if(isNaN(t))throw new Error("Invalid argument passed to jsPDF.f3");return D(t,3)},U=w.scale=w.__private__.scale=function(t){if(isNaN(t))throw new Error("Invalid argument passed to jsPDF.scale");return S===L?t*Nt:S===N?t:void 0},H=function(t){return U(function(t){return S===L?Pn()-t:S===N?t:void 0}(t))};w.__private__.setPrecision=w.setPrecision=function(t){"number"==typeof parseInt(t,10)&&(e=parseInt(t,10))};var W,V="00000000000000000000000000000000",G=w.__private__.getFileId=function(){return V},Y=w.__private__.setFileId=function(t){return V=void 0!==t&&/^[a-fA-F0-9]{32}$/.test(t)?t.toUpperCase():V.split("").map(function(){return"ABCDEF0123456789".charAt(Math.floor(16*Math.random()))}).join(""),null!==m&&(Ee=new I(m.userPermissions,m.userPassword,m.ownerPassword,V)),V};w.setFileId=function(t){return Y(t),this},w.getFileId=function(){return G()};var Z=w.__private__.convertDateToPDFDate=function(t){var e=t.getTimezoneOffset(),n=e<0?"+":"-",r=Math.floor(Math.abs(e/60)),i=Math.abs(e%60),a=[n,Q(r),"'",Q(i),"'"].join("");return["D:",t.getFullYear(),Q(t.getMonth()+1),Q(t.getDate()),Q(t.getHours()),Q(t.getMinutes()),Q(t.getSeconds()),a].join("")},J=w.__private__.convertPDFDateToDate=function(t){var e=parseInt(t.substr(2,4),10),n=parseInt(t.substr(6,2),10)-1,r=parseInt(t.substr(8,2),10),i=parseInt(t.substr(10,2),10),a=parseInt(t.substr(12,2),10),s=parseInt(t.substr(14,2),10);return new Date(e,n,r,i,a,s,0)},X=w.__private__.setCreationDate=function(t){var e;if(void 0===t&&(t=new Date),t instanceof Date)e=Z(t);else{if(!/^D:(20[0-2][0-9]|203[0-7]|19[7-9][0-9])(0[0-9]|1[0-2])([0-2][0-9]|3[0-1])(0[0-9]|1[0-9]|2[0-3])(0[0-9]|[1-5][0-9])(0[0-9]|[1-5][0-9])(\+0[0-9]|\+1[0-4]|-0[0-9]|-1[0-1])'(0[0-9]|[1-5][0-9])'?$/.test(t))throw new Error("Invalid argument passed to jsPDF.setCreationDate");e=t}return W=e},K=w.__private__.getCreationDate=function(t){var e=W;return"jsDate"===t&&(e=J(W)),e};w.setCreationDate=function(t){return X(t),this},w.getCreationDate=function(t){return K(t)};var $,Q=w.__private__.padd2=function(t){return("0"+parseInt(t)).slice(-2)},tt=w.__private__.padd2Hex=function(t){return("00"+(t=t.toString())).substr(t.length)},et=0,nt=[],rt=[],it=0,at=[],st=[],ot=!1,ht=rt;w.__private__.setCustomOutputDestination=function(t){ot=!0,ht=t};var lt=function(t){ot||(ht=t)};w.__private__.resetCustomOutputDestination=function(){ot=!1,ht=rt};var ct=w.__private__.out=function(t){return t=t.toString(),it+=t.length+1,ht.push(t),ht},ut=w.__private__.write=function(t){return ct(1===arguments.length?t.toString():Array.prototype.join.call(arguments," "))},ft=w.__private__.getArrayBuffer=function(t){for(var e=t.length,n=new ArrayBuffer(e),r=new Uint8Array(n);e--;)r[e]=t.charCodeAt(e);return n},dt=[["Helvetica","helvetica","normal","WinAnsiEncoding"],["Helvetica-Bold","helvetica","bold","WinAnsiEncoding"],["Helvetica-Oblique","helvetica","italic","WinAnsiEncoding"],["Helvetica-BoldOblique","helvetica","bolditalic","WinAnsiEncoding"],["Courier","courier","normal","WinAnsiEncoding"],["Courier-Bold","courier","bold","WinAnsiEncoding"],["Courier-Oblique","courier","italic","WinAnsiEncoding"],["Courier-BoldOblique","courier","bolditalic","WinAnsiEncoding"],["Times-Roman","times","normal","WinAnsiEncoding"],["Times-Bold","times","bold","WinAnsiEncoding"],["Times-Italic","times","italic","WinAnsiEncoding"],["Times-BoldItalic","times","bolditalic","WinAnsiEncoding"],["ZapfDingbats","zapfdingbats","normal",null],["Symbol","symbol","normal",null]];w.__private__.getStandardFonts=function(){return dt};var pt=t.fontSize||16;w.__private__.setFontSize=w.setFontSize=function(t){return pt=S===N?t/Nt:t,this};var gt,mt=w.__private__.getFontSize=w.getFontSize=function(){return S===L?pt:pt*Nt},bt=t.R2L||!1;w.__private__.setR2L=w.setR2L=function(t){return bt=t,this},w.__private__.getR2L=w.getR2L=function(){return bt};var vt,wt=w.__private__.setZoomMode=function(t){if(/^(?:\d+\.\d*|\d*\.\d+|\d+)%$/.test(t))gt=t;else if(isNaN(t)){if(-1===[void 0,null,"fullwidth","fullheight","fullpage","original"].indexOf(t))throw new Error('zoom must be Integer (e.g. 2), a percentage Value (e.g. 300%) or fullwidth, fullheight, fullpage, original. "'+t+'" is not recognized.');gt=t}else gt=parseInt(t,10)};w.__private__.getZoomMode=function(){return gt};var yt,_t=w.__private__.setPageMode=function(t){if(-1==[void 0,null,"UseNone","UseOutlines","UseThumbs","FullScreen"].indexOf(t))throw new Error('Page mode must be one of UseNone, UseOutlines, UseThumbs, or FullScreen. "'+t+'" is not recognized.');vt=t};w.__private__.getPageMode=function(){return vt};var xt=w.__private__.setLayoutMode=function(t){if(-1==[void 0,null,"continuous","single","twoleft","tworight","two"].indexOf(t))throw new Error('Layout mode must be one of continuous, single, twoleft, tworight. "'+t+'" is not recognized.');yt=t};w.__private__.getLayoutMode=function(){return yt},w.__private__.setDisplayMode=w.setDisplayMode=function(t,e,n){return wt(t),xt(e),_t(n),this};var At={title:"",subject:"",author:"",keywords:"",creator:""};w.__private__.getDocumentProperty=function(t){if(-1===Object.keys(At).indexOf(t))throw new Error("Invalid argument passed to jsPDF.getDocumentProperty");return At[t]},w.__private__.getDocumentProperties=function(){return At},w.__private__.setDocumentProperties=w.setProperties=w.setDocumentProperties=function(t){for(var e in At)At.hasOwnProperty(e)&&t[e]&&(At[e]=t[e]);return this},w.__private__.setDocumentProperty=function(t,e){if(-1===Object.keys(At).indexOf(t))throw new Error("Invalid arguments passed to jsPDF.setDocumentProperty");return At[t]=e};var Lt,Nt,St,kt,Pt,Ft={},It={},Ct=[],jt={},Et={},Ot={},Bt={},Mt=null,Rt=0,Tt=[],Dt=new j(w),qt=t.hotfixes||[],zt={},Ut={},Ht=[],Wt=function t(e,n,r,i,a,s){if(!(this instanceof t))return new t(e,n,r,i,a,s);isNaN(e)&&(e=1),isNaN(n)&&(n=0),isNaN(r)&&(r=0),isNaN(i)&&(i=1),isNaN(a)&&(a=0),isNaN(s)&&(s=0),this._matrix=[e,n,r,i,a,s]};Object.defineProperty(Wt.prototype,"sx",{get:function(){return this._matrix[0]},set:function(t){this._matrix[0]=t}}),Object.defineProperty(Wt.prototype,"shy",{get:function(){return this._matrix[1]},set:function(t){this._matrix[1]=t}}),Object.defineProperty(Wt.prototype,"shx",{get:function(){return this._matrix[2]},set:function(t){this._matrix[2]=t}}),Object.defineProperty(Wt.prototype,"sy",{get:function(){return this._matrix[3]},set:function(t){this._matrix[3]=t}}),Object.defineProperty(Wt.prototype,"tx",{get:function(){return this._matrix[4]},set:function(t){this._matrix[4]=t}}),Object.defineProperty(Wt.prototype,"ty",{get:function(){return this._matrix[5]},set:function(t){this._matrix[5]=t}}),Object.defineProperty(Wt.prototype,"a",{get:function(){return this._matrix[0]},set:function(t){this._matrix[0]=t}}),Object.defineProperty(Wt.prototype,"b",{get:function(){return this._matrix[1]},set:function(t){this._matrix[1]=t}}),Object.defineProperty(Wt.prototype,"c",{get:function(){return this._matrix[2]},set:function(t){this._matrix[2]=t}}),Object.defineProperty(Wt.prototype,"d",{get:function(){return this._matrix[3]},set:function(t){this._matrix[3]=t}}),Object.defineProperty(Wt.prototype,"e",{get:function(){return this._matrix[4]},set:function(t){this._matrix[4]=t}}),Object.defineProperty(Wt.prototype,"f",{get:function(){return this._matrix[5]},set:function(t){this._matrix[5]=t}}),Object.defineProperty(Wt.prototype,"rotation",{get:function(){return Math.atan2(this.shx,this.sx)}}),Object.defineProperty(Wt.prototype,"scaleX",{get:function(){return this.decompose().scale.sx}}),Object.defineProperty(Wt.prototype,"scaleY",{get:function(){return this.decompose().scale.sy}}),Object.defineProperty(Wt.prototype,"isIdentity",{get:function(){return 1===this.sx&&0===this.shy&&0===this.shx&&1===this.sy&&0===this.tx&&0===this.ty}}),Wt.prototype.join=function(t){return[this.sx,this.shy,this.shx,this.sy,this.tx,this.ty].map(O).join(t)},Wt.prototype.multiply=function(t){var e=t.sx*this.sx+t.shy*this.shx,n=t.sx*this.shy+t.shy*this.sy,r=t.shx*this.sx+t.sy*this.shx,i=t.shx*this.shy+t.sy*this.sy,a=t.tx*this.sx+t.ty*this.shx+this.tx,s=t.tx*this.shy+t.ty*this.sy+this.ty;return new Wt(e,n,r,i,a,s)},Wt.prototype.decompose=function(){var t=this.sx,e=this.shy,n=this.shx,r=this.sy,i=this.tx,a=this.ty,s=Math.sqrt(t*t+e*e),o=(t/=s)*n+(e/=s)*r;n-=t*o,r-=e*o;var h=Math.sqrt(n*n+r*r);return o/=h,t*(r/=h)<e*(n/=h)&&(t=-t,e=-e,o=-o,s=-s),{scale:new Wt(s,0,0,h,0,0),translate:new Wt(1,0,0,1,i,a),rotate:new Wt(t,e,-e,t,0,0),skew:new Wt(1,0,o,1,0,0)}},Wt.prototype.toString=function(t){return this.join(" ")},Wt.prototype.inversed=function(){var t=this.sx,e=this.shy,n=this.shx,r=this.sy,i=this.tx,a=this.ty,s=1/(t*r-e*n),o=r*s,h=-e*s,l=-n*s,c=t*s;return new Wt(o,h,l,c,-o*i-l*a,-h*i-c*a)},Wt.prototype.applyToPoint=function(t){var e=t.x*this.sx+t.y*this.shx+this.tx,n=t.x*this.shy+t.y*this.sy+this.ty;return new bn(e,n)},Wt.prototype.applyToRectangle=function(t){var e=this.applyToPoint(t),n=this.applyToPoint(new bn(t.x+t.w,t.y+t.h));return new vn(e.x,e.y,n.x-e.x,n.y-e.y)},Wt.prototype.clone=function(){var t=this.sx,e=this.shy,n=this.shx,r=this.sy,i=this.tx,a=this.ty;return new Wt(t,e,n,r,i,a)},w.Matrix=Wt;var Vt=w.matrixMult=function(t,e){return e.multiply(t)},Gt=new Wt(1,0,0,1,0,0);w.unitMatrix=w.identityMatrix=Gt;var Yt=function(t,e){if(!Et[t]){var n=(e instanceof B?"Sh":"P")+(Object.keys(jt).length+1).toString(10);e.id=n,Et[t]=n,jt[n]=e,Dt.publish("addPattern",e)}};w.ShadingPattern=B,w.TilingPattern=M,w.addShadingPattern=function(t,e){return T("addShadingPattern()"),Yt(t,e),this},w.beginTilingPattern=function(t){T("beginTilingPattern()"),yn(t.boundingBox[0],t.boundingBox[1],t.boundingBox[2]-t.boundingBox[0],t.boundingBox[3]-t.boundingBox[1],t.matrix)},w.endTilingPattern=function(t,e){T("endTilingPattern()"),e.stream=st[$].join("\n"),Yt(t,e),Dt.publish("endTilingPattern",e),Ht.pop().restore()};var Zt,Jt=w.__private__.newObject=function(){var t=Xt();return Kt(t,!0),t},Xt=w.__private__.newObjectDeferred=function(){return et++,nt[et]=function(){return it},et},Kt=function(t,e){return e="boolean"==typeof e&&e,nt[t]=it,e&&ct(t+" 0 obj"),t},$t=w.__private__.newAdditionalObject=function(){var t={objId:Xt(),content:""};return at.push(t),t},Qt=Xt(),te=Xt(),ee=w.__private__.decodeColorString=function(t){var e=t.split(" ");if(2!==e.length||"g"!==e[1]&&"G"!==e[1])5!==e.length||"k"!==e[4]&&"K"!==e[4]||(e=[(1-e[0])*(1-e[3]),(1-e[1])*(1-e[3]),(1-e[2])*(1-e[3]),"r"]);else{var n=parseFloat(e[0]);e=[n,n,n,"r"]}for(var r="#",i=0;i<3;i++)r+=("0"+Math.floor(255*parseFloat(e[i])).toString(16)).slice(-2);return r},ne=w.__private__.encodeColorString=function(t){var e;"string"==typeof t&&(t={ch1:t});var n=t.ch1,i=t.ch2,a=t.ch3,s=t.ch4,o="draw"===t.pdfColorType?["G","RG","K"]:["g","rg","k"];if("string"==typeof n&&"#"!==n.charAt(0)){var h=new u(n);if(h.ok)n=h.toHex();else if(!/^\d*\.?\d*$/.test(n))throw new Error('Invalid color "'+n+'" passed to jsPDF.encodeColorString.')}if("string"==typeof n&&/^#[0-9A-Fa-f]{3}$/.test(n)&&(n="#"+n[1]+n[1]+n[2]+n[2]+n[3]+n[3]),"string"==typeof n&&/^#[0-9A-Fa-f]{6}$/.test(n)){var l=parseInt(n.substr(1),16);n=l>>16&255,i=l>>8&255,a=255&l}if(void 0===i||void 0===s&&n===i&&i===a)e="string"==typeof n?n+" "+o[0]:2===t.precision?q(n/255)+" "+o[0]:z(n/255)+" "+o[0];else if(void 0===s||"object"===r(s)){if(s&&!isNaN(s.a)&&0===s.a)return["1.","1.","1.",o[1]].join(" ");e="string"==typeof n?[n,i,a,o[1]].join(" "):2===t.precision?[q(n/255),q(i/255),q(a/255),o[1]].join(" "):[z(n/255),z(i/255),z(a/255),o[1]].join(" ")}else e="string"==typeof n?[n,i,a,s,o[2]].join(" "):2===t.precision?[q(n),q(i),q(a),q(s),o[2]].join(" "):[z(n),z(i),z(a),z(s),o[2]].join(" ");return e},re=w.__private__.getFilters=function(){return l},ie=w.__private__.putStream=function(t){var e=(t=t||{}).data||"",n=t.filters||re(),r=t.alreadyAppliedFilters||[],i=t.addLength1||!1,a=e.length,s=t.objectId,o=function(t){return t};if(null!==m&&void 0===s)throw new Error("ObjectId must be passed to putStream for file encryption");null!==m&&(o=Ee.encryptor(s,0));var h={};!0===n&&(n=["FlateEncode"]);var l=t.additionalKeyValues||[],c=(h=void 0!==R.API.processDataByFilters?R.API.processDataByFilters(e,n):{data:e,reverseChain:[]}).reverseChain+(Array.isArray(r)?r.join(" "):r.toString());if(0!==h.data.length&&(l.push({key:"Length",value:h.data.length}),!0===i&&l.push({key:"Length1",value:a})),0!=c.length)if(c.split("/").length-1==1)l.push({key:"Filter",value:c});else{l.push({key:"Filter",value:"["+c+"]"});for(var u=0;u<l.length;u+=1)if("DecodeParms"===l[u].key){for(var f=[],d=0;d<h.reverseChain.split("/").length-1;d+=1)f.push("null");f.push(l[u].value),l[u].value="["+f.join(" ")+"]"}}ct("<<");for(var p=0;p<l.length;p++)ct("/"+l[p].key+" "+l[p].value);ct(">>"),0!==h.data.length&&(ct("stream"),ct(o(h.data)),ct("endstream"))},ae=w.__private__.putPage=function(t){var e=t.number,n=t.data,r=t.objId,i=t.contentsObjId;Kt(r,!0),ct("<</Type /Page"),ct("/Parent "+t.rootDictionaryObjId+" 0 R"),ct("/Resources "+t.resourceDictionaryObjId+" 0 R"),ct("/MediaBox ["+parseFloat(O(t.mediaBox.bottomLeftX))+" "+parseFloat(O(t.mediaBox.bottomLeftY))+" "+O(t.mediaBox.topRightX)+" "+O(t.mediaBox.topRightY)+"]"),null!==t.cropBox&&ct("/CropBox ["+O(t.cropBox.bottomLeftX)+" "+O(t.cropBox.bottomLeftY)+" "+O(t.cropBox.topRightX)+" "+O(t.cropBox.topRightY)+"]"),null!==t.bleedBox&&ct("/BleedBox ["+O(t.bleedBox.bottomLeftX)+" "+O(t.bleedBox.bottomLeftY)+" "+O(t.bleedBox.topRightX)+" "+O(t.bleedBox.topRightY)+"]"),null!==t.trimBox&&ct("/TrimBox ["+O(t.trimBox.bottomLeftX)+" "+O(t.trimBox.bottomLeftY)+" "+O(t.trimBox.topRightX)+" "+O(t.trimBox.topRightY)+"]"),null!==t.artBox&&ct("/ArtBox ["+O(t.artBox.bottomLeftX)+" "+O(t.artBox.bottomLeftY)+" "+O(t.artBox.topRightX)+" "+O(t.artBox.topRightY)+"]"),"number"==typeof t.userUnit&&1!==t.userUnit&&ct("/UserUnit "+t.userUnit),Dt.publish("putPage",{objId:r,pageContext:Tt[e],pageNumber:e,page:n}),ct("/Contents "+i+" 0 R"),ct(">>"),ct("endobj");var a=n.join("\n");return S===N&&(a+="\nQ"),Kt(i,!0),ie({data:a,filters:re(),objectId:i}),ct("endobj"),r},se=w.__private__.putPages=function(){var t,e,n=[];for(t=1;t<=Rt;t++)Tt[t].objId=Xt(),Tt[t].contentsObjId=Xt();for(t=1;t<=Rt;t++)n.push(ae({number:t,data:st[t],objId:Tt[t].objId,contentsObjId:Tt[t].contentsObjId,mediaBox:Tt[t].mediaBox,cropBox:Tt[t].cropBox,bleedBox:Tt[t].bleedBox,trimBox:Tt[t].trimBox,artBox:Tt[t].artBox,userUnit:Tt[t].userUnit,rootDictionaryObjId:Qt,resourceDictionaryObjId:te}));Kt(Qt,!0),ct("<</Type /Pages");var r="/Kids [";for(e=0;e<Rt;e++)r+=n[e]+" 0 R ";ct(r+"]"),ct("/Count "+Rt),ct(">>"),ct("endobj"),Dt.publish("postPutPages")},oe=function(t){Dt.publish("putFont",{font:t,out:ct,newObject:Jt,putStream:ie}),!0!==t.isAlreadyPutted&&(t.objectNumber=Jt(),ct("<<"),ct("/Type /Font"),ct("/BaseFont /"+C(t.postScriptName)),ct("/Subtype /Type1"),"string"==typeof t.encoding&&ct("/Encoding /"+t.encoding),ct("/FirstChar 32"),ct("/LastChar 255"),ct(">>"),ct("endobj"))},he=function(t){t.objectNumber=Jt();var e=[];e.push({key:"Type",value:"/XObject"}),e.push({key:"Subtype",value:"/Form"}),e.push({key:"BBox",value:"["+[O(t.x),O(t.y),O(t.x+t.width),O(t.y+t.height)].join(" ")+"]"}),e.push({key:"Matrix",value:"["+t.matrix.toString()+"]"});var n=t.pages[1].join("\n");ie({data:n,additionalKeyValues:e,objectId:t.objectNumber}),ct("endobj")},le=function(t,e){e||(e=21);var n=Jt(),r=function(t,e){var n,r=[],i=1/(e-1);for(n=0;n<1;n+=i)r.push(n);if(r.push(1),0!=t[0].offset){var a={offset:0,color:t[0].color};t.unshift(a)}if(1!=t[t.length-1].offset){var s={offset:1,color:t[t.length-1].color};t.push(s)}for(var o="",h=0,l=0;l<r.length;l++){for(n=r[l];n>t[h+1].offset;)h++;var c=t[h].offset,u=(n-c)/(t[h+1].offset-c),f=t[h].color,d=t[h+1].color;o+=tt(Math.round((1-u)*f[0]+u*d[0]).toString(16))+tt(Math.round((1-u)*f[1]+u*d[1]).toString(16))+tt(Math.round((1-u)*f[2]+u*d[2]).toString(16))}return o.trim()}(t.colors,e),i=[];i.push({key:"FunctionType",value:"0"}),i.push({key:"Domain",value:"[0.0 1.0]"}),i.push({key:"Size",value:"["+e+"]"}),i.push({key:"BitsPerSample",value:"8"}),i.push({key:"Range",value:"[0.0 1.0 0.0 1.0 0.0 1.0]"}),i.push({key:"Decode",value:"[0.0 1.0 0.0 1.0 0.0 1.0]"}),ie({data:r,additionalKeyValues:i,alreadyAppliedFilters:["/ASCIIHexDecode"],objectId:n}),ct("endobj"),t.objectNumber=Jt(),ct("<< /ShadingType "+t.type),ct("/ColorSpace /DeviceRGB");var a="/Coords ["+O(parseFloat(t.coords[0]))+" "+O(parseFloat(t.coords[1]))+" ";2===t.type?a+=O(parseFloat(t.coords[2]))+" "+O(parseFloat(t.coords[3])):a+=O(parseFloat(t.coords[2]))+" "+O(parseFloat(t.coords[3]))+" "+O(parseFloat(t.coords[4]))+" "+O(parseFloat(t.coords[5])),ct(a+="]"),t.matrix&&ct("/Matrix ["+t.matrix.toString()+"]"),ct("/Function "+n+" 0 R"),ct("/Extend [true true]"),ct(">>"),ct("endobj")},ce=function(t,e){var n=Xt(),r=Jt();e.push({resourcesOid:n,objectOid:r}),t.objectNumber=r;var i=[];i.push({key:"Type",value:"/Pattern"}),i.push({key:"PatternType",value:"1"}),i.push({key:"PaintType",value:"1"}),i.push({key:"TilingType",value:"1"}),i.push({key:"BBox",value:"["+t.boundingBox.map(O).join(" ")+"]"}),i.push({key:"XStep",value:O(t.xStep)}),i.push({key:"YStep",value:O(t.yStep)}),i.push({key:"Resources",value:n+" 0 R"}),t.matrix&&i.push({key:"Matrix",value:"["+t.matrix.toString()+"]"}),ie({data:t.stream,additionalKeyValues:i,objectId:t.objectNumber}),ct("endobj")},ue=function(t){for(var e in t.objectNumber=Jt(),ct("<<"),t)switch(e){case"opacity":ct("/ca "+q(t[e]));break;case"stroke-opacity":ct("/CA "+q(t[e]))}ct(">>"),ct("endobj")},fe=function(t){Kt(t.resourcesOid,!0),ct("<<"),ct("/ProcSet [/PDF /Text /ImageB /ImageC /ImageI]"),function(){for(var t in ct("/Font <<"),Ft)Ft.hasOwnProperty(t)&&(!1===b||!0===b&&v.hasOwnProperty(t))&&ct("/"+t+" "+Ft[t].objectNumber+" 0 R");ct(">>")}(),function(){if(Object.keys(jt).length>0){for(var t in ct("/Shading <<"),jt)jt.hasOwnProperty(t)&&jt[t]instanceof B&&jt[t].objectNumber>=0&&ct("/"+t+" "+jt[t].objectNumber+" 0 R");Dt.publish("putShadingPatternDict"),ct(">>")}}(),function(t){if(Object.keys(jt).length>0){for(var e in ct("/Pattern <<"),jt)jt.hasOwnProperty(e)&&jt[e]instanceof w.TilingPattern&&jt[e].objectNumber>=0&&jt[e].objectNumber<t&&ct("/"+e+" "+jt[e].objectNumber+" 0 R");Dt.publish("putTilingPatternDict"),ct(">>")}}(t.objectOid),function(){if(Object.keys(Ot).length>0){var t;for(t in ct("/ExtGState <<"),Ot)Ot.hasOwnProperty(t)&&Ot[t].objectNumber>=0&&ct("/"+t+" "+Ot[t].objectNumber+" 0 R");Dt.publish("putGStateDict"),ct(">>")}}(),function(){for(var t in ct("/XObject <<"),zt)zt.hasOwnProperty(t)&&zt[t].objectNumber>=0&&ct("/"+t+" "+zt[t].objectNumber+" 0 R");Dt.publish("putXobjectDict"),ct(">>")}(),ct(">>"),ct("endobj")},de=function(t){It[t.fontName]=It[t.fontName]||{},It[t.fontName][t.fontStyle]=t.id},pe=function(t,e,n,r,i){var a={id:"F"+(Object.keys(Ft).length+1).toString(10),postScriptName:t,fontName:e,fontStyle:n,encoding:r,isStandardFont:i||!1,metadata:{}};return Dt.publish("addFont",{font:a,instance:this}),Ft[a.id]=a,de(a),a.id},ge=w.__private__.pdfEscape=w.pdfEscape=function(t,e){return function(t,e){var n,r,i,a,s,o,h,l,c;if(i=(e=e||{}).sourceEncoding||"Unicode",s=e.outputEncoding,(e.autoencode||s)&&Ft[Lt].metadata&&Ft[Lt].metadata[i]&&Ft[Lt].metadata[i].encoding&&(a=Ft[Lt].metadata[i].encoding,!s&&Ft[Lt].encoding&&(s=Ft[Lt].encoding),!s&&a.codePages&&(s=a.codePages[0]),"string"==typeof s&&(s=a[s]),s)){for(h=!1,o=[],n=0,r=t.length;n<r;n++)(l=s[t.charCodeAt(n)])?o.push(String.fromCharCode(l)):o.push(t[n]),o[n].charCodeAt(0)>>8&&(h=!0);t=o.join("")}for(n=t.length;void 0===h&&0!==n;)t.charCodeAt(n-1)>>8&&(h=!0),n--;if(!h)return t;for(o=e.noBOM?[]:[254,255],n=0,r=t.length;n<r;n++){if((c=(l=t.charCodeAt(n))>>8)>>8)throw new Error("Character at position "+n+" of string '"+t+"' exceeds 16bits. Cannot be encoded into UCS-2 BE");o.push(c),o.push(l-(c<<8))}return String.fromCharCode.apply(void 0,o)}(t,e).replace(/\\/g,"\\\\").replace(/\(/g,"\\(").replace(/\)/g,"\\)")},me=w.__private__.beginPage=function(t){st[++Rt]=[],Tt[Rt]={objId:0,contentsObjId:0,userUnit:Number(f),artBox:null,bleedBox:null,cropBox:null,trimBox:null,mediaBox:{bottomLeftX:0,bottomLeftY:0,topRightX:Number(t[0]),topRightY:Number(t[1])}},we(Rt),lt(st[$])},be=function(t,e){var r,i,a;switch(n=e||n,"string"==typeof t&&(r=A(t.toLowerCase()),Array.isArray(r)&&(i=r[0],a=r[1])),Array.isArray(t)&&(i=t[0]*Nt,a=t[1]*Nt),isNaN(i)&&(i=o[0],a=o[1]),(i>14400||a>14400)&&(s.warn("A page in a PDF can not be wider or taller than 14400 userUnit. jsPDF limits the width/height to 14400"),i=Math.min(14400,i),a=Math.min(14400,a)),o=[i,a],n.substr(0,1)){case"l":a>i&&(o=[a,i]);break;case"p":i>a&&(o=[a,i])}me(o),Qe(Ke),ct(hn),0!==pn&&ct(pn+" J"),0!==gn&&ct(gn+" j"),Dt.publish("addPage",{pageNumber:Rt})},ve=function(t){t>0&&t<=Rt&&(st.splice(t,1),Tt.splice(t,1),Rt--,$>Rt&&($=Rt),this.setPage($))},we=function(t){t>0&&t<=Rt&&($=t)},ye=w.__private__.getNumberOfPages=w.getNumberOfPages=function(){return st.length-1},_e=function(t,e,n){var r,i=void 0;return n=n||{},t=void 0!==t?t:Ft[Lt].fontName,e=void 0!==e?e:Ft[Lt].fontStyle,r=t.toLowerCase(),void 0!==It[r]&&void 0!==It[r][e]?i=It[r][e]:void 0!==It[t]&&void 0!==It[t][e]?i=It[t][e]:!1===n.disableWarning&&s.warn("Unable to look up font label for font '"+t+"', '"+e+"'. Refer to getFontList() for available fonts."),i||n.noFallback||null==(i=It.times[e])&&(i=It.times.normal),i},xe=w.__private__.putInfo=function(){var t=Jt(),e=function(t){return t};for(var n in null!==m&&(e=Ee.encryptor(t,0)),ct("<<"),ct("/Producer ("+ge(e("jsPDF "+R.version))+")"),At)At.hasOwnProperty(n)&&At[n]&&ct("/"+n.substr(0,1).toUpperCase()+n.substr(1)+" ("+ge(e(At[n]))+")");ct("/CreationDate ("+ge(e(W))+")"),ct(">>"),ct("endobj")},Ae=w.__private__.putCatalog=function(t){var e=(t=t||{}).rootDictionaryObjId||Qt;switch(Jt(),ct("<<"),ct("/Type /Catalog"),ct("/Pages "+e+" 0 R"),gt||(gt="fullwidth"),gt){case"fullwidth":ct("/OpenAction [3 0 R /FitH null]");break;case"fullheight":ct("/OpenAction [3 0 R /FitV null]");break;case"fullpage":ct("/OpenAction [3 0 R /Fit]");break;case"original":ct("/OpenAction [3 0 R /XYZ null null 1]");break;default:var n=""+gt;"%"===n.substr(n.length-1)&&(gt=parseInt(gt)/100),"number"==typeof gt&&ct("/OpenAction [3 0 R /XYZ null null "+q(gt)+"]")}switch(yt||(yt="continuous"),yt){case"continuous":ct("/PageLayout /OneColumn");break;case"single":ct("/PageLayout /SinglePage");break;case"two":case"twoleft":ct("/PageLayout /TwoColumnLeft");break;case"tworight":ct("/PageLayout /TwoColumnRight")}vt&&ct("/PageMode /"+vt),Dt.publish("putCatalog"),ct(">>"),ct("endobj")},Le=w.__private__.putTrailer=function(){ct("trailer"),ct("<<"),ct("/Size "+(et+1)),ct("/Root "+et+" 0 R"),ct("/Info "+(et-1)+" 0 R"),null!==m&&ct("/Encrypt "+Ee.oid+" 0 R"),ct("/ID [ <"+V+"> <"+V+"> ]"),ct(">>")},Ne=w.__private__.putHeader=function(){ct("%PDF-"+y),ct("%ºß¬à")},Se=w.__private__.putXRef=function(){var t="0000000000";ct("xref"),ct("0 "+(et+1)),ct("0000000000 65535 f ");for(var e=1;e<=et;e++)"function"==typeof nt[e]?ct((t+nt[e]()).slice(-10)+" 00000 n "):void 0!==nt[e]?ct((t+nt[e]).slice(-10)+" 00000 n "):ct("0000000000 00000 n ")},ke=w.__private__.buildDocument=function(){var t;et=0,it=0,rt=[],nt=[],at=[],Qt=Xt(),te=Xt(),lt(rt),Dt.publish("buildDocument"),Ne(),se(),function(){Dt.publish("putAdditionalObjects");for(var t=0;t<at.length;t++){var e=at[t];Kt(e.objId,!0),ct(e.content),ct("endobj")}Dt.publish("postPutAdditionalObjects")}(),t=[],function(){for(var t in Ft)Ft.hasOwnProperty(t)&&(!1===b||!0===b&&v.hasOwnProperty(t))&&oe(Ft[t])}(),function(){var t;for(t in Ot)Ot.hasOwnProperty(t)&&ue(Ot[t])}(),function(){for(var t in zt)zt.hasOwnProperty(t)&&he(zt[t])}(),function(t){var e;for(e in jt)jt.hasOwnProperty(e)&&(jt[e]instanceof B?le(jt[e]):jt[e]instanceof M&&ce(jt[e],t))}(t),Dt.publish("putResources"),t.forEach(fe),fe({resourcesOid:te,objectOid:Number.MAX_SAFE_INTEGER}),Dt.publish("postPutResources"),null!==m&&(Ee.oid=Jt(),ct("<<"),ct("/Filter /Standard"),ct("/V "+Ee.v),ct("/R "+Ee.r),ct("/U <"+Ee.toHexString(Ee.U)+">"),ct("/O <"+Ee.toHexString(Ee.O)+">"),ct("/P "+Ee.P),ct(">>"),ct("endobj")),xe(),Ae();var e=it;return Se(),Le(),ct("startxref"),ct(""+e),ct("%%EOF"),lt(st[$]),rt.join("\n")},Pe=w.__private__.getBlob=function(t){return new Blob([ft(t)],{type:"application/pdf"})},Fe=function(t){for(;t.firstChild;)t.removeChild(t.firstChild)},Ie=function(t){var e,n=t.document,r=n.documentElement,i=n.head,a=n.body;return i||(i=n.createElement("head"),r.appendChild(i)),a||(a=n.createElement("body"),r.appendChild(a)),Fe(i),Fe(a),(e=n.createElement("style")).appendChild(n.createTextNode("html, body { padding: 0; margin: 0; } iframe { width: 100%; height: 100%; border: 0;}")),i.appendChild(e),{document:n,body:a}},Ce=w.output=w.__private__.output=(Zt=function(t,e){switch("string"==typeof(e=e||{})?e={filename:e}:e.filename=e.filename||"generated.pdf",t){case void 0:return ke();case"save":w.save(e.filename);break;case"arraybuffer":return ft(ke());case"blob":return Pe(ke());case"bloburi":case"bloburl":if(void 0!==i.URL&&"function"==typeof i.URL.createObjectURL)return i.URL&&i.URL.createObjectURL(Pe(ke()))||void 0;s.warn("bloburl is not supported by your system, because URL.createObjectURL is not supported by your browser.");break;case"datauristring":case"dataurlstring":var n="",r=ke();try{n=d(r)}catch(x){n=d(unescape(encodeURIComponent(r)))}return"data:application/pdf;filename="+encodeURIComponent(e.filename)+";base64,"+n;case"pdfobjectnewwindow":if("[object Window]"===Object.prototype.toString.call(i)){var a="https://cdnjs.cloudflare.com/ajax/libs/pdfobject/2.1.1/pdfobject.min.js",o=!e.pdfObjectUrl;o||(a=e.pdfObjectUrl);var h=i.open();if(null!==h){var l=Ie(h),c=l.document.createElement("script"),u=this;c.src=a,o&&(c.integrity="sha512-4ze/a9/4jqu+tX9dfOqJYSvyYd5M6qum/3HpCLr+/Jqf0whc37VUbkpNGHR7/8pSnCFw47T1fmIpwBV7UySh3g==",c.crossOrigin="anonymous"),c.onload=function(){h.PDFObject.embed(u.output("dataurlstring"),e)},l.body.appendChild(c)}return h}throw new Error("The option pdfobjectnewwindow just works in a browser-environment.");case"pdfjsnewwindow":if("[object Window]"===Object.prototype.toString.call(i)){var f=e.pdfJsUrl||"examples/PDF.js/web/viewer.html",p=i.open();if(null!==p){var g=Ie(p),m=g.document.createElement("iframe"),b=-1===f.indexOf("?")?"?":"&";u=this,m.id="pdfViewer",m.width="500px",m.height="400px",m.src=f+b+"file=&downloadName="+encodeURIComponent(e.filename),m.onload=function(){p.document.title=e.filename,m.contentWindow.PDFViewerApplication.open(u.output("bloburl"))},g.body.appendChild(m)}return p}throw new Error("The option pdfjsnewwindow just works in a browser-environment.");case"dataurlnewwindow":if("[object Window]"!==Object.prototype.toString.call(i))throw new Error("The option dataurlnewwindow just works in a browser-environment.");var v=i.open();if(null!==v){var y=Ie(v),_=y.document.createElement("iframe");_.src=this.output("datauristring",e),y.body.appendChild(_),v.document.title=e.filename}if(v||"undefined"==typeof safari)return v;break;case"datauri":case"dataurl":return i.document.location.href=this.output("datauristring",e);default:return null}},Zt.foo=function(){try{return Zt.apply(this,arguments)}catch(n){var t=n.stack||"";~t.indexOf(" at ")&&(t=t.split(" at ")[1]);var e="Error in function "+t.split("\n")[0].split("<")[0]+": "+n.message;if(!i.console)throw new Error(e);i.console.error(e,n),i.alert&&alert(e)}},Zt.foo.bar=Zt,Zt.foo),je=function(t){return!0===Array.isArray(qt)&&qt.indexOf(t)>-1};switch(a){case"pt":Nt=1;break;case"mm":Nt=72/25.4;break;case"cm":Nt=72/2.54;break;case"in":Nt=72;break;case"px":Nt=1==je("px_scaling")?.75:96/72;break;case"pc":case"em":Nt=12;break;case"ex":Nt=6;break;default:if("number"!=typeof a)throw new Error("Invalid unit: "+a);Nt=a}var Ee=null;X(),Y();var Oe=w.__private__.getPageInfo=w.getPageInfo=function(t){if(isNaN(t)||t%1!=0)throw new Error("Invalid argument passed to jsPDF.getPageInfo");return{objId:Tt[t].objId,pageNumber:t,pageContext:Tt[t]}},Be=w.__private__.getPageInfoByObjId=function(t){if(isNaN(t)||t%1!=0)throw new Error("Invalid argument passed to jsPDF.getPageInfoByObjId");for(var e in Tt)if(Tt[e].objId===t)break;return Oe(e)},Me=w.__private__.getCurrentPageInfo=w.getCurrentPageInfo=function(){return{objId:Tt[$].objId,pageNumber:$,pageContext:Tt[$]}};w.addPage=function(){return be.apply(this,arguments),this},w.setPage=function(){return we.apply(this,arguments),lt.call(this,st[$]),this},w.insertPage=function(t){return this.addPage(),this.movePage($,t),this},w.movePage=function(t,e){var n,r;if(t>e){n=st[t],r=Tt[t];for(var i=t;i>e;i--)st[i]=st[i-1],Tt[i]=Tt[i-1];st[e]=n,Tt[e]=r,this.setPage(e)}else if(t<e){n=st[t],r=Tt[t];for(var a=t;a<e;a++)st[a]=st[a+1],Tt[a]=Tt[a+1];st[e]=n,Tt[e]=r,this.setPage(e)}return this},w.deletePage=function(){return ve.apply(this,arguments),this},w.__private__.text=w.text=function(t,e,n,i,a){var s,o,h,l,c,u,f,d,p,g=(i=i||{}).scope||this;if("number"==typeof t&&"number"==typeof e&&("string"==typeof n||Array.isArray(n))){var m=n;n=e,e=t,t=m}if(arguments[3]instanceof Wt==0?(h=arguments[4],l=arguments[5],"object"===r(f=arguments[3])&&null!==f||("string"==typeof h&&(l=h,h=null),"string"==typeof f&&(l=f,f=null),"number"==typeof f&&(h=f,f=null),i={flags:f,angle:h,align:l})):(T("The transform parameter of text() with a Matrix value"),p=a),isNaN(e)||isNaN(n)||null==t)throw new Error("Invalid arguments passed to jsPDF.text");if(0===t.length)return g;var b,w="",y="number"==typeof i.lineHeightFactor?i.lineHeightFactor:Xe,_=g.internal.scaleFactor;function x(t){return t=t.split("\t").join(Array(i.TabLen||9).join(" ")),ge(t,f)}function A(t){for(var e,n=t.concat(),r=[],i=n.length;i--;)"string"==typeof(e=n.shift())?r.push(e):Array.isArray(t)&&(1===e.length||void 0===e[1]&&void 0===e[2])?r.push(e[0]):r.push([e[0],e[1],e[2]]);return r}function L(t,e){var n;if("string"==typeof t)n=e(t)[0];else if(Array.isArray(t)){for(var r,i,a=t.concat(),s=[],o=a.length;o--;)"string"==typeof(r=a.shift())?s.push(e(r)[0]):Array.isArray(r)&&"string"==typeof r[0]&&(i=e(r[0],r[1],r[2]),s.push([i[0],i[1],i[2]]));n=s}return n}var k=!1,P=!0;if("string"==typeof t)k=!0;else if(Array.isArray(t)){var F=t.concat();o=[];for(var I,C=F.length;C--;)("string"!=typeof(I=F.shift())||Array.isArray(I)&&"string"!=typeof I[0])&&(P=!1);k=P}if(!1===k)throw new Error('Type of text must be string or Array. "'+t+'" is not recognized.');"string"==typeof t&&(t=t.match(/[\r?\n]/)?t.split(/\r\n|\r|\n/g):[t]);var j=pt/g.internal.scaleFactor,E=j*(y-1);switch(i.baseline){case"bottom":n-=E;break;case"top":n+=j-E;break;case"hanging":n+=j-2*E;break;case"middle":n+=j/2-E}if((u=i.maxWidth||0)>0&&("string"==typeof t?t=g.splitTextToSize(t,u):"[object Array]"===Object.prototype.toString.call(t)&&(t=t.reduce(function(t,e){return t.concat(g.splitTextToSize(e,u))},[]))),s={text:t,x:e,y:n,options:i,mutex:{pdfEscape:ge,activeFontKey:Lt,fonts:Ft,activeFontSize:pt}},Dt.publish("preProcessText",s),t=s.text,h=(i=s.options).angle,p instanceof Wt==0&&h&&"number"==typeof h){h*=Math.PI/180,0===i.rotationDirection&&(h=-h),S===N&&(h=-h);var B=Math.cos(h),M=Math.sin(h);p=new Wt(B,M,-M,B,0,0)}else h&&h instanceof Wt&&(p=h);S!==N||p||(p=Gt),void 0!==(c=i.charSpace||fn)&&(w+=O(U(c))+" Tc\n",this.setCharSpace(this.getCharSpace()||0)),void 0!==(d=i.horizontalScale)&&(w+=O(100*d)+" Tz\n"),i.lang;var R=-1,D=void 0!==i.renderingMode?i.renderingMode:i.stroke,q=g.internal.getCurrentPageInfo().pageContext;switch(D){case 0:case!1:case"fill":R=0;break;case 1:case!0:case"stroke":R=1;break;case 2:case"fillThenStroke":R=2;break;case 3:case"invisible":R=3;break;case 4:case"fillAndAddForClipping":R=4;break;case 5:case"strokeAndAddPathForClipping":R=5;break;case 6:case"fillThenStrokeAndAddToPathForClipping":R=6;break;case 7:case"addToPathForClipping":R=7}var z=void 0!==q.usedRenderingMode?q.usedRenderingMode:-1;-1!==R?w+=R+" Tr\n":-1!==z&&(w+="0 Tr\n"),-1!==R&&(q.usedRenderingMode=R),l=i.align||"left";var H,W=pt*y,V=g.internal.pageSize.getWidth(),G=Ft[Lt];c=i.charSpace||fn,u=i.maxWidth||0,f=Object.assign({autoencode:!0,noBOM:!0},i.flags);var Y=[],Z=function(t){return g.getStringUnitWidth(t,{font:G,charSpace:c,fontSize:pt,doKerning:!1})*pt/_};if("[object Array]"===Object.prototype.toString.call(t)){var J;o=A(t),"left"!==l&&(H=o.map(Z));var X,K=0;if("right"===l){e-=H[0],t=[],C=o.length;for(var $=0;$<C;$++)0===$?(X=rn(e),J=an(n)):(X=U(K-H[$]),J=-W),t.push([o[$],X,J]),K=H[$]}else if("center"===l){e-=H[0]/2,t=[],C=o.length;for(var Q=0;Q<C;Q++)0===Q?(X=rn(e),J=an(n)):(X=U((K-H[Q])/2),J=-W),t.push([o[Q],X,J]),K=H[Q]}else if("left"===l){t=[],C=o.length;for(var tt=0;tt<C;tt++)t.push(o[tt])}else if("justify"===l&&"Identity-H"===G.encoding){t=[],C=o.length,u=0!==u?u:V;for(var et=0,nt=0;nt<C;nt++)if(J=0===nt?an(n):-W,X=0===nt?rn(e):et,nt<C-1){var rt=U((u-H[nt])/(o[nt].split(" ").length-1)),it=o[nt].split(" ");t.push([it[0]+" ",X,J]),et=0;for(var at=1;at<it.length;at++){var st=(Z(it[at-1]+" "+it[at])-Z(it[at]))*_+rt;at==it.length-1?t.push([it[at],st,0]):t.push([it[at]+" ",st,0]),et-=st}}else t.push([o[nt],X,J]);t.push(["",et,0])}else{if("justify"!==l)throw new Error('Unrecognized alignment option, use "left", "center", "right" or "justify".');for(t=[],C=o.length,u=0!==u?u:V,nt=0;nt<C;nt++){J=0===nt?an(n):-W,X=0===nt?rn(e):0;var ot=o[nt].split(" ").length-1,ht=ot>0?(u-H[nt])/ot:0;nt<C-1?Y.push(O(U(ht))):Y.push(0),t.push([o[nt],X,J])}}}!0===("boolean"==typeof i.R2L?i.R2L:bt)&&(t=L(t,function(t,e,n){return[t.split("").reverse().join(""),e,n]})),s={text:t,x:e,y:n,options:i,mutex:{pdfEscape:ge,activeFontKey:Lt,fonts:Ft,activeFontSize:pt}},Dt.publish("postProcessText",s),t=s.text,b=s.mutex.isHex||!1;var lt=Ft[Lt].encoding;"WinAnsiEncoding"!==lt&&"StandardEncoding"!==lt||(t=L(t,function(t,e,n){return[x(t),e,n]})),o=A(t),t=[];for(var ut,ft,dt,gt=Array.isArray(o[0])?1:0,mt="",vt=function(t,e,n){var r="";return n instanceof Wt?(n="number"==typeof i.angle?Vt(n,new Wt(1,0,0,1,t,e)):Vt(new Wt(1,0,0,1,t,e),n),S===N&&(n=Vt(new Wt(1,0,0,-1,0,0),n)),r=n.join(" ")+" Tm\n"):r=O(t)+" "+O(e)+" Td\n",r},wt=0;wt<o.length;wt++){switch(mt="",gt){case 1:dt=(b?"<":"(")+o[wt][0]+(b?">":")"),ut=parseFloat(o[wt][1]),ft=parseFloat(o[wt][2]);break;case 0:dt=(b?"<":"(")+o[wt]+(b?">":")"),ut=rn(e),ft=an(n)}void 0!==Y&&void 0!==Y[wt]&&(mt=Y[wt]+" Tw\n"),0===wt?t.push(mt+vt(ut,ft,p)+dt):0===gt?t.push(mt+dt):1===gt&&t.push(mt+vt(ut,ft,p)+dt)}t=0===gt?t.join(" Tj\nT* "):t.join(" Tj\n"),t+=" Tj\n";var yt="BT\n/";return yt+=Lt+" "+pt+" Tf\n",yt+=O(pt*y)+" TL\n",yt+=cn+"\n",yt+=w,yt+=t,ct(yt+="ET"),v[Lt]=!0,g};var Re=w.__private__.clip=w.clip=function(t){return ct("evenodd"===t?"W*":"W"),this};w.clipEvenOdd=function(){return Re("evenodd")},w.__private__.discardPath=w.discardPath=function(){return ct("n"),this};var Te=w.__private__.isValidStyle=function(t){var e=!1;return-1!==[void 0,null,"S","D","F","DF","FD","f","f*","B","B*","n"].indexOf(t)&&(e=!0),e};w.__private__.setDefaultPathOperation=w.setDefaultPathOperation=function(t){return Te(t)&&(g=t),this};var De=w.__private__.getStyle=w.getStyle=function(t){var e=g;switch(t){case"D":case"S":e="S";break;case"F":e="f";break;case"FD":case"DF":e="B";break;case"f":case"f*":case"B":case"B*":e=t}return e},qe=w.close=function(){return ct("h"),this};w.stroke=function(){return ct("S"),this},w.fill=function(t){return ze("f",t),this},w.fillEvenOdd=function(t){return ze("f*",t),this},w.fillStroke=function(t){return ze("B",t),this},w.fillStrokeEvenOdd=function(t){return ze("B*",t),this};var ze=function(t,e){"object"===r(e)?We(e,t):ct(t)},Ue=function(t){null===t||S===N&&void 0===t||(t=De(t),ct(t))};function He(t,e,n,r,i){var a=new M(e||this.boundingBox,n||this.xStep,r||this.yStep,this.gState,i||this.matrix);a.stream=this.stream;var s=t+"$$"+this.cloneIndex+++"$$";return Yt(s,a),a}var We=function(t,e){var n=Et[t.key],r=jt[n];if(r instanceof B)ct("q"),ct(Ve(e)),r.gState&&w.setGState(r.gState),ct(t.matrix.toString()+" cm"),ct("/"+n+" sh"),ct("Q");else if(r instanceof M){var i=new Wt(1,0,0,-1,0,Pn());t.matrix&&(i=i.multiply(t.matrix||Gt),n=He.call(r,t.key,t.boundingBox,t.xStep,t.yStep,i).id),ct("q"),ct("/Pattern cs"),ct("/"+n+" scn"),r.gState&&w.setGState(r.gState),ct(e),ct("Q")}},Ve=function(t){switch(t){case"f":case"F":case"n":return"W n";case"f*":return"W* n";case"B":case"S":return"W S";case"B*":return"W* S"}},Ge=w.moveTo=function(t,e){return ct(O(U(t))+" "+O(H(e))+" m"),this},Ye=w.lineTo=function(t,e){return ct(O(U(t))+" "+O(H(e))+" l"),this},Ze=w.curveTo=function(t,e,n,r,i,a){return ct([O(U(t)),O(H(e)),O(U(n)),O(H(r)),O(U(i)),O(H(a)),"c"].join(" ")),this};w.__private__.line=w.line=function(t,e,n,r,i){if(isNaN(t)||isNaN(e)||isNaN(n)||isNaN(r)||!Te(i))throw new Error("Invalid arguments passed to jsPDF.line");return S===L?this.lines([[n-t,r-e]],t,e,[1,1],i||"S"):this.lines([[n-t,r-e]],t,e,[1,1]).stroke()},w.__private__.lines=w.lines=function(t,e,n,r,i,a){var s,o,h,l,c,u,f,d,p,g,m,b;if("number"==typeof t&&(b=n,n=e,e=t,t=b),r=r||[1,1],a=a||!1,isNaN(e)||isNaN(n)||!Array.isArray(t)||!Array.isArray(r)||!Te(i)||"boolean"!=typeof a)throw new Error("Invalid arguments passed to jsPDF.lines");for(Ge(e,n),s=r[0],o=r[1],l=t.length,g=e,m=n,h=0;h<l;h++)2===(c=t[h]).length?(g=c[0]*s+g,m=c[1]*o+m,Ye(g,m)):(u=c[0]*s+g,f=c[1]*o+m,d=c[2]*s+g,p=c[3]*o+m,g=c[4]*s+g,m=c[5]*o+m,Ze(u,f,d,p,g,m));return a&&qe(),Ue(i),this},w.path=function(t){for(var e=0;e<t.length;e++){var n=t[e],r=n.c;switch(n.op){case"m":Ge(r[0],r[1]);break;case"l":Ye(r[0],r[1]);break;case"c":Ze.apply(this,r);break;case"h":qe()}}return this},w.__private__.rect=w.rect=function(t,e,n,r,i){if(isNaN(t)||isNaN(e)||isNaN(n)||isNaN(r)||!Te(i))throw new Error("Invalid arguments passed to jsPDF.rect");return S===L&&(r=-r),ct([O(U(t)),O(H(e)),O(U(n)),O(U(r)),"re"].join(" ")),Ue(i),this},w.__private__.triangle=w.triangle=function(t,e,n,r,i,a,s){if(isNaN(t)||isNaN(e)||isNaN(n)||isNaN(r)||isNaN(i)||isNaN(a)||!Te(s))throw new Error("Invalid arguments passed to jsPDF.triangle");return this.lines([[n-t,r-e],[i-n,a-r],[t-i,e-a]],t,e,[1,1],s,!0),this},w.__private__.roundedRect=w.roundedRect=function(t,e,n,r,i,a,s){if(isNaN(t)||isNaN(e)||isNaN(n)||isNaN(r)||isNaN(i)||isNaN(a)||!Te(s))throw new Error("Invalid arguments passed to jsPD

[Diff truncated. Use the raw view or local clone for the full content.]