From c526e18992f74dfa63eb7e2bc16a260fdf404a42 Mon Sep 17 00:00:00 2001 From: Anthony Stirling <77850077+Frooodle@users.noreply.github.com> Date: Sat, 3 Jun 2023 22:56:15 +0100 Subject: [PATCH] Split pages support n function and other stuff --- .github/workflows/swagger.yml | 45 + .gitignore | 1 + SwaggerDoc.json | 2329 +++++++++++++++++ build.gradle | 19 + .../software/SPDF/config/OpenApiConfig.java | 30 +- .../api/RearrangePagesPDFController.java | 74 +- .../controller/api/ScalePagesController.java | 139 +- .../controller/api/SplitPDFController.java | 34 +- .../api/security/CertSignController.java | 5 +- .../controller/web/OtherWebController.java | 7 + .../software/SPDF/utils/GeneralUtils.java | 61 + .../software/SPDF/utils/PdfUtils.java | 6 - src/main/resources/messages_en_GB.properties | 9 +- src/main/resources/static/js/downloader.js | 8 +- .../resources/templates/fragments/common.html | 3 + .../resources/templates/other/auto-crop.html | 31 + 16 files changed, 2682 insertions(+), 119 deletions(-) create mode 100644 .github/workflows/swagger.yml create mode 100644 SwaggerDoc.json create mode 100644 src/main/resources/templates/other/auto-crop.html diff --git a/.github/workflows/swagger.yml b/.github/workflows/swagger.yml new file mode 100644 index 00000000..59258e3e --- /dev/null +++ b/.github/workflows/swagger.yml @@ -0,0 +1,45 @@ +name: Update Swagger + +on: + workflow_dispatch: + push: + branches: + - master +jobs: + push: + + runs-on: ubuntu-latest + steps: + + - uses: actions/checkout@v3.5.2 + + - name: Set up JDK 17 + uses: actions/setup-java@v3.11.0 + with: + java-version: '17' + distribution: 'temurin' + + - name: Grant execute permission for gradlew + run: chmod +x gradlew + + - name: Generate Swagger documentation + run: ./gradlew generateOpenApiDocs + + - name: Get version number + id: versionNumber + run: echo "::set-output name=versionNumber::$(./gradlew printVersion --quiet | tail -1)" + + - name: Commit and push if it changed + run: | + git config --local user.email "action@github.com" + git config --local user.name "GitHub Action" + git add -A + git diff --quiet && git diff --staged --quiet || git commit -m "Updated Swagger documentation" + git remote set-url origin https://x-access-token:${{ secrets.GITHUB_TOKEN }}@github.com/${{ github.repository }}.git + git push + + - name: Upload Swagger Documentation to SwaggerHub + run: | + curl -X POST -H "Authorization: ${SWAGGERHUB_API_KEY}" -H "Content-Type: application/json" -d @SwaggerDoc.json "https://api.swaggerhub.com/apis/Frooodle/Stirling-PDF/${{ steps.versionNumber.outputs.versionNumber }}?isPrivate=false&force=true" + env: + SWAGGERHUB_API_KEY: ${{ secrets.SWAGGERHUB_API_KEY }} diff --git a/.gitignore b/.gitignore index 3c11770c..0a5cd198 100644 --- a/.gitignore +++ b/.gitignore @@ -14,6 +14,7 @@ local.properties .recommenders .classpath .project +version.properties # Gradle .gradle diff --git a/SwaggerDoc.json b/SwaggerDoc.json new file mode 100644 index 00000000..39db60b4 --- /dev/null +++ b/SwaggerDoc.json @@ -0,0 +1,2329 @@ +{ + "openapi": "3.0.1", + "info": { + "title": "Stirling PDF API", + "description": "API documentation for all Server-Side processing.\nPlease note some functionality might be UI only and missing from here.", + "version": "0.10.0" + }, + "servers": [ + { + "url": "http://localhost:8080", + "description": "Generated server url" + } + ], + "paths": { + "/update-metadata": { + "post": { + "tags": [ + "metadata-controller" + ], + "summary": "Update metadata of a PDF file", + "description": "This endpoint allows you to update the metadata of a given PDF file. You can add, modify, or delete standard and custom metadata fields.", + "operationId": "metadata", + "parameters": [ + { + "name": "deleteAll", + "in": "query", + "description": "Delete all metadata if set to true", + "required": false, + "schema": { + "type": "boolean", + "default": false + } + }, + { + "name": "author", + "in": "query", + "description": "The author of the document", + "required": false, + "schema": { + "type": "string" + } + }, + { + "name": "creationDate", + "in": "query", + "description": "The creation date of the document (format: yyyy/MM/dd HH:mm:ss)", + "required": false, + "schema": { + "type": "string" + } + }, + { + "name": "creator", + "in": "query", + "description": "The creator of the document", + "required": false, + "schema": { + "type": "string" + } + }, + { + "name": "keywords", + "in": "query", + "description": "The keywords for the document", + "required": false, + "schema": { + "type": "string" + } + }, + { + "name": "modificationDate", + "in": "query", + "description": "The modification date of the document (format: yyyy/MM/dd HH:mm:ss)", + "required": false, + "schema": { + "type": "string" + } + }, + { + "name": "producer", + "in": "query", + "description": "The producer of the document", + "required": false, + "schema": { + "type": "string" + } + }, + { + "name": "subject", + "in": "query", + "description": "The subject of the document", + "required": false, + "schema": { + "type": "string" + } + }, + { + "name": "title", + "in": "query", + "description": "The title of the document", + "required": false, + "schema": { + "type": "string" + } + }, + { + "name": "trapped", + "in": "query", + "description": "The trapped status of the document", + "required": false, + "schema": { + "type": "string" + } + }, + { + "name": "allRequestParams", + "in": "query", + "required": true, + "schema": { + "type": "object", + "additionalProperties": { + "type": "string" + } + } + } + ], + "requestBody": { + "content": { + "multipart/form-data": { + "schema": { + "required": [ + "fileInput" + ], + "type": "object", + "properties": { + "fileInput": { + "type": "string", + "description": "The input PDF file to update metadata", + "format": "binary" + } + } + } + } + } + }, + "responses": { + "200": { + "description": "OK", + "content": { + "*/*": { + "schema": { + "type": "array", + "items": { + "type": "string", + "format": "byte" + } + } + } + } + } + } + } + }, + "/split-pages": { + "post": { + "tags": [ + "split-pdf-controller" + ], + "summary": "Split a PDF file into separate documents", + "description": "This endpoint splits a given PDF file into separate documents based on the specified page numbers or ranges. Users can specify pages using individual numbers, ranges, or \u0027all\u0027 for every page.", + "operationId": "splitPdf", + "parameters": [ + { + "name": "pages", + "in": "query", + "description": "The pages to be included in separate documents. Specify individual page numbers (e.g., \u00271,3,5\u0027), ranges (e.g., \u00271-3,5-7\u0027), or \u0027all\u0027 for every page.", + "required": true, + "schema": { + "type": "string" + } + } + ], + "requestBody": { + "content": { + "multipart/form-data": { + "schema": { + "required": [ + "fileInput" + ], + "type": "object", + "properties": { + "fileInput": { + "type": "string", + "description": "The input PDF file to be split", + "format": "binary" + } + } + } + } + } + }, + "responses": { + "200": { + "description": "OK", + "content": { + "*/*": { + "schema": { + "type": "string", + "format": "binary" + } + } + } + } + } + } + }, + "/scale-pages": { + "post": { + "tags": [ + "scale-pages-controller" + ], + "summary": "Change the size of a PDF page/document", + "description": "This operation takes an input PDF file and the size to scale the pages to in the output PDF file.", + "operationId": "scalePages", + "parameters": [ + { + "name": "pageSize", + "in": "query", + "description": "The scale of pages in the output PDF. Acceptable values are A0-A10, B0-B9, LETTER, TABLOID, LEDGER, LEGAL, EXECUTIVE.", + "required": true, + "schema": { + "type": "String", + "enum": [ + "A0", + "A1", + "A2", + "A3", + "A4", + "A5", + "A6", + "A7", + "A8", + "A9", + "A10", + "B0", + "B1", + "B2", + "B3", + "B4", + "B5", + "B6", + "B7", + "B8", + "B9", + "LETTER", + "TABLOID", + "LEDGER", + "LEGAL", + "EXECUTIVE" + ] + } + }, + { + "name": "scaleFactor", + "in": "query", + "description": "The scale of the content on the pages of the output PDF. Acceptable values are floats.", + "required": true, + "schema": { + "type": "float" + } + } + ], + "requestBody": { + "content": { + "multipart/form-data": { + "schema": { + "required": [ + "fileInput" + ], + "type": "object", + "properties": { + "fileInput": { + "type": "string", + "description": "The input PDF file", + "format": "binary" + } + } + } + } + } + }, + "responses": { + "200": { + "description": "OK", + "content": { + "*/*": { + "schema": { + "type": "array", + "items": { + "type": "string", + "format": "byte" + } + } + } + } + } + } + } + }, + "/rotate-pdf": { + "post": { + "tags": [ + "rotation-controller" + ], + "summary": "Rotate a PDF file", + "description": "This endpoint rotates a given PDF file by a specified angle. The angle must be a multiple of 90.", + "operationId": "rotatePDF", + "parameters": [ + { + "name": "angle", + "in": "query", + "description": "The angle by which to rotate the PDF file. This should be a multiple of 90.", + "required": true, + "schema": { + "type": "integer", + "format": "int32" + }, + "example": 90 + } + ], + "requestBody": { + "content": { + "multipart/form-data": { + "schema": { + "required": [ + "fileInput" + ], + "type": "object", + "properties": { + "fileInput": { + "type": "string", + "description": "The PDF file to be rotated", + "format": "binary" + } + } + } + } + } + }, + "responses": { + "200": { + "description": "OK", + "content": { + "*/*": { + "schema": { + "type": "array", + "items": { + "type": "string", + "format": "byte" + } + } + } + } + } + } + } + }, + "/repair": { + "post": { + "tags": [ + "repair-controller" + ], + "summary": "Repair a PDF file", + "description": "This endpoint repairs a given PDF file by running Ghostscript command. The PDF is first saved to a temporary location, repaired, read back, and then returned as a response.", + "operationId": "repairPdf", + "requestBody": { + "content": { + "multipart/form-data": { + "schema": { + "required": [ + "fileInput" + ], + "type": "object", + "properties": { + "fileInput": { + "type": "string", + "description": "The input PDF file to be repaired", + "format": "binary" + } + } + } + } + } + }, + "responses": { + "200": { + "description": "OK", + "content": { + "*/*": { + "schema": { + "type": "array", + "items": { + "type": "string", + "format": "byte" + } + } + } + } + } + } + } + }, + "/remove-password": { + "post": { + "tags": [ + "password-controller" + ], + "summary": "Remove password from a PDF file", + "description": "This endpoint removes the password from a protected PDF file. Users need to provide the existing password.", + "operationId": "removePassword", + "parameters": [ + { + "name": "password", + "in": "query", + "description": "The password of the PDF file", + "required": true, + "schema": { + "type": "string" + } + } + ], + "requestBody": { + "content": { + "multipart/form-data": { + "schema": { + "required": [ + "fileInput" + ], + "type": "object", + "properties": { + "fileInput": { + "type": "string", + "description": "The input PDF file from which the password should be removed", + "format": "binary" + } + } + } + } + } + }, + "responses": { + "200": { + "description": "OK", + "content": { + "*/*": { + "schema": { + "type": "array", + "items": { + "type": "string", + "format": "byte" + } + } + } + } + } + } + } + }, + "/remove-pages": { + "post": { + "tags": [ + "rearrange-pages-pdf-controller" + ], + "summary": "Remove pages from a PDF file", + "description": "This endpoint removes specified pages from a given PDF file. Users can provide a comma-separated list of page numbers or ranges to delete.", + "operationId": "deletePages", + "parameters": [ + { + "name": "pagesToDelete", + "in": "query", + "description": "Comma-separated list of pages or page ranges to delete, e.g., \u00271,3,5-8\u0027", + "required": true, + "schema": { + "type": "string" + } + } + ], + "requestBody": { + "content": { + "multipart/form-data": { + "schema": { + "required": [ + "fileInput" + ], + "type": "object", + "properties": { + "fileInput": { + "type": "string", + "description": "The input PDF file from which pages will be removed", + "format": "binary" + } + } + } + } + } + }, + "responses": { + "200": { + "description": "OK", + "content": { + "*/*": { + "schema": { + "type": "array", + "items": { + "type": "string", + "format": "byte" + } + } + } + } + } + } + } + }, + "/remove-blanks": { + "post": { + "tags": [ + "blank-page-controller" + ], + "summary": "Remove blank pages from a PDF file", + "description": "This endpoint removes blank pages from a given PDF file. Users can specify the threshold and white percentage to tune the detection of blank pages.", + "operationId": "removeBlankPages", + "parameters": [ + { + "name": "threshold", + "in": "query", + "description": "The threshold value to determine blank pages", + "required": false, + "schema": { + "type": "integer", + "format": "int32", + "default": 10 + }, + "example": 10 + }, + { + "name": "whitePercent", + "in": "query", + "description": "The percentage of white color on a page to consider it as blank", + "required": false, + "schema": { + "type": "number", + "format": "float", + "default": 99.9 + }, + "example": 99.9 + } + ], + "requestBody": { + "content": { + "multipart/form-data": { + "schema": { + "required": [ + "fileInput" + ], + "type": "object", + "properties": { + "fileInput": { + "type": "string", + "description": "The input PDF file from which blank pages will be removed", + "format": "binary" + } + } + } + } + } + }, + "responses": { + "200": { + "description": "OK", + "content": { + "*/*": { + "schema": { + "type": "array", + "items": { + "type": "string", + "format": "byte" + } + } + } + } + } + } + } + }, + "/rearrange-pages": { + "post": { + "tags": [ + "rearrange-pages-pdf-controller" + ], + "summary": "Rearrange pages in a PDF file", + "description": "This endpoint rearranges pages in a given PDF file based on the specified page order or custom mode. Users can provide a page order as a comma-separated list of page numbers or page ranges, or a custom mode.", + "operationId": "rearrangePages", + "parameters": [ + { + "name": "pageOrder", + "in": "query", + "description": "The new page order as a comma-separated list of page numbers, page ranges (e.g., \u00271,3,5-7\u0027), or functions in the format \u0027an+b\u0027 where \u0027a\u0027 is the multiplier of the page number \u0027n\u0027, and \u0027b\u0027 is a constant (e.g., \u00272n+1\u0027, \u00273n\u0027, \u00276n-5\u0027)", + "required": false, + "schema": { + "type": "string" + } + }, + { + "name": "customMode", + "in": "query", + "required": false, + "schema": { + "type": "string", + "enum": [ + "REVERSE_ORDER", + "DUPLEX_SORT", + "BOOKLET_SORT", + "ODD_EVEN_SPLIT", + "REMOVE_FIRST", + "REMOVE_LAST", + "REMOVE_FIRST_AND_LAST" + ] + } + } + ], + "requestBody": { + "content": { + "multipart/form-data": { + "schema": { + "required": [ + "fileInput" + ], + "type": "object", + "properties": { + "fileInput": { + "type": "string", + "description": "The input PDF file to rearrange pages", + "format": "binary" + } + } + } + } + } + }, + "responses": { + "200": { + "description": "OK", + "content": { + "*/*": { + "schema": { + "type": "array", + "items": { + "type": "string", + "format": "byte" + } + } + } + } + } + } + } + }, + "/pdf-to-xml": { + "post": { + "tags": [ + "convert-pdf-to-office" + ], + "summary": "Convert PDF to XML", + "description": "This endpoint converts a PDF file to an XML file.", + "operationId": "processPdfToXML", + "requestBody": { + "content": { + "multipart/form-data": { + "schema": { + "required": [ + "fileInput" + ], + "type": "object", + "properties": { + "fileInput": { + "type": "string", + "description": "The input PDF file to be converted to an XML file", + "format": "binary" + } + } + } + } + } + }, + "responses": { + "200": { + "description": "OK", + "content": { + "*/*": { + "schema": { + "type": "array", + "items": { + "type": "string", + "format": "byte" + } + } + } + } + } + } + } + }, + "/pdf-to-word": { + "post": { + "tags": [ + "convert-pdf-to-office" + ], + "summary": "Convert PDF to Word document", + "description": "This endpoint converts a given PDF file to a Word document format.", + "operationId": "processPdfToWord", + "parameters": [ + { + "name": "outputFormat", + "in": "query", + "description": "The output Word document format", + "required": true, + "schema": { + "type": "string", + "enum": [ + "doc", + "docx", + "odt" + ] + } + } + ], + "requestBody": { + "content": { + "multipart/form-data": { + "schema": { + "required": [ + "fileInput" + ], + "type": "object", + "properties": { + "fileInput": { + "type": "string", + "description": "The input PDF file", + "format": "binary" + } + } + } + } + } + }, + "responses": { + "200": { + "description": "OK", + "content": { + "*/*": { + "schema": { + "type": "array", + "items": { + "type": "string", + "format": "byte" + } + } + } + } + } + } + } + }, + "/pdf-to-text": { + "post": { + "tags": [ + "convert-pdf-to-office" + ], + "summary": "Convert PDF to Text or RTF format", + "description": "This endpoint converts a given PDF file to Text or RTF format.", + "operationId": "processPdfToRTForTXT", + "parameters": [ + { + "name": "outputFormat", + "in": "query", + "description": "The output Text or RTF format", + "required": true, + "schema": { + "type": "string", + "enum": [ + "rtf", + "txt:Text" + ] + } + } + ], + "requestBody": { + "content": { + "multipart/form-data": { + "schema": { + "required": [ + "fileInput" + ], + "type": "object", + "properties": { + "fileInput": { + "type": "string", + "description": "The input PDF file", + "format": "binary" + } + } + } + } + } + }, + "responses": { + "200": { + "description": "OK", + "content": { + "*/*": { + "schema": { + "type": "array", + "items": { + "type": "string", + "format": "byte" + } + } + } + } + } + } + } + }, + "/pdf-to-presentation": { + "post": { + "tags": [ + "convert-pdf-to-office" + ], + "summary": "Convert PDF to Presentation format", + "description": "This endpoint converts a given PDF file to a Presentation format.", + "operationId": "processPdfToPresentation", + "parameters": [ + { + "name": "outputFormat", + "in": "query", + "description": "The output Presentation format", + "required": true, + "schema": { + "type": "string", + "enum": [ + "ppt", + "pptx", + "odp" + ] + } + } + ], + "requestBody": { + "content": { + "multipart/form-data": { + "schema": { + "required": [ + "fileInput" + ], + "type": "object", + "properties": { + "fileInput": { + "type": "string", + "description": "The input PDF file", + "format": "binary" + } + } + } + } + } + }, + "responses": { + "200": { + "description": "OK", + "content": { + "*/*": { + "schema": { + "type": "array", + "items": { + "type": "string", + "format": "byte" + } + } + } + } + } + } + } + }, + "/pdf-to-pdfa": { + "post": { + "tags": [ + "convert-pdf-to-pdfa" + ], + "summary": "Convert a PDF to a PDF/A", + "description": "This endpoint converts a PDF file to a PDF/A file. PDF/A is a format designed for long-term archiving of digital documents.", + "operationId": "pdfToPdfA", + "requestBody": { + "content": { + "multipart/form-data": { + "schema": { + "required": [ + "fileInput" + ], + "type": "object", + "properties": { + "fileInput": { + "type": "string", + "description": "The input PDF file to be converted to a PDF/A file", + "format": "binary" + } + } + } + } + } + }, + "responses": { + "200": { + "description": "OK", + "content": { + "*/*": { + "schema": { + "type": "array", + "items": { + "type": "string", + "format": "byte" + } + } + } + } + } + } + } + }, + "/pdf-to-img": { + "post": { + "tags": [ + "convert-img-pdf-controller" + ], + "summary": "Convert PDF to image(s)", + "description": "This endpoint converts a PDF file to image(s) with the specified image format, color type, and DPI. Users can choose to get a single image or multiple images.", + "operationId": "convertToImage", + "parameters": [ + { + "name": "imageFormat", + "in": "query", + "description": "The output image format", + "required": true, + "schema": { + "type": "string", + "enum": [ + "png", + "jpeg", + "jpg", + "gif" + ] + } + }, + { + "name": "singleOrMultiple", + "in": "query", + "description": "Choose between a single image containing all pages or separate images for each page", + "required": true, + "schema": { + "type": "string", + "enum": [ + "single", + "multiple" + ] + } + }, + { + "name": "colorType", + "in": "query", + "description": "The color type of the output image(s)", + "required": true, + "schema": { + "type": "string", + "enum": [ + "rgb", + "greyscale", + "blackwhite" + ] + } + }, + { + "name": "dpi", + "in": "query", + "description": "The DPI (dots per inch) for the output image(s)", + "required": true, + "schema": { + "type": "string" + } + } + ], + "requestBody": { + "content": { + "multipart/form-data": { + "schema": { + "required": [ + "fileInput" + ], + "type": "object", + "properties": { + "fileInput": { + "type": "string", + "description": "The input PDF file to be converted", + "format": "binary" + } + } + } + } + } + }, + "responses": { + "200": { + "description": "OK", + "content": { + "*/*": { + "schema": { + "type": "string", + "format": "binary" + } + } + } + } + } + } + }, + "/pdf-to-html": { + "post": { + "tags": [ + "convert-pdf-to-office" + ], + "summary": "Convert PDF to HTML", + "description": "This endpoint converts a PDF file to HTML format.", + "operationId": "processPdfToHTML", + "requestBody": { + "content": { + "multipart/form-data": { + "schema": { + "required": [ + "fileInput" + ], + "type": "object", + "properties": { + "fileInput": { + "type": "string", + "description": "The input PDF file to be converted to HTML format", + "format": "binary" + } + } + } + } + } + }, + "responses": { + "200": { + "description": "OK", + "content": { + "*/*": { + "schema": { + "type": "array", + "items": { + "type": "string", + "format": "byte" + } + } + } + } + } + } + } + }, + "/ocr-pdf": { + "post": { + "tags": [ + "ocr-controller" + ], + "summary": "Process a PDF file with OCR", + "description": "This endpoint processes a PDF file using OCR (Optical Character Recognition). Users can specify languages, sidecar, deskew, clean, cleanFinal, ocrType, ocrRenderType, and removeImagesAfter options.", + "operationId": "processPdfWithOCR", + "parameters": [ + { + "name": "languages", + "in": "query", + "description": "List of languages to use in OCR processing", + "required": true, + "schema": { + "type": "array", + "items": { + "type": "string" + } + } + }, + { + "name": "sidecar", + "in": "query", + "description": "Include OCR text in a sidecar text file if set to true", + "required": false, + "schema": { + "type": "boolean" + } + }, + { + "name": "deskew", + "in": "query", + "description": "Deskew the input file if set to true", + "required": false, + "schema": { + "type": "boolean" + } + }, + { + "name": "clean", + "in": "query", + "description": "Clean the input file if set to true", + "required": false, + "schema": { + "type": "boolean" + } + }, + { + "name": "clean-final", + "in": "query", + "description": "Clean the final output if set to true", + "required": false, + "schema": { + "type": "boolean" + } + }, + { + "name": "ocrType", + "in": "query", + "description": "Specify the OCR type, e.g., \u0027skip-text\u0027, \u0027force-ocr\u0027, or \u0027Normal\u0027", + "required": false, + "schema": { + "type": "string", + "enum": [ + "skip-text", + "force-ocr", + "Normal" + ] + } + }, + { + "name": "ocrRenderType", + "in": "query", + "description": "Specify the OCR render type, either \u0027hocr\u0027 or \u0027sandwich\u0027", + "required": false, + "schema": { + "type": "string", + "enum": [ + "hocr", + "sandwich" + ] + } + }, + { + "name": "removeImagesAfter", + "in": "query", + "description": "Remove images from the output PDF if set to true", + "required": false, + "schema": { + "type": "boolean" + } + } + ], + "requestBody": { + "content": { + "multipart/form-data": { + "schema": { + "required": [ + "fileInput" + ], + "type": "object", + "properties": { + "fileInput": { + "type": "string", + "description": "The input PDF file to be processed with OCR", + "format": "binary" + } + } + } + } + } + }, + "responses": { + "200": { + "description": "OK", + "content": { + "*/*": { + "schema": { + "type": "array", + "items": { + "type": "string", + "format": "byte" + } + } + } + } + } + } + } + }, + "/multi-page-layout": { + "post": { + "tags": [ + "multi-page-layout-controller" + ], + "summary": "Merge multiple pages of a PDF document into a single page", + "description": "This operation takes an input PDF file and the number of pages to merge into a single sheet in the output PDF file.", + "operationId": "mergeMultiplePagesIntoOne", + "parameters": [ + { + "name": "pagesPerSheet", + "in": "query", + "description": "The number of pages to fit onto a single sheet in the output PDF. Acceptable values are 2, 3, 4, 9, 16.", + "required": true, + "schema": { + "type": "integer", + "enum": [ + "2", + "3", + "4", + "9", + "16" + ] + } + } + ], + "requestBody": { + "content": { + "multipart/form-data": { + "schema": { + "required": [ + "fileInput" + ], + "type": "object", + "properties": { + "fileInput": { + "type": "string", + "description": "The input PDF file", + "format": "binary" + } + } + } + } + } + }, + "responses": { + "200": { + "description": "OK", + "content": { + "*/*": { + "schema": { + "type": "array", + "items": { + "type": "string", + "format": "byte" + } + } + } + } + } + } + } + }, + "/merge-pdfs": { + "post": { + "tags": [ + "merge-controller" + ], + "summary": "Merge multiple PDF files into one", + "description": "This endpoint merges multiple PDF files into a single PDF file. The merged file will contain all pages from the input files in the order they were provided.", + "operationId": "mergePdfs", + "requestBody": { + "content": { + "multipart/form-data": { + "schema": { + "required": [ + "fileInput" + ], + "type": "object", + "properties": { + "fileInput": { + "type": "array", + "description": "The input PDF files to be merged into a single file", + "items": { + "type": "string", + "format": "binary" + } + } + } + } + } + } + }, + "responses": { + "200": { + "description": "OK", + "content": { + "*/*": { + "schema": { + "type": "array", + "items": { + "type": "string", + "format": "byte" + } + } + } + } + } + } + } + }, + "/img-to-pdf": { + "post": { + "tags": [ + "convert-img-pdf-controller" + ], + "summary": "Convert images to a PDF file", + "description": "This endpoint converts one or more images to a PDF file. Users can specify whether to stretch the images to fit the PDF page, and whether to automatically rotate the images.", + "operationId": "convertToPdf", + "parameters": [ + { + "name": "stretchToFit", + "in": "query", + "description": "Whether to stretch the images to fit the PDF page or maintain the aspect ratio", + "required": false, + "schema": { + "type": "boolean", + "default": false + }, + "example": false + }, + { + "name": "colorType", + "in": "query", + "description": "The color type of the output image(s)", + "required": true, + "schema": { + "type": "string", + "enum": [ + "rgb", + "greyscale", + "blackwhite" + ] + } + }, + { + "name": "autoRotate", + "in": "query", + "description": "Whether to automatically rotate the images to better fit the PDF page", + "required": false, + "schema": { + "type": "boolean", + "default": false + }, + "example": true + } + ], + "requestBody": { + "content": { + "multipart/form-data": { + "schema": { + "required": [ + "fileInput" + ], + "type": "object", + "properties": { + "fileInput": { + "type": "array", + "description": "The input images to be converted to a PDF file", + "items": { + "type": "string", + "format": "binary" + } + } + } + } + } + } + }, + "responses": { + "200": { + "description": "OK", + "content": { + "*/*": { + "schema": { + "type": "array", + "items": { + "type": "string", + "format": "byte" + } + } + } + } + } + } + } + }, + "/file-to-pdf": { + "post": { + "tags": [ + "convert-office-controller" + ], + "summary": "Convert a file to a PDF using OCR", + "description": "This endpoint converts a given file to a PDF using Optical Character Recognition (OCR). The filename of the resulting PDF will be the original filename with \u0027_convertedToPDF.pdf\u0027 appended.", + "operationId": "processPdfWithOCR_1", + "requestBody": { + "content": { + "multipart/form-data": { + "schema": { + "required": [ + "fileInput" + ], + "type": "object", + "properties": { + "fileInput": { + "type": "string", + "description": "The input file to be converted to a PDF file using OCR", + "format": "binary" + } + } + } + } + } + }, + "responses": { + "200": { + "description": "OK", + "content": { + "*/*": { + "schema": { + "type": "array", + "items": { + "type": "string", + "format": "byte" + } + } + } + } + } + } + } + }, + "/extract-images": { + "post": { + "tags": [ + "extract-images-controller" + ], + "summary": "Extract images from a PDF file", + "description": "This endpoint extracts images from a given PDF file and returns them in a zip file. Users can specify the output image format.", + "operationId": "extractImages", + "parameters": [ + { + "name": "format", + "in": "query", + "description": "The output image format e.g., \u0027png\u0027, \u0027jpeg\u0027, or \u0027gif\u0027", + "required": true, + "schema": { + "type": "string", + "enum": [ + "png", + "jpeg", + "gif" + ] + } + } + ], + "requestBody": { + "content": { + "multipart/form-data": { + "schema": { + "required": [ + "fileInput" + ], + "type": "object", + "properties": { + "fileInput": { + "type": "string", + "description": "The input PDF file containing images", + "format": "binary" + } + } + } + } + } + }, + "responses": { + "200": { + "description": "OK", + "content": { + "*/*": { + "schema": { + "type": "array", + "items": { + "type": "string", + "format": "byte" + } + } + } + } + } + } + } + }, + "/extract-image-scans": { + "post": { + "tags": [ + "extract-image-scans-controller" + ], + "summary": "Extract image scans from an input file", + "description": "This endpoint extracts image scans from a given file based on certain parameters. Users can specify angle threshold, tolerance, minimum area, minimum contour area, and border size.", + "operationId": "extractImageScans", + "parameters": [ + { + "name": "angle_threshold", + "in": "query", + "description": "The angle threshold for the image scan extraction", + "required": false, + "schema": { + "type": "integer", + "format": "int32", + "default": 5 + }, + "example": 5 + }, + { + "name": "tolerance", + "in": "query", + "description": "The tolerance for the image scan extraction", + "required": false, + "schema": { + "type": "integer", + "format": "int32", + "default": 20 + }, + "example": 20 + }, + { + "name": "min_area", + "in": "query", + "description": "The minimum area for the image scan extraction", + "required": false, + "schema": { + "type": "integer", + "format": "int32", + "default": 8000 + }, + "example": 8000 + }, + { + "name": "min_contour_area", + "in": "query", + "description": "The minimum contour area for the image scan extraction", + "required": false, + "schema": { + "type": "integer", + "format": "int32", + "default": 500 + }, + "example": 500 + }, + { + "name": "border_size", + "in": "query", + "description": "The border size for the image scan extraction", + "required": false, + "schema": { + "type": "integer", + "format": "int32", + "default": 1 + }, + "example": 1 + } + ], + "requestBody": { + "content": { + "multipart/form-data": { + "schema": { + "required": [ + "fileInput" + ], + "type": "object", + "properties": { + "fileInput": { + "type": "string", + "description": "The input file containing image scans", + "format": "binary" + } + } + } + } + } + }, + "responses": { + "200": { + "description": "OK", + "content": { + "*/*": { + "schema": { + "type": "array", + "items": { + "type": "string", + "format": "byte" + } + } + } + } + } + } + } + }, + "/compress-pdf": { + "post": { + "tags": [ + "compress-controller" + ], + "summary": "Optimize PDF file", + "description": "This endpoint accepts a PDF file and optimizes it based on the provided parameters.", + "operationId": "optimizePdf", + "parameters": [ + { + "name": "optimizeLevel", + "in": "query", + "description": "The level of optimization to apply to the PDF file. Higher values indicate greater compression but may reduce quality.", + "required": false, + "schema": { + "type": "string", + "enum": [ + "1", + "2", + "3", + "4", + "5" + ] + } + }, + { + "name": "expectedOutputSize", + "in": "query", + "description": "The expected output size, e.g. \u0027100MB\u0027, \u002725KB\u0027, etc.", + "required": false, + "schema": { + "type": "string" + } + } + ], + "requestBody": { + "content": { + "multipart/form-data": { + "schema": { + "required": [ + "fileInput" + ], + "type": "object", + "properties": { + "fileInput": { + "type": "string", + "description": "The input PDF file to be optimized.", + "format": "binary" + } + } + } + } + } + }, + "responses": { + "200": { + "description": "OK", + "content": { + "*/*": { + "schema": { + "type": "array", + "items": { + "type": "string", + "format": "byte" + } + } + } + } + } + } + } + }, + "/cert-sign": { + "post": { + "tags": [ + "cert-sign-controller" + ], + "summary": "Sign PDF with a Digital Certificate", + "description": "This endpoint accepts a PDF file, a digital certificate and related information to sign the PDF. It then returns the digitally signed PDF file.", + "operationId": "signPDF", + "parameters": [ + { + "name": "certType", + "in": "query", + "description": "The type of the digital certificate", + "required": false, + "schema": { + "type": "string", + "enum": [ + "PKCS12", + "PEM" + ] + } + }, + { + "name": "password", + "in": "query", + "description": "The password for the keystore or the private key", + "required": false, + "schema": { + "type": "string" + } + }, + { + "name": "showSignature", + "in": "query", + "description": "Whether to visually show the signature in the PDF file", + "required": false, + "schema": { + "type": "boolean" + } + }, + { + "name": "reason", + "in": "query", + "description": "The reason for signing the PDF", + "required": false, + "schema": { + "type": "string" + } + }, + { + "name": "location", + "in": "query", + "description": "The location where the PDF is signed", + "required": false, + "schema": { + "type": "string" + } + }, + { + "name": "name", + "in": "query", + "description": "The name of the signer", + "required": false, + "schema": { + "type": "string" + } + }, + { + "name": "pageNumber", + "in": "query", + "description": "The page number where the signature should be visible. This is required if showSignature is set to true", + "required": false, + "schema": { + "type": "integer", + "format": "int32" + } + } + ], + "requestBody": { + "content": { + "multipart/form-data": { + "schema": { + "required": [ + "fileInput" + ], + "type": "object", + "properties": { + "fileInput": { + "type": "string", + "description": "The input PDF file to be signed", + "format": "binary" + }, + "key": { + "type": "string", + "description": "The private key for the digital certificate (required for PEM type certificates)", + "format": "binary" + }, + "cert": { + "type": "string", + "description": "The digital certificate (required for PEM type certificates)", + "format": "binary" + }, + "p12": { + "type": "string", + "description": "The PKCS12 keystore file (required for PKCS12 type certificates)", + "format": "binary" + } + } + } + } + } + }, + "responses": { + "200": { + "description": "OK", + "content": { + "*/*": { + "schema": { + "type": "array", + "items": { + "type": "string", + "format": "byte" + } + } + } + } + } + } + } + }, + "/auto-crop": { + "post": { + "tags": [ + "scale-pages-controller" + ], + "operationId": "cropPdf", + "requestBody": { + "content": { + "multipart/form-data": { + "schema": { + "required": [ + "fileInput" + ], + "type": "object", + "properties": { + "fileInput": { + "type": "string", + "format": "binary" + } + } + } + } + } + }, + "responses": { + "200": { + "description": "OK", + "content": { + "*/*": { + "schema": { + "type": "array", + "items": { + "type": "string", + "format": "byte" + } + } + } + } + } + } + } + }, + "/add-watermark": { + "post": { + "tags": [ + "watermark-controller" + ], + "summary": "Add watermark to a PDF file", + "description": "This endpoint adds a watermark to a given PDF file. Users can specify the watermark text, font size, rotation, opacity, width spacer, and height spacer.", + "operationId": "addWatermark", + "parameters": [ + { + "name": "watermarkText", + "in": "query", + "description": "The watermark text to add to the PDF file", + "required": true, + "schema": { + "type": "string" + } + }, + { + "name": "fontSize", + "in": "query", + "description": "The font size of the watermark text", + "required": false, + "schema": { + "type": "number", + "format": "float", + "default": 30.0 + }, + "example": 30 + }, + { + "name": "rotation", + "in": "query", + "description": "The rotation of the watermark text in degrees", + "required": false, + "schema": { + "type": "number", + "format": "float", + "default": 0.0 + }, + "example": 0 + }, + { + "name": "opacity", + "in": "query", + "description": "The opacity of the watermark text (0.0 - 1.0)", + "required": false, + "schema": { + "type": "number", + "format": "float", + "default": 0.5 + }, + "example": 0.5 + }, + { + "name": "widthSpacer", + "in": "query", + "description": "The width spacer between watermark texts", + "required": false, + "schema": { + "type": "integer", + "format": "int32", + "default": 50 + }, + "example": 50 + }, + { + "name": "heightSpacer", + "in": "query", + "description": "The height spacer between watermark texts", + "required": false, + "schema": { + "type": "integer", + "format": "int32", + "default": 50 + }, + "example": 50 + } + ], + "requestBody": { + "content": { + "multipart/form-data": { + "schema": { + "required": [ + "fileInput" + ], + "type": "object", + "properties": { + "fileInput": { + "type": "string", + "description": "The input PDF file to add a watermark", + "format": "binary" + } + } + } + } + } + }, + "responses": { + "200": { + "description": "OK", + "content": { + "*/*": { + "schema": { + "type": "array", + "items": { + "type": "string", + "format": "byte" + } + } + } + } + } + } + } + }, + "/add-password": { + "post": { + "tags": [ + "password-controller" + ], + "summary": "Add password to a PDF file", + "description": "This endpoint adds password protection to a PDF file. Users can specify a set of permissions that should be applied to the file.", + "operationId": "addPassword", + "parameters": [ + { + "name": "ownerPassword", + "in": "query", + "description": "The owner password to be added to the PDF file (Restricts what can be done with the document once it is opened)", + "required": false, + "schema": { + "type": "string", + "default": "" + } + }, + { + "name": "password", + "in": "query", + "description": "The password to be added to the PDF file (Restricts the opening of the document itself.)", + "required": false, + "schema": { + "type": "string", + "default": "" + } + }, + { + "name": "keyLength", + "in": "query", + "description": "The length of the encryption key", + "required": false, + "schema": { + "type": "string", + "enum": [ + "40", + "128", + "256" + ] + } + }, + { + "name": "canAssembleDocument", + "in": "query", + "description": "Whether the document assembly is allowed", + "required": false, + "schema": { + "type": "boolean", + "default": false + }, + "example": false + }, + { + "name": "canExtractContent", + "in": "query", + "description": "Whether content extraction for accessibility is allowed", + "required": false, + "schema": { + "type": "boolean", + "default": false + }, + "example": false + }, + { + "name": "canExtractForAccessibility", + "in": "query", + "description": "Whether content extraction for accessibility is allowed", + "required": false, + "schema": { + "type": "boolean", + "default": false + }, + "example": false + }, + { + "name": "canFillInForm", + "in": "query", + "description": "Whether form filling is allowed", + "required": false, + "schema": { + "type": "boolean", + "default": false + }, + "example": false + }, + { + "name": "canModify", + "in": "query", + "description": "Whether the document modification is allowed", + "required": false, + "schema": { + "type": "boolean", + "default": false + }, + "example": false + }, + { + "name": "canModifyAnnotations", + "in": "query", + "description": "Whether modification of annotations is allowed", + "required": false, + "schema": { + "type": "boolean", + "default": false + }, + "example": false + }, + { + "name": "canPrint", + "in": "query", + "description": "Whether printing of the document is allowed", + "required": false, + "schema": { + "type": "boolean", + "default": false + }, + "example": false + }, + { + "name": "canPrintFaithful", + "in": "query", + "description": "Whether faithful printing is allowed", + "required": false, + "schema": { + "type": "boolean", + "default": false + }, + "example": false + } + ], + "requestBody": { + "content": { + "multipart/form-data": { + "schema": { + "required": [ + "fileInput" + ], + "type": "object", + "properties": { + "fileInput": { + "type": "string", + "description": "The input PDF file to which the password should be added", + "format": "binary" + } + } + } + } + } + }, + "responses": { + "200": { + "description": "OK", + "content": { + "*/*": { + "schema": { + "type": "array", + "items": { + "type": "string", + "format": "byte" + } + } + } + } + } + } + } + }, + "/add-image": { + "post": { + "tags": [ + "overlay-image-controller" + ], + "summary": "Overlay image onto a PDF file", + "description": "This endpoint overlays an image onto a PDF file at the specified coordinates. The image can be overlaid on every page of the PDF if specified.", + "operationId": "overlayImage", + "parameters": [ + { + "name": "x", + "in": "query", + "description": "The x-coordinate at which to place the top-left corner of the image.", + "required": true, + "schema": { + "type": "number", + "format": "float" + }, + "example": 0 + }, + { + "name": "y", + "in": "query", + "description": "The y-coordinate at which to place the top-left corner of the image.", + "required": true, + "schema": { + "type": "number", + "format": "float" + }, + "example": 0 + }, + { + "name": "everyPage", + "in": "query", + "description": "Whether to overlay the image onto every page of the PDF.", + "required": true, + "schema": { + "type": "boolean" + }, + "example": false + } + ], + "requestBody": { + "content": { + "multipart/form-data": { + "schema": { + "required": [ + "fileInput", + "fileInput2" + ], + "type": "object", + "properties": { + "fileInput": { + "type": "string", + "description": "The input PDF file to overlay the image onto.", + "format": "binary" + }, + "fileInput2": { + "type": "string", + "description": "The image file to be overlaid onto the PDF.", + "format": "binary" + } + } + } + } + } + }, + "responses": { + "200": { + "description": "OK", + "content": { + "*/*": { + "schema": { + "type": "array", + "items": { + "type": "string", + "format": "byte" + } + } + } + } + } + } + } + }, + "/api/v1/status": { + "get": { + "tags": [ + "metrics-controller" + ], + "summary": "Application status and version", + "description": "This endpoint returns the status of the application and its version number.", + "operationId": "getStatus", + "responses": { + "200": { + "description": "OK", + "content": { + "*/*": { + "schema": { + "type": "object", + "additionalProperties": { + "type": "string" + } + } + } + } + } + } + } + }, + "/api/v1/requests": { + "get": { + "tags": [ + "metrics-controller" + ], + "summary": "POST request count", + "description": "This endpoint returns the total count of POST requests or the count of POST requests for a specific endpoint.", + "operationId": "getTotalRequests", + "parameters": [ + { + "name": "endpoint", + "in": "query", + "required": false, + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "description": "OK", + "content": { + "*/*": { + "schema": { + "type": "number", + "format": "double" + } + } + } + } + } + } + }, + "/api/v1/loads": { + "get": { + "tags": [ + "metrics-controller" + ], + "summary": "GET request count", + "description": "This endpoint returns the total count of GET requests or the count of GET requests for a specific endpoint.", + "operationId": "getPageLoads", + "parameters": [ + { + "name": "endpoint", + "in": "query", + "required": false, + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "description": "OK", + "content": { + "*/*": { + "schema": { + "type": "number", + "format": "double" + } + } + } + } + } + } + } + }, + "components": {} +} \ No newline at end of file diff --git a/build.gradle b/build.gradle index ea16d563..6c2095f0 100644 --- a/build.gradle +++ b/build.gradle @@ -2,6 +2,7 @@ plugins { id 'java' id 'org.springframework.boot' version '3.1.0' id 'io.spring.dependency-management' version '1.1.0' + id 'org.springdoc.openapi-gradle-plugin' version '1.6.0' } group = 'stirling.software' @@ -12,6 +13,12 @@ repositories { mavenCentral() } +openApi { + apiDocsUrl = "http://localhost:8080/v3/api-docs" + outputDir = file("/") + outputFileName = "SwaggerDoc.json" +} + dependencies { implementation 'org.springframework.boot:spring-boot-starter-web:3.1.0' implementation 'org.springframework.boot:spring-boot-starter-thymeleaf:3.1.0' @@ -34,6 +41,18 @@ dependencies { } +task writeVersion { + def propsFile = file('src/main/resources/version.properties') + def props = new Properties() + props.setProperty('version', version) + props.store(propsFile.newWriter(), null) +} + +tasks.matching { it.name == 'generateOpenApiDocs' }.all { + dependsOn writeVersion +} + + jar { enabled = false manifest { diff --git a/src/main/java/stirling/software/SPDF/config/OpenApiConfig.java b/src/main/java/stirling/software/SPDF/config/OpenApiConfig.java index 0ab63952..d7495aca 100644 --- a/src/main/java/stirling/software/SPDF/config/OpenApiConfig.java +++ b/src/main/java/stirling/software/SPDF/config/OpenApiConfig.java @@ -1,5 +1,9 @@ package stirling.software.SPDF.config; +import java.io.IOException; +import java.io.InputStream; +import java.util.Properties; + import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; @@ -10,13 +14,23 @@ import io.swagger.v3.oas.models.info.Info; @Configuration public class OpenApiConfig { - @Bean - public OpenAPI customOpenAPI() { - String version = getClass().getPackage().getImplementationVersion(); - version = (version != null) ? version : "1.0.0"; - - return new OpenAPI().components(new Components()).info( - new Info().title("Stirling PDF API").version(version).description("API documentation for all Server-Side processing.\nPlease note some functionality might be UI only and missing from here.")); - } + @Bean + public OpenAPI customOpenAPI() { + String version = getClass().getPackage().getImplementationVersion(); + if (version == null) { + Properties props = new Properties(); + try (InputStream input = getClass().getClassLoader().getResourceAsStream("version.properties")) { + props.load(input); + version = props.getProperty("version"); + } catch (IOException ex) { + ex.printStackTrace(); + version = "1.0.0"; // default version if all else fails + } + } + + return new OpenAPI().components(new Components()).info( + new Info().title("Stirling PDF API").version(version).description("API documentation for all Server-Side processing.\nPlease note some functionality might be UI only and missing from here.")); + } + } diff --git a/src/main/java/stirling/software/SPDF/controller/api/RearrangePagesPDFController.java b/src/main/java/stirling/software/SPDF/controller/api/RearrangePagesPDFController.java index 8ae1b533..c1a0c892 100644 --- a/src/main/java/stirling/software/SPDF/controller/api/RearrangePagesPDFController.java +++ b/src/main/java/stirling/software/SPDF/controller/api/RearrangePagesPDFController.java @@ -1,16 +1,8 @@ package stirling.software.SPDF.controller.api; import java.io.IOException; -import io.swagger.v3.oas.annotations.media.Schema; -import stirling.software.SPDF.utils.WebResponseUtils; - import java.util.ArrayList; import java.util.List; -import javax.script.ScriptEngineManager; -import javax.script.ScriptEngine; -import javax.script.ScriptException; - -import javax.script.ScriptEngine; import org.apache.pdfbox.pdmodel.PDDocument; import org.apache.pdfbox.pdmodel.PDPage; @@ -25,6 +17,9 @@ import org.springframework.web.multipart.MultipartFile; import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.Parameter; +import io.swagger.v3.oas.annotations.media.Schema; +import stirling.software.SPDF.utils.GeneralUtils; +import stirling.software.SPDF.utils.WebResponseUtils; @RestController public class RearrangePagesPDFController { @@ -43,7 +38,7 @@ public class RearrangePagesPDFController { // Split the page order string into an array of page numbers or range of numbers String[] pageOrderArr = pagesToDelete.split(","); - List pagesToRemove = pageOrderToString(pageOrderArr, document.getNumberOfPages()); + List pagesToRemove = GeneralUtils.parsePageList(pageOrderArr, document.getNumberOfPages()); for (int i = pagesToRemove.size() - 1; i >= 0; i--) { int pageIndex = pagesToRemove.get(i); @@ -180,7 +175,7 @@ public class RearrangePagesPDFController { if (customMode != null && customMode.length() > 0) { newPageOrder = processCustomMode(customMode, totalPages); } else { - newPageOrder = pageOrderToString(pageOrderArr, totalPages); + newPageOrder = GeneralUtils.parsePageList(pageOrderArr, totalPages); } // Create a new list to hold the pages in the new order @@ -207,63 +202,6 @@ public class RearrangePagesPDFController { } } - private List pageOrderToString(String[] pageOrderArr, int totalPages) { - List newPageOrder = new ArrayList<>(); - - // loop through the page order array - for (String element : pageOrderArr) { - // check if the element contains a range of pages - if (element.matches("\\d*n\\+?-?\\d*|\\d*\\+?n")) { - // Handle page order as a function - int coefficient = 0; - int constant = 0; - boolean coefficientExists = false; - boolean constantExists = false; - - if (element.contains("n")) { - String[] parts = element.split("n"); - if (!parts[0].equals("") && parts[0] != null) { - coefficient = Integer.parseInt(parts[0]); - coefficientExists = true; - } - if (parts.length > 1 && !parts[1].equals("") && parts[1] != null) { - constant = Integer.parseInt(parts[1]); - constantExists = true; - } - } else if (element.contains("+")) { - constant = Integer.parseInt(element.replace("+", "")); - constantExists = true; - } - - for (int i = 1; i <= totalPages; i++) { - int pageNum = coefficientExists ? coefficient * i : i; - pageNum += constantExists ? constant : 0; - - if (pageNum <= totalPages && pageNum > 0) { - newPageOrder.add(pageNum - 1); - } - } - } else if (element.contains("-")) { - // split the range into start and end page - String[] range = element.split("-"); - int start = Integer.parseInt(range[0]); - int end = Integer.parseInt(range[1]); - // check if the end page is greater than total pages - if (end > totalPages) { - end = totalPages; - } - // loop through the range of pages - for (int j = start; j <= end; j++) { - // print the current index - newPageOrder.add(j - 1); - } - } else { - // if the element is a single page - newPageOrder.add(Integer.parseInt(element) - 1); - } - } - - return newPageOrder; - } + } diff --git a/src/main/java/stirling/software/SPDF/controller/api/ScalePagesController.java b/src/main/java/stirling/software/SPDF/controller/api/ScalePagesController.java index 508d4fc3..0c079b9e 100644 --- a/src/main/java/stirling/software/SPDF/controller/api/ScalePagesController.java +++ b/src/main/java/stirling/software/SPDF/controller/api/ScalePagesController.java @@ -3,10 +3,18 @@ package stirling.software.SPDF.controller.api; import java.io.ByteArrayInputStream; import java.io.ByteArrayOutputStream; import java.io.IOException; +import java.util.ArrayList; +import java.util.Collections; +import java.util.Comparator; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Set; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.http.HttpHeaders; +import org.springframework.http.MediaType; import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.RequestParam; @@ -20,14 +28,17 @@ import com.itextpdf.kernel.pdf.PdfPage; import com.itextpdf.kernel.pdf.PdfReader; import com.itextpdf.kernel.pdf.PdfWriter; import com.itextpdf.kernel.pdf.canvas.PdfCanvas; +import com.itextpdf.kernel.pdf.canvas.parser.EventType; +import com.itextpdf.kernel.pdf.canvas.parser.PdfCanvasProcessor; +import com.itextpdf.kernel.pdf.canvas.parser.data.IEventData; +import com.itextpdf.kernel.pdf.canvas.parser.data.TextRenderInfo; +import com.itextpdf.kernel.pdf.canvas.parser.listener.IEventListener; import com.itextpdf.kernel.pdf.xobject.PdfFormXObject; import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.Parameter; import io.swagger.v3.oas.annotations.media.Schema; import stirling.software.SPDF.utils.WebResponseUtils; -import java.util.HashMap; -import java.util.Map; @RestController public class ScalePagesController { @@ -119,4 +130,128 @@ public class ScalePagesController { pdfDoc.close(); return WebResponseUtils.bytesToWebResponse(pdfContent, file.getOriginalFilename().replaceFirst("[.][^.]+$", "") + "_scaled.pdf"); } + + + + + + + + +@PostMapping(value = "/auto-crop", consumes = "multipart/form-data") +public ResponseEntity cropPdf(@RequestParam("fileInput") MultipartFile file) throws IOException { + byte[] bytes = file.getBytes(); + PdfReader reader = new PdfReader(new ByteArrayInputStream(bytes)); + PdfDocument pdfDoc = new PdfDocument(reader); + + ByteArrayOutputStream baos = new ByteArrayOutputStream(); + PdfWriter writer = new PdfWriter(baos); + PdfDocument outputPdf = new PdfDocument(writer); + + int totalPages = pdfDoc.getNumberOfPages(); + for (int i = 1; i <= totalPages; i++) { + PdfPage page = pdfDoc.getPage(i); + Rectangle originalMediaBox = page.getMediaBox(); + + Rectangle contentBox = determineContentBox(page); + + // Make sure we don't go outside the original media box. + Rectangle intersection = originalMediaBox.getIntersection(contentBox); + page.setCropBox(intersection); + + // Copy page to the new document + outputPdf.addPage(page.copyTo(outputPdf)); + } + + outputPdf.close(); + byte[] pdfContent = baos.toByteArray(); + pdfDoc.close(); + return ResponseEntity.ok() + .header(HttpHeaders.CONTENT_DISPOSITION, "attachment; filename=\"" + file.getOriginalFilename().replaceFirst("[.][^.]+$", "") + "_cropped.pdf\"") + .contentType(MediaType.APPLICATION_PDF) + .body(pdfContent); +} + +private Rectangle determineContentBox(PdfPage page) { + // Extract the text from the page and find the bounding box. + TextBoundingRectangleFinder finder = new TextBoundingRectangleFinder(); + PdfCanvasProcessor processor = new PdfCanvasProcessor(finder); + processor.processPageContent(page); + return finder.getBoundingBox(); +} +private static class TextBoundingRectangleFinder implements IEventListener { + private List allTextBoxes = new ArrayList<>(); + + public Rectangle getBoundingBox() { + // Sort the text boxes based on their vertical position + allTextBoxes.sort(Comparator.comparingDouble(Rectangle::getTop)); + + // Consider a box an outlier if its top is more than 1.5 times the IQR above the third quartile. + int q1Index = allTextBoxes.size() / 4; + int q3Index = 3 * allTextBoxes.size() / 4; + double iqr = allTextBoxes.get(q3Index).getTop() - allTextBoxes.get(q1Index).getTop(); + double threshold = allTextBoxes.get(q3Index).getTop() + 1.5 * iqr; + + // Initialize boundingBox to the first non-outlier box + int i = 0; + while (i < allTextBoxes.size() && allTextBoxes.get(i).getTop() > threshold) { + i++; + } + if (i == allTextBoxes.size()) { + // If all boxes are outliers, just return the first one + return allTextBoxes.get(0); + } + Rectangle boundingBox = allTextBoxes.get(i); + + // Extend the bounding box to include all non-outlier boxes + for (; i < allTextBoxes.size(); i++) { + Rectangle textBoundingBox = allTextBoxes.get(i); + if (textBoundingBox.getTop() > threshold) { + // This box is an outlier, skip it + continue; + } + float left = Math.min(boundingBox.getLeft(), textBoundingBox.getLeft()); + float bottom = Math.min(boundingBox.getBottom(), textBoundingBox.getBottom()); + float right = Math.max(boundingBox.getRight(), textBoundingBox.getRight()); + float top = Math.max(boundingBox.getTop(), textBoundingBox.getTop()); + + // Add a small padding around the bounding box + float padding = 10; + boundingBox = new Rectangle(left - padding, bottom - padding, right - left + 2 * padding, top - bottom + 2 * padding); + } + return boundingBox; + } + + @Override + public void eventOccurred(IEventData data, EventType type) { + if (type == EventType.RENDER_TEXT) { + TextRenderInfo renderInfo = (TextRenderInfo) data; + allTextBoxes.add(renderInfo.getBaseline().getBoundingRectangle()); + } + } + + @Override + public Set getSupportedEvents() { + return Collections.singleton(EventType.RENDER_TEXT); + } +} + + + + + + + + + + + + + + + + + + + } diff --git a/src/main/java/stirling/software/SPDF/controller/api/SplitPDFController.java b/src/main/java/stirling/software/SPDF/controller/api/SplitPDFController.java index 99152e69..04301b96 100644 --- a/src/main/java/stirling/software/SPDF/controller/api/SplitPDFController.java +++ b/src/main/java/stirling/software/SPDF/controller/api/SplitPDFController.java @@ -6,7 +6,6 @@ import java.io.InputStream; import java.nio.file.Files; import java.nio.file.Path; import java.util.ArrayList; -import java.util.Arrays; import java.util.List; import java.util.stream.Collectors; import java.util.zip.ZipEntry; @@ -29,6 +28,7 @@ import org.springframework.web.multipart.MultipartFile; import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.Parameter; +import stirling.software.SPDF.utils.GeneralUtils; @RestController public class SplitPDFController { @@ -58,39 +58,28 @@ public class SplitPDFController { pageNumbers.add(i); } } else { - List pageNumbersStr = new ArrayList<>(Arrays.asList(pages.split(","))); - if (!pageNumbersStr.contains(String.valueOf(document.getNumberOfPages()))) { - String lastpage = String.valueOf(document.getNumberOfPages()); - pageNumbersStr.add(lastpage); - } - for (String page : pageNumbersStr) { - if (page.contains("-")) { - String[] range = page.split("-"); - int start = Integer.parseInt(range[0]); - int end = Integer.parseInt(range[1]); - for (int i = start; i <= end; i++) { - pageNumbers.add(i); - } - } else { - pageNumbers.add(Integer.parseInt(page)); - } + String[] splitPoints = pages.split(","); + for (String splitPoint : splitPoints) { + List orderedPages = GeneralUtils.parsePageList(new String[] {splitPoint}, document.getNumberOfPages()); + pageNumbers.addAll(orderedPages); } + // Add the last page as a split point + pageNumbers.add(document.getNumberOfPages() - 1); } logger.info("Splitting PDF into pages: {}", pageNumbers.stream().map(String::valueOf).collect(Collectors.joining(","))); // split the document List splitDocumentsBoas = new ArrayList<>(); - int currentPage = 0; - for (int pageNumber : pageNumbers) { + int previousPageNumber = 0; + for (int splitPoint : pageNumbers) { try (PDDocument splitDocument = new PDDocument()) { - for (int i = currentPage; i < pageNumber; i++) { + for (int i = previousPageNumber; i <= splitPoint; i++) { PDPage page = document.getPage(i); splitDocument.addPage(page); logger.debug("Adding page {} to split document", i); } - currentPage = pageNumber; - logger.debug("Setting current page to {}", currentPage); + previousPageNumber = splitPoint + 1; ByteArrayOutputStream baos = new ByteArrayOutputStream(); splitDocument.save(baos); @@ -102,6 +91,7 @@ public class SplitPDFController { } } + // closing the original document document.close(); diff --git a/src/main/java/stirling/software/SPDF/controller/api/security/CertSignController.java b/src/main/java/stirling/software/SPDF/controller/api/security/CertSignController.java index bd2914bf..9f9dddd8 100644 --- a/src/main/java/stirling/software/SPDF/controller/api/security/CertSignController.java +++ b/src/main/java/stirling/software/SPDF/controller/api/security/CertSignController.java @@ -1,9 +1,6 @@ package stirling.software.SPDF.controller.api.security; import java.io.ByteArrayInputStream; -import io.swagger.v3.oas.annotations.media.Schema; -import stirling.software.SPDF.utils.WebResponseUtils; - import java.io.ByteArrayOutputStream; import java.io.IOException; import java.io.InputStream; @@ -53,6 +50,8 @@ import com.itextpdf.signatures.SignatureUtil; import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.Parameter; +import io.swagger.v3.oas.annotations.media.Schema; +import stirling.software.SPDF.utils.WebResponseUtils; @RestController public class CertSignController { diff --git a/src/main/java/stirling/software/SPDF/controller/web/OtherWebController.java b/src/main/java/stirling/software/SPDF/controller/web/OtherWebController.java index 95cef9e7..5a9e93c0 100644 --- a/src/main/java/stirling/software/SPDF/controller/web/OtherWebController.java +++ b/src/main/java/stirling/software/SPDF/controller/web/OtherWebController.java @@ -122,4 +122,11 @@ public class OtherWebController { return "other/scale-pages"; } + @GetMapping("/auto-crop") + @Hidden + public String autoCropForm(Model model) { + model.addAttribute("currentPage", "auto-crop"); + return "other/auto-crop"; + } + } diff --git a/src/main/java/stirling/software/SPDF/utils/GeneralUtils.java b/src/main/java/stirling/software/SPDF/utils/GeneralUtils.java index 0d8325e4..1e101d09 100644 --- a/src/main/java/stirling/software/SPDF/utils/GeneralUtils.java +++ b/src/main/java/stirling/software/SPDF/utils/GeneralUtils.java @@ -1,5 +1,8 @@ package stirling.software.SPDF.utils; +import java.util.ArrayList; +import java.util.List; + public class GeneralUtils { public static Long convertSizeToBytes(String sizeStr) { @@ -27,4 +30,62 @@ public class GeneralUtils { return null; } + public static List parsePageList(String[] pageOrderArr, int totalPages) { + List newPageOrder = new ArrayList<>(); + + // loop through the page order array + for (String element : pageOrderArr) { + // check if the element contains a range of pages + if (element.matches("\\d*n\\+?-?\\d*|\\d*\\+?n")) { + // Handle page order as a function + int coefficient = 0; + int constant = 0; + boolean coefficientExists = false; + boolean constantExists = false; + + if (element.contains("n")) { + String[] parts = element.split("n"); + if (!parts[0].equals("") && parts[0] != null) { + coefficient = Integer.parseInt(parts[0]); + coefficientExists = true; + } + if (parts.length > 1 && !parts[1].equals("") && parts[1] != null) { + constant = Integer.parseInt(parts[1]); + constantExists = true; + } + } else if (element.contains("+")) { + constant = Integer.parseInt(element.replace("+", "")); + constantExists = true; + } + + for (int i = 1; i <= totalPages; i++) { + int pageNum = coefficientExists ? coefficient * i : i; + pageNum += constantExists ? constant : 0; + + if (pageNum <= totalPages && pageNum > 0) { + newPageOrder.add(pageNum - 1); + } + } + } else if (element.contains("-")) { + // split the range into start and end page + String[] range = element.split("-"); + int start = Integer.parseInt(range[0]); + int end = Integer.parseInt(range[1]); + // check if the end page is greater than total pages + if (end > totalPages) { + end = totalPages; + } + // loop through the range of pages + for (int j = start; j <= end; j++) { + // print the current index + newPageOrder.add(j - 1); + } + } else { + // if the element is a single page + newPageOrder.add(Integer.parseInt(element) - 1); + } + } + + return newPageOrder; + } } diff --git a/src/main/java/stirling/software/SPDF/utils/PdfUtils.java b/src/main/java/stirling/software/SPDF/utils/PdfUtils.java index 3956a4a1..e5d35c08 100644 --- a/src/main/java/stirling/software/SPDF/utils/PdfUtils.java +++ b/src/main/java/stirling/software/SPDF/utils/PdfUtils.java @@ -9,12 +9,6 @@ import java.io.FileOutputStream; import java.io.IOException; import java.io.InputStream; import java.nio.file.Files; -import java.security.KeyPair; -import java.security.KeyStore; -import java.security.PrivateKey; -import java.security.PublicKey; -import java.security.cert.Certificate; -import java.security.cert.X509Certificate; import java.util.ArrayList; import java.util.Arrays; import java.util.List; diff --git a/src/main/resources/messages_en_GB.properties b/src/main/resources/messages_en_GB.properties index 04f7df53..e38bb558 100644 --- a/src/main/resources/messages_en_GB.properties +++ b/src/main/resources/messages_en_GB.properties @@ -54,14 +54,11 @@ home.pdfOrganiser.title=Organise home.pdfOrganiser.desc=Remove/Rearrange pages in any order home.addImage.title=Add image -home.addImage.desc=Adds a image onto a set location on the PDF (Work in progress) +home.addImage.desc=Adds a image onto a set location on the PDF home.watermark.title=Add Watermark home.watermark.desc=Add a custom watermark to your PDF document. -home.remove-watermark.title=Remove Watermark -home.remove-watermark.desc=Remove watermarks from your PDF document. - home.permissions.title=Change Permissions home.permissions.desc=Change the permissions of your PDF document @@ -131,8 +128,8 @@ home.certSign.desc=Signs a PDF with a Certificate/Key (PEM/P12) home.pageLayout.title=Multi-Page Layout home.pageLayout.desc=Merge multiple pages of a PDF document into a single page -home.scalePages.title=Adjust page-scale -home.scalePages.desc=Change the size of page contents while maintaining a set page-size +home.scalePages.title=Adjust page size/scale +home.scalePages.desc=Change the size/scale of page and/or its contents. error.pdfPassword=The PDF Document is passworded and either the password was not provided or was incorrect diff --git a/src/main/resources/static/js/downloader.js b/src/main/resources/static/js/downloader.js index 35623db6..bfc0860a 100644 --- a/src/main/resources/static/js/downloader.js +++ b/src/main/resources/static/js/downloader.js @@ -14,7 +14,7 @@ $(document).ready(function() { const files = $('#fileInput-input')[0].files; const formData = new FormData(this); const override = $('#override').val() || ''; - + const originalButtonText = $('#submitBtn').text(); $('#submitBtn').text('Processing...'); try { @@ -24,10 +24,10 @@ $(document).ready(function() { await handleSingleDownload(url, formData); } - $('#submitBtn').text('Submit'); + $('#submitBtn').text(originalButtonText); } catch (error) { handleDownloadError(error); - $('#submitBtn').text('Submit'); + $('#submitBtn').text(originalButtonText); console.error(error); } }); @@ -83,7 +83,7 @@ async function handleJsonResponse(response) { const json = await response.json(); const errorMessage = JSON.stringify(json, null, 2); if (errorMessage.toLowerCase().includes('the password is incorrect') || errorMessage.toLowerCase().includes('Password is not provided') || errorMessage.toLowerCase().includes('PDF contains an encryption dictionary')) { - alert('[[#{error.pdfPassword}]]'); + alert(pdfPasswordPrompt); } else { showErrorBanner(json.error + ':' + json.message, json.trace); } diff --git a/src/main/resources/templates/fragments/common.html b/src/main/resources/templates/fragments/common.html index e08f7b04..75870630 100644 --- a/src/main/resources/templates/fragments/common.html +++ b/src/main/resources/templates/fragments/common.html @@ -91,6 +91,9 @@ +
diff --git a/src/main/resources/templates/other/auto-crop.html b/src/main/resources/templates/other/auto-crop.html new file mode 100644 index 00000000..a7ba407a --- /dev/null +++ b/src/main/resources/templates/other/auto-crop.html @@ -0,0 +1,31 @@ + + + + + + + + +
+
+
+

+
+
+
+

+
+
+
+ +
+

+
+
+
+ +
+
+
+ + \ No newline at end of file