From b5bdf5b1c2bfddc74afd34752a4df894fdb82b14 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andr=C3=A9=20Antunes=20Santos?= Date: Thu, 15 May 2025 23:16:28 +0100 Subject: [PATCH 1/4] feat(#3453): Add initial structure for Remove Header/Footer feature MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Add backend API controller with dummy request model - Register endpoint in appropriate backend groups - Add test .txt file support for endpoint testing - Create homepage/navbar card for the feature - Implement frontend page structure for the feature - Set up dummy API to test backend–frontend integration --- .../SPDF/config/EndpointConfiguration.java | 2 + .../api/RemoveHeaderFooterController.java | 34 +++++++++ .../controller/web/GeneralWebController.java | 7 ++ .../api/general/RemoveHeaderFooterForm.java | 13 ++++ .../main/resources/messages_en_GB.properties | 8 +++ .../templates/fragments/navElements.html | 5 ++ .../main/resources/templates/home-legacy.html | 3 + .../templates/remove-header-footer.html | 70 +++++++++++++++++++ testing/allEndpointsRemovedSettings.yml | 2 +- testing/endpoints.txt | 1 + testing/webpage_urls.txt | 1 + testing/webpage_urls_full.txt | 3 +- 12 files changed, 147 insertions(+), 2 deletions(-) create mode 100644 stirling-pdf/src/main/java/stirling/software/SPDF/controller/api/RemoveHeaderFooterController.java create mode 100644 stirling-pdf/src/main/java/stirling/software/SPDF/model/api/general/RemoveHeaderFooterForm.java create mode 100644 stirling-pdf/src/main/resources/templates/remove-header-footer.html diff --git a/stirling-pdf/src/main/java/stirling/software/SPDF/config/EndpointConfiguration.java b/stirling-pdf/src/main/java/stirling/software/SPDF/config/EndpointConfiguration.java index c9872992a..631845318 100644 --- a/stirling-pdf/src/main/java/stirling/software/SPDF/config/EndpointConfiguration.java +++ b/stirling-pdf/src/main/java/stirling/software/SPDF/config/EndpointConfiguration.java @@ -120,6 +120,7 @@ public class EndpointConfiguration { addEndpointToGroup("PageOps", "scale-pages"); addEndpointToGroup("PageOps", "adjust-contrast"); addEndpointToGroup("PageOps", "crop"); + addEndpointToGroup("PageOps", "removeHeaderFooter"); addEndpointToGroup("PageOps", "auto-split-pdf"); addEndpointToGroup("PageOps", "extract-page"); addEndpointToGroup("PageOps", "pdf-to-single-page"); @@ -236,6 +237,7 @@ public class EndpointConfiguration { addEndpointToGroup("Java", "auto-split-pdf"); addEndpointToGroup("Java", "sanitize-pdf"); addEndpointToGroup("Java", "crop"); + addEndpointToGroup("Java", "removeHeaderFooter"); addEndpointToGroup("Java", "get-info-on-pdf"); addEndpointToGroup("Java", "extract-page"); addEndpointToGroup("Java", "pdf-to-single-page"); diff --git a/stirling-pdf/src/main/java/stirling/software/SPDF/controller/api/RemoveHeaderFooterController.java b/stirling-pdf/src/main/java/stirling/software/SPDF/controller/api/RemoveHeaderFooterController.java new file mode 100644 index 000000000..3b36e4feb --- /dev/null +++ b/stirling-pdf/src/main/java/stirling/software/SPDF/controller/api/RemoveHeaderFooterController.java @@ -0,0 +1,34 @@ +package stirling.software.SPDF.controller.api; + +import org.springframework.web.bind.annotation.ModelAttribute; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.tags.Tag; + +import lombok.RequiredArgsConstructor; + +import stirling.software.SPDF.model.api.general.RemoveHeaderFooterForm; + +@RestController +@RequestMapping("/api/v1/general") +@Tag(name = "General", description = "General APIs") +@RequiredArgsConstructor +public class RemoveHeaderFooterController { + + // private final CustomPDFDocumentFactory pdfDocumentFactory; + + @PostMapping(value = "/remove-header-footer", consumes = "multipart/form-data") + @Operation( + summary = "Removes headers and/or footers from a PDF document", + description = "Remove header and/or footer") + public String removeHeaderFooter(@ModelAttribute RemoveHeaderFooterForm form) { + // Print the received message + System.out.println("Received message: " + form.getMessage()); + + // Respond with a message + return "footer Removed"; + } +} diff --git a/stirling-pdf/src/main/java/stirling/software/SPDF/controller/web/GeneralWebController.java b/stirling-pdf/src/main/java/stirling/software/SPDF/controller/web/GeneralWebController.java index 72486a28f..d07daed0d 100644 --- a/stirling-pdf/src/main/java/stirling/software/SPDF/controller/web/GeneralWebController.java +++ b/stirling-pdf/src/main/java/stirling/software/SPDF/controller/web/GeneralWebController.java @@ -298,6 +298,13 @@ public class GeneralWebController { return "crop"; } + @GetMapping("/remove-header-footer") + @Hidden + public String removeHeaderFooterForm(Model model) { + model.addAttribute("currentPage", "remove-header-footer"); + return "remove-header-footer"; + } + @GetMapping("/auto-split-pdf") @Hidden public String autoSPlitPDFForm(Model model) { diff --git a/stirling-pdf/src/main/java/stirling/software/SPDF/model/api/general/RemoveHeaderFooterForm.java b/stirling-pdf/src/main/java/stirling/software/SPDF/model/api/general/RemoveHeaderFooterForm.java new file mode 100644 index 000000000..9c948bf66 --- /dev/null +++ b/stirling-pdf/src/main/java/stirling/software/SPDF/model/api/general/RemoveHeaderFooterForm.java @@ -0,0 +1,13 @@ +package stirling.software.SPDF.model.api.general; + +import io.swagger.v3.oas.annotations.media.Schema; + +import lombok.Data; + +@Data +// @EqualsAndHashCode(callSuper = true) +public class RemoveHeaderFooterForm { + + @Schema(description = "Test Message", type = "string") + private String message; +} diff --git a/stirling-pdf/src/main/resources/messages_en_GB.properties b/stirling-pdf/src/main/resources/messages_en_GB.properties index 7dc6cb571..20d45916c 100644 --- a/stirling-pdf/src/main/resources/messages_en_GB.properties +++ b/stirling-pdf/src/main/resources/messages_en_GB.properties @@ -751,6 +751,10 @@ home.validateSignature.title=Validate PDF Signature home.validateSignature.desc=Verify digital signatures and certificates in PDF documents validateSignature.tags=signature,verify,validate,pdf,certificate,digital signature,Validate Signature,Validate certificate +home.remove-header-footer.title=Remove PDF Headers and Footers +home.remove-header-footer.desc=Remove the headers and/or footers from a PDF document +remove-header-footer.tags=remove headers, remove footers, remove, header, footer + #replace-invert-color replace-color.title=Advanced Colour options replace-color.header=Replace-Invert Colour PDF @@ -996,6 +1000,10 @@ crop.title=Crop crop.header=Crop PDF crop.submit=Submit +#remove-header-footer +remove-header-footer.title=Remove Header/Footer +remove-header-footer.header=Remove Header/Footer +remove-header-footer.submit=Remove #autoSplitPDF autoSplitPDF.title=Auto Split PDF diff --git a/stirling-pdf/src/main/resources/templates/fragments/navElements.html b/stirling-pdf/src/main/resources/templates/fragments/navElements.html index cd7fae74b..702bcab1b 100644 --- a/stirling-pdf/src/main/resources/templates/fragments/navElements.html +++ b/stirling-pdf/src/main/resources/templates/fragments/navElements.html @@ -19,6 +19,11 @@
+ +
+
+
diff --git a/stirling-pdf/src/main/resources/templates/home-legacy.html b/stirling-pdf/src/main/resources/templates/home-legacy.html index d60ac220e..4a4579a75 100644 --- a/stirling-pdf/src/main/resources/templates/home-legacy.html +++ b/stirling-pdf/src/main/resources/templates/home-legacy.html @@ -122,6 +122,9 @@
+
+
diff --git a/stirling-pdf/src/main/resources/templates/remove-header-footer.html b/stirling-pdf/src/main/resources/templates/remove-header-footer.html new file mode 100644 index 000000000..402a12a8d --- /dev/null +++ b/stirling-pdf/src/main/resources/templates/remove-header-footer.html @@ -0,0 +1,70 @@ + + + + + + + +
+
+ +

+
+
+
+
+ toolbar + +
+

+ remove_header_footer test +
+
+ + +
+ +
+
+
+
+ + + + + +
+
+ +
+ + diff --git a/testing/allEndpointsRemovedSettings.yml b/testing/allEndpointsRemovedSettings.yml index 3290d6fef..a7141b29a 100644 --- a/testing/allEndpointsRemovedSettings.yml +++ b/testing/allEndpointsRemovedSettings.yml @@ -128,7 +128,7 @@ ui: languages: [] # If empty, all languages are enabled. To display only German and Polish ["de_DE", "pl_PL"]. British English is always enabled. endpoints: # All the possible endpoints are disabled - toRemove: [crop, merge-pdfs, multi-page-layout, overlay-pdfs, pdf-to-single-page, rearrange-pages, remove-image-pdf, remove-pages, rotate-pdf, scale-pages, split-by-size-or-count, split-pages, split-pdf-by-chapters, split-pdf-by-sections, add-password, add-watermark, auto-redact, cert-sign, get-info-on-pdf, redact, remove-cert-sign, remove-password, sanitize-pdf, validate-signature, file-to-pdf, html-to-pdf, img-to-pdf, markdown-to-pdf, pdf-to-csv, pdf-to-html, pdf-to-img, pdf-to-markdown, pdf-to-pdfa, pdf-to-presentation, pdf-to-text, pdf-to-word, pdf-to-xml, url-to-pdf, add-image, add-page-numbers, add-stamp, auto-rename, auto-split-pdf, compress-pdf, decompress-pdf, extract-image-scans, extract-images, flatten, ocr-pdf, remove-blanks, repair, replace-invert-pdf, show-javascript, update-metadata, filter-contains-image, filter-contains-text, filter-file-size, filter-page-count, filter-page-rotation, filter-page-size] # list endpoints to disable (e.g. ['img-to-pdf', 'remove-pages']) + toRemove: [crop, merge-pdfs, multi-page-layout, overlay-pdfs, pdf-to-single-page, rearrange-pages, remove-image-pdf, remove-pages, rotate-pdf, scale-pages, split-by-size-or-count, split-pages, split-pdf-by-chapters, split-pdf-by-sections, add-password, add-watermark, auto-redact, cert-sign, get-info-on-pdf, redact, remove-cert-sign, remove-password, sanitize-pdf, validate-signature, file-to-pdf, html-to-pdf, img-to-pdf, markdown-to-pdf, pdf-to-csv, pdf-to-html, pdf-to-img, pdf-to-markdown, pdf-to-pdfa, pdf-to-presentation, pdf-to-text, pdf-to-word, pdf-to-xml, url-to-pdf, add-image, add-page-numbers, add-stamp, auto-rename, auto-split-pdf, compress-pdf, decompress-pdf, extract-image-scans, extract-images, flatten, ocr-pdf, remove-blanks, repair, replace-invert-pdf, show-javascript, update-metadata, filter-contains-image, filter-contains-text, filter-file-size, filter-page-count, filter-page-rotation, filter-page-size, remove-header-footer] # list endpoints to disable (e.g. ['img-to-pdf', 'remove-pages']) groupsToRemove: [] # list groups to disable (e.g. ['LibreOffice']) metrics: diff --git a/testing/endpoints.txt b/testing/endpoints.txt index 5468ad6c1..5e0eb5146 100644 --- a/testing/endpoints.txt +++ b/testing/endpoints.txt @@ -58,3 +58,4 @@ /api/v1/general/multi-page-layout /api/v1/general/merge-pdfs /api/v1/general/crop +/api/v1/general/remove-header-footer diff --git a/testing/webpage_urls.txt b/testing/webpage_urls.txt index 8ccaaf0b1..9d25ffa76 100644 --- a/testing/webpage_urls.txt +++ b/testing/webpage_urls.txt @@ -51,3 +51,4 @@ /swagger-ui/index.html /licenses /releases +/remove-header-footer diff --git a/testing/webpage_urls_full.txt b/testing/webpage_urls_full.txt index 86b908720..ffc821234 100644 --- a/testing/webpage_urls_full.txt +++ b/testing/webpage_urls_full.txt @@ -62,4 +62,5 @@ /stamp /validate-signature /view-pdf -/swagger-ui/index.html \ No newline at end of file +/swagger-ui/index.html +/remove-header-footer From 61729d324c35fd688640770258c7d37f67642153 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andr=C3=A9=20Antunes=20Santos?= Date: Mon, 2 Jun 2025 17:43:33 +0100 Subject: [PATCH 2/4] feat(#3453): Add frontend for Remove Header/Footer feature - Implement form submission with options to remove header, footer, or both - Add margin mode selection with visual preview of selected header/footer bounds - Integrate PDF.js for live PDF view in margin mode - Create dedicated CSS for styling the feature page - Add localization keys for en-GB and en-US - Implement JavaScript to handle PDF rendering, preview overlays, and submission logic --- .../main/resources/messages_en_GB.properties | 17 + .../main/resources/messages_en_US.properties | 24 + .../static/css/remove-header-footer.css | 157 +++++ .../static/js/pages/remove-header-footer.js | 603 ++++++++++++++++++ .../resources/templates/fragments/common.html | 1 + .../templates/remove-header-footer.html | 287 +++++++-- 6 files changed, 1044 insertions(+), 45 deletions(-) create mode 100644 stirling-pdf/src/main/resources/static/css/remove-header-footer.css create mode 100644 stirling-pdf/src/main/resources/static/js/pages/remove-header-footer.js diff --git a/stirling-pdf/src/main/resources/messages_en_GB.properties b/stirling-pdf/src/main/resources/messages_en_GB.properties index 20d45916c..5e58fc529 100644 --- a/stirling-pdf/src/main/resources/messages_en_GB.properties +++ b/stirling-pdf/src/main/resources/messages_en_GB.properties @@ -1004,6 +1004,23 @@ crop.submit=Submit remove-header-footer.title=Remove Header/Footer remove-header-footer.header=Remove Header/Footer remove-header-footer.submit=Remove +remove-header-footer.removeHeader=Header +remove-header-footer.removeFooter=Footer +remove-header-footer.headerMargin=Header Margin +remove-header-footer.footerMargin=Footer Margin +remove-header-footer.pages=Select Pages +remove-header-footer.selectMode=Select Mode +remove-header-footer.view-more=View More +remove-header-footer.previousPage=Previous Page +remove-header-footer.nextPage=Next Page +remove-header-footer.zoomOut=Zoom Out +remove-header-footer.zoomIn=Zoom In +remove-header-footer.close=Close +remove-header-footer.margin=Margin +remove-header-footer.auto=Auto +remove-header-footer.manual=Manual +remove-header-footer.enterValue=Enter Value + #autoSplitPDF autoSplitPDF.title=Auto Split PDF diff --git a/stirling-pdf/src/main/resources/messages_en_US.properties b/stirling-pdf/src/main/resources/messages_en_US.properties index 162196c53..71b9c1b9f 100644 --- a/stirling-pdf/src/main/resources/messages_en_US.properties +++ b/stirling-pdf/src/main/resources/messages_en_US.properties @@ -751,6 +751,10 @@ home.validateSignature.title=Validate PDF Signature home.validateSignature.desc=Verify digital signatures and certificates in PDF documents validateSignature.tags=signature,verify,validate,pdf,certificate,digital signature,Validate Signature,Validate certificate +home.remove-header-footer.title=Remove Header/Footer +home.remove-header-footer.desc=Remove the headers and/or footers from a PDF document +remove-header-footer.tags=remove headers, remove footers, remove, header, footer + #replace-invert-color replace-color.title=Replace-Invert-Color replace-color.header=Replace-Invert Color PDF @@ -996,6 +1000,26 @@ crop.title=Crop crop.header=Crop PDF crop.submit=Submit +#remove-header-footer +remove-header-footer.title=Remove Header/Footer +remove-header-footer.header=Remove Header/Footer +remove-header-footer.submit=Remove +remove-header-footer.removeHeader=Header +remove-header-footer.removeFooter=Footer +remove-header-footer.headerMargin=Header Margin +remove-header-footer.footerMargin=Footer Margin +remove-header-footer.pages=Select Pages +remove-header-footer.selectMode=Select Mode +remove-header-footer.view-more=View More +remove-header-footer.previousPage=Previous Page +remove-header-footer.nextPage=Next Page +remove-header-footer.zoomOut=Zoom Out +remove-header-footer.zoomIn=Zoom In +remove-header-footer.close=Close +remove-header-footer.margin=Margin +remove-header-footer.auto=Auto +remove-header-footer.manual=Manual +remove-header-footer.enterValue=Enter Value #autoSplitPDF autoSplitPDF.title=Auto Split PDF diff --git a/stirling-pdf/src/main/resources/static/css/remove-header-footer.css b/stirling-pdf/src/main/resources/static/css/remove-header-footer.css new file mode 100644 index 000000000..5bfbc684e --- /dev/null +++ b/stirling-pdf/src/main/resources/static/css/remove-header-footer.css @@ -0,0 +1,157 @@ +#previewContainer { + aspect-ratio: 1; + width: 100%; + border: 1px solid rgba(0, 0, 0, 0.125); + border-radius: 0.25rem; + margin: 1rem 0; + padding: 15px; + display: flex; + align-items: center; + justify-content: center; + position: relative; + overflow: hidden; +} + +#pdf-preview { + max-width: calc(100% - 30px); + max-height: calc(100% - 30px); + position: absolute; + top: 50%; + left: 50%; + transform: translate(-50%, -50%); +} + +.buttonContainer { + display: flex; + justify-content: space-around; +} + +.preview-wrapper { + position: absolute; + top: 35%; + left: 35%; + transform: translate(-25%, -33%); +} + +.margin-line { + position: absolute; + height: 0; + border-top: 2px dashed red; + z-index: 10; + pointer-events: none; +} + +/* Fullscreen overlay */ +#pdfOverlay { + display: none; /* Make it hidden by default */ + position: fixed; + top: 0; + left: 0; + width: 100%; + height: 100%; + background: rgba(30, 30, 30, 0.514); + display: none; + z-index: 1000; + overflow-y: auto; + flex-direction: column; + padding: 60px 0px 0px; +} + +#mainContainer { + display: flex; + flex-direction: column; + height:100%; + width: 100%; + overflow: hidden; +} + +.toolbar { + height: 4rem; + flex-shrink: 0; + z-index: 100; +} + +.toolbarContainer { + height: 4rem !important; +} + +.overlay-content { + flex-grow: 1; + overflow-y: auto; + display: flex; + flex-direction: column; + align-items: center; + padding: 16px; + position: relative; +} + +.pagesView { + flex-grow: 1; + display: flex; + flex-direction: column; + align-items: center; + padding: 16px; + position: relative; +} +.pagesView canvas { + margin-bottom: 24px; +} + +#zoomButton { + position: absolute; + top: 10px; + right: 10px; + color: white; + background: rgba(0, 0, 0, 0); + border: 1px solid #ffffff00; + border-radius: 50%; + width: 32px; + height: 32px; + display: flex; + align-items: center; + justify-content: center; + font-size: 18px; + cursor: pointer; + z-index: 20; + box-shadow: 0 2px 6px rgba(0, 0, 0, 0.253); + transition: background 0.2s; +} + +#zoomButton:hover { + background: rgba(0, 29, 41, 0.9); +} + +button.close-button { + padding: 0; + border: none; + background: rgb(169 201 246); + width: 3rem; + height: 3rem; + border-radius: 15px; + margin-top: 0.5rem; + display: flex; + align-items: center; + justify-content: center; + cursor: pointer; + transition: background 0.15s ease-in-out; +} + +button.close-button .material-symbols-rounded { + font-size: 2rem; + line-height: 1; + color: rgb(58 94 134); +} + +button.close-button:hover { + background: rgb(102, 102, 103); +} + +button.close-button:hover .material-symbols-rounded { + color: #fff; +} + +@media (max-width: 1125px) { + #toolbarViewerRight { + display: flex !important; + } +} diff --git a/stirling-pdf/src/main/resources/static/js/pages/remove-header-footer.js b/stirling-pdf/src/main/resources/static/js/pages/remove-header-footer.js new file mode 100644 index 000000000..edd477a38 --- /dev/null +++ b/stirling-pdf/src/main/resources/static/js/pages/remove-header-footer.js @@ -0,0 +1,603 @@ +import * as pdfjsLib from '../../pdfjs-legacy/pdf.mjs'; +import {PDFViewerApplication} from '../../pdfjs-legacy/js/viewer.mjs'; + +pdfjsLib.GlobalWorkerOptions.workerSrc = './pdfjs-legacy/pdf.worker.mjs'; +let loadedPdf = null; +let pdfFileUrl = null; +let pdfBlobUrl = null; +document.getElementById('removeHeaderFooterForm').addEventListener('submit', async function (event) { + event.preventDefault(); + + const form = event.target; + const formData = new FormData(form); + const responseContainer = document.getElementById('responseContainer'); + + try { + const response = await fetch(form.action, { + method: form.method, + body: formData, + }); + + const responseText = await response.text(); + + responseContainer.textContent = responseText; + responseContainer.className = 'alert alert-success'; + } catch (error) { + responseContainer.textContent = 'An error occurred. Please try again.'; + responseContainer.className = 'alert alert-danger'; + } +}); + +document.addEventListener('DOMContentLoaded', () => { + + PDFViewerApplication.run(); + const fileInput = document.getElementById('fileInput-input'); + const pagesInput = document.getElementById("pageNumbers"); + const previewContainer = document.getElementById('previewContainer'); + const modeSelect = document.querySelector('select[name="mode"]'); + + const overlayHeaderCheckbox = document.getElementById('overlay-removeHeader'); + const overlayFooterCheckbox = document.getElementById('overlay-removeFooter'); + const overlayHeaderMargin = document.getElementById('overlay-headerMargin'); + const overlayFooterMargin = document.getElementById('overlay-footerMargin'); + const customOverlayHeaderWrapper = document.getElementById("overlay-headerCustomMarginWrapper"); + const customOverlayFooterWrapper = document.getElementById("overlay-footerCustomMarginWrapper"); + const customOverlayHeaderInput = document.getElementById("overlay-headerCustomMarginInput"); + const customOverlayFooterInput = document.getElementById("overlay-footerCustomMarginInput"); + + const marginOptions = document.getElementById("margin-options"); + const headerMarginColumn = document.getElementById("header-margin-column"); + const footerMarginColumn = document.getElementById("footer-margin-column"); + const mainHeaderCheckbox = document.getElementById("removeHeader"); + const mainFooterCheckbox = document.getElementById("removeFooter"); + const mainHeaderMargin = document.querySelector('select[name="headerMargin"]'); + const mainFooterMargin = document.querySelector('select[name="footerMargin"]'); + const customMainHeaderWrapper = document.getElementById("headerCustomMarginWrapper"); + const customMainFooterWrapper = document.getElementById("footerCustomMarginWrapper"); + const customMainHeaderInput = document.getElementById("headerCustomMarginInput"); + const customMainFooterInput = document.getElementById("footerCustomMarginInput"); + + const viewerContainer = document.getElementById('viewerContainer'); + const viewer = document.getElementById('viewer'); + const pageNumberInput = document.getElementById('pageNumber'); + const numPagesLabel = document.getElementById('numPages'); + const scaleSelect = document.getElementById('scaleSelect'); + const zoomInBtn = document.getElementById('zoomIn'); + const zoomOutBtn = document.getElementById('zoomOut'); + const nextBtn = document.getElementById('next'); + const prevBtn = document.getElementById('previous'); + const closeBtn = document.getElementById('closeOverlay'); + const zoomButton = document.getElementById('zoomButton'); + + const pageContainer = document.getElementById('page-container'); + const outerContainer = document.getElementById('outerContainer'); + + let pdfDoc = null; + let currentPage = 1; + let currentScale = 1.0; + + // Sync margin controls between overlay and main form + function syncMarginControls(fromOverlay) { + + const copyState = (source, target) => { + target.headerCheckbox.checked = source.headerCheckbox.checked; + target.footerCheckbox.checked = source.footerCheckbox.checked; + target.headerMargin.value = source.headerMargin.value; + target.footerMargin.value = source.footerMargin.value; + target.headerMarginColumn.style.display = source.headerMarginColumn.style.display; + target.footerMarginColumn.style.display = source.footerMarginColumn.style.display; + target.customHeaderWrapper.style.display = source.customHeaderWrapper.style.display; + target.customHeaderInput.value = source.customHeaderInput.value; + target.customFooterWrapper.style.display = source.customFooterWrapper.style.display; + target.customFooterInput.value = source.customFooterInput.value; + }; + + const overlay = { + headerCheckbox: overlayHeaderCheckbox, + footerCheckbox: overlayFooterCheckbox, + headerMargin: overlayHeaderMargin, + footerMargin: overlayFooterMargin, + headerMarginColumn: overlayHeaderMargin, + footerMarginColumn: overlayFooterMargin, + customHeaderWrapper: customOverlayHeaderWrapper, + customHeaderInput: customOverlayHeaderInput, + customFooterWrapper: customOverlayFooterWrapper, + customFooterInput: customOverlayFooterInput + }; + + const main = { + headerCheckbox: mainHeaderCheckbox, + footerCheckbox: mainFooterCheckbox, + headerMargin: mainHeaderMargin, + footerMargin: mainFooterMargin, + headerMarginColumn: headerMarginColumn, + footerMarginColumn: footerMarginColumn, + customHeaderWrapper: customMainHeaderWrapper, + customHeaderInput: customMainHeaderInput, + customFooterWrapper: customMainFooterWrapper, + customFooterInput: customMainFooterInput + }; + + if (fromOverlay) { + copyState(overlay, main); + } else { + copyState(main, overlay); + } + } + + function toggleOverlayMarginOptions() { + overlayHeaderMargin.style.display = overlayHeaderCheckbox.checked ? "block" : "none"; + overlayFooterMargin.style.display = overlayFooterCheckbox.checked ? "block" : "none"; + if(overlayHeaderMargin.value == "custom" && overlayHeaderCheckbox.checked){ + customOverlayHeaderWrapper.style.display = "block"; + } + else{ + customOverlayHeaderWrapper.style.display = "none"; + } + if(overlayFooterMargin.value == "custom" && overlayFooterCheckbox.checked){ + customOverlayFooterWrapper.style.display = "block"; + } + else{ + customOverlayFooterWrapper.style.display = "none"; + } + syncMarginControls(true); + } + + function toggleMainMarginOptions() { + if (modeSelect.value === "margin") { + marginOptions.style.display = "flex"; + headerMarginColumn.style.display = mainHeaderCheckbox.checked ? "block" : "none"; + footerMarginColumn.style.display = mainFooterCheckbox.checked ? "block" : "none"; + if(mainHeaderMargin.value == "custom"){ + customMainHeaderWrapper.style.display = "block"; + } + else{ + customMainHeaderWrapper.style.display = "none"; + } + if(mainFooterMargin.value == "custom"){ + customMainFooterWrapper.style.display = "block"; + } + else{ + customMainFooterWrapper.style.display = "none"; + } + + if (loadedPdf) { + syncMarginControls(false); + } + } else { + marginOptions.style.display = "none"; + } + } + + // Initialize correctly on page load + toggleMainMarginOptions(); + + // Update when mode changes + modeSelect.addEventListener("change", toggleMainMarginOptions); + + + [ + overlayHeaderCheckbox, + overlayFooterCheckbox, + overlayHeaderMargin, + overlayFooterMargin + ].forEach(el => { + el.addEventListener('change', () => { + toggleOverlayMarginOptions(); + renderAllPages(); + }); + }); + + fileInput.addEventListener("change", async function () { + + const existingPreview = document.getElementById("pdf-preview"); + if (existingPreview) existingPreview.remove(); + + const file = fileInput.files[0]; + if (!file || file.type !== 'application/pdf') return; + + + if (pdfFileUrl) URL.revokeObjectURL(pdfFileUrl); + pdfFileUrl = URL.createObjectURL(file); + + loadedPdf = await pdfjsLib.getDocument(pdfFileUrl).promise; + + renderPreview(); + }); + + pagesInput.addEventListener("input", () => { + if (modeSelect.value == "margin" && loadedPdf) { + renderPreview(); + } + }); + + modeSelect.addEventListener("change", () => { + const preview = document.getElementById("pdf-preview"); + + if (modeSelect.value == "margin" && loadedPdf){ + document.querySelector("#editSection").style.display = "block"; + renderPreview(); + } + else { + if (preview) preview.remove(); + document.querySelector("#editSection").style.display = "none"; + } + }); + + [ + mainFooterCheckbox, + mainHeaderCheckbox, + mainFooterMargin, + mainHeaderMargin + ].forEach(el => { + el.addEventListener("input", () => { + toggleMainMarginOptions(); + if (modeSelect.value == "margin" && loadedPdf) + renderPreview(); + }); + }); + + [ + customMainHeaderInput, + customMainFooterInput + ].forEach(el => { + el.addEventListener("input", () => { + if (checkValue(el)) { + toggleMainMarginOptions(); + renderPreview(); + } + }); + }); + + [ + customOverlayHeaderInput, + customOverlayFooterInput + ].forEach(el => { + el.addEventListener("input", () => { + if (checkValue(el)) { + toggleOverlayMarginOptions(); + renderAllPages(); + } + }); + }); + + function checkValue(inputBlock) { + const value = parseInt(inputBlock.value, 10) || 0; + if (!isNaN(value) && value > 0) { + return true; + } + return false; + } + + window.addEventListener("resize", () => { + if (modeSelect.value == "margin" && loadedPdf) { + renderPreview(); + } + }); + + function drawMarginLine(ctx, y, width) { + if (y < 0 || y > ctx.canvas.height) return; + ctx.beginPath(); + ctx.moveTo(0, y); + ctx.lineTo(width, y); + ctx.stroke(); + } + + async function renderPreview() { + if (modeSelect.value != "margin" || !loadedPdf) { + if (preview) preview.remove(); + document.querySelector("#editSection").style.display = "none"; + return; + } else { + document.querySelector("#editSection").style.display = "block"; + } + document.querySelectorAll(".margin-line").forEach(e => e.remove()); + + + const mainHeaderMargin = document.querySelector('select[name="headerMargin"]').value === 'custom' + ? parseInt(document.getElementById('headerCustomMarginInput').value, 10) || 0 + : parseInt(document.querySelector('select[name="headerMargin"]').value, 10) || 0; + const mainFooterMargin = document.querySelector('select[name="footerMargin"]').value === 'custom' + ? parseInt(document.getElementById('footerCustomMarginInput').value, 10) || 0 + : parseInt(document.querySelector('select[name="footerMargin"]').value, 10) || 0; + + const existingPreview = document.getElementById("pdf-preview"); + if (existingPreview) existingPreview.remove(); + + + const pageInput = pagesInput.value.trim(); + let firstPageNumber = 1; + + if (pageInput) { + const pages = pageInput.split(',').flatMap(part => { + if (part.includes('-')) { + const [start, end] = part.split('-').map(Number); + return Array.from({ length: end - start + 1 }, (_, i) => start + i); + } + return [Number(part)]; + }).filter(n => !isNaN(n) && n > 0); + + if (pages.length > 0) { + firstPageNumber = pages[0]; + } + } + + const page = await loadedPdf.getPage(firstPageNumber); + const canvas = document.createElement("canvas"); + + + if (page.rotate == 90 || page.rotate == 270) { + canvas.width = page.view[3]; + canvas.height = page.view[2]; + } else { + canvas.width = page.view[2]; + canvas.height = page.view[3]; + } + + const ctx = canvas.getContext("2d"); + + const renderContext = { + canvasContext: ctx, + viewport: page.getViewport({ scale: 1 }), + }; + + await page.render(renderContext).promise; + + const scale = canvas.height / page.view[3]; // PDF pts to pixels + const headerY = mainHeaderMargin * scale; + const footerY = canvas.height - mainFooterMargin * scale; + + + ctx.strokeStyle = "red"; + ctx.lineWidth = 2; + ctx.setLineDash([5, 5]); + + if (mainHeaderCheckbox.checked) { + drawMarginLine(ctx, headerY, canvas.width); + } + if (mainFooterCheckbox.checked) { + drawMarginLine(ctx, footerY, canvas.width); + } + + const preview = document.createElement("img"); + preview.id = "pdf-preview"; + preview.alt = "preview"; + preview.src = canvas.toDataURL(); + preview.style.position = "absolute"; + preview.style.top = "50%"; + preview.style.left = "50%"; + preview.style.transform = "translate(-50%, -50%)"; + + previewContainer.appendChild(preview); + + URL.revokeObjectURL(pdfFileUrl); + }; + + function parsePagesInput(input, maxPage) { + if (!input) return Array.from({length: maxPage}, (_, i) => i + 1); + const parts = input.split(','); + const pages = new Set(); + for (const part of parts) { + if (part.includes('-')) { + const [start, end] = part.split('-').map(Number); + if (!isNaN(start) && !isNaN(end) && start > 0 && end >= start && end <= maxPage) { + for (let i = start; i <= end; i++) pages.add(i); + } + } else { + const n = Number(part); + if (!isNaN(n) && n > 0 && n <= maxPage) pages.add(n); + } + } + return Array.from(pages).sort((a, b) => a - b); + } + + function renderAllPages() { + viewer.innerHTML = ''; + + const headerMargin = document.querySelector('select[name="headerMargin"]').value === 'custom' + ? parseInt(document.getElementById('headerCustomMarginInput').value, 10) || 0 + : parseInt(document.querySelector('select[name="headerMargin"]').value, 10) || 0; + const footerMargin = document.querySelector('select[name="footerMargin"]').value === 'custom' + ? parseInt(document.getElementById('footerCustomMarginInput').value, 10) || 0 + : parseInt(document.querySelector('select[name="footerMargin"]').value, 10) || 0; + + const removeHeaderChecked = document.getElementById('removeHeader').checked; + const removeFooterChecked = document.getElementById('removeFooter').checked; + + const pageInput = document.getElementById('pageNumbers').value.trim(); + const pagesToShow = parsePagesInput(pageInput, pdfDoc.numPages); + + const renderPage = (num, idx) => { + pdfDoc.getPage(num).then(page => { + const viewport = page.getViewport({ scale: currentScale }); + const canvas = document.createElement('canvas'); + const ctx = canvas.getContext('2d'); + canvas.height = viewport.height; + canvas.width = viewport.width; + canvas.style.display = 'block'; + canvas.style.margin = '0 auto 16px auto'; + viewer.appendChild(canvas); + page.render({ canvasContext: ctx, viewport: viewport }).promise.then(() => { + if (idx === 0) { + pageNumberInput.value = pagesToShow[0]; + numPagesLabel.textContent = `/ ${pagesToShow.length}`; + } + + const scale = canvas.height / page.view[3]; + const headerY = headerMargin * scale; + const footerY = canvas.height - footerMargin * scale; + ctx.strokeStyle = "red"; + ctx.lineWidth = 2; + ctx.setLineDash([5, 5]); + if (removeHeaderChecked) { + drawMarginLine(ctx, headerY, canvas.width); + } + if (removeFooterChecked) { + drawMarginLine(ctx, footerY, canvas.width); + } + }); + }); + }; + pagesToShow.forEach((pageNum, idx) => renderPage(pageNum, idx)); + } + + function scrollToPage(num) { + const canvases = viewer.querySelectorAll('canvas'); + if (canvases[num - 1]) { + canvases[num - 1].scrollIntoView({ behavior: 'smooth', block: 'start' }); + } + } + + zoomButton.addEventListener('click', async () => { + if (!fileInput.files[0]) return; + if (pdfBlobUrl) URL.revokeObjectURL(pdfBlobUrl); + pdfBlobUrl = URL.createObjectURL(fileInput.files[0]); + + syncMarginControls(false); + outerContainer.style.display = 'block'; + pageContainer.style.display = 'none'; + + pdfjsLib.getDocument(pdfBlobUrl).promise.then(pdf => { + pdfDoc = pdf; + currentPage = 1; + currentScale = 1.0; + renderAllPages(); + }); + }); + + closeBtn.addEventListener('click', () => { + syncMarginControls(true); + outerContainer.style.display = 'none'; + pageContainer.style.display = 'block'; + viewer.innerHTML = ''; + if (pdfBlobUrl) URL.revokeObjectURL(pdfBlobUrl); + pdfDoc = null; + renderPreview(); + }); + + nextBtn.addEventListener('click', () => { + if (currentPage < pdfDoc.numPages) { + currentPage++; + pageNumberInput.value = currentPage; + scrollToPage(currentPage); + } + }); + + prevBtn.addEventListener('click', () => { + if (currentPage > 1) { + currentPage--; + pageNumberInput.value = currentPage; + scrollToPage(currentPage); + } + }); + + scaleSelect.addEventListener('change', async e => { + let val = e.target.value; + if (val === 'page-fit' || val === 'page-width' || val === 'page-actual') { + const scale = await getPageScale(val); + onZoomChange(scale); + scaleSelect.value = val; + } else if (val === 'auto') { + onZoomChange(1.25); + scaleSelect.value = val; + } else { + onZoomChange(Number(val)); + } + }); + + pageNumberInput.addEventListener('change', e => { + let num = parseInt(e.target.value, 10); + if (!isNaN(num) && num >= 1 && num <= pdfDoc.numPages) { + currentPage = num; + scrollToPage(currentPage); + } + }); + + function setScaleSelectValue(scale) { + + let found = false; + for (const opt of scaleSelect.options) { + if (Number(opt.value) === scale) { + scaleSelect.value = opt.value; + const customOpt = scaleSelect.querySelector('#customScaleOption'); + if (customOpt) { + customOpt.hidden = true; + customOpt.disabled = true; + } + found = true; + break; + } + } + if (!found) { + + let customOpt = scaleSelect.querySelector('#customScaleOption'); + if (!customOpt) { + customOpt = document.createElement('option'); + customOpt.id = 'customScaleOption'; + scaleSelect.appendChild(customOpt); + } + customOpt.value = 'custom'; + customOpt.textContent = `${Math.round(scale * 100)}%`; + customOpt.hidden = false; + customOpt.disabled = false; + scaleSelect.value = 'custom'; + } + } + + function onZoomChange(newScale) { + currentScale = newScale; + setScaleSelectValue(currentScale); + renderAllPages(); + } + + function getPageScale(mode) { + + if (!pdfDoc) return 1.0; + const firstCanvas = viewer.querySelector('canvas'); + if (!firstCanvas) return 1.0; + const container = viewerContainer; + const pageIndex = 1; + return pdfDoc.getPage(pageIndex).then(page => { + const viewport = page.getViewport({ scale: 1.0 }); + if (mode === 'page-fit') { + // Fit page height to container height + return container.clientHeight / viewport.height; + } else if (mode === 'page-width') { + // Fit page width to container width + return container.clientWidth / viewport.width; + } else if (mode === 'page-actual') { + return 1.0; + } + return 1.0; + }); + } + + // Updates the page number with the visible page number during scroll + viewerContainer.addEventListener('scroll', () => { + const canvases = viewer.querySelectorAll('canvas'); + let closest = 0; + let minDiff = Infinity; + const containerTop = viewerContainer.scrollTop; + for (let i = 0; i < canvases.length; i++) { + const rect = canvases[i].getBoundingClientRect(); + const diff = Math.abs(rect.top - viewerContainer.getBoundingClientRect().top); + if (diff < minDiff) { + minDiff = diff; + closest = i; + } + } + if (pdfDoc && pageNumberInput.value != (closest + 1)) { + pageNumberInput.value = closest + 1; + currentPage = closest + 1; + } + }); + + zoomInBtn.addEventListener('click', () => { + onZoomChange(currentScale + 0.1); + }); + + zoomOutBtn.addEventListener('click', () => { + onZoomChange(currentScale - 0.1); + }); + +}); diff --git a/stirling-pdf/src/main/resources/templates/fragments/common.html b/stirling-pdf/src/main/resources/templates/fragments/common.html index 02d919b2b..c4cf3c996 100644 --- a/stirling-pdf/src/main/resources/templates/fragments/common.html +++ b/stirling-pdf/src/main/resources/templates/fragments/common.html @@ -67,6 +67,7 @@ + diff --git a/stirling-pdf/src/main/resources/templates/remove-header-footer.html b/stirling-pdf/src/main/resources/templates/remove-header-footer.html index 402a12a8d..665a62a3a 100644 --- a/stirling-pdf/src/main/resources/templates/remove-header-footer.html +++ b/stirling-pdf/src/main/resources/templates/remove-header-footer.html @@ -1,14 +1,32 @@ - + + + + + + + + + + + + + + + + + + + +
+

- -

@@ -16,55 +34,234 @@ toolbar
-

- remove_header_footer test -
-
- - + +
+
+
+ + +
+
+ + +
+
+
+ + +
+
+ + +
+
+ + +
+
-
- - - - -
-
+ + + From 93bfcccf5db1fd452a37d8132197ac5d711eba8c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tom=C3=A1s=20Bernardino?= Date: Fri, 6 Jun 2025 16:20:38 +0100 Subject: [PATCH 3/4] feat(#3453): Implement backend logic for removing headers/footers - Implemented backend controller and request model to handle removal of headers and footers from PDFs. - Implemented logic to remove the elements inside the margin limits. - Removed mode logic (manual, margin, auto). - Refactored frontend for improved readability and performance. --- .../api/RemoveHeaderFooterController.java | 137 +++- .../api/general/RemoveHeaderFooterForm.java | 33 +- .../static/css/remove-header-footer.css | 47 -- .../static/js/pages/remove-header-footer.js | 687 +++++++++--------- .../templates/remove-header-footer.html | 25 +- 5 files changed, 506 insertions(+), 423 deletions(-) diff --git a/stirling-pdf/src/main/java/stirling/software/SPDF/controller/api/RemoveHeaderFooterController.java b/stirling-pdf/src/main/java/stirling/software/SPDF/controller/api/RemoveHeaderFooterController.java index 3b36e4feb..146eb55fd 100644 --- a/stirling-pdf/src/main/java/stirling/software/SPDF/controller/api/RemoveHeaderFooterController.java +++ b/stirling-pdf/src/main/java/stirling/software/SPDF/controller/api/RemoveHeaderFooterController.java @@ -1,34 +1,161 @@ package stirling.software.SPDF.controller.api; +import java.io.IOException; +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; + +import org.apache.pdfbox.multipdf.LayerUtility; +import org.apache.pdfbox.pdmodel.PDDocument; +import org.apache.pdfbox.pdmodel.PDPage; +import org.apache.pdfbox.pdmodel.PDPageContentStream; +import org.apache.pdfbox.pdmodel.PDPageContentStream.AppendMode; +import org.apache.pdfbox.pdmodel.common.PDRectangle; +import org.apache.pdfbox.pdmodel.graphics.form.PDFormXObject; +import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.ModelAttribute; import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController; +import org.springframework.web.multipart.MultipartFile; +import io.github.pixee.security.Filenames; import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.tags.Tag; import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; import stirling.software.SPDF.model.api.general.RemoveHeaderFooterForm; +import stirling.software.SPDF.service.CustomPDFDocumentFactory; +import stirling.software.SPDF.utils.GeneralUtils; +import stirling.software.SPDF.utils.WebResponseUtils; @RestController @RequestMapping("/api/v1/general") +@Slf4j @Tag(name = "General", description = "General APIs") @RequiredArgsConstructor public class RemoveHeaderFooterController { - // private final CustomPDFDocumentFactory pdfDocumentFactory; + private final CustomPDFDocumentFactory pdfDocumentFactory; @PostMapping(value = "/remove-header-footer", consumes = "multipart/form-data") @Operation( summary = "Removes headers and/or footers from a PDF document", description = "Remove header and/or footer") - public String removeHeaderFooter(@ModelAttribute RemoveHeaderFooterForm form) { - // Print the received message - System.out.println("Received message: " + form.getMessage()); + public ResponseEntity removeHeaderFooter(@ModelAttribute RemoveHeaderFooterForm form) + throws IOException { + MultipartFile pdfFile = form.getFileInput(); + + String pagesToDelete = form.getPages(); + List pagesToRemove = new ArrayList<>(); + PDDocument sourceDoc = pdfDocumentFactory.load(pdfFile); + PDDocument newDoc = new PDDocument(); + LayerUtility layerUtility = new LayerUtility(newDoc); + + String sufix; // Respond with a message - return "footer Removed"; + if (form.isRemoveHeader()) { + if (form.isRemoveFooter()) { + sufix = "_removed_header_footer.pdf"; + } else sufix = "_removed_header.pdf"; + } else if (form.isRemoveFooter()) { + sufix = "_removed_footer.pdf"; + } else { + return ResponseEntity.badRequest() + .body("No header or footer removal options selected".getBytes()); + } + + if (pagesToDelete == null || pagesToDelete.isEmpty()) { + for (int i = 0; i < sourceDoc.getNumberOfPages(); i++) { + pagesToRemove.add(i); + } + } else { // Split the page order string into an array of page numbers or range of numbers + String[] pageOrderArr = pagesToDelete.split(","); + + pagesToRemove = + GeneralUtils.parsePageList(pageOrderArr, sourceDoc.getNumberOfPages(), false); + + Collections.sort(pagesToRemove); + } + + for (int pageIndex = 0; pageIndex < sourceDoc.getNumberOfPages(); pageIndex++) { + PDPage sourcePage = sourceDoc.getPage(pageIndex); + PDRectangle mediaBox = sourcePage.getMediaBox(); + + PDPage newPage = new PDPage(mediaBox); + newDoc.addPage(newPage); + + try (PDPageContentStream cs = + new PDPageContentStream(newDoc, newPage, AppendMode.OVERWRITE, true, true)) { + PDFormXObject formXObject = layerUtility.importPageAsForm(sourceDoc, pageIndex); + + // Save the current graphics state to restore later + cs.saveGraphicsState(); + + if (pagesToRemove.contains(pageIndex)) { + Float[][] zones = getRemovalZonesForPage(form, sourcePage); + if (zones != null && zones.length > 0) { + cs.addRect(0, 0, mediaBox.getWidth(), mediaBox.getHeight()); + // Add rectangles for each zone to remove (header/footer areas) + // These will be subtracted from the base rectangle using even-odd clipping + // rule + for (Float[] zone : zones) { + if (zone != null && zone.length == 4) { + cs.addRect(zone[0], zone[1], zone[2], zone[3]); + } + } + + cs.clipEvenOdd(); + } + } + + cs.drawForm(formXObject); + // Restore the graphics state to ensure the clipping is applied correctly + cs.restoreGraphicsState(); + } + } + return WebResponseUtils.pdfDocToWebResponse( + newDoc, + Filenames.toSimpleFileName(pdfFile.getOriginalFilename()) + .replaceFirst("[.][^.]+$", "") + + sufix); + } + + /** + * Builds the zones for the header and footer removal based on the form data and the page. + * + * @param form The form containing the removal settings. + * @param page The PDF page to process. + * @return A 2D array of Float representing the zones to remove. + */ + private Float[][] getRemovalZonesForPage(RemoveHeaderFooterForm form, PDPage page) { + float w = page.getMediaBox().getWidth(); + float h = page.getMediaBox().getHeight(); + Float[][] zones = null; + + boolean removeHeader = form.isRemoveHeader(); + boolean removeFooter = form.isRemoveFooter(); + zones = new Float[removeHeader && removeFooter ? 2 : 1][]; + if (removeHeader) { + + Float headerH = form.getHeaderMargin(); + if (headerH == -1) { + headerH = form.getHeaderCustomValue(); // Default value if 'custom' is specified + } + zones[0] = new Float[] {0f, h - headerH, w, headerH}; + } + if (removeFooter) { + + Float footerH = form.getFooterMargin(); + if (footerH == -1) { + footerH = form.getFooterCustomValue(); // Default value if 'custom' is specified + } + zones[zones[0] == null ? 0 : 1] = new Float[] {0f, 0f, w, footerH}; + } + + return zones; } } diff --git a/stirling-pdf/src/main/java/stirling/software/SPDF/model/api/general/RemoveHeaderFooterForm.java b/stirling-pdf/src/main/java/stirling/software/SPDF/model/api/general/RemoveHeaderFooterForm.java index 9c948bf66..f6ee2e074 100644 --- a/stirling-pdf/src/main/java/stirling/software/SPDF/model/api/general/RemoveHeaderFooterForm.java +++ b/stirling-pdf/src/main/java/stirling/software/SPDF/model/api/general/RemoveHeaderFooterForm.java @@ -3,11 +3,36 @@ package stirling.software.SPDF.model.api.general; import io.swagger.v3.oas.annotations.media.Schema; import lombok.Data; +import lombok.EqualsAndHashCode; + +import stirling.software.SPDF.model.api.PDFFile; @Data -// @EqualsAndHashCode(callSuper = true) -public class RemoveHeaderFooterForm { +@EqualsAndHashCode(callSuper = true) +public class RemoveHeaderFooterForm extends PDFFile { - @Schema(description = "Test Message", type = "string") - private String message; + @Schema(description = "Pages to apply the removal to (e.g., ['1', '2-4'])") + private String pages; + + @Schema(description = "Set to true to remove the header region") + private boolean removeHeader; + + @Schema(description = "Set to true to remove the footer region") + private boolean removeFooter; + + @Schema( + description = + "Margin from top for header removal (used in margin mode). If the value is '-1' it means a custom value was send.") + private Float headerMargin; + + @Schema( + description = + "Margin from bottom for footer removal (used in margin mode). If the value is '-1' it means a custom value was send.") + private Float footerMargin; + + @Schema(description = "Custom header height used if footerMargin is '-1'") + private Float headerCustomValue; + + @Schema(description = "Custom footer height used if footerMargin is '-1'") + private Float footerCustomValue; } diff --git a/stirling-pdf/src/main/resources/static/css/remove-header-footer.css b/stirling-pdf/src/main/resources/static/css/remove-header-footer.css index 5bfbc684e..fff036f45 100644 --- a/stirling-pdf/src/main/resources/static/css/remove-header-footer.css +++ b/stirling-pdf/src/main/resources/static/css/remove-header-footer.css @@ -26,37 +26,6 @@ justify-content: space-around; } -.preview-wrapper { - position: absolute; - top: 35%; - left: 35%; - transform: translate(-25%, -33%); -} - -.margin-line { - position: absolute; - height: 0; - border-top: 2px dashed red; - z-index: 10; - pointer-events: none; -} - -/* Fullscreen overlay */ -#pdfOverlay { - display: none; /* Make it hidden by default */ - position: fixed; - top: 0; - left: 0; - width: 100%; - height: 100%; - background: rgba(30, 30, 30, 0.514); - display: none; - z-index: 1000; - overflow-y: auto; - flex-direction: column; - padding: 60px 0px 0px; -} - #mainContainer { display: flex; flex-direction: column; @@ -71,10 +40,6 @@ z-index: 100; } -.toolbarContainer { - height: 4rem !important; -} - .overlay-content { flex-grow: 1; overflow-y: auto; @@ -85,18 +50,6 @@ position: relative; } -.pagesView { - flex-grow: 1; - display: flex; - flex-direction: column; - align-items: center; - padding: 16px; - position: relative; -} -.pagesView canvas { - margin-bottom: 24px; -} - #zoomButton { position: absolute; top: 10px; diff --git a/stirling-pdf/src/main/resources/static/js/pages/remove-header-footer.js b/stirling-pdf/src/main/resources/static/js/pages/remove-header-footer.js index edd477a38..58c78362e 100644 --- a/stirling-pdf/src/main/resources/static/js/pages/remove-header-footer.js +++ b/stirling-pdf/src/main/resources/static/js/pages/remove-header-footer.js @@ -2,39 +2,14 @@ import * as pdfjsLib from '../../pdfjs-legacy/pdf.mjs'; import {PDFViewerApplication} from '../../pdfjs-legacy/js/viewer.mjs'; pdfjsLib.GlobalWorkerOptions.workerSrc = './pdfjs-legacy/pdf.worker.mjs'; -let loadedPdf = null; -let pdfFileUrl = null; -let pdfBlobUrl = null; -document.getElementById('removeHeaderFooterForm').addEventListener('submit', async function (event) { - event.preventDefault(); - - const form = event.target; - const formData = new FormData(form); - const responseContainer = document.getElementById('responseContainer'); - - try { - const response = await fetch(form.action, { - method: form.method, - body: formData, - }); - - const responseText = await response.text(); - - responseContainer.textContent = responseText; - responseContainer.className = 'alert alert-success'; - } catch (error) { - responseContainer.textContent = 'An error occurred. Please try again.'; - responseContainer.className = 'alert alert-danger'; - } -}); document.addEventListener('DOMContentLoaded', () => { PDFViewerApplication.run(); + const CUSTOM = "-1"; const fileInput = document.getElementById('fileInput-input'); const pagesInput = document.getElementById("pageNumbers"); const previewContainer = document.getElementById('previewContainer'); - const modeSelect = document.querySelector('select[name="mode"]'); const overlayHeaderCheckbox = document.getElementById('overlay-removeHeader'); const overlayFooterCheckbox = document.getElementById('overlay-removeFooter'); @@ -45,7 +20,6 @@ document.addEventListener('DOMContentLoaded', () => { const customOverlayHeaderInput = document.getElementById("overlay-headerCustomMarginInput"); const customOverlayFooterInput = document.getElementById("overlay-footerCustomMarginInput"); - const marginOptions = document.getElementById("margin-options"); const headerMarginColumn = document.getElementById("header-margin-column"); const footerMarginColumn = document.getElementById("footer-margin-column"); const mainHeaderCheckbox = document.getElementById("removeHeader"); @@ -62,244 +36,47 @@ document.addEventListener('DOMContentLoaded', () => { const pageNumberInput = document.getElementById('pageNumber'); const numPagesLabel = document.getElementById('numPages'); const scaleSelect = document.getElementById('scaleSelect'); - const zoomInBtn = document.getElementById('zoomIn'); - const zoomOutBtn = document.getElementById('zoomOut'); - const nextBtn = document.getElementById('next'); - const prevBtn = document.getElementById('previous'); - const closeBtn = document.getElementById('closeOverlay'); - const zoomButton = document.getElementById('zoomButton'); const pageContainer = document.getElementById('page-container'); const outerContainer = document.getElementById('outerContainer'); - let pdfDoc = null; + let pdfFileUrl = null; + let loadedPdf = null; let currentPage = 1; let currentScale = 1.0; - // Sync margin controls between overlay and main form - function syncMarginControls(fromOverlay) { + const drawMarginLine = (ctx, y, width, type) => { + if (y < 0 || y > ctx.canvas.height) return; + ctx.save(); + ctx.globalAlpha = 0.5; + ctx.fillStyle = type === "header" ? "red" : "blue"; + ctx.fillRect(0, type === "header" ? 0 : y, width, type === "header" ? y : ctx.canvas.height - y); + ctx.restore(); + }; - const copyState = (source, target) => { - target.headerCheckbox.checked = source.headerCheckbox.checked; - target.footerCheckbox.checked = source.footerCheckbox.checked; - target.headerMargin.value = source.headerMargin.value; - target.footerMargin.value = source.footerMargin.value; - target.headerMarginColumn.style.display = source.headerMarginColumn.style.display; - target.footerMarginColumn.style.display = source.footerMarginColumn.style.display; - target.customHeaderWrapper.style.display = source.customHeaderWrapper.style.display; - target.customHeaderInput.value = source.customHeaderInput.value; - target.customFooterWrapper.style.display = source.customFooterWrapper.style.display; - target.customFooterInput.value = source.customFooterInput.value; - }; - - const overlay = { - headerCheckbox: overlayHeaderCheckbox, - footerCheckbox: overlayFooterCheckbox, - headerMargin: overlayHeaderMargin, - footerMargin: overlayFooterMargin, - headerMarginColumn: overlayHeaderMargin, - footerMarginColumn: overlayFooterMargin, - customHeaderWrapper: customOverlayHeaderWrapper, - customHeaderInput: customOverlayHeaderInput, - customFooterWrapper: customOverlayFooterWrapper, - customFooterInput: customOverlayFooterInput - }; - - const main = { - headerCheckbox: mainHeaderCheckbox, - footerCheckbox: mainFooterCheckbox, - headerMargin: mainHeaderMargin, - footerMargin: mainFooterMargin, - headerMarginColumn: headerMarginColumn, - footerMarginColumn: footerMarginColumn, - customHeaderWrapper: customMainHeaderWrapper, - customHeaderInput: customMainHeaderInput, - customFooterWrapper: customMainFooterWrapper, - customFooterInput: customMainFooterInput - }; - - if (fromOverlay) { - copyState(overlay, main); - } else { - copyState(main, overlay); + function getMarginValue(type, context = 'main') { + const select = context === 'main' + ? document.querySelector(`select[name="${type}Margin"]`) + : document.getElementById(`overlay-${type}Margin`); + if (select.value === CUSTOM) { + const input = context === 'main' + ? document.getElementById(`${type}CustomMarginInput`) + : document.getElementById(`overlay-${type}CustomMarginInput`); + return parseInt(input.value, 10) || 0; } - } - - function toggleOverlayMarginOptions() { - overlayHeaderMargin.style.display = overlayHeaderCheckbox.checked ? "block" : "none"; - overlayFooterMargin.style.display = overlayFooterCheckbox.checked ? "block" : "none"; - if(overlayHeaderMargin.value == "custom" && overlayHeaderCheckbox.checked){ - customOverlayHeaderWrapper.style.display = "block"; - } - else{ - customOverlayHeaderWrapper.style.display = "none"; - } - if(overlayFooterMargin.value == "custom" && overlayFooterCheckbox.checked){ - customOverlayFooterWrapper.style.display = "block"; - } - else{ - customOverlayFooterWrapper.style.display = "none"; - } - syncMarginControls(true); - } - - function toggleMainMarginOptions() { - if (modeSelect.value === "margin") { - marginOptions.style.display = "flex"; - headerMarginColumn.style.display = mainHeaderCheckbox.checked ? "block" : "none"; - footerMarginColumn.style.display = mainFooterCheckbox.checked ? "block" : "none"; - if(mainHeaderMargin.value == "custom"){ - customMainHeaderWrapper.style.display = "block"; - } - else{ - customMainHeaderWrapper.style.display = "none"; - } - if(mainFooterMargin.value == "custom"){ - customMainFooterWrapper.style.display = "block"; - } - else{ - customMainFooterWrapper.style.display = "none"; - } - - if (loadedPdf) { - syncMarginControls(false); - } - } else { - marginOptions.style.display = "none"; - } - } - - // Initialize correctly on page load - toggleMainMarginOptions(); - - // Update when mode changes - modeSelect.addEventListener("change", toggleMainMarginOptions); - - - [ - overlayHeaderCheckbox, - overlayFooterCheckbox, - overlayHeaderMargin, - overlayFooterMargin - ].forEach(el => { - el.addEventListener('change', () => { - toggleOverlayMarginOptions(); - renderAllPages(); - }); - }); - - fileInput.addEventListener("change", async function () { - - const existingPreview = document.getElementById("pdf-preview"); - if (existingPreview) existingPreview.remove(); - - const file = fileInput.files[0]; - if (!file || file.type !== 'application/pdf') return; - - - if (pdfFileUrl) URL.revokeObjectURL(pdfFileUrl); - pdfFileUrl = URL.createObjectURL(file); - - loadedPdf = await pdfjsLib.getDocument(pdfFileUrl).promise; - - renderPreview(); - }); - - pagesInput.addEventListener("input", () => { - if (modeSelect.value == "margin" && loadedPdf) { - renderPreview(); - } - }); - - modeSelect.addEventListener("change", () => { - const preview = document.getElementById("pdf-preview"); - - if (modeSelect.value == "margin" && loadedPdf){ - document.querySelector("#editSection").style.display = "block"; - renderPreview(); - } - else { - if (preview) preview.remove(); - document.querySelector("#editSection").style.display = "none"; - } - }); - - [ - mainFooterCheckbox, - mainHeaderCheckbox, - mainFooterMargin, - mainHeaderMargin - ].forEach(el => { - el.addEventListener("input", () => { - toggleMainMarginOptions(); - if (modeSelect.value == "margin" && loadedPdf) - renderPreview(); - }); - }); - - [ - customMainHeaderInput, - customMainFooterInput - ].forEach(el => { - el.addEventListener("input", () => { - if (checkValue(el)) { - toggleMainMarginOptions(); - renderPreview(); - } - }); - }); - - [ - customOverlayHeaderInput, - customOverlayFooterInput - ].forEach(el => { - el.addEventListener("input", () => { - if (checkValue(el)) { - toggleOverlayMarginOptions(); - renderAllPages(); - } - }); - }); - - function checkValue(inputBlock) { - const value = parseInt(inputBlock.value, 10) || 0; - if (!isNaN(value) && value > 0) { - return true; - } - return false; - } - - window.addEventListener("resize", () => { - if (modeSelect.value == "margin" && loadedPdf) { - renderPreview(); - } - }); - - function drawMarginLine(ctx, y, width) { - if (y < 0 || y > ctx.canvas.height) return; - ctx.beginPath(); - ctx.moveTo(0, y); - ctx.lineTo(width, y); - ctx.stroke(); + return parseInt(select.value, 10) || 0; } async function renderPreview() { - if (modeSelect.value != "margin" || !loadedPdf) { + if (!loadedPdf) { if (preview) preview.remove(); document.querySelector("#editSection").style.display = "none"; return; } else { document.querySelector("#editSection").style.display = "block"; } - document.querySelectorAll(".margin-line").forEach(e => e.remove()); - - - const mainHeaderMargin = document.querySelector('select[name="headerMargin"]').value === 'custom' - ? parseInt(document.getElementById('headerCustomMarginInput').value, 10) || 0 - : parseInt(document.querySelector('select[name="headerMargin"]').value, 10) || 0; - const mainFooterMargin = document.querySelector('select[name="footerMargin"]').value === 'custom' - ? parseInt(document.getElementById('footerCustomMarginInput').value, 10) || 0 - : parseInt(document.querySelector('select[name="footerMargin"]').value, 10) || 0; + const mainHeaderMargin = getMarginValue('header'); + const mainFooterMargin = getMarginValue('footer'); const existingPreview = document.getElementById("pdf-preview"); if (existingPreview) existingPreview.remove(); @@ -325,13 +102,15 @@ document.addEventListener('DOMContentLoaded', () => { const page = await loadedPdf.getPage(firstPageNumber); const canvas = document.createElement("canvas"); - - if (page.rotate == 90 || page.rotate == 270) { + let scale; + if (page.rotate === 90 || page.rotate === 270) { canvas.width = page.view[3]; canvas.height = page.view[2]; + scale = canvas.height / page.view[2]; } else { canvas.width = page.view[2]; canvas.height = page.view[3]; + scale = canvas.height / page.view[3]; } const ctx = canvas.getContext("2d"); @@ -343,20 +122,14 @@ document.addEventListener('DOMContentLoaded', () => { await page.render(renderContext).promise; - const scale = canvas.height / page.view[3]; // PDF pts to pixels const headerY = mainHeaderMargin * scale; const footerY = canvas.height - mainFooterMargin * scale; - - ctx.strokeStyle = "red"; - ctx.lineWidth = 2; - ctx.setLineDash([5, 5]); - if (mainHeaderCheckbox.checked) { - drawMarginLine(ctx, headerY, canvas.width); + drawMarginLine(ctx, headerY, canvas.width, "header"); } if (mainFooterCheckbox.checked) { - drawMarginLine(ctx, footerY, canvas.width); + drawMarginLine(ctx, footerY, canvas.width, "footer"); } const preview = document.createElement("img"); @@ -373,9 +146,10 @@ document.addEventListener('DOMContentLoaded', () => { URL.revokeObjectURL(pdfFileUrl); }; - function parsePagesInput(input, maxPage) { - if (!input) return Array.from({length: maxPage}, (_, i) => i + 1); - const parts = input.split(','); + function parsePagesInput(maxPage) { + const pageInput = pagesInput.value.trim(); + if (!pageInput) return Array.from({length: maxPage}, (_, i) => i + 1); + const parts = pageInput.split(','); const pages = new Set(); for (const part of parts) { if (part.includes('-')) { @@ -394,21 +168,15 @@ document.addEventListener('DOMContentLoaded', () => { function renderAllPages() { viewer.innerHTML = ''; - const headerMargin = document.querySelector('select[name="headerMargin"]').value === 'custom' - ? parseInt(document.getElementById('headerCustomMarginInput').value, 10) || 0 - : parseInt(document.querySelector('select[name="headerMargin"]').value, 10) || 0; - const footerMargin = document.querySelector('select[name="footerMargin"]').value === 'custom' - ? parseInt(document.getElementById('footerCustomMarginInput').value, 10) || 0 - : parseInt(document.querySelector('select[name="footerMargin"]').value, 10) || 0; + const headerMargin = getMarginValue('header', 'overlay'); + const footerMargin = getMarginValue('footer', 'overlay'); - const removeHeaderChecked = document.getElementById('removeHeader').checked; - const removeFooterChecked = document.getElementById('removeFooter').checked; - - const pageInput = document.getElementById('pageNumbers').value.trim(); - const pagesToShow = parsePagesInput(pageInput, pdfDoc.numPages); + const removeHeaderChecked = overlayHeaderCheckbox.checked; + const removeFooterChecked = overlayFooterCheckbox.checked; + const pagesToShow = parsePagesInput(loadedPdf.numPages); const renderPage = (num, idx) => { - pdfDoc.getPage(num).then(page => { + loadedPdf.getPage(num).then(page => { const viewport = page.getViewport({ scale: currentScale }); const canvas = document.createElement('canvas'); const ctx = canvas.getContext('2d'); @@ -419,8 +187,8 @@ document.addEventListener('DOMContentLoaded', () => { viewer.appendChild(canvas); page.render({ canvasContext: ctx, viewport: viewport }).promise.then(() => { if (idx === 0) { - pageNumberInput.value = pagesToShow[0]; - numPagesLabel.textContent = `/ ${pagesToShow.length}`; + pageNumberInput.value = pagesToShow[0]; + numPagesLabel.textContent = `/ ${pagesToShow.length}`; } const scale = canvas.height / page.view[3]; @@ -430,10 +198,10 @@ document.addEventListener('DOMContentLoaded', () => { ctx.lineWidth = 2; ctx.setLineDash([5, 5]); if (removeHeaderChecked) { - drawMarginLine(ctx, headerY, canvas.width); + drawMarginLine(ctx, headerY, canvas.width, "header"); } if (removeFooterChecked) { - drawMarginLine(ctx, footerY, canvas.width); + drawMarginLine(ctx, footerY, canvas.width, "footer"); } }); }); @@ -448,42 +216,315 @@ document.addEventListener('DOMContentLoaded', () => { } } - zoomButton.addEventListener('click', async () => { + function syncMarginControls(fromOverlay) { + + const overlay = { + check:{ + headerCheckbox: overlayHeaderCheckbox, + footerCheckbox: overlayFooterCheckbox + }, + value: { + headerMargin: overlayHeaderMargin, + footerMargin: overlayFooterMargin, + customFooterInput: customOverlayFooterInput, + customHeaderInput: customOverlayHeaderInput + }, + style: { + headerMarginColumn: overlayHeaderMargin, + footerMarginColumn: overlayFooterMargin, + customHeaderWrapper: customOverlayHeaderWrapper, + customFooterWrapper: customOverlayFooterWrapper + } + }; + + const main = { + check:{ + headerCheckbox: mainHeaderCheckbox, + footerCheckbox: mainFooterCheckbox, + }, + value: { + headerMargin: mainHeaderMargin, + footerMargin: mainFooterMargin, + customFooterInput: customMainFooterInput, + customHeaderInput: customMainHeaderInput, + }, + style: { + headerMarginColumn: headerMarginColumn, + footerMarginColumn: footerMarginColumn, + customHeaderWrapper: customMainHeaderWrapper, + customFooterWrapper: customMainFooterWrapper, + } + }; + + const src = fromOverlay ? overlay : main; + const tgt = fromOverlay ? main : overlay; + + Object.keys(src.check).forEach(key => { + if (src.check[key] && tgt.check[key]) { + tgt.check[key].checked = src.check[key].checked; + } + }); + Object.keys(src.value).forEach(key => { + if (src.value[key] && tgt.value[key]) { + tgt.value[key].value = src.value[key].value; + } + }); + Object.keys(src.style).forEach(key => { + if (src.style[key] && tgt.style[key]) { + tgt.style[key].style.display = src.style[key].style.display; + } + }); + } + + function toggleOverlayMarginOptions() { + + overlayHeaderMargin.style.display = overlayHeaderCheckbox.checked ? "block" : "none"; + overlayFooterMargin.style.display = overlayFooterCheckbox.checked ? "block" : "none"; + + if(overlayHeaderMargin.value == CUSTOM && overlayHeaderCheckbox.checked){ + customOverlayHeaderWrapper.style.display = "block"; + } + else{ + customOverlayHeaderWrapper.style.display = "none"; + } + if(overlayFooterMargin.value == CUSTOM && overlayFooterCheckbox.checked){ + customOverlayFooterWrapper.style.display = "block"; + } + else{ + customOverlayFooterWrapper.style.display = "none"; + } + syncMarginControls(true); + } + + function toggleMainMarginOptions() { + + headerMarginColumn.style.display = mainHeaderCheckbox.checked ? "block" : "none"; + footerMarginColumn.style.display = mainFooterCheckbox.checked ? "block" : "none"; + + if(mainHeaderMargin.value == CUSTOM && mainHeaderCheckbox.checked){ + customMainHeaderWrapper.style.display = "block"; + } + else{ + customMainHeaderWrapper.style.display = "none"; + } + if(mainFooterMargin.value == CUSTOM && mainFooterCheckbox.checked){ + customMainFooterWrapper.style.display = "block"; + } + else{ + customMainFooterWrapper.style.display = "none"; + } + + if (loadedPdf) { + syncMarginControls(false); + } + } + + async function checkValue(inputBlock) { + + const value = parseInt(inputBlock.value, 10) || 0; + const pagesToShow = parsePagesInput(loadedPdf.numPages); + const page = await loadedPdf.getPage(pagesToShow[0]); + if (!isNaN(value) && value > 0) { + if(value > page.view[3]) inputBlock.value = page.view[3]; + return true; + } + return false; + } + + function setScaleSelectValue(scale) { + + let found = false; + for (const opt of scaleSelect.options) { + if (Number(opt.value) === scale) { + scaleSelect.value = opt.value; + const customOpt = scaleSelect.querySelector('#customScaleOption'); + if (customOpt) { + customOpt.hidden = true; + customOpt.disabled = true; + } + found = true; + break; + } + } + + if (!found) { + let customOpt = scaleSelect.querySelector('#customScaleOption'); + if (!customOpt) { + customOpt = document.createElement('option'); + customOpt.id = 'customScaleOption'; + scaleSelect.appendChild(customOpt); + } + customOpt.value = 'custom'; + customOpt.textContent = `${Math.round(scale * 100)}%`; + customOpt.hidden = false; + customOpt.disabled = false; + scaleSelect.value = 'custom'; + } + } + + function onZoomChange(newScale) { + currentScale = newScale; + setScaleSelectValue(currentScale); + renderAllPages(); + } + + function getPageScale(mode) { + + if (!loadedPdf) return 1.0; + const firstCanvas = viewer.querySelector('canvas'); + if (!firstCanvas) return 1.0; + const container = viewerContainer; + const pageIndex = 1; + return loadedPdf.getPage(pageIndex).then(page => { + const viewport = page.getViewport({ scale: 1.0 }); + + if (mode === 'page-fit') { + return container.clientHeight / viewport.height; + } else if (mode === 'page-width') { + return container.clientWidth / viewport.width; + } else if (mode === 'page-actual') { + return 1.0; + } + return 1.0; + }); + } + + [ + overlayHeaderCheckbox, + overlayFooterCheckbox, + overlayHeaderMargin, + overlayFooterMargin + ].forEach(el => { + el.addEventListener('change', () => { + toggleOverlayMarginOptions(); + renderAllPages(); + }); + }); + + fileInput.addEventListener("change", async function () { + + const existingPreview = document.getElementById("pdf-preview"); + if (existingPreview) existingPreview.remove(); + + const file = fileInput.files[0]; + if (!file || file.type !== 'application/pdf') return; + + + if (pdfFileUrl) URL.revokeObjectURL(pdfFileUrl); + pdfFileUrl = URL.createObjectURL(file); + + loadedPdf = await pdfjsLib.getDocument(pdfFileUrl).promise; + + renderPreview(); + }); + + pagesInput.addEventListener("input", () => { + if (loadedPdf) { + renderPreview(); + } + }); + + document.getElementById('removeHeaderFooterForm').addEventListener('submit', async function (event) { + event.preventDefault(); + + const form = event.target; + + const formData = new FormData(form); + + const responseContainer = document.getElementById('responseContainer'); + responseContainer.textContent = ''; + responseContainer.className = ''; + console.log("Form data:", Array.from(formData.entries())); + try { + const response = await fetch(form.action, { + method: form.method, + body: formData, + }); + + await response.text(); + if (response.ok) { + if (formData.get('removeHeader')) { + if (formData.get('removeFooter')) { + responseContainer.textContent = 'Header and Footer removed successfully.'; + } + else { + responseContainer.textContent = 'Header removed successfully.'; + } + } + else if (formData.get('removeFooter')) { + responseContainer.textContent = 'Footer removed successfully.'; + } + responseContainer.className = 'alert alert-success'; + } + + } catch (error) { + responseContainer.textContent = 'An error occurred. Please try again.'; + responseContainer.className = 'alert alert-danger'; + } + }); + + [ + mainFooterCheckbox, + mainHeaderCheckbox, + mainFooterMargin, + mainHeaderMargin + ].forEach(el => { + el.addEventListener("input", () => { + toggleMainMarginOptions(); + if (loadedPdf) + renderPreview(); + }); + }); + + [ + [customMainHeaderInput, toggleMainMarginOptions, renderPreview], + [customMainFooterInput, toggleMainMarginOptions, renderPreview], + [customOverlayHeaderInput, toggleOverlayMarginOptions, renderAllPages], + [customOverlayFooterInput, toggleOverlayMarginOptions, renderAllPages] + ].forEach(([el, toggleFn, renderFn]) => { + el.addEventListener("input", async () => { + if (await checkValue(el)) { + toggleFn(); + renderFn(); + } + }); + }); + + window.addEventListener("resize", () => { + if (loadedPdf) { + renderPreview(); + } + }); + + document.getElementById('zoomButton').addEventListener('click', async () => { if (!fileInput.files[0]) return; - if (pdfBlobUrl) URL.revokeObjectURL(pdfBlobUrl); - pdfBlobUrl = URL.createObjectURL(fileInput.files[0]); syncMarginControls(false); outerContainer.style.display = 'block'; pageContainer.style.display = 'none'; - pdfjsLib.getDocument(pdfBlobUrl).promise.then(pdf => { - pdfDoc = pdf; currentPage = 1; currentScale = 1.0; renderAllPages(); - }); }); - closeBtn.addEventListener('click', () => { + document.getElementById('closeOverlay').addEventListener('click', () => { syncMarginControls(true); outerContainer.style.display = 'none'; pageContainer.style.display = 'block'; viewer.innerHTML = ''; - if (pdfBlobUrl) URL.revokeObjectURL(pdfBlobUrl); - pdfDoc = null; renderPreview(); }); - nextBtn.addEventListener('click', () => { - if (currentPage < pdfDoc.numPages) { + document.getElementById('next').addEventListener('click', () => { + if (currentPage < loadedPdf.numPages) { currentPage++; pageNumberInput.value = currentPage; scrollToPage(currentPage); } }); - prevBtn.addEventListener('click', () => { + document.getElementById('previous').addEventListener('click', () => { if (currentPage > 1) { currentPage--; pageNumberInput.value = currentPage; @@ -507,77 +548,18 @@ document.addEventListener('DOMContentLoaded', () => { pageNumberInput.addEventListener('change', e => { let num = parseInt(e.target.value, 10); - if (!isNaN(num) && num >= 1 && num <= pdfDoc.numPages) { + if (!isNaN(num) && num >= 1 && num <= loadedPdf.numPages) { currentPage = num; scrollToPage(currentPage); } }); - function setScaleSelectValue(scale) { - let found = false; - for (const opt of scaleSelect.options) { - if (Number(opt.value) === scale) { - scaleSelect.value = opt.value; - const customOpt = scaleSelect.querySelector('#customScaleOption'); - if (customOpt) { - customOpt.hidden = true; - customOpt.disabled = true; - } - found = true; - break; - } - } - if (!found) { - - let customOpt = scaleSelect.querySelector('#customScaleOption'); - if (!customOpt) { - customOpt = document.createElement('option'); - customOpt.id = 'customScaleOption'; - scaleSelect.appendChild(customOpt); - } - customOpt.value = 'custom'; - customOpt.textContent = `${Math.round(scale * 100)}%`; - customOpt.hidden = false; - customOpt.disabled = false; - scaleSelect.value = 'custom'; - } - } - - function onZoomChange(newScale) { - currentScale = newScale; - setScaleSelectValue(currentScale); - renderAllPages(); - } - - function getPageScale(mode) { - - if (!pdfDoc) return 1.0; - const firstCanvas = viewer.querySelector('canvas'); - if (!firstCanvas) return 1.0; - const container = viewerContainer; - const pageIndex = 1; - return pdfDoc.getPage(pageIndex).then(page => { - const viewport = page.getViewport({ scale: 1.0 }); - if (mode === 'page-fit') { - // Fit page height to container height - return container.clientHeight / viewport.height; - } else if (mode === 'page-width') { - // Fit page width to container width - return container.clientWidth / viewport.width; - } else if (mode === 'page-actual') { - return 1.0; - } - return 1.0; - }); - } - - // Updates the page number with the visible page number during scroll viewerContainer.addEventListener('scroll', () => { const canvases = viewer.querySelectorAll('canvas'); let closest = 0; let minDiff = Infinity; - const containerTop = viewerContainer.scrollTop; + for (let i = 0; i < canvases.length; i++) { const rect = canvases[i].getBoundingClientRect(); const diff = Math.abs(rect.top - viewerContainer.getBoundingClientRect().top); @@ -586,18 +568,19 @@ document.addEventListener('DOMContentLoaded', () => { closest = i; } } - if (pdfDoc && pageNumberInput.value != (closest + 1)) { + if (loadedPdf && pageNumberInput.value != (closest + 1)) { pageNumberInput.value = closest + 1; currentPage = closest + 1; } }); - zoomInBtn.addEventListener('click', () => { + document.getElementById('zoomIn').addEventListener('click', () => { onZoomChange(currentScale + 0.1); }); - zoomOutBtn.addEventListener('click', () => { + document.getElementById('zoomOut').addEventListener('click', () => { onZoomChange(currentScale - 0.1); }); + toggleMainMarginOptions(); }); diff --git a/stirling-pdf/src/main/resources/templates/remove-header-footer.html b/stirling-pdf/src/main/resources/templates/remove-header-footer.html index 665a62a3a..117ced00d 100644 --- a/stirling-pdf/src/main/resources/templates/remove-header-footer.html +++ b/stirling-pdf/src/main/resources/templates/remove-header-footer.html @@ -42,14 +42,6 @@ -
- - -
@@ -60,11 +52,11 @@
- @@ -157,7 +151,7 @@ - + @@ -255,7 +249,7 @@ -
+
@@ -265,3 +259,4 @@
+ From dc1050cee5e9d4e57c638f0f6bdd8d041ae29255 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andr=C3=A9=20Antunes=20Santos?= Date: Fri, 13 Jun 2025 03:58:07 +0100 Subject: [PATCH 4/4] feat(#3453): Fix imports bug and language translation --- .../SPDF/controller/api/RemoveHeaderFooterController.java | 6 +++--- .../SPDF/model/api/general/RemoveHeaderFooterForm.java | 2 +- stirling-pdf/src/main/resources/messages_en_US.properties | 1 + 3 files changed, 5 insertions(+), 4 deletions(-) diff --git a/stirling-pdf/src/main/java/stirling/software/SPDF/controller/api/RemoveHeaderFooterController.java b/stirling-pdf/src/main/java/stirling/software/SPDF/controller/api/RemoveHeaderFooterController.java index 146eb55fd..a6549152b 100644 --- a/stirling-pdf/src/main/java/stirling/software/SPDF/controller/api/RemoveHeaderFooterController.java +++ b/stirling-pdf/src/main/java/stirling/software/SPDF/controller/api/RemoveHeaderFooterController.java @@ -27,9 +27,9 @@ import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import stirling.software.SPDF.model.api.general.RemoveHeaderFooterForm; -import stirling.software.SPDF.service.CustomPDFDocumentFactory; -import stirling.software.SPDF.utils.GeneralUtils; -import stirling.software.SPDF.utils.WebResponseUtils; +import stirling.software.common.service.CustomPDFDocumentFactory; +import stirling.software.common.util.GeneralUtils; +import stirling.software.common.util.WebResponseUtils; @RestController @RequestMapping("/api/v1/general") diff --git a/stirling-pdf/src/main/java/stirling/software/SPDF/model/api/general/RemoveHeaderFooterForm.java b/stirling-pdf/src/main/java/stirling/software/SPDF/model/api/general/RemoveHeaderFooterForm.java index f6ee2e074..9996d375d 100644 --- a/stirling-pdf/src/main/java/stirling/software/SPDF/model/api/general/RemoveHeaderFooterForm.java +++ b/stirling-pdf/src/main/java/stirling/software/SPDF/model/api/general/RemoveHeaderFooterForm.java @@ -5,7 +5,7 @@ import io.swagger.v3.oas.annotations.media.Schema; import lombok.Data; import lombok.EqualsAndHashCode; -import stirling.software.SPDF.model.api.PDFFile; +import stirling.software.common.model.api.PDFFile; @Data @EqualsAndHashCode(callSuper = true) diff --git a/stirling-pdf/src/main/resources/messages_en_US.properties b/stirling-pdf/src/main/resources/messages_en_US.properties index 71b9c1b9f..d32f70a61 100644 --- a/stirling-pdf/src/main/resources/messages_en_US.properties +++ b/stirling-pdf/src/main/resources/messages_en_US.properties @@ -1021,6 +1021,7 @@ remove-header-footer.auto=Auto remove-header-footer.manual=Manual remove-header-footer.enterValue=Enter Value + #autoSplitPDF autoSplitPDF.title=Auto Split PDF autoSplitPDF.header=Auto Split PDF