Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
51 changes: 33 additions & 18 deletions src/main/java/uk/gov/hmcts/reform/em/stitching/domain/Bundle.java
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@
import org.apache.pdfbox.pdmodel.PDDocument;
import org.apache.pdfbox.pdmodel.interactive.documentnavigation.outline.PDDocumentOutline;
import org.apache.pdfbox.pdmodel.interactive.documentnavigation.outline.PDOutlineItem;
import org.apache.pdfbox.pdmodel.interactive.documentnavigation.outline.PDOutlineNode;
import org.hibernate.annotations.Fetch;
import org.hibernate.annotations.FetchMode;
import org.hibernate.annotations.Type;
Expand All @@ -29,11 +30,11 @@
import java.util.ArrayList;
import java.util.Collections;
import java.util.Comparator;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.function.Function;
import java.util.function.ToIntFunction;
import java.util.Set;
import java.util.stream.Stream;

@Entity
Expand Down Expand Up @@ -276,7 +277,9 @@ public Integer getNumberOfSubtitles(SortableBundleItem container,
if (container.getSortedDocuments().count() == documentBundledFilesRef.size()) {
List<PDDocument> docsToClose = new ArrayList<>();
int subtitles = extractDocumentOutlineStream(container, documentBundledFilesRef, docsToClose)
.mapToInt(getItemsFromOutline)
.mapToInt(outline -> Objects.isNull(outline)
? 0
: countNestedItems(outline, 0, new HashSet<>()))
.sum();
closeDocuments(docsToClose);
return subtitles;
Expand All @@ -290,7 +293,9 @@ public List<String> getSubtitles(SortableBundleItem container, Map<BundleDocumen
if (container.getSortedDocuments().count() == documentBundledFilesRef.size()) {
List<PDDocument> docsToClose = new ArrayList<>();
List<String> subtitles = extractDocumentOutlineStream(container, documentBundledFilesRef, docsToClose)
.map(getItemTitlesFromOutline)
.map(outline -> Objects.isNull(outline)
? Collections.<String>emptyList()
: extractNestedTitles(outline, 0, new HashSet<>()))
.flatMap(List::stream)
.toList();
closeDocuments(docsToClose);
Expand Down Expand Up @@ -343,33 +348,43 @@ private PDDocumentOutline extractDocumentOutline(
return null;
}

@Transient
private final transient ToIntFunction<PDDocumentOutline> getItemsFromOutline = outline -> {
if (Objects.isNull(outline)) {
private int countNestedItems(PDOutlineNode node, int depth, Set<PDOutlineItem> visited) {
if (depth > 10 || Objects.isNull(node)) {
return 0;
}
int count = 0;
PDOutlineItem current = outline.getFirstChild();
PDOutlineItem current = node.getFirstChild();
while (Objects.nonNull(current)) {
count++;
if (!visited.add(current)) {
break;
}
if (Objects.nonNull(current.getTitle())) {
count++;
}
count += countNestedItems(current, depth + 1, visited);
current = current.getNextSibling();
}
return count;
};
}

@Transient
private final transient Function<PDDocumentOutline, List<String>> getItemTitlesFromOutline = outline -> {
if (Objects.isNull(outline)) {
return Collections.emptyList();
}
private List<String> extractNestedTitles(PDOutlineNode node, int depth, Set<PDOutlineItem> visited) {
List<String> titles = new ArrayList<>();
PDOutlineItem current = outline.getFirstChild();
if (depth > 10 || Objects.isNull(node)) {
return titles;
}
PDOutlineItem current = node.getFirstChild();
while (Objects.nonNull(current)) {
titles.add(current.getTitle());
if (!visited.add(current)) {
break;
}
if (Objects.nonNull(current.getTitle())) {
titles.add(current.getTitle());
}
titles.addAll(extractNestedTitles(current, depth + 1, visited));
current = current.getNextSibling();
}
return titles;
};
}

@Override
@Transient
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -101,9 +101,9 @@ static float getStringWidth(String string, PDFont font, float fontSize) {
}

static void addSubtitleLink(PDDocument document, PDPage from, PDPage to, String text, float yyOffset,
PDType1Font pdType1Font) throws IOException {
PDType1Font pdType1Font, int depth) throws IOException {

float xxOffset = 45;
float xxOffset = 45 + (depth * 15f);
int noOfLines = splitString(text, TableOfContents.SPACE_PER_SUBTITLE_LINE,
pdType1Font, FONT_SIZE_SUBTITLES).length;
PDAnnotationLink link = generateLink(to, from, xxOffset, yyOffset, noOfLines);
Expand Down
151 changes: 111 additions & 40 deletions src/main/java/uk/gov/hmcts/reform/em/stitching/pdf/TableOfContents.java
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@
import org.apache.pdfbox.pdmodel.PDPage;
import org.apache.pdfbox.pdmodel.font.PDType1Font;
import org.apache.pdfbox.pdmodel.font.Standard14Fonts;
import org.apache.pdfbox.pdmodel.interactive.action.PDActionGoTo;
import org.apache.pdfbox.pdmodel.interactive.documentnavigation.destination.PDDestination;
import org.apache.pdfbox.pdmodel.interactive.documentnavigation.destination.PDPageDestination;
import org.apache.pdfbox.pdmodel.interactive.documentnavigation.outline.PDOutlineItem;
import org.slf4j.Logger;
Expand All @@ -15,13 +17,16 @@
import java.io.File;
import java.io.IOException;
import java.util.ArrayList;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Set;

import static java.lang.Math.max;
import static org.springframework.util.ObjectUtils.isEmpty;
import static uk.gov.hmcts.reform.em.stitching.pdf.PDFMerger.INDEX_PAGE;
import static uk.gov.hmcts.reform.em.stitching.pdf.PDFUtility.FONT_SIZE_SUBTITLES;
import static uk.gov.hmcts.reform.em.stitching.pdf.PDFUtility.LINE_HEIGHT;
import static uk.gov.hmcts.reform.em.stitching.pdf.PDFUtility.addCenterText;
import static uk.gov.hmcts.reform.em.stitching.pdf.PDFUtility.addLink;
Expand All @@ -45,6 +50,9 @@ public class TableOfContents {
private final Logger logger = LoggerFactory.getLogger(TableOfContents.class);
private static final int TITLE_XX_OFFSET = 50;

// Maximum allowed outline nesting depth
private static final int MAX_OUTLINE_DEPTH = 10;

public TableOfContents(PDDocument document, Bundle bundle, Map<BundleDocument, File> documents) throws IOException {
this.document = document;
this.bundle = bundle;
Expand Down Expand Up @@ -79,18 +87,9 @@ public TableOfContents(PDDocument document, Bundle bundle, Map<BundleDocument, F
}

public void addDocument(String documentTitle, int pageNumber, int noOfPages) throws IOException {
addSpaceAfterFolder();

float yyOffset = getVerticalOffset();

// add an extra space after a folder so the document doesn't look like it's in the folder
if (endOfFolder) {
PDFText pdfText = new PDFText(" ", TITLE_XX_OFFSET, yyOffset,
new PDType1Font(Standard14Fonts.FontName.HELVETICA_BOLD), 13);
addText(document, getPage(), pdfText);
yyOffset += LINE_HEIGHT;
numLinesAdded += 1;
}

final PDPage destination = document.getPage(pageNumber);

int noOfLines = splitString(documentTitle, SPACE_PER_TITLE_LINE,
Expand All @@ -108,39 +107,100 @@ public void addDocument(String documentTitle, int pageNumber, int noOfPages) thr
endOfFolder = false;
}

public void addDocumentWithOutline(String documentTitle, int pageNumber, PDOutlineItem sibling) throws IOException {
float yyOffset = getVerticalOffset();
// add an extra space after a folder so the document doesn't look like it's in the folder
if (endOfFolder) {
PDFText pdfText = new PDFText(" ", TITLE_XX_OFFSET, yyOffset,
new PDType1Font(Standard14Fonts.FontName.HELVETICA_BOLD), 13);
addText(document, getPage(), pdfText);
yyOffset += LINE_HEIGHT;
numLinesAdded += 1;
public void addDocumentWithOutline(String documentTitle, int pageNumber, PDOutlineItem firstOutlineItem)
throws IOException {
addSpaceAfterFolder();

if (Objects.nonNull(firstOutlineItem)) {
Set<PDOutlineItem> visited = new HashSet<>();
processOutlineNode(documentTitle, pageNumber, firstOutlineItem, 0, visited);
}
if (Objects.nonNull(sibling)) {
int noOfLines = splitString(sibling.getTitle(), SPACE_PER_SUBTITLE_LINE,
new PDType1Font(Standard14Fonts.FontName.HELVETICA), 12).length;
PDPage destination = new PDPage();
try {
if (sibling.getDestination() instanceof PDPageDestination pdPageDestination) {
destination = document.getPage(pdPageDestination.retrievePageNumber() + pageNumber);
}
if (documentTitle != null && !documentTitle.equalsIgnoreCase(sibling.getTitle())) {
addSubtitleLink(
document,
getPage(),
destination,
sibling.getTitle(),
yyOffset,
new PDType1Font(Standard14Fonts.FontName.HELVETICA));
numLinesAdded += noOfLines;

endOfFolder = false;
}

private void processOutlineNode(String documentTitle, int basePageNumber,
PDOutlineItem item, int depth, Set<PDOutlineItem> visited) {
if (Objects.isNull(item)) {
return;
}

if (!visited.add(item)) {
logger.warn("Circular reference detected in PDF outline for document: {}", documentTitle);
return;
}
if (depth > MAX_OUTLINE_DEPTH) {
logger.warn("Outline depth limit exceeded for document: {}", documentTitle);
return;
}

drawOutlineItem(documentTitle, basePageNumber, item, depth);

if (Objects.nonNull(item.getFirstChild())) {
processOutlineNode(documentTitle, basePageNumber, item.getFirstChild(), depth + 1, visited);
}

if (depth > 0 && Objects.nonNull(item.getNextSibling())) {
processOutlineNode(documentTitle, basePageNumber, item.getNextSibling(), depth, visited);
}
}

private void drawOutlineItem(String documentTitle, int basePageNumber, PDOutlineItem item, int depth) {
if (Objects.isNull(item.getTitle())) {
return;
}

if (Objects.nonNull(documentTitle) && documentTitle.equalsIgnoreCase(item.getTitle())) {
return;
}

try {
float yyOffset = getVerticalOffset();
int noOfLines = splitString(item.getTitle(), SPACE_PER_SUBTITLE_LINE,
new PDType1Font(Standard14Fonts.FontName.HELVETICA), FONT_SIZE_SUBTITLES).length;

PDPage destination = getDestinationPage(item, basePageNumber, documentTitle);

addSubtitleLink(
document,
getPage(),
destination,
item.getTitle(),
yyOffset,
new PDType1Font(Standard14Fonts.FontName.HELVETICA),
depth
);

numLinesAdded += noOfLines;
} catch (Exception e) {
logger.error("Error processing outline item: {}", item.getTitle(), e);
}
}

private PDPage getDestinationPage(PDOutlineItem item, int basePageNumber, String documentTitle)
throws IOException {
PDDestination pdDestination = item.getDestination();

if (Objects.isNull(pdDestination) && item.getAction() instanceof PDActionGoTo actionGoTo) {
pdDestination = actionGoTo.getDestination();
}

if (pdDestination instanceof PDPageDestination pdPageDestination) {
int destPageNum = pdPageDestination.retrievePageNumber();
if (destPageNum >= 0) {
int targetPageIndex = destPageNum + basePageNumber;
if (targetPageIndex < document.getNumberOfPages()) {
return document.getPage(targetPageIndex);
} else {
logger.warn("Calculated page index {} is out of bounds for document {}",
targetPageIndex, documentTitle);
}
} catch (Exception e) {
logger.error("Error processing subtitles: {}", documentTitle, e);
}
} else if (Objects.nonNull(pdDestination)) {
logger.debug("Unsupported destination type found: {}", pdDestination.getClass().getSimpleName());
}
endOfFolder = false;

return new PDPage();
}

public void addFolder(String title, int pageNumber) throws IOException {
Expand All @@ -165,6 +225,17 @@ public void addFolder(String title, int pageNumber) throws IOException {
endOfFolder = false;
}

private void addSpaceAfterFolder() throws IOException {
if (endOfFolder) {
float yyOffset = getVerticalOffset();
PDFText pdfText = new PDFText(" ", TITLE_XX_OFFSET, yyOffset,
new PDType1Font(Standard14Fonts.FontName.HELVETICA_BOLD), 13);
addText(document, getPage(), pdfText);
numLinesAdded += 1;
endOfFolder = false;
}
}

private float getVerticalOffset() {
return TOP_MARGIN_OFFSET + ((numLinesAdded % NUM_LINES_PER_PAGE) * LINE_HEIGHT);
}
Expand Down Expand Up @@ -217,4 +288,4 @@ private int getNumberOfLinesForAllTitles() {
public void setEndOfFolder(boolean value) {
endOfFolder = value;
}
}
}
Loading