Fixes#211 — print output was mirrored since M12. TargetPageCanvasView.isFlipped = true put the canvas in a top-down CTM where CGContext.draw(_:in:) renders the CGImage upside-down.
PR #212 (now un-merged; merge commit 273c319 removed from develop) set isFlipped = false — correct for the mirror — but left destinationRect(forPage:) anchored at pageRect.minY, which is the page's bottom edge in bottom-up space. Sub-page targets would have printed at the foot of the sheet and oversize targets would clip their top rows. This PR re-lands the un-flip and restores the spec'd top-left anchor (docs/14 §6, D9): y = snap(pageRect.maxY - size.height).
Root Cause Analysis
CGContext.draw(_:in:) draws a CGImage upright in a bottom-up (y-up) CTM and upside-down in a flipped (y-down) CTM — the flipped view produced the mirrored pages.
After un-flipping, pageRect.minY changed meaning from top edge to bottom edge; the anchor expression was not updated.
Existing coverage could not catch either defect: all geometry fixtures are ~full-page rasters (the two anchors differ by ~0.03 pt), NativeSpoolPDFTests located the painted origin dynamically, and every fixture was a solid fill — a mirrored solid block is pixel-identical.
Fix
TargetPageCanvasView: isFlipped = false (bottom-up space); destinationRect anchors y = snap(pageRect.maxY - size.height); stale flipped-space comments corrected (rectForPage stacks bottom-up; page 1 is the bottom band, the non-flipped pagination convention).
TargetCanvasGeometryTests: testDestinationRectAnchorsTopLeftAndSnaps now asserts the top anchor with a sub-page 72×72 pt raster (y == paperHeight - 72) plus the near-full-page clip edge (y ≈ -0.03); testRectForPageStacksBottomUp renamed/documented.
TargetRasterFixtures: makeImage/makeTIFF accept bottomHalfPixelBytes — a top/bottom banded image for orientation assertions.
NativeSpoolPDFTests: new testSavePDFDrawsPageUprightTopAnchored saves a 144 pt red-over-blue block via .save PDF and asserts red band at the painted top edge, blue below it, white beneath the block — fails on both a mirrored draw and a bottom anchor. Shared makeSaveInfo/renderPageToBitmap helpers extracted.
Verification
xcodebuild test -scheme ICCery -destination 'platform=macOS' ARCHS=x86_64 — TargetCanvasGeometryTests, NativeSpoolPDFTests, TargetRasterTests: 23/23 pass, including the new banded-orientation test.
## Summary
Fixes #211 — print output was mirrored since M12. `TargetPageCanvasView.isFlipped = true` put the canvas in a top-down CTM where `CGContext.draw(_:in:)` renders the CGImage upside-down.
PR #212 (now un-merged; merge commit 273c319 removed from `develop`) set `isFlipped = false` — correct for the mirror — but left `destinationRect(forPage:)` anchored at `pageRect.minY`, which is the page's **bottom** edge in bottom-up space. Sub-page targets would have printed at the foot of the sheet and oversize targets would clip their top rows. This PR re-lands the un-flip **and** restores the spec'd top-left anchor (docs/14 §6, D9): `y = snap(pageRect.maxY - size.height)`.
## Root Cause Analysis
- `CGContext.draw(_:in:)` draws a CGImage upright in a bottom-up (y-up) CTM and upside-down in a flipped (y-down) CTM — the flipped view produced the mirrored pages.
- After un-flipping, `pageRect.minY` changed meaning from top edge to bottom edge; the anchor expression was not updated.
- Existing coverage could not catch either defect: all geometry fixtures are ~full-page rasters (the two anchors differ by ~0.03 pt), `NativeSpoolPDFTests` located the painted origin dynamically, and every fixture was a solid fill — a mirrored solid block is pixel-identical.
## Fix
- `TargetPageCanvasView`: `isFlipped = false` (bottom-up space); `destinationRect` anchors `y = snap(pageRect.maxY - size.height)`; stale flipped-space comments corrected (`rectForPage` stacks bottom-up; page 1 is the bottom band, the non-flipped pagination convention).
- `TargetCanvasGeometryTests`: `testDestinationRectAnchorsTopLeftAndSnaps` now asserts the top anchor with a sub-page 72×72 pt raster (`y == paperHeight - 72`) plus the near-full-page clip edge (`y ≈ -0.03`); `testRectForPageStacksBottomUp` renamed/documented.
- `TargetRasterFixtures`: `makeImage`/`makeTIFF` accept `bottomHalfPixelBytes` — a top/bottom banded image for orientation assertions.
- `NativeSpoolPDFTests`: new `testSavePDFDrawsPageUprightTopAnchored` saves a 144 pt red-over-blue block via `.save` PDF and asserts red band at the painted top edge, blue below it, white beneath the block — fails on both a mirrored draw and a bottom anchor. Shared `makeSaveInfo`/`renderPageToBitmap` helpers extracted.
## Verification
`xcodebuild test -scheme ICCery -destination 'platform=macOS' ARCHS=x86_64` — TargetCanvasGeometryTests, NativeSpoolPDFTests, TargetRasterTests: 23/23 pass, including the new banded-orientation test.
Closes #211
226e7b2 un-flipped the canvas but left destinationRect anchored at
pageRect.minY — the page's BOTTOM edge in bottom-up space — so
sub-page targets printed at the foot of the sheet and oversize
targets clipped their top rows. Anchor at pageRect.maxY - height,
restoring the spec'd top-left anchor (docs/14 §6, D9).
Adds the missing regression coverage: the geometry test now asserts
the top anchor with a sub-page raster, and the PDF harness gains a
banded (red-over-blue) fixture that fails on either a mirrored draw
or a bottom anchor — solid fills could catch neither.
gronod
merged commit ddb9245f44 into develop2026-09-17 21:59:47 +01:00
Blocking a user prevents them from interacting with repositories, such as opening or commenting on pull requests or issues. Learn more about blocking a user.
Summary
Fixes #211 — print output was mirrored since M12.
TargetPageCanvasView.isFlipped = trueput the canvas in a top-down CTM whereCGContext.draw(_:in:)renders the CGImage upside-down.PR #212 (now un-merged; merge commit
273c319removed fromdevelop) setisFlipped = false— correct for the mirror — but leftdestinationRect(forPage:)anchored atpageRect.minY, which is the page's bottom edge in bottom-up space. Sub-page targets would have printed at the foot of the sheet and oversize targets would clip their top rows. This PR re-lands the un-flip and restores the spec'd top-left anchor (docs/14 §6, D9):y = snap(pageRect.maxY - size.height).Root Cause Analysis
CGContext.draw(_:in:)draws a CGImage upright in a bottom-up (y-up) CTM and upside-down in a flipped (y-down) CTM — the flipped view produced the mirrored pages.pageRect.minYchanged meaning from top edge to bottom edge; the anchor expression was not updated.NativeSpoolPDFTestslocated the painted origin dynamically, and every fixture was a solid fill — a mirrored solid block is pixel-identical.Fix
TargetPageCanvasView:isFlipped = false(bottom-up space);destinationRectanchorsy = snap(pageRect.maxY - size.height); stale flipped-space comments corrected (rectForPagestacks bottom-up; page 1 is the bottom band, the non-flipped pagination convention).TargetCanvasGeometryTests:testDestinationRectAnchorsTopLeftAndSnapsnow asserts the top anchor with a sub-page 72×72 pt raster (y == paperHeight - 72) plus the near-full-page clip edge (y ≈ -0.03);testRectForPageStacksBottomUprenamed/documented.TargetRasterFixtures:makeImage/makeTIFFacceptbottomHalfPixelBytes— a top/bottom banded image for orientation assertions.NativeSpoolPDFTests: newtestSavePDFDrawsPageUprightTopAnchoredsaves a 144 pt red-over-blue block via.savePDF and asserts red band at the painted top edge, blue below it, white beneath the block — fails on both a mirrored draw and a bottom anchor. SharedmakeSaveInfo/renderPageToBitmaphelpers extracted.Verification
xcodebuild test -scheme ICCery -destination 'platform=macOS' ARCHS=x86_64— TargetCanvasGeometryTests, NativeSpoolPDFTests, TargetRasterTests: 23/23 pass, including the new banded-orientation test.Closes #211