@@ -166,35 +166,79 @@ $(function () {
166166 $ ( ".loader" ) . addClass ( 'hidden' ) ;
167167 } ) ;
168168
169+ // --- Image workspace (crop, rotate, OCR results) ---
169170 var $ocrOutputDiv = $ ( '.ocr-output' ) ;
170- if ( $ocrOutputDiv . length ) {
171- // Cropper.
172- const img = document . getElementById ( 'source-image' ) ,
173- x = document . querySelector ( '[name="crop[x]"]' ) ,
174- y = document . querySelector ( '[name="crop[y]"]' ) ,
175- width = document . querySelector ( '[name="crop[width]"]' ) ,
176- height = document . querySelector ( '[name="crop[height]"]' ) ,
177- $modeButtons = $ ( '.drag-mode' ) ;
171+ const img = document . getElementById ( 'source-image' ) ;
172+ const x = document . querySelector ( '[name="crop[x]"]' ) ;
173+ const y = document . querySelector ( '[name="crop[y]"]' ) ;
174+ const width = document . querySelector ( '[name="crop[width]"]' ) ;
175+ const height = document . querySelector ( '[name="crop[height]"]' ) ;
176+ const rotate = document . getElementById ( 'rotate' ) ;
177+ const $modeButtons = $ ( '.drag-mode' ) ;
178+ let cropperInstance = null ;
179+
180+ /**
181+ * Initialize (or re-initialize) Cropper.js on the source image.
182+ */
183+ function initCropper ( ) {
184+ if ( cropperInstance ) {
185+ cropperInstance . destroy ( ) ;
186+ cropperInstance = null ;
187+ }
188+
178189 new Cropper ( img , {
179190 viewMode : 2 ,
180191 dragMode : 'move' ,
181- // Remove double-click drag mode toggling, because we've got buttons for that.
182192 toggleDragModeOnDblclick : false ,
183- // Only show a crop area if it is defined.
184193 autoCrop : width . value > 0 && height . value > 0 ,
185194 responsive : true ,
186195 ready ( ) {
196+ cropperInstance = this . cropper ;
197+
187198 // Make textarea match height of image.
188199 $ ( '#text' ) . css ( {
189200 height : this . cropper . getContainerData ( ) . height ,
190201 } ) ;
191202 // React to changes in the crop-mode buttons.
192- $modeButtons . on ( 'click' , event => {
203+ $modeButtons . off ( 'click.cropmode' ) . on ( 'click.cropmode ' , event => {
193204 const $button = $ ( event . currentTarget ) ;
194205 $modeButtons . removeClass ( 'active' ) ;
195206 $button . addClass ( 'active' ) ;
196207 this . cropper . setDragMode ( $button . data ( 'drag-mode' ) ) ;
197208 } ) ;
209+ // Initialize rotation if present
210+ if ( rotate && rotate . value && rotate . value !== '0' ) {
211+ this . cropper . rotate ( Number . parseFloat ( rotate . value ) ) ;
212+ }
213+
214+ // Update rotation value from cropper
215+ const updateRotationValue = ( ) => {
216+ if ( rotate && cropperInstance ) {
217+ const currentRotate = cropperInstance . getData ( ) . rotate || 0 ;
218+ // Normalize to 0-359 range
219+ const normalizedRotate = ( ( currentRotate % 360 ) + 360 ) % 360 ;
220+ rotate . value = Math . round ( normalizedRotate ) ;
221+ }
222+ } ;
223+
224+ // Rotation buttons
225+ $ ( '.rotate-left' ) . off ( 'click.rotate' ) . on ( 'click.rotate' , function ( e ) {
226+ e . preventDefault ( ) ;
227+ e . stopPropagation ( ) ;
228+ if ( cropperInstance && typeof cropperInstance . rotate === 'function' ) {
229+ cropperInstance . rotate ( - 90 ) ;
230+ updateRotationValue ( ) ;
231+ }
232+ } ) ;
233+
234+ $ ( '.rotate-right' ) . off ( 'click.rotate' ) . on ( 'click.rotate' , function ( e ) {
235+ e . preventDefault ( ) ;
236+ e . stopPropagation ( ) ;
237+ if ( cropperInstance && typeof cropperInstance . rotate === 'function' ) {
238+ cropperInstance . rotate ( 90 ) ;
239+ updateRotationValue ( ) ;
240+ }
241+ } ) ;
198242 } ,
199243 data : {
200244 x : Number . parseFloat ( x . value ) ,
@@ -207,23 +251,67 @@ $(function () {
207251 y . value = Math . round ( event . detail . y ) ;
208252 width . value = Math . round ( event . detail . width ) ;
209253 height . value = Math . round ( event . detail . height ) ;
210- // Enable the cropping buttons. No need to disable them ever because there's no way to remove the crop box.
211- $ ( '.btn.submit-crop' ) . attr ( 'disabled' , false ) . removeClass ( 'disabled' ) ;
212- $modeButtons . attr ( 'disabled' , false ) ;
254+ // Only enable the crop-submit button when a real crop area has been drawn by the user.
255+ if ( event . detail . width > 0 && event . detail . height > 0 && cropperInstance && cropperInstance . cropped ) {
256+ $ ( '.btn.submit-crop' ) . attr ( 'disabled' , false ) . removeClass ( 'disabled' ) ;
257+ $modeButtons . attr ( 'disabled' , false ) ;
258+ }
213259 }
214260 } ) ;
261+ }
215262
216- // When setting a new image URL, remove the preview and the crop dimensions.
217- $ ( '[name=image]' ) . on ( 'change' , e => {
218- $ocrOutputDiv . remove ( ) ;
219- } ) ;
263+ // Initialize cropper when image loads (works for both server-rendered and JS-set src).
264+ $ ( img ) . on ( 'load' , function ( ) {
265+ $ocrOutputDiv . removeClass ( 'hidden' ) ;
266+ initCropper ( ) ;
267+ } ) ;
220268
221- // When submitting the main 'transcribe' button, do not send crop dimensions.
222- $ ( '.submit-full' ) . on ( 'click' , e => {
223- x . value = null ;
224- y . value = null ;
225- width . value = null ;
226- height . value = null ;
227- } ) ;
269+ // If image is already loaded (cached), initialize immediately.
270+ if ( img . src && img . complete && img . naturalWidth > 0 ) {
271+ initCropper ( ) ;
228272 }
273+
274+ // Hide output section on image load error (only when no OCR text present).
275+ $ ( img ) . on ( 'error' , function ( ) {
276+ if ( ! $ ( '#text' ) . val ( ) ) {
277+ $ocrOutputDiv . addClass ( 'hidden' ) ;
278+ }
279+ } ) ;
280+
281+ // When the image URL input changes, update the source image.
282+ $ ( '[name=image]' ) . on ( 'input' , function ( ) {
283+ const url = $ ( this ) . val ( ) ;
284+ if ( url ) {
285+ // Reset crop and rotation for new image.
286+ x . value = '' ;
287+ y . value = '' ;
288+ width . value = '' ;
289+ height . value = '' ;
290+ if ( rotate ) {
291+ rotate . value = 0 ;
292+ }
293+ // Clear any existing OCR text.
294+ $ ( '#text' ) . val ( '' ) ;
295+ $ ( '.copy-button' ) . addClass ( 'hidden' ) ;
296+
297+ // Destroy existing cropper before changing src.
298+ if ( cropperInstance ) {
299+ cropperInstance . destroy ( ) ;
300+ cropperInstance = null ;
301+ }
302+ img . src = url ;
303+ // The 'load' event will show the output div and init cropper.
304+ } else {
305+ $ocrOutputDiv . addClass ( 'hidden' ) ;
306+ }
307+ } ) ;
308+
309+ // When submitting the main 'transcribe' button, do not send crop dimensions.
310+ // Rotation is preserved — the user intentionally set it.
311+ $ ( '.submit-full' ) . on ( 'click' , e => {
312+ x . value = null ;
313+ y . value = null ;
314+ width . value = null ;
315+ height . value = null ;
316+ } ) ;
229317} ) ;
0 commit comments