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 2e7a197de..47b078880 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 @@ -119,6 +119,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..a6549152b --- /dev/null +++ b/stirling-pdf/src/main/java/stirling/software/SPDF/controller/api/RemoveHeaderFooterController.java @@ -0,0 +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.common.service.CustomPDFDocumentFactory; +import stirling.software.common.util.GeneralUtils; +import stirling.software.common.util.WebResponseUtils; + +@RestController +@RequestMapping("/api/v1/general") +@Slf4j +@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 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 + 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/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..9996d375d --- /dev/null +++ b/stirling-pdf/src/main/java/stirling/software/SPDF/model/api/general/RemoveHeaderFooterForm.java @@ -0,0 +1,38 @@ +package stirling.software.SPDF.model.api.general; + +import io.swagger.v3.oas.annotations.media.Schema; + +import lombok.Data; +import lombok.EqualsAndHashCode; + +import stirling.software.common.model.api.PDFFile; + +@Data +@EqualsAndHashCode(callSuper = true) +public class RemoveHeaderFooterForm extends PDFFile { + + @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/messages_en_GB.properties b/stirling-pdf/src/main/resources/messages_en_GB.properties index 22cbfaf17..98aabe922 100644 --- a/stirling-pdf/src/main/resources/messages_en_GB.properties +++ b/stirling-pdf/src/main/resources/messages_en_GB.properties @@ -763,6 +763,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 @@ -1008,6 +1012,27 @@ 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/messages_en_US.properties b/stirling-pdf/src/main/resources/messages_en_US.properties index 4b76acbb8..93ff4a522 100644 --- a/stirling-pdf/src/main/resources/messages_en_US.properties +++ b/stirling-pdf/src/main/resources/messages_en_US.properties @@ -763,6 +763,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 @@ -1008,6 +1012,27 @@ 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..fff036f45 --- /dev/null +++ b/stirling-pdf/src/main/resources/static/css/remove-header-footer.css @@ -0,0 +1,110 @@ +#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; +} + +#mainContainer { + display: flex; + flex-direction: column; + height:100%; + width: 100%; + overflow: hidden; +} + +.toolbar { + height: 4rem; + flex-shrink: 0; + z-index: 100; +} + +.overlay-content { + flex-grow: 1; + overflow-y: auto; + display: flex; + flex-direction: column; + align-items: center; + padding: 16px; + position: relative; +} + +#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..58c78362e --- /dev/null +++ b/stirling-pdf/src/main/resources/static/js/pages/remove-header-footer.js @@ -0,0 +1,586 @@ +import * as pdfjsLib from '../../pdfjs-legacy/pdf.mjs'; +import {PDFViewerApplication} from '../../pdfjs-legacy/js/viewer.mjs'; + +pdfjsLib.GlobalWorkerOptions.workerSrc = './pdfjs-legacy/pdf.worker.mjs'; + +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 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 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 pageContainer = document.getElementById('page-container'); + const outerContainer = document.getElementById('outerContainer'); + + let pdfFileUrl = null; + let loadedPdf = null; + let currentPage = 1; + let currentScale = 1.0; + + 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(); + }; + + 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; + } + return parseInt(select.value, 10) || 0; + } + + async function renderPreview() { + if (!loadedPdf) { + if (preview) preview.remove(); + document.querySelector("#editSection").style.display = "none"; + return; + } else { + document.querySelector("#editSection").style.display = "block"; + } + const mainHeaderMargin = getMarginValue('header'); + const mainFooterMargin = getMarginValue('footer'); + + 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"); + + 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"); + + const renderContext = { + canvasContext: ctx, + viewport: page.getViewport({ scale: 1 }), + }; + + await page.render(renderContext).promise; + + const headerY = mainHeaderMargin * scale; + const footerY = canvas.height - mainFooterMargin * scale; + + if (mainHeaderCheckbox.checked) { + drawMarginLine(ctx, headerY, canvas.width, "header"); + } + if (mainFooterCheckbox.checked) { + drawMarginLine(ctx, footerY, canvas.width, "footer"); + } + + 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(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('-')) { + 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 = getMarginValue('header', 'overlay'); + const footerMargin = getMarginValue('footer', 'overlay'); + + const removeHeaderChecked = overlayHeaderCheckbox.checked; + const removeFooterChecked = overlayFooterCheckbox.checked; + const pagesToShow = parsePagesInput(loadedPdf.numPages); + + const renderPage = (num, idx) => { + loadedPdf.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, "header"); + } + if (removeFooterChecked) { + drawMarginLine(ctx, footerY, canvas.width, "footer"); + } + }); + }); + }; + 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' }); + } + } + + 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; + + syncMarginControls(false); + outerContainer.style.display = 'block'; + pageContainer.style.display = 'none'; + + currentPage = 1; + currentScale = 1.0; + renderAllPages(); + }); + + document.getElementById('closeOverlay').addEventListener('click', () => { + syncMarginControls(true); + outerContainer.style.display = 'none'; + pageContainer.style.display = 'block'; + viewer.innerHTML = ''; + renderPreview(); + }); + + document.getElementById('next').addEventListener('click', () => { + if (currentPage < loadedPdf.numPages) { + currentPage++; + pageNumberInput.value = currentPage; + scrollToPage(currentPage); + } + }); + + document.getElementById('previous').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 <= loadedPdf.numPages) { + currentPage = num; + scrollToPage(currentPage); + } + }); + + + viewerContainer.addEventListener('scroll', () => { + const canvases = viewer.querySelectorAll('canvas'); + let closest = 0; + let minDiff = Infinity; + + 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 (loadedPdf && pageNumberInput.value != (closest + 1)) { + pageNumberInput.value = closest + 1; + currentPage = closest + 1; + } + }); + + document.getElementById('zoomIn').addEventListener('click', () => { + onZoomChange(currentScale + 0.1); + }); + + document.getElementById('zoomOut').addEventListener('click', () => { + onZoomChange(currentScale - 0.1); + }); + + toggleMainMarginOptions(); +}); 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/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..117ced00d --- /dev/null +++ b/stirling-pdf/src/main/resources/templates/remove-header-footer.html @@ -0,0 +1,262 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + +
+

+
+
+
+
+
+ toolbar + +
+
+
+
+
+ + +
+
+
+ + +
+
+ + +
+
+
+ + +
+ +
+ +
+
+
+
+
+
+
+ +
+ + + + + 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