Add one image per place: schema, processing and API (task 249, backend) #192
Loading…
Reference in a new issue
No description provided.
Delete branch "feature/task-249-place-images"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
Backend half of task 249. UI (upload/replace/remove in
PlaceFormPanel, thumbnail and popup viewer inPlaceDetailPanel, WebUI tests, E2E journey) follows in a second PR — this one is schema, domain, infrastructure, endpoints and their own tests, so it's reviewable on its own rather than landing as one large change.What's here
place_imagesrow per place (place_iditself as the PK — a second upload replaces the first viaON CONFLICT), cascading fromplaces. Resize/crop arithmetic lives inPlaceMark.Domainwith zero packages (ImageBoundingBoxCalculator,ThumbnailCropCalculator), proved against plain integers;PlaceMark.Infrastructure.Places.PlaceImageProcessor(SkiaSharp — MIT, licence read before adding; ImageSharp's split licence was considered and rejected) applies it to real pixels, rotates upright from EXIF orientation, then re-encodes with no EXIF. Explicitly reverses ADR-0006's "no photo attachments" exclusion at Rob's own request — ADR-0006 gainsPartially superseded by: 0163for that one bullet only.GET /api/places/{id}/image,.../image/thumbnail), never inlined in the places list.PlaceResponse/PlaceForCallerResponsecarry onlyImageVersion(optional, defaults tonull— no existing call site needed touching). Cachedpublic, max-age=31536000, immutable, keyed by the client appending?v={ImageVersion}rather than a conditional-GET round trip. Corrected during review: this record now says plainly that the WebUI (#194) can't put that URL in the DOM at all — the endpoints need a bearer token a plain<img src>can't carry, so it fetches viaHttpClientand renders adata:URI, which the browser's own image cache never sees.Memberto view,MayEditPlacesto add/replace/delete — the same thresholds the rest ofPlaceEndpointsalready uses. All four image routes carryRequireGroupCapabilityForPlace, none skipped.docs/data-model.mdgets its ownplace_imagessection and FK row;SchemaVersionHealthCheck.RequiredScriptand every test pinned to "the newest script" moved to0005-place-images.sql.Method
Test-first throughout. The two arithmetic rules were each proved to redden against a deliberately wrong implementation (a naive squash to 240×135 for the thumbnail; the FK cascade removed outright) before being trusted. The EXIF rotation is proved against a JPEG this test suite constructs itself — a genuine, standards-compliant EXIF
Orientationtag spliced onto a plain encode, not a downloaded sample.Corrected during review, and stated precisely rather than rounded up:
PlaceImageProcessorTestsasserts real pixel positions after rotation for five of the eight EXIF orientation values — 2, 3, 4, 6 and 8 — each verified to redden against a deliberately wrong transform (6/8 confirmed by swapping their rotation directions). Orientation 1 is the identity transform and needs no positional proof; every other test in the class exercises it implicitly. Orientations 5 and 7 have only the EXIF-stripped theory tests (Process_AnImageCarryingExifOrientation_StripsAllExifFromTheFullImage/...Thumbnail) and no positional proof of their own rotation — disclosed, not silently missing: both are mirrored-and-rotated values a consumer phone or camera essentially never produces, unlike 6 and 8, the two ordinary portrait-hold rotations every phone photograph actually carries, which now do carry positional proof. All positional and stripping assertions run against both outputs; the rotation direction was verified empirically rather than only derived on paper.The endpoint tests found a real bug this way: minimal API's automatic antiforgery requirement for
IFormFile-binding endpoints 500ed until.DisableAntiforgery()was added, which is correct here since this API has no cookie session for antiforgery to protect.Judgement calls worth flagging
ImageVersiondefaults tonullrather than being a required positional member, so this doesn't touchPlaceFormPanel.razor.csor any WebUI test's existing positional construction — deliberately, to keep this PR backend-only and avoid colliding with the parallel agent on task 245.LICENSE.md; present only inPlaceMark.Infrastructure.SOF0marker rather than a giant bitmap.Tests
Rebased twice, onto
mainafter #195 and again after #193 (task 250's tile-buffer/marker-retention change — conflicted only indocs/adr/README.md's generated index, resolved and confirmed identical toAdrIndex.GenerateTable()'s own canonical output viascripts/regenerate-adr-readme.cs). Current counts: 622 inPlaceMark.Api.Tests, 360 inPlaceMark.Infrastructure.Tests, 39 inPlaceMark.Domain.Tests, 131 inPlaceMark.Architecture.Tests, 844 inPlaceMark.WebUI.Tests(840 passing + 4 pre-existing skips) — all green locally against the pinned SDK, plus a full-solution build with zero warnings,dotnet format --verify-no-changes, and the coverage ratchet (every assembly at or above baseline). CI green at the current head.130f5281910593ff13110593ff13119e050624a6Verdict: changes needed
Reviewed at
0593ff13.EXIF orientation 8 has no pixel-position assertion, and the gap isn't disclosed.
PlaceImageProcessorTestsonly asserts pixel positions for orientations 2, 3, 4 and 6 (Process_AnImageWithExifOrientation{6,3,2,4}_...). Orientations 1, 5, 7 and 8 are exercised only by the two[Theory]tests that check EXIF is stripped — those assert nothing about correct rotation. That's four cases without positional proof, not two: the PR describes 5 and 7 as intentionally decode/strip-only (honest), but says nothing about 8, which is a real, non-trivial transform (Translate(0, height); RotateDegrees(270)inApplyOrientation) sharing no code path with 6'sRotateDegrees(90)case. A swapped rotation direction or wrong translate there would silently sideways-rotate real photos from that orientation — exactly the failure mode task 249 calls out — and nothing in this suite would catch it. Please add a positional case for orientation 8 (same technique as the existing 6/3/2/4 tests), or if that's deliberately deferred, correct the claim rather than leaving it unstated. (Orientation 1 is the true no-op case, so its absence is lower stakes, but it's also not disclosed.)Everything else held up:
ImageBoundingBoxCalculator/ThumbnailCropCalculatoragainst every listed test input (panorama, tall, square, already-smaller, exact-fit, sub-135 shrunk height, narrower-than-240 source, odd-remainder crop rounding). All correct; the tests genuinely discriminate a naive squash (they assert the intermediateShrunkHeight, not just final output).SKBitmap.Decodeallocates; the bomb test patches only the JPEG's SOF0 header, proving the check doesn't require materialising the claimed pixel grid.Process.PUT/DELETE/GET image/GET thumbnail) chainRequireGroupCapabilityForPlace,Memberfor the twoGETs andMayEditPlacesfor the writes, reusing the existing ADR-0043 extension rather than a bespoke check. No route skips it.PlaceResponse/PlaceForCallerResponsecarry onlyImageVersion(nullable, defaultsnull);PlaceRepository's new correlated subqueries select onlyxmin, neverfull_image/thumbnail.place_idas PK doubles as the one-image-per-place guarantee;ON DELETE CASCADEproved against real Postgres inDeletingThePlace_CascadesToRemoveItsImage, which would fail (FK violation) rather than pass if the cascade clause were removed.Status, newPartially superseded by) changed; body untouched.ADR-0107's conditions are correctly reasoned through in ADR-0163 rather than assumed. ADR-0164's caching-consequence correction is stated plainly, as requested.LICENSE.mdboth confirm MIT. Package sits only inPlaceMark.Infrastructure.mainrebase (PR #193) rather than authored here — but since it's an increase it doesn't block.The orientation-8 gap is the one thing I'd want fixed (or knowingly and explicitly accepted) before merge; everything else is solid, careful work.
Verdict: mergeable
Reviewed at
450005c5, confirming against the priorchanges needed.x:16,y:16→ Red,x:48,y:16→ Blue), which is the true mirror of orientation 6's expectation, and I independently checked the geometry: CCW rotation moves the source's top edge to the destination's left (red at x:16) and its bottom edge to the right (blue at x:48) — exactly opposite to 6's CW mapping (blue left, red right). SwappingRotateDegrees(270)for90in theLeftBottomcase would produce orientation 6's arrangement instead, which fails this exact assertion — the claimed mutation would genuinely redden it.ALandscapeImage...,AnImageAlreadySmallerThanTheBox..., etc.) only assert width/height, never colour. Low stakes given 1 is a literalsource.Copy()no-op, but worth a follow-up tightening of the wording — not a blocker.PlaceImageEndpoints.cs,PlaceImageProcessor.cs,PlaceMark.Infrastructure.csprojandPlaceRepository.csshow identical diff sizes to the previous review, and the ADR index has all three new rows (0163, 0164, 0165) with nothing else moved.No further changes needed.