diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 193ea45a..3c7922ba 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -36,7 +36,7 @@ jobs: extensions: ast - name: Read .nvmrc - run: echo ::set-output name=NODE_VERSION::$(cat .nvmrc) + run: echo "NODE_VERSION=$(cat .nvmrc)" >> $GITHUB_OUTPUT id: nvm - name: Set up Node ${{ steps.nvm.outputs.NODE_VERSION }} diff --git a/assets/app.js b/assets/app.js index 70d2019b..564421d6 100644 --- a/assets/app.js +++ b/assets/app.js @@ -175,6 +175,9 @@ $(function () { width = document.querySelector('[name="crop[width]"]'), height = document.querySelector('[name="crop[height]"]'), $modeButtons = $('.drag-mode'); + const rotateInput = document.querySelector('[name="rotate"]'); + const $rotateButtons = $('.ocr-rotate-left, .ocr-rotate-right'); + new Cropper(img, { viewMode: 2, dragMode: 'move', @@ -195,6 +198,20 @@ $(function () { $button.addClass('active'); this.cropper.setDragMode($button.data('drag-mode')); }); + // React to rotate buttons. + if (rotateInput) { + $rotateButtons.on('click', event => { + const $button = $(event.currentTarget); + const delta = $button.hasClass('ocr-rotate-left') ? -90 : 90; + + // Update Cropper rotation. + this.cropper.rotate(delta); + + // Track cumulative rotation in the hidden field. + const current = Number.parseFloat(rotateInput.value) || 0; + rotateInput.value = (current + delta + 360) % 360; + }); + } }, data: { x: Number.parseFloat(x.value), diff --git a/assets/images/rotate_left.svg b/assets/images/rotate_left.svg new file mode 100644 index 00000000..ac3ca69b --- /dev/null +++ b/assets/images/rotate_left.svg @@ -0,0 +1,4 @@ + + diff --git a/assets/images/rotate_right.svg b/assets/images/rotate_right.svg new file mode 100644 index 00000000..6731a328 --- /dev/null +++ b/assets/images/rotate_right.svg @@ -0,0 +1,3 @@ + diff --git a/src/Controller/OcrController.php b/src/Controller/OcrController.php index c6075e89..6b56d746 100644 --- a/src/Controller/OcrController.php +++ b/src/Controller/OcrController.php @@ -57,6 +57,7 @@ class OcrController extends AbstractController { * @var mixed[] */ public static $params = [ + 'rotate' => 0, 'image' => '', 'engine' => self::DEFAULT_ENGINE, 'langs' => [], @@ -120,6 +121,10 @@ private function setup(): void { $crop = []; } static::$params['crop'] = array_map( 'intval', $crop ); + // NEW: normalize rotation (degrees) + $rotate = (int)$this->request->query->get( 'rotate', static::$params['rotate'] ); + $rotate = ( $rotate % 360 + 360 ) % 360; + static::$params['rotate'] = $rotate; } /** @@ -390,6 +395,8 @@ private function getResult( string $invalidLangsMode ): EngineResult { implode( '|', array_map( 'strval', static::$params['crop'] ) ), static::$params['psm'], static::$params['line_id'], + // NEW + static::$params['rotate'], // Warning messages are localized $this->intuition->getLang(), ] @@ -401,7 +408,9 @@ private function getResult( string $invalidLangsMode ): EngineResult { static::$params['image'], $invalidLangsMode, static::$params['crop'], - static::$params['langs'] + static::$params['langs'], + // NEW + static::$params['rotate'] ); } ); if ( !$result instanceof EngineResult ) { diff --git a/src/Engine/EngineBase.php b/src/Engine/EngineBase.php index c00737ce..68dae8fd 100644 --- a/src/Engine/EngineBase.php +++ b/src/Engine/EngineBase.php @@ -58,13 +58,15 @@ abstract public static function getId(): string; * @param string $invalidLangsMode * @param int[] $crop * @param string[]|null $models + * @param int $rotate Rotation in degrees (0-359) * @return EngineResult */ abstract public function getResult( string $imageUrl, string $invalidLangsMode, array $crop, - ?array $models = null + ?array $models = null, + int $rotate = 0 ): EngineResult; /** diff --git a/src/Engine/GoogleCloudVisionEngine.php b/src/Engine/GoogleCloudVisionEngine.php index 8b97b64b..4f05ba35 100644 --- a/src/Engine/GoogleCloudVisionEngine.php +++ b/src/Engine/GoogleCloudVisionEngine.php @@ -1,5 +1,6 @@ checkImageUrl( $imageUrl ); @@ -67,6 +70,12 @@ public function getResult( } $image = $this->getImage( $imageUrl, $crop ); + if ( $rotate !== 0 ) { + $imagine = new Imagine(); + $loaded = $imagine->load( $image->getData() ?: file_get_contents( $image->getUrl() ) ); + $loaded->rotate( $rotate ); + $image->setData( $loaded->get( 'jpg' ) ); + } $imageUrlOrData = $image->hasData() ? $image->getData() : $image->getUrl(); $response = $this->imageAnnotator->textDetection( $imageUrlOrData, [ 'imageContext' => $imageContext ] ); @@ -78,6 +87,12 @@ public function getResult( && stripos( $response->getError()->getMessage(), 'download the content and pass it in' ) !== false ) { $image = $this->getImage( $imageUrl, $crop, self::DO_DOWNLOAD_IMAGE ); + if ( $rotate !== 0 ) { + $imagine = new Imagine(); + $loaded = $imagine->load( $image->getData() ); + $loaded->rotate( $rotate ); + $image->setData( $loaded->get( 'jpg' ) ); + } $response = $this->imageAnnotator->textDetection( $image->getData(), [ 'imageContext' => $imageContext ] ); } diff --git a/src/Engine/TesseractEngine.php b/src/Engine/TesseractEngine.php index 80c0d419..93f69921 100644 --- a/src/Engine/TesseractEngine.php +++ b/src/Engine/TesseractEngine.php @@ -4,6 +4,7 @@ namespace App\Engine; use App\Exception\OcrException; +use Imagine\Gd\Imagine; use Krinkle\Intuition\Intuition; use Symfony\Contracts\HttpClient\HttpClientInterface; use thiagoalessio\TesseractOCR\TesseractOCR; @@ -51,7 +52,8 @@ public function getResult( string $imageUrl, string $invalidLangsMode, array $crop, - ?array $langs = null + ?array $langs = null, + int $rotate = 0 ): EngineResult { // Check the URL and fetch the image data. $this->checkImageUrl( $imageUrl ); @@ -59,6 +61,13 @@ public function getResult( [ $validLangs, $invalidLangs ] = $this->filterValidLangs( $langs, $invalidLangsMode ); $image = $this->getImage( $imageUrl, $crop, self::DO_DOWNLOAD_IMAGE ); + // If there is a rotation, apply it to the image data. + if ( $rotate !== 0 ) { + $imagine = new Imagine(); + $loaded = $imagine->load( $image->getData() ); + $loaded->rotate( $rotate ); + $image->setData( $loaded->get( 'jpg' ) ); + } $this->ocr->imageData( $image->getData(), $image->getSize() ); if ( $validLangs ) { diff --git a/src/Engine/TranskribusEngine.php b/src/Engine/TranskribusEngine.php index 12c15c23..19d29a74 100644 --- a/src/Engine/TranskribusEngine.php +++ b/src/Engine/TranskribusEngine.php @@ -4,6 +4,7 @@ namespace App\Engine; use App\Exception\OcrException; +use Imagine\Gd\Imagine; use Krinkle\Intuition\Intuition; use Symfony\Contracts\HttpClient\HttpClientInterface; @@ -114,7 +115,8 @@ public function getResult( string $imageUrl, string $invalidLangsMode, array $crop, - ?array $langs = null + ?array $langs = null, + int $rotate = 0 ): EngineResult { $this->checkImageUrl( $imageUrl ); @@ -141,6 +143,13 @@ public function getResult( $modelInfo = $this->getModelList()[$modelCode]; $htrModelId = (int)$modelInfo['htr']; $image = $this->getImage( $imageUrl, $crop, self::DO_DOWNLOAD_IMAGE ); + if ( $rotate !== 0 ) { + $imagine = new Imagine(); + $loaded = $imagine->load( $image->getData() ); + $loaded->rotate( $rotate ); + $image->setData( $loaded->get( 'jpg' ) ); + } + $processId = $this->transkribusClient->initProcess( $image, $htrModelId, $this->lineId, $points ); $resText = ''; diff --git a/templates/output.html.twig b/templates/output.html.twig index cbcc5660..eba081a1 100644 --- a/templates/output.html.twig +++ b/templates/output.html.twig @@ -92,6 +92,23 @@ alt="{{ msg('drag-mode-move-alt') }}" /> + + + @@ -99,6 +116,9 @@ + + {# NEW: rotate hidden field #} +