diff --git a/app/core/src/main/java/stirling/software/SPDF/controller/api/RearrangePagesPDFController.java b/app/core/src/main/java/stirling/software/SPDF/controller/api/RearrangePagesPDFController.java index 6dd7aacd79..12e3a15b89 100644 --- a/app/core/src/main/java/stirling/software/SPDF/controller/api/RearrangePagesPDFController.java +++ b/app/core/src/main/java/stirling/software/SPDF/controller/api/RearrangePagesPDFController.java @@ -3,9 +3,12 @@ package stirling.software.SPDF.controller.api; import java.io.IOException; import java.util.ArrayList; import java.util.Collections; +import java.util.HashSet; import java.util.List; import java.util.Locale; +import java.util.Set; +import org.apache.pdfbox.cos.COSDictionary; import org.apache.pdfbox.pdmodel.PDDocument; import org.apache.pdfbox.pdmodel.PDPage; import org.apache.pdfbox.pdmodel.PDPageTree; @@ -261,10 +264,19 @@ public class RearrangePagesPDFController { log.info("newPageOrder = {}", newPageOrder); log.info("totalPages = {}", totalPages); - // Snapshot the desired pages before mutating the source document's page tree. + // Snapshot desired pages before mutating the tree; clone repeats (e.g. DUPLICATE) + // so each slot is a distinct node, not one PDPage under multiple /Kids. List newPages = new ArrayList<>(newPageOrder.size()); + Set seenIndices = new HashSet<>(); for (Integer idx : newPageOrder) { - newPages.add(document.getPage(idx)); + PDPage page = document.getPage(idx); + if (!seenIndices.add(idx)) { + // Duplicate index: distinct page node sharing content/resources. + COSDictionary clonedDict = new COSDictionary(); + clonedDict.addAll(page.getCOSObject()); + page = new PDPage(clonedDict); + } + newPages.add(page); } // Rearrange in-place on the source document rather than copying pages into a diff --git a/app/core/src/test/java/stirling/software/SPDF/controller/api/RearrangePagesPDFControllerTest.java b/app/core/src/test/java/stirling/software/SPDF/controller/api/RearrangePagesPDFControllerTest.java index a225c3fc52..14b1d9892a 100644 --- a/app/core/src/test/java/stirling/software/SPDF/controller/api/RearrangePagesPDFControllerTest.java +++ b/app/core/src/test/java/stirling/software/SPDF/controller/api/RearrangePagesPDFControllerTest.java @@ -9,6 +9,7 @@ import java.io.File; import java.io.IOException; import java.nio.file.Files; import java.util.ArrayList; +import java.util.HashSet; import java.util.List; import org.apache.pdfbox.Loader; @@ -302,6 +303,11 @@ class RearrangePagesPDFControllerTest { assertNotNull(response); // 2 pages * 3 duplicates = 6 final pages assertEquals(6, realDoc.getNumberOfPages()); + // Each duplicate must be a distinct page node in the saved output; a shared + // node under multiple /Kids is an invalid tree readers reject as cyclic. + List savedPages = reloadAndSnapshot(response); + assertEquals(6, savedPages.size()); + assertEquals(6, new HashSet<>(savedPages).size()); } } @@ -323,4 +329,29 @@ class RearrangePagesPDFControllerTest { assertEquals(4, realDoc.getNumberOfPages()); } } + + @Test + void testRearrangePages_SideStitchBooklet_RepeatedPaddingPagesAreDistinctNodes() + throws IOException { + MockMultipartFile file = createMockPdf(); + RearrangePagesRequest request = new RearrangePagesRequest(); + request.setFileInput(file); + request.setPageNumbers(""); + request.setCustomMode("SIDE_STITCH_BOOKLET_SORT"); + + // 6 pages is not a multiple of 4, so booklet padding repeats the last page index + // several times; each repeat must be a distinct page node, not one shared node. + try (PDDocument realDoc = buildRealPdf(6)) { + when(pdfDocumentFactory.load(file)).thenReturn(realDoc); + + ResponseEntity response = controller.rearrangePages(request); + + assertNotNull(response); + assertEquals(200, response.getStatusCode().value()); + assertEquals(8, realDoc.getNumberOfPages()); + List savedPages = reloadAndSnapshot(response); + assertEquals(8, savedPages.size()); + assertEquals(8, new HashSet<>(savedPages).size()); + } + } }