Add tier enum (fast/balanced/high) with balanced default to the Zod
settings schema. Pass tier through to outpaint options in both the HTTP
route and the pipeline/batch registry. Fix log message to say
"Starting AI canvas expand" and include tier in structured log fields.
Add TIER_PARAMS dict with fast/balanced/high presets controlling band
size, mask dilation, seam strip width, and Telea pre-inpainting. Parse
tier from sys.argv[7] with balanced fallback. Conditional Telea and
seam refinement steps skip cleanly for fast tier. Progressive outpaint
now accepts band_size and progress bounds for tier-appropriate scaling.
- Fix dispatcher pipe deadlock: drain stdout pipe in a background thread
to prevent blocking when ONNX runtime output exceeds 64KB pipe buffer
- Add 5-minute SSE stall timeout so the UI shows an error instead of
hanging forever when async AI processing stalls
- Guard CPU colorization: skip for images >2MP on CPU and when DDColor
model is not installed, with clear user-facing messages
- Add AVIF decode fallback via ImageMagick for bitstream variants that
Sharp's bundled libheif cannot decode (affects all tools)
EXR: add ffmpeg fallback when ImageMagick lacks the OpenEXR delegate
(common on macOS Homebrew installs). HDR: force 8-bit depth output to
prevent CLAHE crash (hist_local requires VIPS_FORMAT_UCHAR). Batch:
disable socket timeout and increase server requestTimeout to 30 min
so large AI batches don't get killed by Node.js defaults.
AVIF (and other Sharp-native formats) were written as raw bytes to a
.png temp file, causing PIL to fail with "cannot identify image file".
Every other AI module wrapper already converts via sharp().png().toBuffer()
before writing; face-landmarks was the only one that skipped this step.
HDR and EXR files decoded by ImageMagick can produce 16-bit PNG buffers.
Sharp's CLAHE operation (hist_local) requires VIPS_FORMAT_UCHAR (8-bit).
Check the buffer depth and convert to 8-bit sRGB before processing.
The wrapper used inline-block for tools without overlay children (e.g.
border). CSS maxHeight: 100% on the img inside an inline-block parent
without explicit height does not resolve, so tall images rendered at
natural height and got clipped by overflow: hidden. Unify both code
paths to always use inline-flex + column, where the flex item properly
shrinks within the container's maxHeight constraint.
Sharp's TIFF encoder silently strips the alpha channel, flattening
transparency against black. This caused border (corner radius, shadow),
beautify, and replace-color tools to produce wrong output for TIFF
inputs when the operation needs transparency. Remove TIFF from
ALPHA_FORMATS so these tools fall back to PNG output.
The find-duplicates tool failed entirely when any uploaded file couldn't
be processed, returning "Duplicate detection failed" or a format-specific
error that aborted the whole batch. With mixed-format uploads (77 files),
this made the tool unusable.
- Skip unprocessable files instead of aborting; return skippedFiles in response
- Switch from fetch() to XHR with upload progress tracking (Uploading X%)
- Add Vite proxy timeout config (5min) to prevent connection drops on large uploads
- Add "Download Grouped" button: ZIP with each duplicate group in its own folder
- Add collapsible skipped-files section in the results UI
- Add 3 integration tests for skip behavior (43 total)
The image element used flex: 1 1 0 (basis 0) inside the inline-flex
preview wrapper, causing it to collapse to zero height whenever a
device frame or watermark overlay was present. Changed to flex: 0 1 auto
so the image uses its intrinsic height and can shrink if needed.
Added regression e2e tests verifying every preset keeps the image
visible, and that adding a watermark does not collapse the image
regardless of whether a frame is active.
setError() in the file store unconditionally set processing to false,
even when called with null to clear a previous error. This meant
setProcessing(true) followed by setError(null) at the start of
processing would immediately kill the processing state, preventing the
progress bar from ever rendering.
Fix setError to only reset processing when there is an actual error.
This also fixes the same latent bug in 10+ other custom tool settings
components that call setProcessing before setError.
Custom-route tools (split, compare, collage, find-duplicates, etc.) only
handled HEIC via ensureSharpCompat, failing on BMP, PSD, RAW, TGA, EXR,
HDR, JXL, and other formats Sharp cannot decode natively. Added the full
decode pipeline from createToolRoute to all 16 affected routes: format
validation via validateImageBuffer, HEIC decoding with actionable error
messages, CLI-based exotic format decoding with nested fallback, and SVG
sanitization to prevent XXE/SSRF/script injection.
The vectorize tool had a custom processing flow that bypassed the
standard useToolProcessor hook -- no progress indication, no server-side
batch, and the Download All ZIP relied on a client-side sequential loop.
Backend: extract core logic into vectorizeBuffer(), register via
registerToolProcessFn() so the /batch endpoint works with p-queue
concurrency and SSE progress events.
Frontend: replace custom fetch loop with useToolProcessor hook and
ProgressCard, giving upload progress, per-file batch status, and
automatic Download All ZIP via the existing tool-page infrastructure.
Also set image/svg+xml MIME type on SVG blobs during batch ZIP
extraction to ensure reliable rendering in <img> tags across browsers.
Adds fileFilter and acceptDescription props to Dropzone so tools can
restrict accepted file types across all entry paths (file picker,
drag-and-drop, paste, single URL import, bulk URL import).
Background images, device frames, custom shadows, and watermark text
were not rendering in the right-pane preview. The preview now updates
in real time for all settings: gradient/solid/image backgrounds, macOS/
Windows/Browser frame chrome, iPhone/MacBook/iPad frame indicators,
custom shadow parameters, and watermark text overlay.
Also fixes a React StrictMode effect-ordering race where the parent
tool-page reset cleared preview state set by the child Settings
component on initial mount.
The stitch route only decoded HEIC via ensureSharpCompat, so exotic
formats (DNG, PSD, TGA, BMP, JXL, HDR, QOI, DDS, ICO, JP2, DPX, etc.)
crashed Sharp at metadata read time, causing "Stitch creation failed".
Add the full CLI decode pipeline (decodeToSharpCompat) matching the
tool-factory pattern, plus SVG sanitization with proper error handling
for each format category.
Replace fetch() with XMLHttpRequest in the frontend to surface upload
progress via onprogress events, and add a progress bar that shows upload
percentage then pulses during server-side stitching.
Replace fragile Unicode-range regex with positive ASCII check
(/^[\x20-\x7E]+$/) that also catches emoji and supplementary plane
characters. Update OpenAPI spec to document percent-encoding.
The X-File-Results header contained raw JSON with non-ASCII characters
from filenames (Chinese, Japanese, etc.), violating RFC 7230. Node.js
threw ERR_INVALID_CHAR on writeHead(). Fixed by wrapping the JSON in
encodeURIComponent() on the backend and decodeURIComponent() on the
frontend, ensuring only ASCII goes into the header while preserving
the original filenames after decoding.
Closes#133
The preview endpoint now returns X-Original-Width/Height headers with
dimensions read from Sharp metadata (or ExifTool for RAW files). The
frontend stores these in FileEntry and the ImageViewer prefers them over
the browser's naturalWidth/naturalHeight, which reflects the resized
preview rather than the original sensor dimensions.
Sharp can read TIFF-based RAW files (DNG, CR2, NEF) directly for
metadata without requiring ImageMagick/darktable to fully decode them.
Try Sharp on the raw buffer first; only fall back to the decode pipeline
for formats Sharp cannot open (PSD, ICO, TGA). For RAW files, enrich
with ExifTool to get real sensor dimensions instead of thumbnail size.
Error from a failed image (e.g. DNG) persisted when navigating to a
previously cached image because the cache path skipped setError(null).
Also pass the original file extension to decodeToSharpCompat so RAW
variants get the correct temp file suffix for ExifTool/ImageMagick.
The compose route only decoded HEIC/HEIF via ensureSharpCompat, causing
EPS, PSD, BMP, RAW, and other exotic formats to fail with
"Processing failed". Now uses the same full format pipeline as the
tool-factory for both base and overlay buffers.
Info panel always read files[0] and stored results in a single useState,
so it never changed when switching images. Now tracks selectedIndex,
auto-fetches on navigation after initial "Read Info" click, caches
results per-index, and aborts stale requests on rapid navigation.
Replace mock HTTP server + vi.mock approach with vi.stubGlobal('fetch')
using a public IP (1.2.3.4) that passes real SSRF validation. This
eliminates both the fragile vi.mock (broken under V8 coverage) and the
localhost network dependency (unreliable in CI).
Revert the SSRF_ALLOW_PRIVATE env var that broke ssrf unit tests.
Extend timeout for exotic format error resilience tests to 120s to
accommodate slow JXL + Image enhancement combination in CI.
Install ImageMagick, Ghostscript, libjxl-tools, and libopenjp2-tools in
CI so exotic format decoder tests (PSD, EPS, HDR, ICO, JP2, etc.) can
run. Relax ImageMagick EPS/PS security policy to match the Dockerfile.
Replace fragile vi.mock() of the SSRF module in fetch-urls tests with an
env-var guard (SSRF_ALLOW_PRIVATE) that bypasses private-IP checks in
the test environment. The vi.mock approach broke under V8 coverage
instrumentation in CI.
The favicon route bypassed createToolRoute and only handled HEIC/HEIF
via ensureSharpCompat. Exotic formats (PSD, BMP, JXL, JP2, EXR, HDR,
DNG, ICO, TGA, etc.) passed validation but crashed Sharp after
reply.hijack(), causing the response stream to hang silently until the
5-minute XHR timeout.
Now decodes all formats before hijacking the response, matching the
pipeline used by createToolRoute. Files that fail to decode are skipped
with a skipped-files.txt manifest in the ZIP rather than aborting the
entire batch.
Pass onUrlImport handler through AppLayout to Dropzone so URL-imported
files are loaded on the home page. Add stopPropagation on the modal
overlay to prevent clicks from reaching elements underneath.
Per-image try-catch with Sharp fallback for CLI-decoded formats (matching
createToolRoute), SVG decompression/sanitization, and filename-specific
error messages so users know which image failed and why.
The image-to-pdf route used ensureSharpCompat (HEIC-only) instead of the
full format decode pipeline from tool-factory. Formats like FITS, PSD,
RAW, EXR, HDR, TGA, etc. passed through undecoded and crashed Sharp.
Replace with validateImageBuffer + decodeToSharpCompat to match the
standard tool pipeline.