You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
Region removal with LaMa (issue #176). A new remove_region(x,y,w,h) filter erases a rectangle and inpaints it from the surrounding pixels, mirroring blur_region/pixelate_region exactly, including the bounds discipline (a rectangle that overflows the image in effect at that point in the chain is a 400). The model is operator-supplied through CRABCROP_INPAINT_MODEL: a LaMa-family ONNX export with two inputs (image plus mask) at a fixed 512×512, preloaded and probed at boot like the other models; the OpenCV zoo export of big-lama follows the contract. Each removal runs the model once on a square crop window around the rectangle and takes only the rectangle's pixels from the result, so everything outside it is bit-identical; unconfigured servers answer 501, inference runs behind the shared AI gate, and meta responses carry an inpaint object listing the erased rectangles. The TypeScript SDK gains a removeRegion builder. The docs call out the known failure mode plainly: the fill is fabricated, plausible on smooth surroundings and visibly wrong on structured content (text, faces, repeating patterns).
Per-object segmentation for the class operations (issue #175). A MobileSAM model pair, operator-supplied through CRABCROP_SEGMENT_ENCODER_MODEL and CRABCROP_SEGMENT_DECODER_MODEL (both required together, preloaded and probed end to end at boot like the other models), gives the class operations per-object masks: each detection is prompted to the decoder as a box, and the returned silhouette decides what the operation touches. With the pair configured, blurclass and pixelateclass redact the silhouette instead of the rectangle (the mask's soft edge blends, so the boundary antialiases), and a new recolorclass(<class>,<hex>) filter paints the detected objects of a class a solid color, alpha preserved, mask-tight with the pair and box-shaped without it. The image is encoded once per request however many class operations chain; a segmentation failure fails the request like a detection failure does, never a silently rectangular redaction. Meta responses for a request that segmented carry a segments object: per object, the label, detection score and box, the mask's tight box, its coverage of the box, and its centroid. The TypeScript SDK gains a recolorClass builder and the matching explain support.
OCR text in meta (issue #177). Meta requests that opt in with ocr=1 gain an ocr object: the text read from the processed image as lines in reading order, each {text, score, x, y, w, h} with the box in the image's pixels, plus the lines joined as text_content, the recognition model's identity, and the language the deployment serves. The pipeline is operator-supplied through CRABCROP_OCR_DETECT_MODEL, CRABCROP_OCR_RECOGNIZE_MODEL, and CRABCROP_OCR_DICT (all three required together, preloaded and probed at boot; the probe checks the dictionary's character count against the recognition model's output width, so a mismatched set fails startup). CRABCROP_OCR_LANGUAGE declares the language tag echoed in responses (default und). The reference artifacts are the RapidOCR exports of PP-OCRv6 small (Apache-2.0): a DB detector, a CTC recognizer, and an 18,708-character dictionary. The text is an annotation only: an OCR request builds and caches exactly the image entry any other request for the URL would, byte for byte, and an opt-in that cannot be answered reports {"available": false, "reason": ...} in the field while the meta request still succeeds.
Studio scene mode (issue #198). filters:studio(scene) is studio's second mode: where studio(product) cuts the subject out onto a backdrop, scene mode keeps the photograph's scene and expands to a smart crop on the detected subject plus a per-image light and color correction. The subject is chosen from the mask model's output (the same model removebg and product mode run, so no new configuration): the mask's connected components at the subject threshold compete, the largest wins, ties break toward the frame center then reading order, and a winner that is a degenerate speck against the covered region takes the no-subject fallback, which serves the original framing corrected. The reframe crops to the smallest frame-aspect window holding the subject with its pad margin (the one argument scene takes, same range and default as product) and resizes back, so the canvas in effect never changes and downstream operations compose unchanged. The correction is histogram-driven and deterministic: a per-channel levels stretch between the 0.5th and 99.5th percentiles (a channel flatter than a 16-value span is left unmapped), then a saturation factor steering the image's mean saturation toward 0.30, capped at 1.35 and never below 1. Meta's studio object gains a mode field (product or scene); scene runs report a scene object with the winning subject's box, the candidate count, the crop window, the per-channel levels applied, and the saturation factor, so every step of the expansion stays accountable. The product-only arguments (bg, shadow) are bad arguments under scene, unconfigured servers answer 501 exactly as for product, and inference runs behind the shared AI gate.
Install crabcrop 0.7.0
Install prebuilt binaries via shell script
curl --proto '=https' --tlsv1.2 -LsSf https://github.com/dayvson/crabcrop/releases/download/v0.7.0/crabcrop-installer.sh | sh
PDF pages as an image source (issue #215). A new page: operation segment selects one page of a PDF source: page:1 is the first page, pages are 1-based, the last segment wins, and leading zeros canonicalize (page:007 is page:7, one shared cache entry). The rendered page becomes the source image the rest of the URL operates on, so crops, filters, studio mode, presets, and purge all apply unchanged. The number is exact, never clamped: a page past the end of the document is a 400 naming the page and the count, a document that needs a password to open is a 400 of its own class, and a malformed selector is a 400 with a new frozen invalid_page code; a page: segment on a non-PDF source fails as a bad source. Rendering runs through a new CRABCROP_PDF_RENDERER external program (Poppler's pdftocairo the reference, its pdfinfo sibling derived and used for probing) under the frame side's bounds: both paths are validated at boot, runs share the video gate under their own CRABCROP_PDF_TIMEOUT_SECS wall clock, output is capped, and the tools receive only the document bytes on stdin, so embedded references and actions have nothing to resolve. Only a request carrying a page: segment may fetch an application/pdf Content-Type, and documents over 1000 pages are refused. Meta responses for page renders carry a pdf object (page count, selected page, and the page size in points), and explain labels PDF sources as kind pdf.
Video frames as an image source (issue #214). A new frame: operation segment selects one frame of a video source: frame:poster (the first frame), frame:<seconds> (a timestamp), or frame:<percent>% (a share of the duration). The extracted frame becomes the source image the rest of the URL operates on, so crops, filters, studio mode, presets, and purge all apply unchanged. Selections past the end clamp to the last frame; a malformed selector is a 400 with a new frozen invalid_frame code; a frame: segment on an image source fails as a bad source. Extraction runs through a new CRABCROP_VIDEO_DECODER external program (FFmpeg the reference) under the encode side's bounds: the path is validated at boot, runs share the video gate and the CRABCROP_VIDEO_TIMEOUT_SECS wall clock, and output is capped. Only a request carrying a frame: segment may fetch a video/* Content-Type. Meta responses for frame renders carry a video object (duration, stream dimensions, and the selected timestamp), and explain labels video sources as kind video.
Install crabcrop 0.6.0
Install prebuilt binaries via shell script
curl --proto '=https' --tlsv1.2 -LsSf https://github.com/dayvson/crabcrop/releases/download/v0.6.0/crabcrop-installer.sh | sh
Named source roots (issue #248). An operator can name origins in a roots file (CRABCROP_SOURCE_ROOTS_FILE, one name = base-url entry per line, validated at boot), and a request can then use root:<name>/<path> as its source. The signature covers the short form exactly as written; the server expands it after verification and before cache keying, so the short spelling and the full URL it expands to share one cache entry and purge treats them as the same render. The allowlist and every loader check apply to the expanded URL, a path that climbs above the root is a 400 (invalid_root), an unknown name is a 400 (unknown_root), and explain reports the root's name without ever printing a base URL. The signing spec names the short form and carries a canonical test vector for it, which the Rust crate and both SDKs run.
Install crabcrop 0.5.0
Install prebuilt binaries via shell script
curl --proto '=https' --tlsv1.2 -LsSf https://github.com/dayvson/crabcrop/releases/download/v0.5.0/crabcrop-installer.sh | sh
A preset: segment in a URL is now parsed even when no presets file is configured (issue #247). Before, the segment was never recognized as part of the operations on a server without presets configured, so it was treated as the source and the request failed with a 502 source error; an unknown preset name now returns the documented 400 unknown_preset error, and a configured server behaves exactly as before.
Install crabcrop 0.4.2
Install prebuilt binaries via shell script
curl --proto '=https' --tlsv1.2 -LsSf https://github.com/dayvson/crabcrop/releases/download/v0.4.2/crabcrop-installer.sh | sh
upscale() now budgets its input (issue #244). A dynamic-size super-resolution model spends activation memory in proportion to its input pixels, and the 4096 px output cap did not bind that: a 900x560 request OOM-killed the server. A single model run now takes at most 262,144 input pixels (the request image, or the intermediate between composed runs), checked before any inference runs, and a larger input fails with a documented 400.
studio() no longer aborts the server when the mask's subject box is a speck (issue #243). A mask that barely crosses the subject threshold in a few pixels while carrying faint coverage across a large region now takes the documented no_subject fallback instead of scaling the whole covered region by the speck's ratio (the reproduction attempted an 87 GB allocation and killed the process). The placement scale is also bounded outright: the scaled piece never exceeds twice the canvas per axis or the platform's aggregate output pixel budget.
Install crabcrop 0.4.1
Install prebuilt binaries via shell script
curl --proto '=https' --tlsv1.2 -LsSf https://github.com/dayvson/crabcrop/releases/download/v0.4.1/crabcrop-installer.sh | sh
Graceful shutdown on SIGTERM (PR #235). The server now drains instead of dropping work when it is stopped: on SIGTERM it stops accepting new connections and lets in-flight requests finish before exiting, so a redeploy or container stop no longer kills a render mid-flight.
ETag and If-None-Match revalidation (PR #236). Image responses carry an ETag, and a request whose If-None-Match matches is answered 304 Not Modified with no body, so clients and CDNs can revalidate a cached render cheaply instead of downloading it again.
Request ids in log lines and responses (PR #237). Every request gets an id that is recorded in its log line and returned in the x-request-id response header; a caller-supplied x-request-id is honored when it is a valid token, so a response can be matched to the log entry that produced it.
Community-health files (PR #233). CONTRIBUTING.md documents how to build, test, and propose changes; CODE_OF_CONDUCT.md adopts the Contributor Covenant with a filled-in enforcement contact; issue and pull request templates and a CODEOWNERS file round out the set.
A declared minimum Rust version and Dependabot (PR #234). The crabcrop-url crate declares rust-version = "1.88", CI checks that floor in a new msrv job, rust-toolchain.toml pins the development toolchain, and Dependabot opens weekly grouped update pull requests across the Cargo, npm, pip, and GitHub Actions ecosystems.
Changed
The six-hex-digit color parser is deduplicated (PR #238). The URL grammar open-coded the same hex color parse at six sites (studio background, pad, border, duotone, monochrome, fill); one shared helper now serves all six, with no behavior change.
Fixed
border(0) is accepted as a no-op (issue #239). A zero-width border parses and renders nothing, matching radius(0), instead of failing as an invalid filter; the README documents the border width range as 0 to 16383 pixels.
Install crabcrop 0.4.0
Install prebuilt binaries via shell script
curl --proto '=https' --tlsv1.2 -LsSf https://github.com/dayvson/crabcrop/releases/download/v0.4.0/crabcrop-installer.sh | sh
Malformed size segments now return 400 instead of 502 (issue #228). A size segment past the 16383 px axis cap, or garbled like abcx100, fell out of the parse and the rest of the URL was treated as a source path, so the request failed as a source fetch error; a segment shaped like a size that fails to parse now returns the invalid_size error.
The max_bytes quality ladder now reaches quality 1 instead of stalling at 15 (issue #229). The ladder spent its encode attempts on fixed steps and ran out before reaching the floor; it now plans its descent against the attempts remaining, so the last attempt lands on quality 1 and tight byte budgets actually shrink the output.
no_upscale no longer crops a plain resize to the requested aspect (issue #230). A size that would upscale now clamps each axis at the source dimensions, so a 200x150 source asked for 300x200 returns 200x150 instead of a resampled 200x133.
Animated WebP encodes no longer starve unrelated renders (issue #231). Animated output encodes through the slow lossless path, and one render could hold the process gate for minutes while other requests shed with 503; animated encodes now run behind a dedicated single-permit gate, so at most one runs at a time and still renders keep the process permits. The encode itself remains slow, and that cost stays tracked on the issue.
Install crabcrop 0.3.2
Install prebuilt binaries via shell script
curl --proto '=https' --tlsv1.2 -LsSf https://github.com/dayvson/crabcrop/releases/download/v0.3.2/crabcrop-installer.sh | sh
Security policy (PR #221). SECURITY.md documents private vulnerability reporting through GitHub's private advisories, the supported-version line for the pre-1.0 releases (fixes land on the latest main and the next release, no backports yet), what a report should include, and the coordinated disclosure process; the README links it from a new Security section.
Fixed
Studio renders to WebP took about 51 s at 1000x667 and failed outright at 2000x1334 (issue #220, PR #226). The compositing pipeline was never the problem (about 100 ms end to end); the lossy WebP encoder encoded the studio canvas's alpha plane through its slow VP8L path even when the backdrop made the output fully opaque, and that path panicked on the larger canvas. When the backdrop resolves to a color, the composite now states its alpha as exactly 255, so the encoder skips the alpha plane: 0.95 s at 1000x667 and 1.98 s at 2000x1334, with RGB output bit-identical to before. Per-stage timing for the studio pipeline is available behind the CRABCROP_STUDIO_TIMING environment variable.
The color filters silently discarded transparency (PR #222). grayscale, contrast, sharpen, saturation, rgb, and equalize normalized the image to RGB8, so a filter chain that later produced alpha (radius, mask, removebg) or an alpha-capable output lost it: grayscale into radius into PNG came out with opaque corners. The six filters now transform the RGB plane and re-attach the source alpha, following the map_rgb pattern the other color filters already used; opaque inputs render exactly as before.
The Thumbor converter mapped contrast on the wrong scale (PR #223). convert-thumbor translated Thumbor contrast(N) into a factor (1 + N/100), but the server's contrast has been a percent on the -100..=100 scale since studio mode (#197), so converted URLs applied almost no contrast. The converter now emits the percent directly, clamping out-of-range inputs with a warning, and the Thumbor, Cloudinary, and imgproxy migration guides document the mapping correctly (imgproxy's co is a factor; crabcrop's contrast is a percent).
The color management documentation claimed ICC profiles are read from WebP sources (PR #224). extract_source_meta reads ICC, EXIF, and XMP from JPEG and PNG only; the README now says so.
Install crabcrop 0.3.1
Install prebuilt binaries via shell script
curl --proto '=https' --tlsv1.2 -LsSf https://github.com/dayvson/crabcrop/releases/download/v0.3.1/crabcrop-installer.sh | sh
Moderation verdicts in meta (issue #171). Meta requests that opt in with moderation=1 on the Accept: application/json entry gain a moderation object. With CRABCROP_MODERATION_MODEL pointed at a ViT classifier export (the Falconsai nsfw_image_detection shape, Apache-2.0: a [1, 3, 224, 224] input, one logits row out, softmaxed into per-category probabilities), the object carries the model identity and scores in output order, rounded to 4 decimals. CRABCROP_MODERATION_LABELS names the outputs (one per line, count checked against the model at boot); without it, a two-output model uses normal and nsfw, and any other output count is a startup error. With no model set, the labels file instead names zero-shot prompt labels scored by the configured CLIP pair, reported under signals with review_only: true: review signals, never verdicts, sorted best first. An opt-in that cannot be answered (nothing configured, a busy AI gate, a failed inference) reports {"available": false, "reason": ...} and the meta request still succeeds; inference runs behind the AI gate under the same permit rules as tags and detections, and both variables join the configuration fingerprint. Scope, following the spike that de-risked this issue: verdicts only. The serve-time blur policy action is deliberately not built: a blurred and a clean render of one immutable URL are different bytes (the policy would have to join the cache key), inference would sit on every serve, and false positives would land on end users with no appeal path. If enforcement is ever revisited, verdicts should attach to the source and be cached per source, the policy decision should join the cache key, failure should fail closed to blurred, and the action should stay opt-in and off by default.
Image embeddings and aesthetic score in meta (issue #172). Meta requests that opt in with embeddings=1 on the Accept: application/json entry gain the CLIP tagger's image embedding, the same vector its tag scores are computed from: embedding carries it packed as float16 little-endian and base64-encoded (512 dimensions travel as 1,368 characters instead of roughly 4 KB of JSON floats; the float16 rounding costs about 1e-3 relative error, which does not move cosine rankings), and embedding_model, embedding_dim, embedding_encoding (f16le-base64), and embedding_normalized: true ride along so a stored vector stays interpretable across encoder swaps. No new model is loaded: the provider wraps the configured tagger. With CRABCROP_AESTHETIC_HEAD pointed at a LAION-style linear head (the PyTorch save, parsed by a dependency-free reader: a stored-or-deflate ZIP walk plus a restricted pickle interpreter that builds tensor state dicts and refuses every other callable by name), meta also carries aesthetic: the head's score of the same embedding, with both identities. A dimension mismatch between head and encoder fails soft per request (null score, reason: "dimension_mismatch", expected and got); a malformed head file fails at boot. An opt-in that cannot be answered reports the embedding as null with a reason and the meta request still succeeds; inference runs behind the AI gate under the same permit rules as tags, detections, and moderation, and the new variable joins the configuration fingerprint. The score is documented as descriptive only, never a gate.
Install crabcrop 0.3.0
Install prebuilt binaries via shell script
curl --proto '=https' --tlsv1.2 -LsSf https://github.com/dayvson/crabcrop/releases/download/v0.3.0/crabcrop-installer.sh | sh
Face-safe smart crops (issue #199). When a face model is configured, smart alignment no longer places its window on the detected faces' area-weighted centroid alone: window placement treats the face boxes as keep-whole constraints. A position keeping every detected face fully inside wins, at the point nearest the focal-point window, so behavior changes only when the focal window would have cut a face; when no position keeps every face, the window keeps the most face area whole (larger faces win, and a face counts only kept whole, never partially); a face larger than the window on the cropped axis stays centered, as before. Placement is deterministic, with fixed tie-breaking, and runs off the same single detection pass as before. With no face model configured, no faces found, or a detection failure, the window is exactly the saliency window of before (regression-pinned in tests). Literal alignments, caller focal points and rectangles, and crop-rect are unchanged, and there is no new operation or grammar. The crop decision record behind debug:focal now counts the faces detected and the faces the final window keeps whole, so the guarantee is inspectable.
Studio mode (issue #197). A new intent filter, filters:studio(product,bg:white,shadow:soft,pad:10), names an outcome instead of a mechanism: the subject the segmentation model finds is composited into a listing-ready product shot. The expansion is fixed: mask (the removebg model, with the same not-configured 501 when absent), defringe (edge colors decontaminated against the local background so no halo of the old backdrop survives), backdrop and shadow (the resolved backdrop fills the canvas; a contact shadow is synthesized from the subject alpha, blurred, darkened, offset down, strength per the shadow argument; bg:none keeps transparency and skips the shadow), and pad (the subject scaled and centered to the pad margin, 0 to 40 percent per side, on the canvas in effect at the filter's position). The backdrop resolves as: the named color or white; brand, the new CRABCROP_BRAND_COLOR operator setting (six hex digits), falling back to white when unset; or the brand color by default when the URL names none. A mask that finds no subject serves the original image centered and padded on the backdrop instead of an empty canvas. The composite is a normal image in the chain, valid in every output format; studio takes an AI gate permit (it is part of RequestDirectives::uses_ai), counts once against the operation cap, and its output counts against the aggregate output budget. Meta responses gain a studio record with the resolved backdrop and its source (argument, brand, or default), the shadow and pad applied, the steps that ran, the fallbacks taken (bg_brand_unset, no_subject), and the mask confidence when the provider exposes one (the built-in provider's mask field is min-max normalized, not a probability, so it exposes none). CRABCROP_BRAND_COLOR joins the configuration fingerprint, so changing it starts a fresh cache epoch. The filter chain splitter is now parenthesis-aware, since the named arguments carry : inside the argument list; the frozen ParseError set is unchanged, and bad studio arguments fail as the existing invalid filter error.
Self-explaining URLs (issue #196). GET /explain/<signature>/<operations>/<source> verifies the remainder exactly as the image path does and answers with a report on the URL instead of the image: the signature verdict (with key ID and expiry), the source with its probed facts (media kind, pixel dimensions, encoded size), every operation glossed in plain language (the CLI explain verb's gloss table, shared so a new filter cannot ship undescribed), referenced presets with their expanded operations, a forecast (output dimensions when the chain determines them, the output format, the output pixel count against the aggregate budget), the canonical cache form and group, and links to the image and to the debug focal remainder when the chain contains a crop. Browsers get an HTML page; Accept: application/json gets the same report as JSON. The security posture is part of the feature: signature and expiry are verified before any source access, so a bad or expired explain request answers with the image path's own status and body and performs zero source fetches (test-proven with hit counting, the audit R2 pattern); the probe is one bounded loader fetch read from headers only, with no full decode, no detection or AI, and no process, AI, or video gate, so one explain costs at most one image request; the report carries no secrets or derived key material, no configuration values, file paths, or config fingerprint, and no redirect or resolved-address details, with failures mapped to the frozen codes and generic messages; the HTML page escapes every interpolated string, ships no scripts, and is served with Content-Security-Policy: default-src 'none' and nosniff; and responses are no-store and write nothing to any cache tier.
Install crabcrop 0.2.0
Install prebuilt binaries via shell script
curl --proto '=https' --tlsv1.2 -LsSf https://github.com/dayvson/crabcrop/releases/download/v0.2.0/crabcrop-installer.sh | sh