Skip to content

IIIF v3: confined size (!w,h) should clamp to region instead of rejecting - #287

Open
avalyset wants to merge 3 commits into
ruven:masterfrom
avalyset:fix-271-confined-no-upscale
Open

IIIF v3: confined size (!w,h) should clamp to region instead of rejecting#287
avalyset wants to merge 3 commits into
ruven:masterfrom
avalyset:fix-271-confined-no-upscale

Conversation

@avalyset

Copy link
Copy Markdown

Fixes #271.

In IIIF Image API v3, a confined size request !w,h against a region smaller
than the box returns HTTP 400 ("upscaling should be prefixed with ^") instead
of the region at its own size.

Spec

v3 §4.2, !w,h: the result must be "as large as possible but not larger than
the extracted region, w or h", aspect preserved. Upscaling uses the separate
^ prefix. So !300,300 on a 200×200 region should return 200×200, not 400.

Cause

The upscale guard in IIIF.cc (around line 551) throws whenever a requested
dimension exceeds the region, with no special case for the confined ! flag.
! is stripped at line 505 without recording that the request was confined, so
later there's no way to tell it apart from a forced w,h. maintain_aspect
doesn't help — it's also set for ,h and w,, which are genuine upscale
requests that should still throw.

One thing to flag: the downstream aspect fit in View::getRequestSize() has no
no-upscale clamp, so this guard is currently the only thing preventing
upscaling. The clamp therefore has to happen here, in the parse path.

Fix

Record a confined flag where ! is detected. In the v3 upscale guard, when
the request is confined and exceeds the region, clamp the requested dimensions
to the region instead of throwing; the downstream aspect fit then produces the
correct result. Forced w,h and single-dimension ,h / w, still throw, and
^!w,h still upscales.

The clamp is per-dimension against the region rather than reusing the nearby
max_size clamp, because the region can be non-square while max_size is a scalar
cap.

Verification

Confirmed against the parse logic at IIIF.cc:479-571 (current master), run
through the full chain including View::getRequestSize():

size (v3, no-upscale) source result
!300,300 200×200 200×200 (was: 400)
300,300 (forced) 200×200 400 (unchanged)
,300 / 300, 200×200 400 (unchanged)
^!300,300 200×200 300×300 (unchanged)
!300,300 400×200 300×150
!300,300 100×200 100×200

iipsrv has no test suite in-tree, so this was verified with an isolated harness
running the parse block verbatim. A no-upscale test case modelled on the IIIF
validator's size_noup.py would be the natural place for this to live
permanently.

In IIIF Image API v3, a confined "!w,h" request whose box is larger than
the source must return the largest image that fits within the region
("as large as possible but not larger than the extracted region", §4.2),
not be rejected. Previously any v3 request exceeding the region with
upscaling disallowed threw "upscaling should be prefixed with ^",
including confined requests on small sources.

Track confined "!w,h" requests with a dedicated flag and, in that case,
clamp the requested size down to the region rather than throwing; aspect
ratio is preserved downstream in View::getRequestSize(). Forced "w,h" and
single-dimension "w,"/",h" requests that exceed the region are genuine
upscaling and continue to be rejected.

Closes ruven#271

Co-authored-by: Claude <noreply@anthropic.com>
@avalyset
avalyset force-pushed the fix-271-confined-no-upscale branch from 4fe63f5 to f8a252f Compare June 16, 2026 06:53
@avalyset

Copy link
Copy Markdown
Author

Sonar's gate flags three smells on the new lines, but two of them are the
existing style of this file rather than anything the change introduces:

  • The S6178 hint (use starts_with instead of substr(0,1) ==) is a C++20
    rule. iipsrv builds as C++11 (configure.ac), where starts_with isn't
    available — and every prefix check in IIIF.cc (^, pct:, !) already uses
    the substr form. Switching would break the build.

  • The S134 nesting flags sit on the confined branch and the clamp block. The
    depth comes from the surrounding code they live in: the clamp mirrors the
    existing max_size block right below it, and the same pattern appears ~18
    times across IIIF.cc on master. Matching that structure seemed better than
    making these few lines stricter than everything around them.

I did take the one improvement that fit: the clamp now uses std::min against
the region dimensions instead of nested ifs, which drops one of the three.

Happy to refactor the nesting further if you'd rather have the gate green — I
just didn't want to reshape the surrounding block without your call.

avalyset and others added 2 commits June 27, 2026 19:32
On MSVC, <windows.h> defines min/max as macros, so std::min(...) expands
to std::(...) and fails to compile (C2589/C2059 at IIIF.cc:566-567). The
codebase uses no std::min/std::max elsewhere; replace the two calls with
an equivalent ternary clamp, matching the surrounding style.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…ar SonarCloud new-code smells

The ruven#271 fix had modified the "!w,h" prefix line (adding braces + confined=true),
which made Ruven's pre-existing S134/S6178 smells on that line count as new code.
Restore that line byte-identical to master and detect the confined flag on a
separate single-char check (no substr prefix, so no S6178), so those pre-existing
smells revert to the baseline. Also restructure the upscaling branch into an
over_region guard with sibling if/else-if (depth 3) plus a clamp_to_region helper,
removing the depth-4 nested if (S134). Behaviour and the throw message are
identical; equivalence of the confined check verified across empty/"!"/non-"!".

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@sonarqubecloud

Copy link
Copy Markdown

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Images smaller than the size parameter constraint can't be returned using !w,h (IIIF V3)

1 participant