Skip to content

v0.4.0

Choose a tag to compare

@github-actions github-actions released this 23 Oct 15:38
· 68 commits to main since this release
7402ee5

image_pipe_url

Added

  • With :validate_against, ImagePipe.URL.validate/1 warns about a crop
    or region that covers the whole frame, as an :inert_option issue with
    detail: :full_frame. It also warns about the options only that crop or
    region made useful, such as anchor.

Changed

  • Parsing a URL takes less time, most of all for a URL without presets or
    unset when no request defaults are set.
  • A watermark_source: loses an optional leading /, as the main source
    does, so its wm-src64 value and its deterministic wm-enc token change.
  • A URL's anchor-offset, extend-at, or extend-offset no longer
    replaces the crop target or canvas from a preset or the request defaults,
    and its unset removes only that option. Before, anchor-offset=5,5
    dropped a preset's anchor, and extend-at=unset dropped its extend.
  • A trim tolerance above 2147483647 is rejected, both in a URL and by
    ImagePipe.URL.group/2.
  • Errors from parsing a URL state the accepted range for sizes, offsets,
    pad, wm-gap, dpr, zoom, crop-ratio, detect, blur,
    progressive-blur, sharpen, pixelate, contrast, saturation, and
    the trim tolerance, and show which gradient values are optional.
  • An invalid value given to the URL builder appears in its error, with the
    accepted range for numbers, as in invalid value for :dpr option: expected a number above 0, at most 2147483647, got: 0. An invalid source
    isn't repeated in its error.
  • Breaking: The :presets in :validate_against must be a map with
    string keys. Change a list of {name, fragment} pairs to a map such as
    %{"card" => "w=400"}. ImagePipe.URL.config/1 raises an
    ArgumentError that names :presets for any other value, instead of a
    Protocol.UndefinedError.
  • A region whose x or y is 0pct gives the same plan as one written with
    0.
  • detect=all:3, one weight for every class, gives the same plan as
    detect=all.
  • crop=100pct,100pct without crop-ratio, and region=0,0,100pct,100pct,
    give the same plan as the request without them.
  • autoquality and max-bytes are reported as ignored options with
    format=webp and lossless webp-options, as with format=png.
  • Checking and decoding the percent escapes of a src source takes less
    time, up to about 0.5 ms less for an 8 KB source.

Removed

  • Breaking: request_watermarks in validate_against, and the
    :watermark_source_disabled issue. Servers accept every watermark source
    that a configured source serves. Remove the option from validate_against.
  • Breaking: mount: on ImagePipe.URL.Helpers. Start an instance with
    the mount's URL settings and pass its name as instance:.

Fixed

  • The URL builder's validation against a server's presets resolves them
    the way the server does: a value that a later layer unsets or replaces no
    longer removes what it would have replaced, and a preset that names other
    presets gives the same result as listing them.
  • ImagePipe.URL.url/3 returns {:error, :invalid_source} for a source
    that is just / or holds a NUL byte, which the server refuses.
    ImagePipe.URL.group/2 rejects such a watermark_source:.
  • Checking a URL with :validate_against applies the request defaults to
    the URL's first group, as the server does, even when that group holds
    only unset options or names a preset the check doesn't know. Before,
    the defaults were checked with the next group, so the check could
    disagree with the server.
  • A preset in :validate_against with a group holding only unset
    options is a pipeline preset, as on the server, so adding other options
    to it is an error, and a first group like that clears the request
    defaults. Before, the check dropped that group.
  • An issue that validate/1 reports with :validate_against names the
    group as the builder numbers it. Before, when a group held only unset
    options, issues in later groups named the group before theirs.
  • With preset_lookup: true, ImagePipe.URL.validate/1 reports a
    conflict between two options the plan sets, such as quality: with
    autoquality: true, even when the plan names a preset missing from
    :presets, and ImagePipe.URL.url/3 returns
    {:error, {:invalid_request, issues}}. Before, ImagePipe.URL.url/3
    built a URL the server rejects with 400.

image_pipe

Added

  • A [:source, :svg_parse] span wraps parsing an SVG watermark, and names
    the limit that rejected it, such as :svg_bytes. The default Logger logs
    it with stages: true.
  • The [:parse] and [:request] stops and the request summary carry
    :source_url, the request's source as ImagePipe parsed it, decoded from
    src64 or decrypted from enc, and :watermark_source_urls, each
    group's watermark source. Each query value, such as an
    X-Amz-Signature, and a URL's user name and password are replaced with
    REDACTED. The default Logger writes them as source_url= and
    watermark_source_url= fields, and the tracer records them on the
    image_pipe.parse and image_pipe.request spans. See
    [:parse].
  • ImagePipe.cache_stats/1 reports the cache's stored bytes and entries,
    its size cap, its hits, misses, and failed reads and writes since start,
    and the entries it keeps in memory, or refused: true when another node
    owns its root. It also reports how many originals the instance
    remembers. A cache that doesn't report its size within a second has
    available: false. See
    Usage and hit rate.
  • max_remembered_originals sets how many originals an instance remembers
    in memory: a local file's hash, or an origin image's cache headers. It
    defaults to one for each 50 KB of :cache's :max_size_bytes, at least
    10,000 and at most 200,000, and forgets originals that weren't used
    recently first. A remembered file isn't read and hashed again while it's
    unchanged, and the processed images of a remembered origin image are
    served without downloading it again. Each takes about 1 KB for a local
    file and about 1.4 KB for an image from an origin.
  • The X-ImagePipe-Source-Progressive debug header says whether a JPEG
    original is progressive. See
    Debug response headers.
  • ImagePipe.Source.HTTP and ImagePipe.Source.S3 take pool_size, the
    maximum number of open connections to each origin address or S3 endpoint.
    It defaults to 50, the limit that applied before. Over HTTP/1.1, raise it
    when more downloads from one origin run at once, so they don't wait for a
    free connection and fail with 504 after pool_timeout.
  • max_search_pixels limits the pixels that autoquality and max-bytes
    encode in one request, 100,000,000 by default. A search stops early when
    the next quality would pass it. On a 40-megapixel AVIF or WebP result it
    tries two qualities, plus up to two encodes to deliver one, where a search
    with both options could encode up to 11 times and take up to 45 seconds.
    :infinity turns the limit off. The [:encode, :search] stop event
    carries :pixel_limited, and its :limiting_factor can be
    :search_pixels. See
    Limiting quality search work.

Changed

  • A cache whose root can't be created or written logs an error at
    startup that names the root and the reason, such as permission denied,
    and says images are served without being stored. Before, it logged a
    warning that it "could not claim" the root.

  • Breaking: The :error metadata of [:debug, :collect, :error] is
    the module of the raised exception, such as RuntimeError, instead of
    always :error. Handlers that match on error: :error must match the
    module.

  • Breaking: The HTTP source refuses origins in 192.0.0.0/24,
    192.88.99.0/24, and 2001::/23, which are reserved for protocol use
    (the last includes Teredo tunnel addresses). They were treated as
    public. To reach one, set :allow_reserved or list its range in
    :allow in the source's :address_policy.

  • The HTTP source allows an IPv4 origin reached through a NAT64 gateway
    from an IPv6-only network, at an address such as 64:ff9b::93.184.216.34,
    when the IPv4 address is public. Before, every such address was refused.
    An IPv4 range in :allow also matches the NAT64 form of its addresses.

  • Breaking: ImagePipe.Source.Path has only a segments field. Its
    scheme field was always nil in a path an adapter receives, so remove
    scheme: from any pattern that matches the struct.

  • Breaking: storage_inputs is now vary_on, and the
    ImagePipe.run/4 option request_inputs is now vary_values. Rename
    them in your configuration and calls.

  • Breaking: An ImagePipe.Plug mount without :keys raises
    ArgumentError on every request unless its configuration sets
    allow_unsigned: true. Serve public traffic with signed URLs, and set
    allow_unsigned: true only for a mount that nothing outside a private
    network can reach. An instance with allow_unsigned: true and no :keys
    logs a warning when it starts. Running a plan with ImagePipe.run/4 or
    ImagePipe.write/5 needs neither. See Signing URLs.

  • Breaking: The [:request] event's :path, the request summary's
    path, and the url.path trace attribute end at the source: the first
    src, src64, or enc segment is followed by REDACTED, and the rest
    of the path and the query string are left out, as in
    /w=200/src/REDACTED. Each wm-src64= value is REDACTED too,
    including the rest of a standard-base64 value split at its /. A path
    without a source keeps its query string, and url.query, with every
    value replaced with REDACTED. Read the source from the new
    :source_url instead. Before, the path was as sent, with only the values
    of a fixed list of credential parameters redacted, so values such as a
    token or api_key were logged.

  • Breaking: enc and wm-enc= sources are now visible to telemetry
    handlers, logs, and traces, decrypted, in :source_url and
    :watermark_source_urls. Source encryption hides a source from whoever
    requests the image, not from whoever reads your telemetry. If you send
    telemetry somewhere you don't want your source URLs, drop these keys in
    your handler.

  • A request with crop=100pct,100pct and no crop-ratio, or with
    region=0,0,100pct,100pct, is processed as the same request without them,
    since they keep the whole image, and gets its cache key and ETag. The
    [:request, :ignored_options] event, and with debug the
    X-ImagePipe-Ignored-Options header, list the crop or region and the
    options only it made useful, such as anchor.

  • Requests that use autoquality or max-bytes have new ETags, so
    clients and CDNs download them again once after the upgrade.

  • A filesystem cache misses once after upgrading for every image it stored
    before, and its startup sweep deletes those entries.

  • A filesystem cache's startup sweep reads its metadata files only when the
    cache was last checked by an ImagePipe version with another metadata
    format, so restarting a cache with many entries reads far less from disk.
    The cache records the format it checked in .metadata-version in its
    root. The first start after this upgrade still reads every metadata
    file.

  • Breaking: :max_blur_work limits the total work of all blur,
    progressive-blur, and sharpen steps in a request, across all its
    groups, instead of each blur on its own. Each step counts the pixels of
    its image times (sigma² + 10,000), so a single blur may use a slightly
    smaller sigma than before (up to 624 on a 10-megapixel image). A
    progressive blur counts (2 × sigma² + 50,000), so it allows a smaller
    sigma (up to 418 on a 10-megapixel image), and the default limit refuses
    any progressive blur on a 100-megapixel image. A request over the limit
    fails with 422 and the message "blur and sharpen work exceeds the
    limit". Raise the limit if your requests chain large blurs, or set it to
    :infinity to turn it off. See
    Limiting blur and sharpen work.

  • When a cache with max_size_bytes is full, it keeps images made from
    progressive JPEGs longer, since they cost more to render again than
    images from baseline JPEGs of the same size. Their :estimated_cost_us
    on [:cache, :write] is higher.

  • A detect or anchor=smart-face crop of a large JPEG, WebP, or SVG with
    a small crop box decodes at a reduced size. Detection's minimum of 640
    pixels on the long edge applies to the whole image the detector searches,
    not to the crop box. A crop box that covers the whole image runs no
    detection, so without a fit=cover resize after it, it decodes as small
    as the output allows.

  • Concurrent requests for one new original share its download in more
    configurations, and no longer wait for a download they would repeat. See
    request coalescing.

    • Requests for an original from an HTTP or S3 source download it once,
      whatever size or format they ask for, with or without a cache, while
      up to 4,096 originals are being fetched at once. Before, without an
      originals cache, requests for the same processed image waited for the
      first download and then downloaded it again, and requests for other
      sizes each downloaded it themselves.
    • Requests for an original from a source with caching turned off
      download it at the same time instead of one after another.
    • When the original may not be stored, waiting requests start their own
      downloads as soon as the origin responds, instead of after the first
      download finishes.
  • gradient on an 8-bit image takes less time, since it blends in one pass.
    A blended color can differ from before by one level.

  • autoquality on an image with transparency takes less time. Each quality
    it tries is scored against a dark and a light background at the same time
    instead of one after the other.

  • [:cache, :coordination] events for :source come only from requests
    that download an original, check a remote original with its origin, read
    a local file, or wait for another request doing so. No event is emitted
    for an original from a source with internal_cache: :disabled, or for a
    local file from an immutable source. :source can report :bypass with
    reason: :not_stored when the original may not be stored.

  • Breaking: :presets must be a map. Change a list of
    {name, fragment} pairs to a map such as %{"card" => "w=400"}.

  • Breaking: An ImagePipe.Source.S3.CredentialProvider's
    fetch_credentials/1 takes only the provider's options, without the
    bucket, and ImagePipe.Source.S3.CredentialWarmup has no :scope
    option. Credentials are now cached per provider and options and shared
    by every bucket. Remove the bucket argument from fetch_credentials,
    give buckets that need different credentials different provider options
    under :buckets, and remove scope: from each warmup, keeping one per
    provider and options.

  • A WebP original scaled down a lot matches the same image as a JPEG or PNG
    original. Like a JPEG, it's decoded at 1/2, 1/4, or 1/8 of its size, the
    smallest that stays at or above the requested size, and the resize does the
    rest. Before, it was decoded straight at the requested size, so a thumbnail
    under about 200 pixels wide differed visibly from the JPEG or PNG one.

  • A cache's max_size_bytes counts each entry's body rounded up to whole
    4 KiB blocks, plus one block for its metadata file, instead of the body
    alone, so many tiny entries, such as BlurHash or LQIP responses, can't
    take far more disk than the cap. A cache whose entries now count past its
    cap evicts down to it at startup after the upgrade.

  • When a cache with max_size_bytes is full, a new processed image that
    many requests want is kept sooner. The cache now counts each request that
    misses it, including requests that wait for another request's copy,
    instead of counting how often it's written.

  • A cache with max_size_bytes uses less than half the memory to track
    its entries: about 175 bytes per stored image instead of over 400, which
    saves at least 45 MB for a cache of 200,000 images.

  • The default Logger shows a coordination event's reason after its result,
    as in bypass (deadline). It logs a :bypass with reason :not_stored
    at its base level.

  • An HTTP source whose host doesn't resolve fails with
    {:source, :unresolved_host} in logs and telemetry instead of
    {:source, :denied_address}. It still answers 404.

  • autoquality with max-bytes scores only the quality it delivers, not
    each quality it tries to fit the byte limit, and reuses the encodes
    autoquality already made to narrow that search, so such a request takes
    less time. [:encode, :search, :probe] spans of the :cap phase no
    longer carry :score.

  • The hash and timestamps kept for each local file, and the cache headers
    kept for each origin's image, are held in memory instead of in the
    processed-image cache, for up to max_remembered_originals originals.
    They no longer take its space or get evicted with its images. After a
    restart, a local file is hashed again, and an image from an origin is
    downloaded again, on its first request. Remembering them uses none of the
    cache's 64 concurrent writes, so it never causes a processed image's
    write to be dropped.

  • A cache with max_size_bytes deletes a rejected write without syncing it
    to disk. Once the popular images don't fit in the cache, that saves most
    of its write I/O. A write that waits more than 5 seconds to be kept or
    rejected is dropped and reported as error: :admission_timeout.
    [:cache, :admission] is now emitted before the sync, and its
    :victim_count counts the entries the write was compared with.

  • [:transform, :detect, :skipped] is also emitted when anchor=smart-face
    uses attention because detector_required is set and the face model isn't
    loaded yet (result: :not_ready) or the detector has no face model
    (result: :unavailable). The default Logger logs both at warning.

  • When an HTTP source follows a redirect to another host, port, or scheme,
    the request no longer carries the headers or credentials set in
    req_options, such as an API key. Before, only authorization,
    proxy-authorization, and cookie were dropped. A redirect from https
    to http, or to a URL with a user name or password, now fails with
    502.

  • A blur, progressive-blur, or sharpen result is copied to memory
    before a later resize or a step that rereads pixels (rotate, trim,
    pixelate, blur, progressive-blur, sharpen, or a crop with
    anchor=smart or detect). Before, each of those steps recomputed the
    blur for every region it read, so a request could take many times the
    CPU that max_blur_work allows. A rotate by an angle other than a
    quarter turn is copied the same way before such a step.

  • A request for a cached image from a local file no longer opens the file
    while its size and timestamps are unchanged.

  • A cache with max_size_bytes keeps its counts of the requests that miss
    each image in 4 MB per 50 GB of max_size_bytes, allocated when it
    starts. Before, a busy 50 GB cache's counts grew to about 70 MB and slowed
    its garbage collection.

  • A downloaded original is written to disk in 64 KiB batches instead of one
    write per network read, which takes less CPU for a large original.

  • With format=jpeg, pad and extend fill with white directly, instead
    of adding transparency that the encoder then flattens onto white. This
    takes less time to encode and gives the same image, unless
    :max_result_width, :max_result_height, or :max_result_pixels then
    scale it down. Then pixels next to the padding can differ.

  • A detect or anchor=smart-face request decodes and buffers less when no
    detection will run:

    • The detector isn't available for any of the requested classes.
    • With detector_required: true, the detector's face model isn't ready
      yet, and the request uses anchor=smart-face.
    • The image already has the fit=cover target's aspect ratio, so the crop
      can't move.

    When the crop falls back to attention cropping, it runs on the smaller
    decode, so its result can differ slightly.

  • Pixels of a source whose embedded RGB profile matches sRGB to within 1
    level are used as sRGB, without converting them through the profile, for
    the output, for placeholders, and for bg, gray, bitonal,
    monochrome, duotone, colorize, gradient, trim, and wm. The
    output still embeds the profile with profile=preserve. With the libvips
    operation cache off, this saves about 7 ms on a small JPEG resize and up
    to about 220 ms on a large trim request. Some pixels can differ from
    before by 1 out of 255.

  • The [:request] event's :path, and the request line and the
    url.path trace attribute that come from it, include the path that
    ImagePipe.Plug is mounted at, such as /images for
    forward "/images", ImagePipe.Plug. Before, the mount path was left out.

  • A FileSystem cache keeps the metadata of recently served entries in
    memory, so a repeated hit reads only the body file from disk. It takes
    about 36 MB for a 1 GB cache, and at most about 90 MB unless
    max_remembered_entries sets the number of entries. See
    Cache process and restarts.

  • A second instance whose cache root another instance in the VM already
    uses raises ArgumentError naming the root when it starts. Before, its
    start failed with {:already_started, pid}.

  • The cache, downloads, and decoding no longer queue their file operations
    behind the Erlang VM's single file-server process.

  • An instance keeps the bytes of up to 64 recently used watermark images in
    memory, so a watermark from an origin is no longer downloaded for every
    processed image. See What is stored.

  • The processing pool's max_queue defaults to 1,024 instead of 64, so a
    burst of quick requests waits for a turn instead of answering 503. See
    choosing values.

  • With meta=strip, a JPEG, WebP, or AVIF image at 72 dpi no longer
    carries an EXIF block, so it's about 190 bytes smaller. An image at
    another density, set by the dpi URL option or the stripped_dpi
    setting, keeps an EXIF block with only the density. PNG still stores its
    density in pHYs.

Removed

  • Breaking: The originals cache: the input_cache option, and the file
    source's copy option, which kept copies of local files in it. Remove
    them from your configuration, and delete the originals cache's directory.
    The [:cache, :input] event and the request summary's :input_cache
    field are gone, and [:cache, :sweep] no longer reports :pins.
  • Breaking: The ImagePipe.Plug option allow_debug_headers. A
    request's debug option always adds the debug headers. Remove the option
    from your configuration. With signed URLs, only your app can add debug.
  • Breaking: The ImagePipe.config/1 option request_watermarks.
    Requests may always name their own watermark with wm-src64 or wm-enc,
    and its image still has to come from a configured source, like the main
    image. Remove the option from your configuration.
  • Breaking: max_preset_lookups. A request may look up any number of
    presets. Remove the option from your configuration. ImagePipe.run/4
    no longer returns {:preset, :too_many_presets}, and [:preset, :lookup]
    no longer reports that reason.
  • Breaking: The instance option mounts:, and mount: on
    ImagePipe.Plug and ImagePipe.url_config/2, which is now
    ImagePipe.url_config/1. An instance checks URLs against its own URL
    settings only. To serve URLs with different keys, such as signed and
    unsigned URLs, start an instance for each, and mount it or build its URLs
    by its name. Instances that use caches each need their own cache roots.

Fixed

  • A debug response from a configuration without a cache leaves out
    X-ImagePipe-Cache-Key, as documented. Before, it carried a key that
    nothing could be stored under.
  • When a later preset or URL option unsets or replaces a value from a
    preset or the request defaults, the value that one replaced still
    applies. With
    request defaults of format-q=avif:50 and a preset q80 defined as
    q=80, /preset=q80/q=unset encodes AVIF at 50. A preset that names
    other presets gives the same result as listing them in the URL. Requests
    that hit these cases get new cache keys and ETags. See
    clearing inherited values.
  • A request that uses a preset your :preset_lookup returns, naming a
    watermark the configuration doesn't define, or carrying a wm-enc token
    that no key in :source_encryption_keys decrypts, answers 500, and
    ImagePipe.run/4 returns {:preset, :invalid_definition}, like other
    invalid stored presets. Before, it failed with 400 or 404, as if the
    requester had made a mistake.
  • The daily sweep of a filesystem cache's leftover files keeps running after
    one of the cache's processes restarts, and after a sweep fails. Before, it
    stopped until ImagePipe restarted.
  • The request summary and the default Logger's request line carry :reason
    for a 403 invalid signature, a 400 for a signed URL sent to a mount
    without signing keys, a 410 expired URL, and a 404 concealed source
    that doesn't decrypt. Before, these lines had no reason.
  • A crash report from a streaming response, or from
    ImagePipe.Source.S3.CredentialWarmup, no longer prints the configured
    origin credentials, such as S3 keys and authorization headers.
  • A request path with bytes that aren't valid UTF-8 no longer makes the
    telemetry path invalid UTF-8. They become U+FFFD.
  • ImagePipe.config/1 raises a duplicate-option error for an option given
    more than once. Before, it raised an unknown-option error for it.
  • Requests that differ only in the header or cookie values named in
    vary_on share one download of their original and one remembered
    original. Only the processed images differ. A new cachebuster still
    downloads the original again.
  • A preset or :request_defaults with a wm-enc token that the
    :source_encryption_keys can't decrypt raises ArgumentError when
    ImagePipe starts. Before, the configuration was accepted and every request
    using the token failed with 404.
  • A preset with wm-enc=unset no longer needs :source_encryption_keys.
  • The request defaults apply to a URL's first group even when that group
    holds only unset options. Before, they moved to the next group, so
    w=unset/-/blur=2 with a default w=200 was still resized to 200
    pixels wide.
  • A cached image whose delivery failed after its headers were sent logs its
    request line at error, with status 200. Before, the line had no status
    and logged at warning.
  • A request that stopped waiting for another request's download of an
    original, just as that download finished, no longer keeps the original
    locked, even when the server takes more than 5 seconds to respond. Later
    requests for that original waited behind the lock until the process that
    served the request exited.
  • An enlarge or a kernel=nearest resize of an image with an EXIF
    orientation, rotate, or flip now gives the same pixels as rotating or
    flipping first and then resizing. Before, enlarged pixels could be off by
    a fraction of a pixel, and a kernel=nearest downscale picked different
    source pixels.
  • An image delivered in sRGB without a color profile, with its metadata
    kept, has the EXIF ColorSpace tag set to sRGB when the original has the
    tag. Before, the tag said Uncalibrated.
  • An image decoded while its original downloads no longer waits behind
    newer requests for its encode. The encode's turn in the processing pool
    goes ahead of waiting requests and keeps the deadline of its first turn.
    Before, it queued at the back and could fail with a 503 after its
    decode had finished.
  • An image whose EXIF date fields aren't plain numbers (such as
    2024:01:01 02+02:00:00) or aren't stored as text no longer fails with
    a 500 when metadata is stripped. All of its metadata is removed,
    including the copyright and artist that keep_copyright would keep.
  • A watermark with an opacity below 1 blends with the image at its exact
    faded alpha. Before, the faded alpha was rounded to a whole level first,
    so a watermark's edges or faint areas could be off by one level.
  • An S3 credential fetch from a provider that hangs fails after 30 seconds,
    and the next request fetches again. Before, a hung fetch held up every
    request for those credentials until the server restarted.
  • An HTTP source fetch that runs out of fetch_timeout while waiting for a
    pooled connection reports :receive_timeout in logs and telemetry
    instead of :pool_timeout. When the source's host resolves to several
    addresses, a fetch no longer tries the next one with less than 100 ms of
    fetch_timeout left.
  • A request with format=webp and lossless webp-options no longer fails
    with 400 when it, a preset, or the request defaults set autoquality or
    max-bytes. The options are ignored, as with format=png.
  • A preset group holding only unset options is kept when the preset is
    compiled, so a first group like that clears the request defaults, as the
    same groups written in the URL do. Before, the defaults applied to the
    preset's next group: blur=unset/-/w=100 with a default blur=3 still
    blurred. A preset with such a group, in any position, is a pipeline
    preset, so adding other options to it answers 400.
  • A :presets option that isn't a map with string keys, such as a string
    or nil, raises an ArgumentError that names :presets instead of a
    Protocol.UndefinedError.
  • A region whose x or y is 0pct gets the cache key and ETag of the same
    region written with 0.
  • A detector whose warmup/1 raises no longer stops its ImagePipe
    instance. The warmup logs a warning and retries, as it does for
    {:error, reason}. A detector whose available?/1 raises is skipped
    with a warning. Before, these crashes used up the instance's restart
    limit, and every later request raised. A crash report from the warmup no
    longer prints the detector's options.
  • detect=all:3, one weight for every class, gets the cache key and ETag of
    detect=all, since a weight that every class shares doesn't move the
    crop.
  • The request summary of a HEAD request that misses the cache no longer
    reports body bytes (:bytes, and the size in its log line), since none are
    sent.
  • A request whose src URL holds a user name or password that isn't valid
    UTF-8 once decoded keeps its request summary log line.
  • A crop placed by an anchor, including the default center, or by a focus
    point, followed by a resize, now lands where it would on a full-size
    decode. Before, a source decoded at a reduced size for the resize could
    place it up to one output pixel off.
  • Downscaling a 16-bit image or watermark without transparency, including
    the scale-down to fit the result limits or a format's size limit, no
    longer darkens it slightly. Each value lost about 0.1%, so white came
    back as 65455 instead of 65535.
  • An image scaled down to fit :max_result_width, :max_result_height,
    :max_result_pixels, or a format's size limit keeps the exact colors of
    its semi-transparent pixels and uses the request's kernel. Before, a
    semi-transparent pixel could lose a level, so [200, 120, 37, 100] became
    [200, 119, 37, 100], and a kernel=nearest result was smoothed.
  • When the first load of the face or object model failed, each detection
    request waiting for it tried the load again in turn, so the last one
    waited for every attempt. Those requests now fail with the first load's
    error at once: the crop falls back to attention, or the request fails with
    500 when detector_required is set. The next request loads again.
  • png-options=palette,bitdepth:16 multiplies 8-bit color by 257, as
    bitdepth:16 without palette does. Before, it multiplied by 256.
  • png-options=bitdepth:1, 2, or 4 on a 16-bit color or gray-and-alpha
    result encodes at 8 bits, as on an 8-bit one. Before, it encoded at
    16 bits.
  • A crop-ratio far from the crop size's own ratio, such as 1:4000
    for a square crop, no longer fails with a 500. Each side of the crop
    keeps at least one pixel.
  • A client error, such as a 404 for a missing source, on a URL with
    expires caps a host's Cache-Control at the URL's expiry, as an image
    response does. Before, a host's max-age=86400 kept the error cached
    after the URL expired. Errors found while parsing the URL, and the 410
    for an expired URL, keep the host's Cache-Control unchanged.
  • A thin crop or region of a JPEG or WebP image blends in far fewer
    pixels from just outside it. A crop under 16 pixels on its short side,
    such as an 8-pixel strip, decodes at full size and takes in none.
    Otherwise the reduced-size decode shrinks by at most an eighth of the
    crop's short side.
  • When none of an HTTP source's addresses accepts a connection or answers
    in time, the source keeps its cached DNS answer while it looks the host
    up again, and replaces the answer only when it changed. Before, the
    cached answer was dropped first, so other requests to that origin waited
    for a new lookup.
  • output=info reports an exact dpr for a tall, narrow image that the
    result limits scale down. Before, a 7×4000 image scaled to 2×1000
    reported dpr 0.29 instead of 0.25. The :scale measurement of
    [:output, :clamp] now uses the long side too.
  • A URL's anchor-offset, extend-at, or extend-offset no longer
    replaces the crop target or canvas from a preset or the request defaults,
    and its unset removes only that option. Before, anchor-offset=5,5
    dropped a preset's anchor. See
    presets.
  • A trim tolerance with hundreds of digits no longer fails with a 500.
    Tolerances above 2147483647 now fail with a 400.
  • Resizing a JPEG or WebP image only a few pixels high or wide, such as a
    2000×3 image at w=100, no longer fails with a 415. Neither does
    shrinking a WebP so far that its short side would be under one pixel, such
    as w=1/dpr=0.5.
  • S3 credentials that a provider returns with less than five minutes left
    are fetched again at half their remaining time, instead of every five
    seconds until the provider rotates them.
  • A cache's own events, [:cache, :admission], [:cache, :eviction, :stop],
    and [:cache, :sweep], use the instance's telemetry_prefix. Before, they
    always used [:image_pipe]. Handlers attached for a custom prefix,
    including attach_default_logger(prefix: ...) and attach_tracer, now get
    them, and handlers attached under [:image_pipe] for such an instance no
    longer do.
  • An issue that ImagePipe.validate/2 or ImagePipe.run/4 reports, and
    the :locations of [:request, :ignored_options], name the group as it's
    written in the URL or in the URL builder. Before, when a group held only
    unset options, issues in later groups named the group before theirs.
  • png-options=bitdepth:1 on a grayscale result now turns values below 128
    black and the rest white. Before, every gray above black came out white.
  • With a processing pool, a request that
    waits for its turn while its original is still downloading:
    • Answers 503 once it has waited queue_timeout. Before, it waited
      for a second turn after the download finished.
    • Answers an error from processing, such as a 415 for an image that
      can't be decoded, without waiting for a second turn. Before, it could
      answer 503 instead when that wait timed out.
    • No longer fails with 503 60 seconds after its original finished
      downloading when queue_timeout plus processing_timeout allows
      longer.
  • S3 credentials whose access key, secret, or session token holds a line
    break or NUL are rejected: static ones when the configuration is checked,
    and ones from a credential provider when they're fetched, which answers
    500 and logs which field was invalid. Before, every fetch with them
    failed with 502, or with 404 when the secret held one.
  • A detect or anchor=smart-face crop whose detector isn't available in
    this build no longer adds a detection to the work the cache estimates for
    the image, which the cache uses to choose what to evict and reports as
    :estimated_cost_us on [:cache, :write].
  • The default Logger logs every 4xx request line at :warning. Before, a
    4xx for a failed decode, an input limit, a transform, or an invalid
    source logged at :error.
  • A request with an invalid source has the result :parser_error, not
    :processing_error. Like other parser errors, its
    [:request] stop from ImagePipe.run/4 has no :error.
  • The request line for an invalid source or invalid output gives the
    reason: reason="invalid source" or reason="invalid output".
  • A 16-bit original delivered at 8 bits per channel has each value rounded
    to the nearest 8-bit level. Before, about a quarter of the values were one
    level off, so 65400 became 255 instead of 254. This also applies to
    png-options=bitdepth:8 with hdr=preserve.
  • A request that chains many operations, such as a watermark in each of a
    dozen groups, no longer crashes the Erlang VM. libvips ran out of stack
    rendering the chain. The image is now copied to memory once six
    operations have run since its last copy, which keeps the chain short.
  • png-options=palette gives a palette PNG from a 16-bit result, such as
    one with hdr=preserve. Without a bitdepth, the palette was dropped and
    the PNG stayed 16-bit.
  • With meta=keep and profile=srgb, display-p3, or adobe-rgb, the
    kept EXIF no longer carries the original's white point and primaries, and
    its ColorSpace tag is sRGB only for profile=srgb and Uncalibrated
    otherwise. Before, a Display P3 result could keep ColorSpace sRGB from its
    original.

Security

  • Breaking: ImagePipe.Source.S3.ContainerCredentials rejects a
    base_url that doesn't use https, a loopback host, or the ECS or EKS
    credential endpoint, as it already did for full_uri. Before, a
    relative_uri sent the authorization token to any base_url. The default
    base_url is unaffected. Change any other base_url to use https or a
    loopback host.
  • Redacting a request path for telemetry, and echoing it in a 400
    response, take time in proportion to the path's length. A path with many
    wm-src64= segments took seconds to redact, and other crafted 10 KB
    paths took tens of milliseconds. Telemetry redacts every request path
    before its URL signature is checked, so an unsigned request could cause
    this.
  • Plain-text error responses, such as the 400 and the 405, now send
    X-Content-Type-Options: nosniff. A 400 body quotes the request
    path, so a browser that sniffs content types could otherwise render it as
    HTML.
  • The default Logger and the span tracer cap how much of each error reason
    and attribute value they print, so a large term can't produce an
    unbounded log line or span. Before, the Logger relied on Logger's own
    truncation, which a host can turn off.