fixes
This commit is contained in:
@@ -287,6 +287,7 @@ public class MergeController {
|
||||
|
||||
boolean removeCertSign = Boolean.TRUE.equals(request.getRemoveCertSign());
|
||||
boolean generateToc = request.isGenerateToc();
|
||||
boolean preserveAccessibility = request.isPreserveAccessibility();
|
||||
|
||||
MultipartFile[] files = request.getFileInput();
|
||||
if (files == null) {
|
||||
@@ -312,11 +313,12 @@ public class MergeController {
|
||||
try {
|
||||
PDFMergerUtility mergerUtility = new PDFMergerUtility();
|
||||
// OPTIMIZE_RESOURCES_MODE closes source documents progressively and skips
|
||||
// structure-tree copying. Trade-off: PDF/UA tags (used by screen readers) are not
|
||||
// preserved in the merged output. Most users don't have tagged PDFs and this trades
|
||||
// negligibly-different output for measurably lower peak heap during merge.
|
||||
// structure-tree copying — drops PDF/UA tags but uses much less heap.
|
||||
// PDFBOX_LEGACY_MODE preserves tags at the cost of higher peak heap.
|
||||
mergerUtility.setDocumentMergeMode(
|
||||
PDFMergerUtility.DocumentMergeMode.OPTIMIZE_RESOURCES_MODE);
|
||||
preserveAccessibility
|
||||
? PDFMergerUtility.DocumentMergeMode.PDFBOX_LEGACY_MODE
|
||||
: PDFMergerUtility.DocumentMergeMode.OPTIMIZE_RESOURCES_MODE);
|
||||
long totalSize = 0;
|
||||
File[] sourceFiles = new File[files.length];
|
||||
for (int index = 0; index < files.length; index++) {
|
||||
@@ -340,6 +342,21 @@ public class MergeController {
|
||||
} catch (IOException e) {
|
||||
ExceptionUtils.logException("PDF merge", e);
|
||||
if (PdfErrorUtils.isCorruptedPdfError(e)) {
|
||||
// Identify which source file(s) are corrupt for operator diagnostics.
|
||||
// Only runs on the failure path so the happy path stays fast.
|
||||
List<String> badFiles = new ArrayList<>();
|
||||
for (int i = 0; i < sourceFiles.length; i++) {
|
||||
try (PDDocument ignored =
|
||||
pdfDocumentFactory.load(sourceFiles[i], true)) {
|
||||
// OK
|
||||
} catch (IOException corruptInput) {
|
||||
String name = files[i].getOriginalFilename();
|
||||
badFiles.add(name != null ? name : ("index " + i));
|
||||
}
|
||||
}
|
||||
if (!badFiles.isEmpty()) {
|
||||
log.warn("Corrupted PDFs in merge input: {}", badFiles);
|
||||
}
|
||||
throw ExceptionUtils.createMultiplePdfCorruptedException(e);
|
||||
}
|
||||
throw e;
|
||||
@@ -357,10 +374,6 @@ public class MergeController {
|
||||
int[] pageCounts = generateToc ? collectPageCounts(sourceFiles) : null;
|
||||
|
||||
outputTempFile = new TempFile(tempFileManager, ".pdf");
|
||||
// Hint the GC to reclaim merger transients before we open the merged document.
|
||||
// The merger has just dropped its destination COSDocument; reclaiming that heap
|
||||
// before loading the merged file again limits live-set during the modify pass.
|
||||
System.gc();
|
||||
try (PDDocument mergedDocument = pdfDocumentFactory.load(mergeOutput.getFile())) {
|
||||
// Resource cache off for the modify pass — we never call getImage() here,
|
||||
// and disabling it prevents PDFBox from caching XObjects when the page tree
|
||||
|
||||
@@ -40,6 +40,16 @@ public class MergePdfsRequest extends MultiplePDFFiles {
|
||||
defaultValue = "false")
|
||||
private boolean generateToc = false;
|
||||
|
||||
@Schema(
|
||||
description =
|
||||
"Flag indicating whether to preserve PDF/UA accessibility tags (structure"
|
||||
+ " tree) in the merged output. When false (default) the merger runs in"
|
||||
+ " resource-optimised mode which drops tags but uses significantly less"
|
||||
+ " heap. Set true when merging tagged PDFs intended for screen readers.",
|
||||
requiredMode = Schema.RequiredMode.NOT_REQUIRED,
|
||||
defaultValue = "false")
|
||||
private boolean preserveAccessibility = false;
|
||||
|
||||
@Schema(
|
||||
description =
|
||||
"JSON array of client-provided IDs for each uploaded file (same order as fileInput)",
|
||||
|
||||
@@ -19,6 +19,7 @@ describe("MergeSettings", () => {
|
||||
const defaultParameters: MergeParameters = {
|
||||
removeDigitalSignature: false,
|
||||
generateTableOfContents: false,
|
||||
preserveAccessibility: false,
|
||||
};
|
||||
|
||||
const mockOnParameterChange = vi.fn();
|
||||
@@ -86,6 +87,13 @@ describe("MergeSettings", () => {
|
||||
"generateTableOfContents",
|
||||
true,
|
||||
);
|
||||
|
||||
// Click the third checkbox (preserveAccessibility - should toggle from false to true)
|
||||
fireEvent.click(checkboxes[2]);
|
||||
expect(mockOnParameterChange).toHaveBeenCalledWith(
|
||||
"preserveAccessibility",
|
||||
true,
|
||||
);
|
||||
});
|
||||
|
||||
test("should call translation function with correct keys", () => {
|
||||
@@ -107,5 +115,9 @@ describe("MergeSettings", () => {
|
||||
"merge.generateTableOfContents.label",
|
||||
"Generate table of contents in the merged file?",
|
||||
);
|
||||
expect(mockT).toHaveBeenCalledWith(
|
||||
"merge.preserveAccessibility.label",
|
||||
"Preserve accessibility tags? (uses more memory on large merges)",
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -50,6 +50,21 @@ const MergeSettings: React.FC<MergeSettingsProps> = ({
|
||||
}
|
||||
disabled={disabled}
|
||||
/>
|
||||
|
||||
<Checkbox
|
||||
label={t(
|
||||
"merge.preserveAccessibility.label",
|
||||
"Preserve accessibility tags? (uses more memory on large merges)",
|
||||
)}
|
||||
checked={parameters.preserveAccessibility}
|
||||
onChange={(event) =>
|
||||
onParameterChange(
|
||||
"preserveAccessibility",
|
||||
event.currentTarget.checked,
|
||||
)
|
||||
}
|
||||
disabled={disabled}
|
||||
/>
|
||||
</Stack>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -33,6 +33,10 @@ const buildFormData = (
|
||||
"generateToc",
|
||||
(parameters.generateTableOfContents ?? false).toString(),
|
||||
);
|
||||
formData.append(
|
||||
"preserveAccessibility",
|
||||
(parameters.preserveAccessibility ?? false).toString(),
|
||||
);
|
||||
|
||||
return formData;
|
||||
};
|
||||
|
||||
@@ -7,11 +7,13 @@ import {
|
||||
export interface MergeParameters extends BaseParameters {
|
||||
removeDigitalSignature: boolean;
|
||||
generateTableOfContents: boolean;
|
||||
preserveAccessibility: boolean;
|
||||
}
|
||||
|
||||
export const defaultParameters: MergeParameters = {
|
||||
removeDigitalSignature: false,
|
||||
generateTableOfContents: false,
|
||||
preserveAccessibility: false,
|
||||
};
|
||||
|
||||
export type MergeParametersHook = BaseParametersHook<MergeParameters>;
|
||||
|
||||
Reference in New Issue
Block a user