The first rotation pass picked CW server-side / CCW preview-side based on
"ribbon on left" → user rotates frame 90° CCW. On hardware the photo came
out upside down, which means the user's physical rotation is the opposite
of what was assumed: 90° CW from landscape native, putting the ribbon to
the left from the user's POV but to the right from the EPD's reference
frame. The two rotation signs always need to stay opposite — flipping
both keeps the webapp preview upright while fixing the device.
Also drops the temporary upload debug log; the cropOrientation persistence
issue resolved on its own once Doctrine's metadata cache was cleared.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
StickerCanvas was being passed contextOrientation (the target device's
orientation), so the final composited.jpg was always sized to the device's
aspect — even when the user toggled the crop tool to a different orientation.
A landscape crop on a portrait device would produce a 1600x960 cropped
blob, then the StickerCanvas would re-render it into a 960x1600 frame,
visibly stretching the image into portrait dimensions and saving it that
way.
UploadView now derives an effectiveOrientation that prefers the user's
chosen crop orientation (uploadStore.cropOrientation) and falls back to
the device's orientation only before the crop step has run. The
StickerCanvas honors that.
Also adds a temporary debug log in the upload controller to verify the
cropOrientation form field is arriving and being persisted — recent
uploads have NULL cropOrientation despite the frontend sending it, and
this log will make the next upload's payload visible. Will remove once
diagnosed.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The crop tool now exposes a landscape/portrait toggle next to the
device-name label, and the canvas crop frame snaps to the chosen
aspect when toggled. Choosing an orientation that does not match
the target frame's current orientation surfaces a yellow informational
chip — purely informational, no action required, clears as soon as
the user toggles back to the matching orientation (or changes the
frame in Settings).
The chosen orientation rides along on the upload/reprocess request
as a new cropOrientation form field and is persisted on the Image
entity, so the library view and rotation logic can later surface
the same mismatch state for already-uploaded photos. Existing photos
without a stored orientation get null and are unaffected.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>