Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 13 additions & 3 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -60,10 +60,17 @@ listed under **Changed** and are the reason this is a minor bump rather than a p
`IMGFORGE_RETURN_ATTACHMENT`, and `IMGFORGE_QUALITY` set the starting value for the matching option, which a URL
still overrides.

- **Smart gravity.** `gravity:sm` hands the window choice to libvips' `smartcrop`, which scores the image for the
region a viewer's eye would settle on. It is the answer when no fixed anchor is right for every image in a
catalogue, and it applies to `crop` and to the implicit crop a `fill` resize performs. imgproxy gates this one
behind its Pro tier.

- **New processing options.** `extend_aspect_ratio` (`exar`) pads to the requested shape without reaching its size.
`keep_copyright` (`kcr`) carries the EXIF copyright across a metadata strip. `preserve_hdr` (`ph`) keeps a high
bit-depth image high bit-depth and retains its gain map. `enforce_thumbnail` (`eth`) uses the source's embedded
EXIF thumbnail. `skip_processing` (`skp`) returns the source untouched for the formats it lists.
`keep_copyright` (`kcr`) carries the EXIF copyright across a metadata strip for JPEG, PNG and WebP output — an
APP1 segment, an `eXIf` chunk, and an `EXIF` chunk respectively, synthesising the extended header WebP needs.
TIFF, AVIF and HEIF can hold EXIF too but are not implemented; they strip as normal. `preserve_hdr` (`ph`) keeps a high bit-depth image high bit-depth and retains its gain map.
`enforce_thumbnail` (`eth`) uses the source's embedded EXIF thumbnail. `skip_processing` (`skp`) returns the
source untouched for the formats it lists.

- **Colour management.** The image is converted into a colourspace the pipeline's operations are written for before
processing and back for the encoder, through the source's embedded ICC profile when it has one. A CMYK source no
Expand Down Expand Up @@ -114,6 +121,9 @@ listed under **Changed** and are the reason this is a minor bump rather than a p
- **A resizing type with no dimensions leaves the image alone** instead of failing. `resize:fill` on its own names
no target; imgproxy returns the image unresized, and now so does imgforge.

- **An unrecognised `webp_options` preset is refused** rather than accepted and then dropped on the way to the
encoder, where a typo silently produced a different image.

- Booleans accept imgproxy's spellings (`1`, `t`, `T`, `true`, `TRUE`, `True`) rather than only `1` and `true`.
- `min_width` and `min_height` are accepted alongside imgforge's hyphenated `min-width` and `min-height`.
- `padding` accepts three values, following the CSS shorthand, rather than rejecting them.
Expand Down
40 changes: 25 additions & 15 deletions ROADMAP.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,31 +10,38 @@ Measurements below were taken on libvips 8.16.1 (the version the published image

imgproxy gates a number of options behind its Pro tier that imgforge implements for free: `resizing_algorithm`,
`background_alpha`, `watermark_url`, all four `*_options` encoder groups, the `adjust`/`brightness`/`contrast`/
`saturation` family, and `page`/`pages`/`disable_animation`.
`saturation` family, `page`/`pages`/`disable_animation`, and smart gravity (`gravity:sm`).

0.18.0 closed the free-tier gap that remained. Every option in imgproxy's free tier is now implemented rather than
merely parsed, with the exceptions listed under **Known gaps** below.
0.18.0 closed the free-tier gap that remained. Every option in imgproxy's free tier is implemented rather than
merely parsed, and every option imgforge parses is applied.

## Known gaps in what imgforge accepts

- **`keep_copyright` on non-JPEG output** — libvips' `keep` flags have no copyright granularity, so imgforge reads
the EXIF `Copyright` and `Artist` fields from the source and splices a minimal EXIF segment into the encoded
result. That mechanism only exists for JPEG. PNG and WebP can carry EXIF too, and the same approach would work
for them; nobody has needed it yet.
- **`preserve_hdr` on libvips below 8.16** — the `gainmap` keep flag does not exist there. imgforge checks the
runtime version once and drops the flag rather than naming it, so the request succeeds and loses only the gain
map; the high bit-depth half still works. The drop is logged. The published image ships 8.16.1 and is
unaffected.
- **`webp_options` preset** — only libvips' own preset names reach the encoder. Others are ignored rather than
failing, because an unknown name makes libvips reject the whole encode.
Every option imgforge parses is applied, with three conditionals — each of which degrades rather than failing the
request, and each logged when it takes effect:

- **`preserve_hdr` on libvips below 8.16.** The `gainmap` keep flag does not exist there, so imgforge checks the
runtime version once and drops the flag rather than failing the encode. The request loses the gain map and keeps
the high bit depth. The published image ships 8.16.1 and is unaffected.
- **`keep_copyright` on TIFF, AVIF or HEIF output.** All three containers can hold EXIF, but imgforge only writes
it into JPEG, PNG and WebP. The others strip as normal and the option is a no-op for them.
- **`trim` on an animated source.** Trim measures one image's borders and each frame has its own, which no animated
container can represent — the frames share a canvas. The option is dropped for animations, as it is in imgproxy.

## imgproxy Pro options imgforge does not implement

Listed so the comparison is honest rather than because they are planned: `autoquality`, `crop_aspect_ratio`,
`objects_position` and the object-detection family, `monochrome`, `duotone`, `colorize`, `gradient`,
`unsharp_masking`, `blur_areas`, `style`, `dpi`, `color_profile`, `hashsum`, `watermark_text`/`_size`/`_rotate`/
`_shadow`, `fallback_image_url`, and the `video_thumbnail_*` family. Smart gravity (`gravity:sm`) is Pro as well;
libvips does expose `smartcrop`, so it is the one entry here that would be cheap.
`_shadow`, `fallback_image_url`, and the `video_thumbnail_*` family.

Most of these need something libvips does not provide on its own — an object detector, a quality search loop, a CSS
parser. The ones that do not are `crop_aspect_ratio` (pure arithmetic on the crop extents), `monochrome`, `duotone`,
and `colorize` (each a recombination matrix or a blend), and `watermark_size`/`watermark_rotate`. None is hard;
none has been asked for either, which is why they sit here rather than in the code.

Smart gravity used to be on this list with a note that it would be cheap. It was, and it shipped: `gravity:sm`
hands the window choice to libvips' `smartcrop`, which is the one thing a geometric anchor cannot do.

## Performance

Expand Down Expand Up @@ -103,6 +110,9 @@ and they are the only form that can express a *combination* of metadata `keep` f
in the repository and generating a correct one by hand is not obviously cheaper than checking in a file.
- **`enforce_thumbnail`** is covered for the "no thumbnail" and "malformed thumbnail" paths but not for a JPEG that
actually carries one, for the same reason.
- **`keep_copyright`** round-trips through a real encode of each container imgforge writes EXIF into — JPEG, PNG
and WebP, including a WebP that already has an extended header. Not covered: a container whose existing EXIF
block is malformed. TIFF, AVIF and HEIF can carry EXIF but have no writer, so there is nothing to cover yet.

## How to extend this list

Expand Down
22 changes: 17 additions & 5 deletions doc/5_processing_options.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ Unrecognised directive *names* are ignored rather than rejected, so a typo silen
| `resizing_algorithm` | `ra` | `algorithm` | Interpolation kernel for resize operations. Defaults to `lanczos3`. |
| `width` | `w` | `value` | Sets a target width (infers height). Implies `fit`. |
| `height` | `h` | `value` | Sets a target height (infers width). Implies `fit`. |
| `gravity` | `g` | `type[:x_offset[:y_offset]]` | Controls crop/fill anchoring (`ce`, `noea`, `fp`, ...). Defaults to `ce:0:0`. |
| `gravity` | `g` | `type[:x_offset[:y_offset]]` | Controls crop/fill anchoring (`ce`, `noea`, `fp`, `sm`, ...). Defaults to `ce:0:0`. |
| `flip` | `fl` | `horizontal[:vertical]` | Flips the image horizontally and/or vertically. Defaults to no flip. |
| `enlarge` | `el` | `bool` | Allows upscaling globally. Defaults to `false`. |
| `extend` | `ex` | `bool[:gravity]` | Pads to target dimensions after resize. Defaults to `false:ce:0:0`. |
Expand Down Expand Up @@ -43,7 +43,7 @@ Unrecognised directive *names* are ignored rather than rejected, so a typo silen
| `max_bytes` | `mb` | `bytes` | Re-encodes lossy formats at lower quality until the byte target is reached or quality reaches `1`. |
| `strip_metadata` | `sm` | `bool` | Drops encoder metadata when supported by the output format. |
| `strip_color_profile` | `scp` | `bool` | Drops the embedded colour profile, leaving other metadata alone. |
| `keep_copyright` | `kcr` | `bool` | Retains the EXIF copyright and artist tags across a metadata strip. JPEG output only. |
| `keep_copyright` | `kcr` | `bool` | Retains the EXIF copyright and artist tags across a metadata strip. JPEG, PNG, and WebP output. |
| `preserve_hdr` | `ph` | `bool` | Keeps a high bit-depth image high bit-depth and carries its gain map through. Gain maps need libvips 8.16+; older builds keep the depth and drop the map. |
| `enforce_thumbnail` | `eth` | `bool` | Uses the source's embedded EXIF thumbnail instead of the full image when one is present. |
| `jpeg_options` | `jpgo` | `progressive:no_subsample:trellis:dering:scans:quant_table` | Advanced JPEG encoder switches. |
Expand Down Expand Up @@ -151,12 +151,24 @@ Pick on appearance rather than speed: the kernel is rarely where the processing
- Canvas alignment for `extend` and `extend_aspect_ratio`, each of which can also carry its own.
- Watermark positioning, via the `watermark` option's own position argument.

imgforge accepts imgproxy's gravity anchors: `ce`, `no`, `so`, `ea`, `we`, `noea`, `nowe`, `soea`, `sowe`, and `fp`.
imgforge accepts imgproxy's gravity anchors: `ce`, `no`, `so`, `ea`, `we`, `noea`, `nowe`, `soea`, `sowe`, `fp`, and `sm`. The last two are scoped, exactly as in imgproxy, because they do not mean anything everywhere:

| Where | `fp` | `sm` |
| ----- | ---- | ---- |
| `crop`, `fill`, `fill-down`, `gravity` | yes | yes |
| `extend`, `extend_aspect_ratio` | yes | **no** — nothing to examine |
| `watermark` position | **no** | **no** — a watermark is placed, not found |

A gravity used where it does not apply is rejected with `400 Bad Request` rather than quietly falling back to `ce`.

**Offsets** nudge the window away from its anchor. A magnitude of 1 or more is a pixel count; anything smaller is a fraction of the axis being positioned. `gravity:no:0:20` takes the window from 20px below the top edge; `gravity:no:0:0.1` takes it from a tenth of the way down. The window is still clamped to the image, so an offset cannot push it off the edge.

**Focus point** — `gravity:fp:x:y` — reads the two arguments as coordinates between 0 and 1 and centres the result on that point. `gravity:fp:0.5:0.25` keeps the middle of the upper quarter in view, which is the usual answer for portraits where a centre crop cuts off the head.

**Smart** — `gravity:sm` — hands the choice to libvips, which scores the image for the region a viewer's eye would settle on and puts the window there. It is the answer when no fixed anchor is right for every image: a catalogue of mixed portraits and landscapes has no single correct crop, and a focus point has to be supplied per image. imgproxy charges for this one.

Two costs. It has to examine real pixels, so it forces the decode rather than composing into libvips' lazy pipeline — though in a `fill` it runs *after* the resize, so it examines the small image rather than the source. And it takes no offsets: the window is chosen, not positioned. It applies only where there is content to choose from, which is why the table above rules it out for watermark placement and for the two extends: both position the image on a canvas larger than itself rather than selecting a window inside it.

### Minimum dimensions & zoom

- `min-width` and `min-height` trigger an extra resize pass when the image is still smaller after primary resizing. This pass **upscales regardless of `enlarge`** — the minimums are a floor, and `enlarge:false` does not override them. Use them only when you actually want a guaranteed size.
Expand Down Expand Up @@ -235,7 +247,7 @@ Defaults to `85` for lossy codecs (JPEG, WebP, AVIF). `quality` is ignored for l
- `strip_metadata` and `strip_color_profile` map to libvips metadata retention controls for formats that expose them.
- `jpeg_options` maps to progressive JPEG, chroma subsampling, trellis quantization, overshoot deringing, optimized scans, and quant table controls.
- `png_options` maps to interlacing and palette quantization controls.
- `webp_options` maps to lossless, smart chroma subsampling, and encoder preset controls. Preset names outside libvips' own set (`default`, `picture`, `photo`, `drawing`, `icon`, `text`) are accepted in URLs but ignored by the encoder.
- `webp_options` maps to lossless, smart chroma subsampling, and encoder preset controls. The preset must be one of libvips' own `default`, `picture`, `photo`, `drawing`, `icon`, `text` — and anything else is refused with `400`. It used to be accepted and then dropped on the way to the encoder, so a typo silently produced a different image.
- `avif_options` maps to AVIF/HEIF chroma subsampling.

Every format is encoded through the libvips save suffix (`.webp[Q=80,keep=all]` and friends) rather than the crate's generated save bindings. Those bindings name encoder properties that only exist in libvips 8.16 and later — `exact` on webpsave, `tune` on heifsave, `keep-duplicate-frames` on gifsave — and an older libvips rejects the whole call with `no property named ...`, so nothing encodes at all. The suffix parser sets only the options named, which keeps one code path working across libvips versions, and it is also the only form that can express a *combination* of metadata `keep` flags.
Expand All @@ -249,7 +261,7 @@ A result too large for its output container is scaled down to fit rather than ha
### Metadata

- **`strip_metadata`** drops the descriptive tags (EXIF, XMP, IPTC) and leaves the colour profile alone. **`strip_color_profile`** does the reverse. Set both to drop everything.
- **`keep_copyright`** carries the EXIF `Copyright` and `Artist` tags across a `strip_metadata`. libvips has no copyright granularity in its `keep` flags — they are `none|exif|xmp|iptc|icc|other|gainmap|all` — so imgforge reads the two fields from the source and splices a minimal EXIF segment back into the encoded result. That mechanism is JPEG-only; other output formats strip as normal, and the option is a no-op for them.
- **`keep_copyright`** carries the EXIF `Copyright` and `Artist` tags across a `strip_metadata`. libvips has no copyright granularity in its `keep` flags — they are `none|exif|xmp|iptc|icc|other|gainmap|all` — so imgforge reads the two fields from the source and writes a minimal EXIF block back into the encoded result: an APP1 segment for JPEG, an `eXIf` chunk for PNG, and an `EXIF` chunk for WebP, synthesising the extended header WebP needs to carry one. Every other output format strips as normal and the option is a no-op for it — including TIFF, AVIF and HEIF, which *can* hold EXIF but have no writer here yet.
- **`preserve_hdr`** keeps a high bit-depth source at its own depth when the output format can carry it (PNG, TIFF, AVIF, HEIF) and retains the gain map that makes the image HDR, even while other metadata is being stripped. The gain-map half needs libvips 8.16 or later, where the `gainmap` keep flag was added. On an older build imgforge detects the runtime version and drops that flag rather than failing: the request succeeds, keeps its bit depth, and loses only the gain map. A successful response on such a build is therefore not proof that the gain map survived — the drop is logged when it happens.
- **`enforce_thumbnail`** uses the source's embedded EXIF thumbnail in place of the full image whenever one is present, which turns a large JPEG into a very cheap request. The thumbnail is usually a few hundred pixels wide, so the result is only as good as that; a thumbnail that will not decode falls back to the full image rather than failing.

Expand Down
Loading
Loading