diff --git a/app/core/src/main/java/stirling/software/SPDF/controller/web/ReactRoutingController.java b/app/core/src/main/java/stirling/software/SPDF/controller/web/ReactRoutingController.java index 424621b8a..7741220f2 100644 --- a/app/core/src/main/java/stirling/software/SPDF/controller/web/ReactRoutingController.java +++ b/app/core/src/main/java/stirling/software/SPDF/controller/web/ReactRoutingController.java @@ -75,8 +75,7 @@ public class ReactRoutingController { @GetMapping( "/{path:^(?!api|static|robots\\.txt|favicon\\.ico|manifest.*\\.json|pipeline|pdfjs|pdfjs-legacy|fonts|images|files|css|js|assets|locales|modern-logo|classic-logo|Login|og_images|samples)[^\\.]*$}") - public ResponseEntity forwardRootPaths(HttpServletRequest request) - throws IOException { + public ResponseEntity forwardRootPaths(HttpServletRequest request) throws IOException { return serveIndexHtml(request); } diff --git a/app/proprietary/src/main/java/stirling/software/proprietary/security/configuration/SecurityConfiguration.java b/app/proprietary/src/main/java/stirling/software/proprietary/security/configuration/SecurityConfiguration.java index a3a5eee4f..ab1e4934d 100644 --- a/app/proprietary/src/main/java/stirling/software/proprietary/security/configuration/SecurityConfiguration.java +++ b/app/proprietary/src/main/java/stirling/software/proprietary/security/configuration/SecurityConfiguration.java @@ -331,7 +331,9 @@ public class SecurityConfiguration { formLogin -> formLogin .loginPage("/login") // Redirect here when unauthenticated - .loginProcessingUrl("/perform_login") // Process form posts here (not /login) + .loginProcessingUrl( + "/perform_login") // Process form posts here (not + // /login) .successHandler( new CustomAuthenticationSuccessHandler( loginAttemptService, diff --git a/frontend/package-lock.json b/frontend/package-lock.json index 6e1f885e6..8d90d9658 100644 --- a/frontend/package-lock.json +++ b/frontend/package-lock.json @@ -456,6 +456,7 @@ } ], "license": "MIT", + "peer": true, "engines": { "node": ">=18" }, @@ -499,6 +500,7 @@ } ], "license": "MIT", + "peer": true, "engines": { "node": ">=18" } @@ -579,6 +581,7 @@ "resolved": "https://registry.npmjs.org/@embedpdf/core/-/core-1.5.0.tgz", "integrity": "sha512-Yrh9XoVaT8cUgzgqpJ7hx5wg6BqQrCFirqqlSwVb+Ly9oNn4fZbR9GycIWmzJOU5XBnaOJjXfQSaDyoNP0woNA==", "license": "MIT", + "peer": true, "dependencies": { "@embedpdf/engines": "1.5.0", "@embedpdf/models": "1.5.0" @@ -678,6 +681,7 @@ "resolved": "https://registry.npmjs.org/@embedpdf/plugin-history/-/plugin-history-1.5.0.tgz", "integrity": "sha512-p7PTNNaIr4gH3jLwX+eLJe1DeUXgi21kVGN6SRx/pocH8esg4jqoOeD/YiRRZoZnPOiy0jBXVhkPkwSmY7a2hQ==", "license": "MIT", + "peer": true, "dependencies": { "@embedpdf/models": "1.5.0" }, @@ -694,6 +698,7 @@ "resolved": "https://registry.npmjs.org/@embedpdf/plugin-interaction-manager/-/plugin-interaction-manager-1.5.0.tgz", "integrity": "sha512-ckHgTfvkW6c5Ta7Mc+Dl9C2foVnvEpqEJ84wyBnqrU0OWbe/jsiPhyKBVeartMGqNI/kVfaQTXupyrKhekAVmg==", "license": "MIT", + "peer": true, "dependencies": { "@embedpdf/models": "1.5.0" }, @@ -711,6 +716,7 @@ "resolved": "https://registry.npmjs.org/@embedpdf/plugin-loader/-/plugin-loader-1.5.0.tgz", "integrity": "sha512-P4YpIZfaW69etYIjphyaL4cGl2pB14h3OdTE0tRQ2pZYZHFLTvlt4q9B3PVSdhlSrHK5nob7jfLGon2U7xCslg==", "license": "MIT", + "peer": true, "dependencies": { "@embedpdf/models": "1.5.0" }, @@ -764,6 +770,7 @@ "resolved": "https://registry.npmjs.org/@embedpdf/plugin-render/-/plugin-render-1.5.0.tgz", "integrity": "sha512-ywwSj0ByrlkvrJIHKRzqxARkOZriki8VJUC+T4MV8fGyF4CzvCRJyKlPktahFz+VxhoodqTh7lBCib68dH+GvA==", "license": "MIT", + "peer": true, "dependencies": { "@embedpdf/models": "1.5.0" }, @@ -798,6 +805,7 @@ "resolved": "https://registry.npmjs.org/@embedpdf/plugin-scroll/-/plugin-scroll-1.5.0.tgz", "integrity": "sha512-RNmTZCZ8X1mA8cw9M7TMDuhO9GtkOalGha2bBL3En3D1IlDRS7PzNNMSMV7eqT7OQICSTltlpJ8p8Qi5esvL/Q==", "license": "MIT", + "peer": true, "dependencies": { "@embedpdf/models": "1.5.0" }, @@ -834,6 +842,7 @@ "resolved": "https://registry.npmjs.org/@embedpdf/plugin-selection/-/plugin-selection-1.5.0.tgz", "integrity": "sha512-zrxLBAZQoPswDuf9q9DrYaQc6B0Ysc2U1hueTjNH/4+ydfl0BFXZkKR63C2e3YmWtXvKjkoIj0GyPzsiBORLUw==", "license": "MIT", + "peer": true, "dependencies": { "@embedpdf/models": "1.5.0" }, @@ -909,6 +918,7 @@ "resolved": "https://registry.npmjs.org/@embedpdf/plugin-viewport/-/plugin-viewport-1.5.0.tgz", "integrity": "sha512-G8GDyYRhfehw72+r4qKkydnA5+AU8qH67g01Y12b0DzI0VIzymh/05Z4dK8DsY3jyWPXJfw2hlg5+KDHaMBHgQ==", "license": "MIT", + "peer": true, "dependencies": { "@embedpdf/models": "1.5.0" }, @@ -1064,6 +1074,7 @@ "resolved": "https://registry.npmjs.org/@emotion/react/-/react-11.14.0.tgz", "integrity": "sha512-O000MLDBDdk/EohJPFUqvnp4qnHeYkVP5B0xEG0D/L7cOKP9kefu2DXn8dj74cQfsEzUqh+sr1RzFqiL1o+PpA==", "license": "MIT", + "peer": true, "dependencies": { "@babel/runtime": "^7.18.3", "@emotion/babel-plugin": "^11.13.5", @@ -1107,6 +1118,7 @@ "resolved": "https://registry.npmjs.org/@emotion/styled/-/styled-11.14.1.tgz", "integrity": "sha512-qEEJt42DuToa3gurlH4Qqc1kVpNq8wO8cJtDzU46TjlzWjDlsVyevtYCRijVq3SrHsROS+gVQ8Fnea108GnKzw==", "license": "MIT", + "peer": true, "dependencies": { "@babel/runtime": "^7.18.3", "@emotion/babel-plugin": "^11.13.5", @@ -2137,6 +2149,7 @@ "resolved": "https://registry.npmjs.org/@mantine/core/-/core-8.3.6.tgz", "integrity": "sha512-paTl+0x+O/QtgMtqVJaG8maD8sfiOdgPmLOyG485FmeGZ1L3KMdEkhxZtmdGlDFsLXhmMGQ57ducT90bvhXX5A==", "license": "MIT", + "peer": true, "dependencies": { "@floating-ui/react": "^0.27.16", "clsx": "^2.1.1", @@ -2187,6 +2200,7 @@ "resolved": "https://registry.npmjs.org/@mantine/hooks/-/hooks-8.3.6.tgz", "integrity": "sha512-liHfaWXHAkLjJy+Bkr29UsCwAoDQ/a64WrM67lksx8F0qqyjR5RQH8zVlhuOjdpQnwtlUkE/YiTvbJiPcoI0bw==", "license": "MIT", + "peer": true, "peerDependencies": { "react": "^18.x || ^19.x" } @@ -2254,6 +2268,7 @@ "resolved": "https://registry.npmjs.org/@mui/material/-/material-7.3.5.tgz", "integrity": "sha512-8VVxFmp1GIm9PpmnQoCoYo0UWHoOrdA57tDL62vkpzEgvb/d71Wsbv4FRg7r1Gyx7PuSo0tflH34cdl/NvfHNQ==", "license": "MIT", + "peer": true, "dependencies": { "@babel/runtime": "^7.28.4", "@mui/core-downloads-tracker": "^7.3.5", @@ -3186,6 +3201,7 @@ "resolved": "https://registry.npmjs.org/@stripe/stripe-js/-/stripe-js-7.9.0.tgz", "integrity": "sha512-ggs5k+/0FUJcIgNY08aZTqpBTtbExkJMYMLSMwyucrhtWexVOEY1KJmhBsxf+E/Q15f5rbwBpj+t0t2AW2oCsQ==", "license": "MIT", + "peer": true, "engines": { "node": ">=12.16" } @@ -3304,7 +3320,6 @@ "resolved": "https://registry.npmjs.org/@sveltejs/acorn-typescript/-/acorn-typescript-1.0.6.tgz", "integrity": "sha512-4awhxtMh4cx9blePWl10HRHj8Iivtqj+2QdDCSMDzxG+XKa9+VCNupQuCuvzEhYPzZSrX+0gC+0lHA/0fFKKQQ==", "license": "MIT", - "peer": true, "peerDependencies": { "acorn": "^8.9.0" } @@ -4081,6 +4096,7 @@ "integrity": "sha512-o4PXJQidqJl82ckFaXUeoAW+XysPLauYI43Abki5hABd853iMhitooc6znOnczgbTYmEP6U6/y1ZyKAIsvMKGg==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@babel/code-frame": "^7.10.4", "@babel/runtime": "^7.12.5", @@ -4409,6 +4425,7 @@ "resolved": "https://registry.npmjs.org/@types/react/-/react-19.2.2.tgz", "integrity": "sha512-6mDvHUFSjyT2B2yeNx2nUgMxh9LtOWvkhIU3uePn2I2oyNymUAX1NIsdgviM4CH+JSrp2D2hsMvJOkxY+0wNRA==", "license": "MIT", + "peer": true, "dependencies": { "csstype": "^3.0.2" } @@ -4419,6 +4436,7 @@ "integrity": "sha512-9KQPoO6mZCi7jcIStSnlOWn2nEF3mNmyr3rIAsGnAbQKYbRLyqmeSc39EVgtxXVia+LMT8j3knZLAZAh+xLmrw==", "dev": true, "license": "MIT", + "peer": true, "peerDependencies": { "@types/react": "^19.2.0" } @@ -4488,6 +4506,7 @@ "integrity": "sha512-6m1I5RmHBGTnUGS113G04DMu3CpSdxCAU/UvtjNWL4Nuf3MW9tQhiJqRlHzChIkhy6kZSAQmc+I1bcGjE3yNKg==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@typescript-eslint/scope-manager": "8.46.3", "@typescript-eslint/types": "8.46.3", @@ -5201,7 +5220,6 @@ "resolved": "https://registry.npmjs.org/@vue/reactivity/-/reactivity-3.5.24.tgz", "integrity": "sha512-BM8kBhtlkkbnyl4q+HiF5R5BL0ycDPfihowulm02q3WYp2vxgPcJuZO866qa/0u3idbMntKEtVNuAUp5bw4teg==", "license": "MIT", - "peer": true, "dependencies": { "@vue/shared": "3.5.24" } @@ -5211,7 +5229,6 @@ "resolved": "https://registry.npmjs.org/@vue/runtime-core/-/runtime-core-3.5.24.tgz", "integrity": "sha512-RYP/byyKDgNIqfX/gNb2PB55dJmM97jc9wyF3jK7QUInYKypK2exmZMNwnjueWwGceEkP6NChd3D2ZVEp9undQ==", "license": "MIT", - "peer": true, "dependencies": { "@vue/reactivity": "3.5.24", "@vue/shared": "3.5.24" @@ -5222,7 +5239,6 @@ "resolved": "https://registry.npmjs.org/@vue/runtime-dom/-/runtime-dom-3.5.24.tgz", "integrity": "sha512-Z8ANhr/i0XIluonHVjbUkjvn+CyrxbXRIxR7wn7+X7xlcb7dJsfITZbkVOeJZdP8VZwfrWRsWdShH6pngMxRjw==", "license": "MIT", - "peer": true, "dependencies": { "@vue/reactivity": "3.5.24", "@vue/runtime-core": "3.5.24", @@ -5235,7 +5251,6 @@ "resolved": "https://registry.npmjs.org/@vue/server-renderer/-/server-renderer-3.5.24.tgz", "integrity": "sha512-Yh2j2Y4G/0/4z/xJ1Bad4mxaAk++C2v4kaa8oSYTMJBJ00/ndPuxCnWeot0/7/qafQFLh5pr6xeV6SdMcE/G1w==", "license": "MIT", - "peer": true, "dependencies": { "@vue/compiler-ssr": "3.5.24", "@vue/shared": "3.5.24" @@ -5262,6 +5277,7 @@ "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.15.0.tgz", "integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==", "license": "MIT", + "peer": true, "bin": { "acorn": "bin/acorn" }, @@ -5669,7 +5685,6 @@ "resolved": "https://registry.npmjs.org/axobject-query/-/axobject-query-4.1.0.tgz", "integrity": "sha512-qIj0G9wZbMGNLjLmg1PT6v2mE9AH2zlnADJD/2tC6E00hgmhUOfEB6greHPAfLRSufHqROIUTkw6E+M3lH0PTQ==", "license": "Apache-2.0", - "peer": true, "engines": { "node": ">= 0.4" } @@ -5946,6 +5961,7 @@ } ], "license": "MIT", + "peer": true, "dependencies": { "baseline-browser-mapping": "^2.8.19", "caniuse-lite": "^1.0.30001751", @@ -6993,7 +7009,8 @@ "resolved": "https://registry.npmjs.org/devtools-protocol/-/devtools-protocol-0.0.1521046.tgz", "integrity": "sha512-vhE6eymDQSKWUXwwA37NtTTVEzjtGVfDr3pRbsWEQ5onH/Snp2c+2xZHWJJawG/0hCCJLRGt4xVtEVUVILol4w==", "dev": true, - "license": "BSD-3-Clause" + "license": "BSD-3-Clause", + "peer": true }, "node_modules/dezalgo": { "version": "1.0.4", @@ -7388,6 +7405,7 @@ "integrity": "sha512-BhHmn2yNOFA9H9JmmIVKJmd288g9hrVRDkdoIgRCRuSySRUHH7r/DI6aAXW9T1WwUuY3DFgrcaqB+deURBLR5g==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@eslint-community/eslint-utils": "^4.8.0", "@eslint-community/regexpp": "^4.12.1", @@ -7558,6 +7576,7 @@ "integrity": "sha512-whOE1HFo/qJDyX4SnXzP4N6zOWn79WhnCUY/iDR0mPfQZO8wcYE4JClzI2oZrhBnnMUCBCHZhO6VQyoBU95mZA==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@rtsao/scc": "^1.1.0", "array-includes": "^3.1.9", @@ -7724,8 +7743,7 @@ "version": "1.2.2", "resolved": "https://registry.npmjs.org/esm-env/-/esm-env-1.2.2.tgz", "integrity": "sha512-Epxrv+Nr/CaL4ZcFGPJIYLWFom+YeV1DqMLHJoEd9SYRxNbaFruBwfEX/kkHUJf55j2+TUbmDcmuilbP1TmXHA==", - "license": "MIT", - "peer": true + "license": "MIT" }, "node_modules/espree": { "version": "10.4.0", @@ -7790,7 +7808,6 @@ "resolved": "https://registry.npmjs.org/esrap/-/esrap-2.1.2.tgz", "integrity": "sha512-DgvlIQeowRNyvLPWW4PT7Gu13WznY288Du086E751mwwbsgr29ytBiYeLzAGIo0qk3Ujob0SDk8TiSaM5WQzNg==", "license": "MIT", - "peer": true, "dependencies": { "@jridgewell/sourcemap-codec": "^1.4.15" } @@ -8881,6 +8898,7 @@ } ], "license": "MIT", + "peer": true, "dependencies": { "@babel/runtime": "^7.27.6" }, @@ -9357,7 +9375,6 @@ "resolved": "https://registry.npmjs.org/is-reference/-/is-reference-3.0.3.tgz", "integrity": "sha512-ixkJoqQvAP88E6wLydLGGqCJsrFUnqoH6HnaczB8XmDH1oaWU+xxdptvikTgaEhtZ53Ky6YXiBuUI2WXLMCwjw==", "license": "MIT", - "peer": true, "dependencies": { "@types/estree": "^1.0.6" } @@ -9678,6 +9695,7 @@ "integrity": "sha512-Pcfm3eZ+eO4JdZCXthW9tCDT3nF4K+9dmeZ+5X39n+Kqz0DDIABRP5CAEOHRFZk8RGuC2efksTJxrjp8EXCunQ==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@acemir/cssom": "^0.9.19", "@asamuzakjp/dom-selector": "^6.7.3", @@ -10264,8 +10282,7 @@ "version": "3.0.0", "resolved": "https://registry.npmjs.org/locate-character/-/locate-character-3.0.0.tgz", "integrity": "sha512-SW13ws7BjaeJ6p7Q6CO2nchbYEc3X3J6WrmTTDto7yMPqVSZTUyY5Tjbid+Ab8gLnATtygYtiDIJGQRRn2ZOiA==", - "license": "MIT", - "peer": true + "license": "MIT" }, "node_modules/locate-path": { "version": "6.0.0", @@ -11411,6 +11428,7 @@ } ], "license": "MIT", + "peer": true, "dependencies": { "nanoid": "^3.3.11", "picocolors": "^1.1.1", @@ -11690,6 +11708,7 @@ "resolved": "https://registry.npmjs.org/preact/-/preact-10.27.2.tgz", "integrity": "sha512-5SYSgFKSyhCbk6SrXyMpqjb5+MQBgfvEKE/OC+PujcY34sOpqtr+0AZQtPYx5IA6VxynQ7rUPCtKzyovpj9Bpg==", "license": "MIT", + "peer": true, "funding": { "type": "opencollective", "url": "https://opencollective.com/preact" @@ -12072,6 +12091,7 @@ "resolved": "https://registry.npmjs.org/react/-/react-19.2.0.tgz", "integrity": "sha512-tmbWg6W31tQLeB5cdIBOicJDJRR2KzXsV7uSK9iNfLWQ5bIZfxuPEHp7M8wiHyHnn0DD1i7w3Zmin0FtkrwoCQ==", "license": "MIT", + "peer": true, "engines": { "node": ">=0.10.0" } @@ -12081,6 +12101,7 @@ "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.2.0.tgz", "integrity": "sha512-UlbRu4cAiGaIewkPyiRGJk0imDN2T3JjieT6spoL2UeSf5od4n5LB/mQ4ejmxhCFT1tYe8IvaFulzynWovsEFQ==", "license": "MIT", + "peer": true, "dependencies": { "scheduler": "^0.27.0" }, @@ -13592,7 +13613,6 @@ "resolved": "https://registry.npmjs.org/aria-query/-/aria-query-5.3.2.tgz", "integrity": "sha512-COROpnaoap1E2F000S62r6A60uHZnmlvomhfyT2DlTcrY1OrBKn2UhH7qn5wTC9zMvD0AY7csdPSNwKP+7WiQw==", "license": "Apache-2.0", - "peer": true, "engines": { "node": ">= 0.4" } @@ -13801,6 +13821,7 @@ "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", "dev": true, "license": "MIT", + "peer": true, "engines": { "node": ">=12" }, @@ -14102,6 +14123,7 @@ "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", "devOptional": true, "license": "Apache-2.0", + "peer": true, "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" @@ -14183,6 +14205,7 @@ "dev": true, "hasInstallScript": true, "license": "MIT", + "peer": true, "dependencies": { "napi-postinstall": "^0.3.0" }, @@ -14387,6 +14410,7 @@ "integrity": "sha512-BxAKBWmIbrDgrokdGZH1IgkIk/5mMHDreLDmCJ0qpyJaAteP8NvMhkwr/ZCQNqNH97bw/dANTE9PDzqwJghfMQ==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "esbuild": "^0.25.0", "fdir": "^6.5.0", @@ -14538,6 +14562,7 @@ "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", "dev": true, "license": "MIT", + "peer": true, "engines": { "node": ">=12" }, @@ -14551,6 +14576,7 @@ "integrity": "sha512-LUCP5ev3GURDysTWiP47wRRUpLKMOfPh+yKTx3kVIEiu5KOMeqzpnYNsKyOoVrULivR8tLcks4+lga33Whn90A==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@types/chai": "^5.2.2", "@vitest/expect": "3.2.4", @@ -15162,8 +15188,7 @@ "version": "1.1.4", "resolved": "https://registry.npmjs.org/zimmerframe/-/zimmerframe-1.1.4.tgz", "integrity": "sha512-B58NGBEoc8Y9MWWCQGl/gq9xBCe4IiKM0a2x7GZdQKOW5Exr8S1W24J6OgM1njK8xCRGvAJIL/MxXHf6SkmQKQ==", - "license": "MIT", - "peer": true + "license": "MIT" }, "node_modules/zod": { "version": "3.25.76", diff --git a/frontend/src/core/hooks/tools/adjustContrast/useAdjustContrastOperation.ts b/frontend/src/core/hooks/tools/adjustContrast/useAdjustContrastOperation.ts index 176ddd4b5..6524a2585 100644 --- a/frontend/src/core/hooks/tools/adjustContrast/useAdjustContrastOperation.ts +++ b/frontend/src/core/hooks/tools/adjustContrast/useAdjustContrastOperation.ts @@ -1,5 +1,5 @@ import { useTranslation } from 'react-i18next'; -import { ToolType, useToolOperation } from '@app/hooks/tools/shared/useToolOperation'; +import { ToolType, useToolOperation, CustomProcessorResult } from '@app/hooks/tools/shared/useToolOperation'; import { AdjustContrastParameters, defaultParameters } from '@app/hooks/tools/adjustContrast/useAdjustContrastParameters'; import { PDFDocument as PDFLibDocument } from 'pdf-lib'; import { applyAdjustmentsToCanvas } from '@app/components/tools/adjustContrast/utils'; @@ -46,7 +46,7 @@ async function buildAdjustedPdfForFile(file: File, params: AdjustContrastParamet return out; } -async function processPdfClientSide(params: AdjustContrastParameters, files: File[]): Promise { +async function processPdfClientSide(params: AdjustContrastParameters, files: File[]): Promise { // Limit concurrency to avoid exhausting memory/CPU while still getting speedups // Heuristic: use up to 4 workers on capable machines, otherwise 2-3 let CONCURRENCY_LIMIT = 2; @@ -72,7 +72,12 @@ async function processPdfClientSide(params: AdjustContrastParameters, files: Fil return results; }; - return mapWithConcurrency(files, CONCURRENCY_LIMIT, (file) => buildAdjustedPdfForFile(file, params)); + const processedFiles = await mapWithConcurrency(files, CONCURRENCY_LIMIT, (file) => buildAdjustedPdfForFile(file, params)); + + return { + files: processedFiles, + consumedAllInputs: false, + }; } export const adjustContrastOperationConfig = { diff --git a/frontend/src/core/hooks/tools/automate/useAutomateOperation.ts b/frontend/src/core/hooks/tools/automate/useAutomateOperation.ts index 004f28902..f6fbcacda 100644 --- a/frontend/src/core/hooks/tools/automate/useAutomateOperation.ts +++ b/frontend/src/core/hooks/tools/automate/useAutomateOperation.ts @@ -36,7 +36,10 @@ export function useAutomateOperation() { ); console.log(`✅ Automation completed, returning ${finalResults.length} files`); - return finalResults; + return { + files: finalResults, + consumedAllInputs: false, + }; }, [toolRegistry]); return useToolOperation({ diff --git a/frontend/src/core/hooks/tools/convert/useConvertOperation.ts b/frontend/src/core/hooks/tools/convert/useConvertOperation.ts index 9134c9db4..9650ac0e2 100644 --- a/frontend/src/core/hooks/tools/convert/useConvertOperation.ts +++ b/frontend/src/core/hooks/tools/convert/useConvertOperation.ts @@ -3,8 +3,8 @@ import apiClient from '@app/services/apiClient'; import { useTranslation } from 'react-i18next'; import { ConvertParameters, defaultParameters } from '@app/hooks/tools/convert/useConvertParameters'; import { createFileFromApiResponse } from '@app/utils/fileResponseUtils'; -import { useToolOperation, ToolType } from '@app/hooks/tools/shared/useToolOperation'; -import { getEndpointUrl, isImageFormat, isWebFormat } from '@app/utils/convertUtils'; +import { useToolOperation, ToolType, CustomProcessorResult } from '@app/hooks/tools/shared/useToolOperation'; +import { getEndpointUrl, isImageFormat, isWebFormat, isOfficeFormat } from '@app/utils/convertUtils'; // Static function that can be used by both the hook and automation executor export const shouldProcessFilesSeparately = ( @@ -21,6 +21,10 @@ export const shouldProcessFilesSeparately = ( (parameters.fromExtension === 'pdf' && parameters.toExtension === 'pdfa') || // PDF to text-like formats should be one output per input (parameters.fromExtension === 'pdf' && ['txt', 'rtf', 'csv'].includes(parameters.toExtension)) || + // PDF to office format conversions (each PDF should generate its own office file) + (parameters.fromExtension === 'pdf' && isOfficeFormat(parameters.toExtension)) || + // Office files to PDF conversions (each file should be processed separately via LibreOffice) + (isOfficeFormat(parameters.fromExtension) && parameters.toExtension === 'pdf') || // Web files to PDF conversions (each web file should generate its own PDF) ((isWebFormat(parameters.fromExtension) || parameters.fromExtension === 'web') && parameters.toExtension === 'pdf') || @@ -98,7 +102,7 @@ export const createFileFromResponse = ( export const convertProcessor = async ( parameters: ConvertParameters, selectedFiles: File[] -): Promise => { +): Promise => { const processedFiles: File[] = []; const endpoint = getEndpointUrl(parameters.fromExtension, parameters.toExtension); @@ -107,7 +111,9 @@ export const convertProcessor = async ( } // Convert-specific routing logic: decide batch vs individual processing - if (shouldProcessFilesSeparately(selectedFiles, parameters)) { + const isSeparateProcessing = shouldProcessFilesSeparately(selectedFiles, parameters); + + if (isSeparateProcessing) { // Individual processing for complex cases (PDF→image, smart detection, etc.) for (const file of selectedFiles) { try { @@ -134,7 +140,14 @@ export const convertProcessor = async ( processedFiles.push(convertedFile); } - return processedFiles; + // When batch processing multiple files into one output (e.g., 3 images → 1 PDF), + // mark all inputs as consumed even though there's only 1 output file + const isCombiningMultiple = !isSeparateProcessing && selectedFiles.length > 1; + + return { + files: processedFiles, + consumedAllInputs: isCombiningMultiple, + }; }; // Static configuration object @@ -151,7 +164,7 @@ export const useConvertOperation = () => { const customConvertProcessor = useCallback(async ( parameters: ConvertParameters, selectedFiles: File[] - ): Promise => { + ): Promise => { return convertProcessor(parameters, selectedFiles); }, []); diff --git a/frontend/src/core/hooks/tools/extractPages/useExtractPagesOperation.ts b/frontend/src/core/hooks/tools/extractPages/useExtractPagesOperation.ts index 086fd65cc..687dde170 100644 --- a/frontend/src/core/hooks/tools/extractPages/useExtractPagesOperation.ts +++ b/frontend/src/core/hooks/tools/extractPages/useExtractPagesOperation.ts @@ -1,6 +1,6 @@ import apiClient from '@app/services/apiClient'; import { useTranslation } from 'react-i18next'; -import { ToolType, useToolOperation } from '@app/hooks/tools/shared/useToolOperation'; +import { ToolType, useToolOperation, CustomProcessorResult } from '@app/hooks/tools/shared/useToolOperation'; import { createStandardErrorHandler } from '@app/utils/toolErrorHandler'; import { ExtractPagesParameters, defaultParameters } from '@app/hooks/tools/extractPages/useExtractPagesParameters'; import { pdfWorkerManager } from '@app/services/pdfWorkerManager'; @@ -23,7 +23,7 @@ async function resolveSelectionToCsv(expression: string, file: File): Promise => { + customProcessor: async (parameters: ExtractPagesParameters, files: File[]): Promise => { const outputs: File[] = []; for (const file of files) { @@ -43,7 +43,10 @@ export const extractPagesOperationConfig = { outputs.push(outFile); } - return outputs; + return { + files: outputs, + consumedAllInputs: false, + }; }, defaultParameters, } as const; diff --git a/frontend/src/core/hooks/tools/removeAnnotations/useRemoveAnnotationsOperation.ts b/frontend/src/core/hooks/tools/removeAnnotations/useRemoveAnnotationsOperation.ts index 4078b2b07..e4b176c8d 100644 --- a/frontend/src/core/hooks/tools/removeAnnotations/useRemoveAnnotationsOperation.ts +++ b/frontend/src/core/hooks/tools/removeAnnotations/useRemoveAnnotationsOperation.ts @@ -1,10 +1,10 @@ import { useTranslation } from 'react-i18next'; -import { useToolOperation, ToolType } from '@app/hooks/tools/shared/useToolOperation'; +import { useToolOperation, ToolType, CustomProcessorResult } from '@app/hooks/tools/shared/useToolOperation'; import { createStandardErrorHandler } from '@app/utils/toolErrorHandler'; import { RemoveAnnotationsParameters, defaultParameters } from '@app/hooks/tools/removeAnnotations/useRemoveAnnotationsParameters'; import { PDFDocument, PDFName, PDFRef, PDFDict } from 'pdf-lib'; // Client-side PDF processing using PDF-lib -const removeAnnotationsProcessor = async (_parameters: RemoveAnnotationsParameters, files: File[]): Promise => { +const removeAnnotationsProcessor = async (_parameters: RemoveAnnotationsParameters, files: File[]): Promise => { const processedFiles: File[] = []; for (const file of files) { @@ -75,7 +75,10 @@ const removeAnnotationsProcessor = async (_parameters: RemoveAnnotationsParamete } } - return processedFiles; + return { + files: processedFiles, + consumedAllInputs: false, + }; }; // Static configuration object diff --git a/frontend/src/core/hooks/tools/shared/useToolApiCalls.ts b/frontend/src/core/hooks/tools/shared/useToolApiCalls.ts index a8e6a88a0..1ab8925f1 100644 --- a/frontend/src/core/hooks/tools/shared/useToolApiCalls.ts +++ b/frontend/src/core/hooks/tools/shared/useToolApiCalls.ts @@ -4,6 +4,7 @@ import apiClient from '@app/services/apiClient'; // Our configured instance import { processResponse, ResponseHandler } from '@app/utils/toolResponseProcessor'; import { isEmptyOutput } from '@app/services/errorUtils'; import type { ProcessingProgress } from '@app/hooks/tools/shared/useToolState'; +import type { StirlingFile, FileId } from '@app/types/fileContext'; export interface ApiCallsConfig { endpoint: string | ((params: TParams) => string); @@ -18,14 +19,14 @@ export const useToolApiCalls = () => { const processFiles = useCallback(async ( params: TParams, - validFiles: File[], + validFiles: StirlingFile[], config: ApiCallsConfig, onProgress: (progress: ProcessingProgress) => void, onStatus: (status: string) => void, - markFileError?: (fileId: string) => void, - ): Promise<{ outputFiles: File[]; successSourceIds: string[] }> => { + markFileError?: (fileId: FileId) => void, + ): Promise<{ outputFiles: File[]; successSourceIds: FileId[] }> => { const processedFiles: File[] = []; - const successSourceIds: string[] = []; + const successSourceIds: FileId[] = []; const failedFiles: string[] = []; const total = validFiles.length; @@ -35,7 +36,7 @@ export const useToolApiCalls = () => { for (let i = 0; i < validFiles.length; i++) { const file = validFiles[i]; - console.debug('[processFiles] Start', { index: i, total, name: file.name, fileId: (file as any).fileId }); + console.debug('[processFiles] Start', { index: i, total, name: file.name, fileId: file.fileId }); onProgress({ current: i + 1, total, currentFileName: file.name }); onStatus(`Processing ${file.name} (${i + 1}/${total})`); @@ -47,7 +48,7 @@ export const useToolApiCalls = () => { responseType: 'blob', cancelToken: cancelTokenRef.current?.token, }); - console.debug('[processFiles] Response OK', { name: file.name, status: (response as any)?.status }); + console.debug('[processFiles] Response OK', { name: file.name, status: response.status }); // Forward to shared response processor (uses tool-specific responseHandler if provided) const responseFiles = await processResponse( @@ -63,7 +64,7 @@ export const useToolApiCalls = () => { console.warn('[processFiles] Empty output treated as failure', { name: file.name }); failedFiles.push(file.name); try { - (markFileError as any)?.((file as any).fileId); + markFileError?.(file.fileId); } catch (e) { console.debug('markFileError', e); } @@ -71,7 +72,7 @@ export const useToolApiCalls = () => { } processedFiles.push(...responseFiles); // record source id as successful - successSourceIds.push((file as any).fileId); + successSourceIds.push(file.fileId); console.debug('[processFiles] Success', { name: file.name, produced: responseFiles.length }); } catch (error) { @@ -82,7 +83,7 @@ export const useToolApiCalls = () => { failedFiles.push(file.name); // mark errored file so UI can highlight try { - (markFileError as any)?.((file as any).fileId); + markFileError?.(file.fileId); } catch (e) { console.debug('markFileError', e); } diff --git a/frontend/src/core/hooks/tools/shared/useToolOperation.ts b/frontend/src/core/hooks/tools/shared/useToolOperation.ts index e1dccd1e7..4804032d0 100644 --- a/frontend/src/core/hooks/tools/shared/useToolOperation.ts +++ b/frontend/src/core/hooks/tools/shared/useToolOperation.ts @@ -8,6 +8,7 @@ import { useToolResources } from '@app/hooks/tools/shared/useToolResources'; import { extractErrorMessage } from '@app/utils/toolErrorHandler'; import { StirlingFile, extractFiles, FileId, StirlingFileStub, createStirlingFile } from '@app/types/fileContext'; import { FILE_EVENTS } from '@app/services/errorUtils'; +import { getFilenameWithoutExtension } from '@app/utils/fileUtils'; import { ResponseHandler } from '@app/utils/toolResponseProcessor'; import { createChildStub, generateProcessedFileMetadata } from '@app/contexts/file/fileActions'; import { ToolOperation } from '@app/types/file'; @@ -23,6 +24,20 @@ export enum ToolType { custom, } +/** + * Result from custom processor with optional metadata about input consumption. + */ +export interface CustomProcessorResult { + /** Processed output files */ + files: File[]; + /** + * When true, marks all input files as successfully consumed regardless of output count. + * Use when operation combines N inputs into fewer outputs (e.g., 3 images → 1 PDF). + * When false/undefined, uses filename-based mapping to determine which inputs succeeded. + */ + consumedAllInputs?: boolean; +} + /** * Configuration for tool operations defining processing behavior and API integration. * @@ -98,8 +113,12 @@ export interface CustomToolOperationConfig extends BaseToolOperationCon * Custom processing logic that completely bypasses standard file processing. * This tool handles all API calls, response processing, and file creation. * Use for tools with complex routing logic or non-standard processing requirements. + * + * Returns CustomProcessorResult with: + * - files: Processed output files + * - consumedAllInputs: true if operation combines N inputs → fewer outputs */ - customProcessor: (params: TParams, files: File[]) => Promise; + customProcessor: (params: TParams, files: File[]) => Promise; } export type ToolOperationConfig = SingleFileToolOperationConfig | MultiFileToolOperationConfig | CustomToolOperationConfig; @@ -172,17 +191,17 @@ export const useToolOperation = ( } // Handle zero-byte inputs explicitly: mark as error and continue with others - const zeroByteFiles = selectedFiles.filter(file => (file as any)?.size === 0); + const zeroByteFiles = selectedFiles.filter(file => file.size === 0); if (zeroByteFiles.length > 0) { try { for (const f of zeroByteFiles) { - (fileActions.markFileError as any)((f as any).fileId); + fileActions.markFileError(f.fileId); } } catch (e) { console.log('markFileError', e); } } - const validFiles = selectedFiles.filter(file => (file as any)?.size > 0); + const validFiles: StirlingFile[] = selectedFiles.filter(file => file.size > 0); if (validFiles.length === 0) { actions.setError(t('noValidFiles', 'No valid files to process')); return; @@ -215,7 +234,7 @@ export const useToolOperation = ( try { let processedFiles: File[]; - let successSourceIds: string[] = []; + let successSourceIds: FileId[] = []; // Use original files directly (no PDF metadata injection - history stored in IndexedDB) const filesForAPI = extractFiles(validFiles); @@ -233,14 +252,14 @@ export const useToolOperation = ( console.debug('[useToolOperation] Multi-file start', { count: filesForAPI.length }); const result = await processFiles( params, - filesForAPI, + validFiles, apiCallsConfig, actions.setProgress, actions.setStatus, - fileActions.markFileError as any + fileActions.markFileError ); processedFiles = result.outputFiles; - successSourceIds = result.successSourceIds as any; + successSourceIds = result.successSourceIds; console.debug('[useToolOperation] Multi-file results', { outputFiles: processedFiles.length, successSources: result.successSourceIds.length }); break; } @@ -268,30 +287,40 @@ export const useToolOperation = ( processedFiles = await extractZipFiles(response.data); } // Assume all inputs succeeded together unless server provided an error earlier - successSourceIds = validFiles.map(f => (f as any).fileId) as any; + successSourceIds = validFiles.map(f => f.fileId); break; } case ToolType.custom: { actions.setStatus('Processing files...'); - processedFiles = await config.customProcessor(params, filesForAPI); - // Try to map outputs back to inputs by filename (before extension) - const inputBaseNames = new Map(); - for (const f of validFiles) { - const base = (f.name || '').replace(/\.[^.]+$/, '').toLowerCase(); - inputBaseNames.set(base, (f as any).fileId); - } - const mappedSuccess: string[] = []; - for (const out of processedFiles) { - const base = (out.name || '').replace(/\.[^.]+$/, '').toLowerCase(); - const id = inputBaseNames.get(base); - if (id) mappedSuccess.push(id); - } - // Fallback to naive alignment if names don't match - if (mappedSuccess.length === 0) { - successSourceIds = validFiles.slice(0, processedFiles.length).map(f => (f as any).fileId) as any; + const result = await config.customProcessor(params, filesForAPI); + + processedFiles = result.files; + const consumedAllInputs = result.consumedAllInputs || false; + + // If consumedAllInputs flag is set, mark all inputs as successful + // (used for operations that combine N inputs into fewer outputs) + if (consumedAllInputs) { + successSourceIds = validFiles.map(f => f.fileId); } else { - successSourceIds = mappedSuccess as any; + // Try to map outputs back to inputs by filename (before extension) + const inputBaseNames = new Map(); + for (const f of validFiles) { + const base = getFilenameWithoutExtension(f.name || ''); + inputBaseNames.set(base, f.fileId); + } + const mappedSuccess: FileId[] = []; + for (const out of processedFiles) { + const base = getFilenameWithoutExtension(out.name || ''); + const id = inputBaseNames.get(base); + if (id) mappedSuccess.push(id); + } + // Fallback to naive alignment if names don't match + if (mappedSuccess.length === 0) { + successSourceIds = validFiles.slice(0, processedFiles.length).map(f => f.fileId); + } else { + successSourceIds = mappedSuccess; + } } break; } @@ -299,16 +328,16 @@ export const useToolOperation = ( // Normalize error flags across tool types: mark failures, clear successes try { - const allInputIds = validFiles.map(f => (f as any).fileId) as unknown as string[]; - const okSet = new Set((successSourceIds as unknown as string[]) || []); + const allInputIds = validFiles.map(f => f.fileId); + const okSet = new Set(successSourceIds); // Clear errors on successes for (const okId of okSet) { - try { (fileActions.clearFileError as any)(okId); } catch (_e) { void _e; } + try { fileActions.clearFileError(okId); } catch (_e) { void _e; } } // Mark errors on inputs that didn't succeed for (const id of allInputIds) { if (!okSet.has(id)) { - try { (fileActions.markFileError as any)(id); } catch (_e) { void _e; } + try { fileActions.markFileError(id); } catch (_e) { void _e; } } } } catch (_e) { void _e; } @@ -316,12 +345,12 @@ export const useToolOperation = ( if (externalErrorFileIds.length > 0) { // If backend told us which sources failed, prefer that mapping successSourceIds = validFiles - .map(f => (f as any).fileId) - .filter(id => !externalErrorFileIds.includes(id)) as any; + .map(f => f.fileId) + .filter(id => !externalErrorFileIds.includes(id)); // Also mark failed IDs immediately try { for (const badId of externalErrorFileIds) { - (fileActions.markFileError as any)(badId); + fileActions.markFileError(badId as FileId); } } catch (_e) { void _e; } } @@ -370,7 +399,7 @@ export const useToolOperation = ( ); // Always create child stubs linking back to the successful source inputs const successInputStubs = successSourceIds - .map((id) => selectors.getStirlingFileStub(id as any)) + .map((id) => selectors.getStirlingFileStub(id)) .filter(Boolean) as StirlingFileStub[]; if (successInputStubs.length !== processedFiles.length) { @@ -396,7 +425,7 @@ export const useToolOperation = ( return createStirlingFile(file, childStub.id); }); // Build consumption arrays aligned to the successful source IDs - const toConsumeInputIds = successSourceIds.filter((id: string) => inputFileIds.includes(id as any)) as unknown as FileId[]; + const toConsumeInputIds = successSourceIds.filter((id) => inputFileIds.includes(id)); // Outputs and stubs are already ordered by success sequence console.debug('[useToolOperation] Consuming files', { inputCount: inputFileIds.length, toConsume: toConsumeInputIds.length }); const outputFileIds = await consumeFiles(toConsumeInputIds, outputStirlingFiles, outputStirlingFileStubs); @@ -413,25 +442,27 @@ export const useToolOperation = ( } catch (error: any) { // Centralized 422 handler: mark provided IDs in errorFileIds try { - const status = (error?.response?.status as number | undefined); - if (status === 422) { + const status = error?.response?.status; + if (typeof status === 'number' && status === 422) { const payload = error?.response?.data; - let parsed: any = payload; + let parsed: unknown = payload; if (typeof payload === 'string') { try { parsed = JSON.parse(payload); } catch { parsed = payload; } - } else if (payload && typeof (payload as any).text === 'function') { + } else if (payload && typeof (payload as Blob).text === 'function') { // Blob or Response-like object from axios when responseType='blob' const text = await (payload as Blob).text(); try { parsed = JSON.parse(text); } catch { parsed = text; } } - let ids: string[] | undefined = Array.isArray(parsed?.errorFileIds) ? parsed.errorFileIds : undefined; + let ids: string[] | undefined = Array.isArray((parsed as { errorFileIds?: unknown })?.errorFileIds) + ? (parsed as { errorFileIds: string[] }).errorFileIds + : undefined; if (!ids && typeof parsed === 'string') { const match = parsed.match(/[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}/g); if (match && match.length > 0) ids = Array.from(new Set(match)); } if (ids && ids.length > 0) { for (const badId of ids) { - try { (fileActions.markFileError as any)(badId); } catch (_e) { void _e; } + try { fileActions.markFileError(badId as FileId); } catch (_e) { void _e; } } actions.setStatus('Process failed due to invalid/corrupted file(s)'); // Avoid duplicating toast messaging here diff --git a/frontend/src/core/utils/automationExecutor.ts b/frontend/src/core/utils/automationExecutor.ts index 6f1234f81..2651784e1 100644 --- a/frontend/src/core/utils/automationExecutor.ts +++ b/frontend/src/core/utils/automationExecutor.ts @@ -158,8 +158,8 @@ export const executeToolOperationWithPrefix = async ( try { // Check if tool uses custom processor (like Convert tool) if (config.customProcessor) { - const resultFiles = await config.customProcessor(parameters, files); - return resultFiles; + const result = await config.customProcessor(parameters, files); + return result.files; } // Execute based on tool type diff --git a/frontend/src/core/utils/convertUtils.ts b/frontend/src/core/utils/convertUtils.ts index ef2836058..b1e87161b 100644 --- a/frontend/src/core/utils/convertUtils.ts +++ b/frontend/src/core/utils/convertUtils.ts @@ -60,6 +60,18 @@ export const isWebFormat = (extension: string): boolean => { return ['html', 'zip'].includes(extension.toLowerCase()); }; +/** + * Checks if the given extension is an office format (Word, Excel, PowerPoint, OpenOffice) + * These formats use LibreOffice for conversion and require individual file processing + */ +export const isOfficeFormat = (extension: string): boolean => { + return [ + 'docx', 'doc', 'odt', // Word processors + 'xlsx', 'xls', 'ods', // Spreadsheets + 'pptx', 'ppt', 'odp' // Presentations + ].includes(extension.toLowerCase()); +}; + /** * Gets available target extensions for a given source extension * Extracted from useConvertParameters to be reusable in automation settings diff --git a/frontend/src/core/utils/fileUtils.ts b/frontend/src/core/utils/fileUtils.ts index 0f1471401..4061884b5 100644 --- a/frontend/src/core/utils/fileUtils.ts +++ b/frontend/src/core/utils/fileUtils.ts @@ -52,6 +52,29 @@ export function detectFileExtension(filename: string): string { return extension; } +/** + * Removes the file extension from a filename + * @param filename - The filename to process + * @param options - Options for processing + * @param options.preserveCase - If true, preserves original case. If false (default), converts to lowercase + * @returns Filename without extension + * @example + * getFilenameWithoutExtension('document.pdf') // 'document' + * getFilenameWithoutExtension('my.file.name.txt') // 'my.file.name' + * getFilenameWithoutExtension('REPORT.PDF', { preserveCase: true }) // 'REPORT' + */ +export function getFilenameWithoutExtension( + filename: string, + options: { preserveCase?: boolean } = {} +): string { + if (!filename || typeof filename !== 'string') return ''; + + const { preserveCase = false } = options; + const withoutExtension = filename.replace(/\.[^.]+$/, ''); + + return preserveCase ? withoutExtension : withoutExtension.toLowerCase(); +} + /** * Checks if a file is a PDF based on extension and MIME type * @param file - File or file-like object with name and type properties