Feature: Crop and resize step in the editor's image uploader As a verified RenewCMS owner adding an image to an article I want to crop and resize a JPEG, PNG or WEBP before it uploads So that I control the framing and file size of images in my articles, without needing an external editor Background: Given ImageUploader inserts a crop-and-resize step between the idle file picker and the upload, for any JPEG, PNG or WEBP that already passed the browser validation described in features/editor_image_upload.feature; that feature's idle, dragging-over, rejected, uploading, inserted and failed states and its POST /v1/article/{articleId}/image contract are unchanged and out of scope here And an SVG file skips this step entirely and uploads immediately, as before And opening the step retitles the modal from "Add an image" to "Crop and resize", with the description "{filename} · {size}" (e.g. "north-wall.jpg · 3.9 MB") And the stage shows the chosen image at its natural pixel dimensions with a crop frame that starts as the whole image: a rule-of-thirds grid (lines at 33.3 %/66.6 %), four 12×12 px corner handles, and a mono pixel-size label in a dark pill at the frame's bottom-left corner, tracking the frame's current size And the frame is reachable by Tab (tabindex="0"), aria-label "Crop area. Arrow keys move it, Shift and arrow keys resize it."; dragging the frame moves it, dragging a handle resizes it, arrow keys nudge it by 1 px, and Shift+arrow keys resize it by 1 px And above the frame, "Aspect ratio" is a radiogroup of five pills — Original, Free, 16:9, 4:3, 1:1 — next to "Rotate" and "Reset" actions; "Width" is a second radiogroup — Original, 1600 px (default), 1200 px, 800 px And a mono line below reads "Uploads as {width} × {height} px · about {size}" (KB under 1 MB, MB at or above it), recomputed whenever the ratio, width, frame rect or rotation changes And the output width is min(the chosen width preset, the crop frame's own pixel width) — never upscaled — and the output height keeps the crop frame's own aspect ratio; when the crop is narrower than the chosen width, an info row reads "The crop is {crop width} px wide, so it's uploaded at that size. Enlarging it would make it blurry." And the footer offers "Choose another file" (discards this step and returns to the idle file picker with no adjustments carried over) and "Upload image" (uploads the cropped, resized file through the unchanged POST /v1/article/{articleId}/image flow) And "Rotate" turns the image 90° clockwise each activation, cycling 0°→90°→180°→270°→0°, and width presets apply to the post-rotation image And "Reset" returns the ratio to Original, the width to 1600 px, the crop frame to the whole unrotated image, and the rotation to 0°, without closing the modal Scenario: A raster file opens the crop-and-resize step instead of uploading immediately Given ImageUploader is open in the idle state for article 12 When the user chooses a file named "north-wall.jpg", content type "image/jpeg", size 3.9 MB, natural dimensions 4032 × 3024 px Then no request is sent to POST /v1/article/12/image And the modal retitles to "Crop and resize" with the description "north-wall.jpg · 3.9 MB" And the crop frame starts as the whole image, ratio "Original" is selected, and width "1600 px" is selected And the mono line reads "Uploads as 1600 × 1200 px · about 420 KB" And the footer reads "Choose another file" and "Upload image" Scenario: An SVG skips the crop step and uploads immediately Given ImageUploader is open in the idle state for article 12 When the user chooses a file named "diagram.svg", content type "image/svg+xml", size 40 KB Then POST /v1/article/12/image is sent immediately with that file And the crop-and-resize step is never shown Scenario Outline: Choosing an aspect ratio locks the frame to it Given the crop-and-resize step is open for "north-wall.jpg" (4032 × 3024 px), ratio "Original", width "1600 px" When the user selects ratio "" Then the crop frame becomes × px, centered on the image And dragging or resizing the frame afterwards keeps that aspect ratio And the mono line reads "Uploads as × px · about {size}" Examples: | ratio | crop_width | crop_height | out_width | out_height | | 16:9 | 4032 | 2268 | 1600 | 900 | | 4:3 | 4032 | 3024 | 1600 | 1200 | | 1:1 | 3024 | 3024 | 1600 | 1600 | Scenario: Choosing a narrower width preset recomputes the output without upscaling Given the crop-and-resize step is open for "north-wall.jpg", ratio "Original", width "1600 px" When the user selects width "800 px" Then the mono line reads "Uploads as 800 × 600 px · about {size}", a smaller estimate than before Scenario: A crop narrower than the chosen width is uploaded at its own size, not upscaled Given the crop-and-resize step is open for "north-wall.jpg", ratio "1:1", width "1600 px" When the user resizes the crop frame down to 640 × 640 px Then the mono line reads "Uploads as 640 × 640 px · about {size}" And an info row reads "The crop is 640 px wide, so it's uploaded at that size. Enlarging it would make it blurry." Scenario: Free ratio removes the aspect-ratio constraint Given the crop-and-resize step is open for "north-wall.jpg", ratio "Original" When the user selects ratio "Free" Then the crop frame can be dragged or resized to any width and height independently, no longer snapping to a fixed ratio Scenario: Arrow keys move the crop frame and Shift+arrow keys resize it Given the crop-and-resize step is open for "north-wall.jpg" with the crop frame focused When the user presses ArrowRight Then the frame moves 1 px right and the mono line's size is unchanged When the user presses Shift+ArrowRight Then the frame's width grows by 1 px and the mono line updates to the new output size Scenario: Rotate cycles the image in 90° steps and Reset returns to the opened defaults Given the crop-and-resize step is open for "north-wall.jpg" at ratio "16:9", width "800 px" When the user activates "Rotate" Then the image and crop frame rotate 90° clockwise When the user activates "Reset" Then ratio returns to "Original", width returns to "1600 px", the frame returns to the whole, unrotated image, and the mono line reads "Uploads as 1600 × 1200 px · about 420 KB" Scenario: "Choose another file" returns to the file picker without uploading Given the crop-and-resize step is open for "north-wall.jpg" When the user activates "Choose another file" Then no request is sent to POST /v1/article/12/image And the idle state is shown again, with the file picker available Scenario: A newly chosen file opens a fresh crop step, not the previous file's adjustments Given the user returned to idle via "Choose another file" from a crop step that had ratio "16:9" and width "800 px" When the user chooses a new file named "sunset.jpg", content type "image/jpeg", size 2.1 MB, natural dimensions 3000 × 2000 px Then the crop-and-resize step reopens with ratio "Original", width "1600 px", and the frame as the whole new image Scenario: "Upload image" uploads the cropped, resized file, not the original Given the crop-and-resize step is open for "north-wall.jpg" with ratio "Original", width "1600 px" and the mono line "Uploads as 1600 × 1200 px · about 420 KB" When the user activates "Upload image" Then POST /v1/article/12/image is sent with a single file under the field name "file" whose pixel dimensions are 1600 × 1200 and whose size is about 420 KB, not the original 4032 × 3024, 3.9 MB file And the uploading state shows "Uploading 0.0 of 420 KB" When the upload reports 180 KB sent of 420 KB Then the uploading state shows "north-wall.jpg 43%" and "Uploading 180 of 420 KB"