diff --git a/app/proprietary/src/main/java/stirling/software/proprietary/formdetection/service/FormDetectionModelManager.java b/app/proprietary/src/main/java/stirling/software/proprietary/formdetection/service/FormDetectionModelManager.java index 73798e727a..4e1d2f67d3 100644 --- a/app/proprietary/src/main/java/stirling/software/proprietary/formdetection/service/FormDetectionModelManager.java +++ b/app/proprietary/src/main/java/stirling/software/proprietary/formdetection/service/FormDetectionModelManager.java @@ -170,9 +170,9 @@ public class FormDetectionModelManager { throw new IllegalStateException( "Model '" + modelId + "' has no download URL/checksum configured yet"); } - String scheme = URI.create(url).getScheme(); - if (!"https".equalsIgnoreCase(scheme) && !"http".equalsIgnoreCase(scheme)) { - throw new IllegalArgumentException("Model URL must be http(s): " + url); + // Same allowlist the download enforces, so a bad catalog entry fails here, not mid-install. + if (!url.startsWith(ALLOWED_MODEL_URL_PREFIX)) { + throw new IllegalArgumentException("Model URL is not on the allowlist: " + url); } if (!SHA256_HEX.matcher(sha).matches()) { throw new IllegalArgumentException("Checksum must be a 64-char hex SHA-256"); diff --git a/app/proprietary/src/test/java/stirling/software/proprietary/formdetection/service/FormDetectionModelManagerTest.java b/app/proprietary/src/test/java/stirling/software/proprietary/formdetection/service/FormDetectionModelManagerTest.java index 33b742de77..cb50ce7031 100644 --- a/app/proprietary/src/test/java/stirling/software/proprietary/formdetection/service/FormDetectionModelManagerTest.java +++ b/app/proprietary/src/test/java/stirling/software/proprietary/formdetection/service/FormDetectionModelManagerTest.java @@ -38,6 +38,9 @@ import stirling.software.proprietary.formdetection.model.ModelCatalogEntry; class FormDetectionModelManagerTest { + /** Catalog URLs must satisfy the production allowlist; the stub below serves them locally. */ + private static final String ALLOWED_URL = "https://huggingface.co"; + private HttpServer server; private byte[] modelBytes; private String modelSha; @@ -103,8 +106,9 @@ class FormDetectionModelManagerTest { return new FormDetectionModelManager(paths, catalog, props, ep, noEngine()) { @Override HttpURLConnection openModelDownload(String url) throws IOException { + String local = "http://127.0.0.1:" + port + URI.create(url).getPath(); HttpURLConnection conn = - (HttpURLConnection) URI.create(url).toURL().openConnection(); + (HttpURLConnection) URI.create(local).toURL().openConnection(); conn.setConnectTimeout(5000); conn.setReadTimeout(10000); return conn; @@ -128,7 +132,7 @@ class FormDetectionModelManagerTest { void installsDownloadsVerifiesAndPublishesAtomically(@TempDir Path dir) throws Exception { EndpointConfiguration ep = Mockito.mock(EndpointConfiguration.class); FormDetectionModelManager m = - manager(dir, entry("http://127.0.0.1:" + port + "/model.onnx", modelSha), ep); + manager(dir, entry(ALLOWED_URL + "/model.onnx", modelSha), ep); m.startInstall("test-model"); awaitState(m, "ready", 5000); @@ -145,7 +149,7 @@ class FormDetectionModelManagerTest { void rejectsChecksumMismatchAndLeavesNoFile(@TempDir Path dir) throws Exception { EndpointConfiguration ep = Mockito.mock(EndpointConfiguration.class); FormDetectionModelManager m = - manager(dir, entry("http://127.0.0.1:" + port + "/model.onnx", "0".repeat(64)), ep); + manager(dir, entry(ALLOWED_URL + "/model.onnx", "0".repeat(64)), ep); m.startInstall("test-model"); awaitState(m, "failed", 5000); @@ -174,7 +178,7 @@ class FormDetectionModelManagerTest { FormDetectionModelManager m = manager( dir, - entry("http://127.0.0.1:" + port + "/gated.onnx", modelSha), + entry(ALLOWED_URL + "/gated.onnx", modelSha), Mockito.mock(EndpointConfiguration.class)); m.startInstall("test-model"); // begins, blocks in handler @@ -191,6 +195,16 @@ class FormDetectionModelManagerTest { assertThrows(IllegalStateException.class, () -> m.startInstall("test-model")); } + @Test + void rejectsCatalogUrlOutsideAllowlist(@TempDir Path dir) { + FormDetectionModelManager m = + manager( + dir, + entry("http://127.0.0.1:" + port + "/model.onnx", modelSha), + Mockito.mock(EndpointConfiguration.class)); + assertThrows(IllegalArgumentException.class, () -> m.startInstall("test-model")); + } + @Test void rejectsDownloadUrlOutsideAllowlist(@TempDir Path dir) { RuntimePathConfig paths = Mockito.mock(RuntimePathConfig.class); @@ -216,7 +230,7 @@ class FormDetectionModelManagerTest { FormDetectionModelManager m = manager( dir, - entry("http://127.0.0.1:" + port + "/model.onnx", modelSha), + entry(ALLOWED_URL + "/model.onnx", modelSha), Mockito.mock(EndpointConfiguration.class)); assertThrows(IllegalArgumentException.class, () -> m.startInstall("unknown")); } @@ -227,7 +241,7 @@ class FormDetectionModelManagerTest { Path preDir = root.resolve("preinstalled"); Files.createDirectories(preDir); Files.write(preDir.resolve("test-model.onnx"), modelBytes); - ModelCatalogEntry e = entry("http://127.0.0.1:" + port + "/model.onnx", modelSha); + ModelCatalogEntry e = entry(ALLOWED_URL + "/model.onnx", modelSha); ApplicationProperties props = new ApplicationProperties(); props.getFormDetection().setPreinstalledModelDir(preDir.toString()); diff --git a/engine/src/stirling/models/tool_models.py b/engine/src/stirling/models/tool_models.py index 6650942b6f..dff977a7f7 100644 --- a/engine/src/stirling/models/tool_models.py +++ b/engine/src/stirling/models/tool_models.py @@ -725,6 +725,9 @@ class OcrPdfParams(ApiModel): ) ocr_type: OcrType = Field(..., description="Specify the OCR type, e.g., 'skip-text', 'force-ocr', or 'Normal'") remove_images_after: bool | None = Field(None, description="Remove images from the output PDF if set to true") + rotate_pages: bool | None = Field( + None, description="Auto-correct page orientation (90/180/270) using Tesseract OSD if set to true" + ) sidecar: bool | None = Field(None, description="Include OCR text in a sidecar text file if set to true") diff --git a/frontend/editor/src/core/services/formDetection/applyFields.ts b/frontend/editor/src/core/services/formDetection/applyFields.ts index 7cdbf5c0b1..0c1026a272 100644 --- a/frontend/editor/src/core/services/formDetection/applyFields.ts +++ b/frontend/editor/src/core/services/formDetection/applyFields.ts @@ -37,7 +37,8 @@ export async function applyFields( const cb = form.createCheckBox(name); cb.addToPage(page, { x: r.x, y: r.y, width: r.w, height: r.h }); } else { - // pdf-lib has no first-class signature widget; a text field keeps it fillable. + // Signature and radio both land here: FormUtils.addFields coerces them to text too, + // so a document is identical whichever side applied the fields. const tf = form.createTextField(name); tf.addToPage(page, { x: r.x, y: r.y, width: r.w, height: r.h }); } diff --git a/frontend/editor/src/core/types/toolApiTypes.ts b/frontend/editor/src/core/types/toolApiTypes.ts index 55938d231e..d980029624 100644 --- a/frontend/editor/src/core/types/toolApiTypes.ts +++ b/frontend/editor/src/core/types/toolApiTypes.ts @@ -1022,6 +1022,10 @@ export interface ProcessPdfWithOcrRequest { * Remove images from the output PDF if set to true */ removeImagesAfter?: boolean; + /** + * Auto-correct page orientation (90/180/270) using Tesseract OSD if set to true + */ + rotatePages?: boolean; /** * Include OCR text in a sidecar text file if set to true */