Skip to content

Commit 1ba8c13

Browse files
authored
Treat maxDPI as a ceiling rather than the render resolution (#7824)
## Description of Changes `system.maxDPI` (default **500**) is a *safety ceiling* for user-supplied DPI — `PdfUtils.convertFromPdf`, `ScannerEffectController` and `ConvertPdfToVideoController` all correctly reject values above it. But six other places used it as **the working render resolution**, so every one of them rasterises at 500 DPI on a default install. The giveaway is that each site declares a sensible constant and then throws it away — the fallback only applies when `ApplicationProperties` is *absent*, which never happens in a running app: | Call site | Intended | Actual (default) | CPU overshoot (∝ DPI²) | |---|---|---|---| | `BlankPageController` | 30 | 500 | **278×** | | `FlattenController` (no `renderDpi` sent) | 100 | 500 | 25× | | `PdfUtils.convertPdfToPdfImage` — watermark→image, redact, manual redact | 300 | 500 | 2.8× | | `OCRController` | 300 | 500 | 2.8× | | `ExtractImageScansController` | 300 | 500 | 2.8× | | `InvertFullColorStrategy` | 300 | 500 | 2.8× | Blank-page detection is the standout: it renders at 500 DPI purely to compute a white-pixel percentage and throw the image away. ### What changed Each site now uses `Math.min(<its own target>, maxDPI)`, so `maxDPI` clamps but never raises. This is the idiom `AutoRotateController` and `FormUtils` already use. Targets are named constants rather than inline literals. `FlattenController`'s explicit-`renderDpi` path was already correct and is untouched; only its no-request default changed. ### Chosen values - **300** for everything that rasterises output a user keeps (watermark→image, redact, flatten, invert, extract-image-scans) — the print standard, consistent with the DPI default in #7823. - **300** for OCR — Tesseract's own recommended minimum. More pixels cost time without improving recognition. - **150** for blank-page detection, *not* the author's 30. See below. ### Why blank detection is 150, not 30 I measured it rather than trusting the constant. Rendering scan-like pages and running `isBlankImage`'s statistic (threshold 10, blank at ≥99.9% white): ``` content 30dpi 50dpi 72dpi 100dpi 150dpi 300dpi 500dpi empty BLANK BLANK BLANK BLANK BLANK BLANK BLANK specks BLANK BLANK BLANK BLANK BLANK BLANK BLANK thin-line BLANK keep BLANK keep keep keep keep one-word BLANK BLANK BLANK BLANK BLANK BLANK BLANK paragraph keep keep keep keep keep keep keep ``` **At 30 DPI a page containing a thin rule is classified blank and deleted** — a thin line antialiases into near-white. 72 flips the same way. 150 matches the 300/500 verdict on every case, so it preserves today's behaviour exactly at 1/11th the pixels. Using the author's 30 would have shipped data loss. (Unrelated but visible above: `one-word` is already classified blank at *every* DPI including today's 500. That is a threshold weakness, not a DPI one, and is out of scope here.) ## Verification `PdfUtilsMoreTest.ConvertPdfToPdfImageDpi` renders a one-inch-square page so the embedded image's pixel width *is* the effective DPI: - `capsAtPrintQuality` — with `maxDPI=500`, asserts 300. **Confirmed it catches the regression**: reverting the `Math.min` fails it with `expected: 300 but was: 500`. - `respectsLowerCeiling` — with `maxDPI=150`, asserts 150, so the ceiling still clamps downward. Full suites for PdfUtils, blank-page, flatten, OCR, extract-image-scans, invert-colour, watermark and redact all pass unchanged. ## Checklist - [x] I have read the [Contribution Guidelines](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/CONTRIBUTING.md) - [x] My changes generate no new warnings - [x] I have added tests that prove my fix is effective - [x] New and existing unit tests pass locally with my changes
1 parent d6bcf58 commit 1ba8c13

7 files changed

Lines changed: 86 additions & 18 deletions

File tree

app/common/src/main/java/stirling/software/common/util/PdfUtils.java

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,9 @@
4747
@UtilityClass
4848
public class PdfUtils {
4949

50+
// Print-quality raster for flattening a page to an image; capped by system maxDPI
51+
private static final int PDF_TO_IMAGE_DPI = 300;
52+
5053
private final RegexPatternUtils patternCache = RegexPatternUtils.getInstance();
5154

5255
public PDRectangle textToPageSize(String size) {
@@ -370,12 +373,12 @@ public PDDocument convertPdfToPdfImage(PDDocument document) throws IOException {
370373
final int pageIndex = page;
371374
BufferedImage bim;
372375

373-
// Use global maximum DPI setting, fallback to 300 if not set
374-
int renderDpi = 300; // Default fallback
376+
// maxDPI is a safety ceiling for user-supplied values, not a target resolution
377+
int renderDpi = PDF_TO_IMAGE_DPI;
375378
ApplicationProperties properties =
376379
ApplicationContextProvider.getBean(ApplicationProperties.class);
377380
if (properties != null && properties.getSystem() != null) {
378-
renderDpi = properties.getSystem().getMaxDPI();
381+
renderDpi = Math.min(renderDpi, properties.getSystem().getMaxDPI());
379382
}
380383
final int dpi = renderDpi;
381384

app/common/src/main/java/stirling/software/common/util/misc/InvertFullColorStrategy.java

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,9 @@
3030
@Slf4j
3131
public class InvertFullColorStrategy extends ReplaceAndInvertColorStrategy {
3232

33+
// Print-quality raster for the inverted page; capped by system maxDPI
34+
private static final int INVERT_RENDER_DPI = 300;
35+
3336
public InvertFullColorStrategy(MultipartFile file, ReplaceAndInvert replaceAndInvert) {
3437
super(file, replaceAndInvert);
3538
}
@@ -52,12 +55,12 @@ public InputStreamResource replace() throws IOException {
5255
for (int page = 0; page < document.getNumberOfPages(); page++) {
5356
BufferedImage image;
5457

55-
// Use global maximum DPI setting, fallback to 300 if not set
56-
int renderDpi = 300; // Default fallback
58+
// maxDPI is a safety ceiling for user-supplied values, not a target resolution
59+
int renderDpi = INVERT_RENDER_DPI;
5760
ApplicationProperties properties =
5861
ApplicationContextProvider.getBean(ApplicationProperties.class);
5962
if (properties != null && properties.getSystem() != null) {
60-
renderDpi = properties.getSystem().getMaxDPI();
63+
renderDpi = Math.min(renderDpi, properties.getSystem().getMaxDPI());
6164
}
6265
final int dpi = renderDpi;
6366
final int pageNum = page;

app/common/src/test/java/stirling/software/common/util/PdfUtilsMoreTest.java

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -153,6 +153,52 @@ void findsPhraseOnSecondPage() throws IOException {
153153
}
154154
}
155155

156+
@Nested
157+
@DisplayName("convertPdfToPdfImage render DPI")
158+
class ConvertPdfToPdfImageDpi {
159+
160+
/** A one-inch-square page, so the embedded image's pixel width is the render DPI. */
161+
private PDDocument oneInchPage() {
162+
PDDocument doc = new PDDocument();
163+
doc.addPage(new PDPage(new PDRectangle(72f, 72f)));
164+
return doc;
165+
}
166+
167+
private int embeddedImageWidth(PDDocument doc) throws IOException {
168+
PDResources resources = doc.getPage(0).getResources();
169+
for (var name : resources.getXObjectNames()) {
170+
if (resources.isImageXObject(name)) {
171+
return ((PDImageXObject) resources.getXObject(name)).getWidth();
172+
}
173+
}
174+
throw new IllegalStateException("no image on page");
175+
}
176+
177+
private int renderedWidthWithMaxDpi(int maxDpi) throws IOException {
178+
try (PDDocument src = oneInchPage();
179+
MockedStatic<ApplicationContextProvider> ctx =
180+
Mockito.mockStatic(ApplicationContextProvider.class)) {
181+
ctx.when(() -> ApplicationContextProvider.getBean(ApplicationProperties.class))
182+
.thenReturn(propsWithMaxDpi(maxDpi));
183+
try (PDDocument out = PdfUtils.convertPdfToPdfImage(src)) {
184+
return embeddedImageWidth(out);
185+
}
186+
}
187+
}
188+
189+
@Test
190+
@DisplayName("a high maxDPI does not raise the render resolution above 300")
191+
void capsAtPrintQuality() throws IOException {
192+
assertThat(renderedWidthWithMaxDpi(500)).isEqualTo(300);
193+
}
194+
195+
@Test
196+
@DisplayName("a maxDPI below the target still clamps the render resolution")
197+
void respectsLowerCeiling() throws IOException {
198+
assertThat(renderedWidthWithMaxDpi(150)).isEqualTo(150);
199+
}
200+
}
201+
156202
// ---- convertFromPdf with ApplicationProperties present ------------------
157203

158204
@Nested

app/core/src/main/java/stirling/software/SPDF/controller/api/misc/BlankPageController.java

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,10 @@
5050
@RequiredArgsConstructor
5151
public class BlankPageController {
5252

53+
// Lowest resolution that classifies the same as 300-500 DPI. Below this a thin rule can
54+
// antialias away and a page with content is dropped as blank.
55+
private static final int BLANK_DETECTION_DPI = 150;
56+
5357
private final CustomPDFDocumentFactory pdfDocumentFactory;
5458
private final TempFileManager tempFileManager;
5559

@@ -130,12 +134,12 @@ public ResponseEntity<Resource> removeBlankPages(
130134
// Render image and save as temp file
131135
BufferedImage image;
132136

133-
// Use global maximum DPI setting
134-
int renderDpi = 30; // Default fallback
137+
// maxDPI is a safety ceiling for user-supplied values, not a target
138+
int renderDpi = BLANK_DETECTION_DPI;
135139
ApplicationProperties properties =
136140
ApplicationContextProvider.getBean(ApplicationProperties.class);
137141
if (properties != null && properties.getSystem() != null) {
138-
renderDpi = properties.getSystem().getMaxDPI();
142+
renderDpi = Math.min(renderDpi, properties.getSystem().getMaxDPI());
139143
}
140144
final int dpi = renderDpi;
141145
final int currentPageIndex = pageIndex;

app/core/src/main/java/stirling/software/SPDF/controller/api/misc/ExtractImageScansController.java

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,9 @@
5353
@RequiredArgsConstructor
5454
public class ExtractImageScansController {
5555

56+
// Print-quality raster for the extracted scan; capped by system maxDPI
57+
private static final int SCAN_RENDER_DPI = 300;
58+
5659
private static final String REPLACEFIRST = "[.][^.]+$";
5760

5861
private final CustomPDFDocumentFactory pdfDocumentFactory;
@@ -112,12 +115,12 @@ public ResponseEntity<Resource> extractImageScans(
112115
// Render image and save as temp file
113116
BufferedImage image;
114117

115-
// Use global maximum DPI setting, fallback to 300 if not set
116-
int renderDpi = 300; // Default fallback
118+
// maxDPI is a safety ceiling for user-supplied values, not a target
119+
int renderDpi = SCAN_RENDER_DPI;
117120
ApplicationProperties properties =
118121
ApplicationContextProvider.getBean(ApplicationProperties.class);
119122
if (properties != null && properties.getSystem() != null) {
120-
renderDpi = properties.getSystem().getMaxDPI();
123+
renderDpi = Math.min(renderDpi, properties.getSystem().getMaxDPI());
121124
}
122125
final int dpi = renderDpi;
123126
final int pageIndex = i;

app/core/src/main/java/stirling/software/SPDF/controller/api/misc/FlattenController.java

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,9 @@
4242
@RequiredArgsConstructor
4343
public class FlattenController {
4444

45+
// Default when the caller sends no renderDpi; an explicit request still wins up to maxDPI
46+
private static final int FLATTEN_RENDER_DPI = 300;
47+
4548
private final CustomPDFDocumentFactory pdfDocumentFactory;
4649
private final TempFileManager tempFileManager;
4750

@@ -82,7 +85,6 @@ public ResponseEntity<Resource> flatten(@ModelAttribute FlattenRequest request)
8285
try (PDDocument newDocument =
8386
pdfDocumentFactory.createNewDocumentBasedOnOldDocument(document)) {
8487

85-
int defaultRenderDpi = 100; // Default fallback
8688
ApplicationProperties properties =
8789
ApplicationContextProvider.getBean(ApplicationProperties.class);
8890
Integer configuredMaxDpi = null;
@@ -93,10 +95,11 @@ public ResponseEntity<Resource> flatten(@ModelAttribute FlattenRequest request)
9395
int maxDpi =
9496
(configuredMaxDpi != null && configuredMaxDpi > 0)
9597
? configuredMaxDpi
96-
: defaultRenderDpi;
98+
: FLATTEN_RENDER_DPI;
9799

98100
Integer requestedDpi = request.getRenderDpi();
99-
int renderDpiTemp = maxDpi;
101+
// maxDpi is a ceiling; without an explicit request use the print-quality target
102+
int renderDpiTemp = Math.min(FLATTEN_RENDER_DPI, maxDpi);
100103
if (requestedDpi != null) {
101104
renderDpiTemp = Math.min(requestedDpi, maxDpi);
102105
renderDpiTemp = Math.max(renderDpiTemp, 72);

app/core/src/main/java/stirling/software/SPDF/controller/api/misc/OCRController.java

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -61,6 +61,9 @@
6161
@RequiredArgsConstructor
6262
public class OCRController {
6363

64+
// Tesseract's recommended minimum; more pixels cost time without improving recognition
65+
private static final int OCR_RENDER_DPI = 300;
66+
6467
private final ApplicationProperties applicationProperties;
6568
private final CustomPDFDocumentFactory pdfDocumentFactory;
6669
private final TempFileManager tempFileManager;
@@ -392,11 +395,14 @@ private void processWithTesseract(
392395
// Convert page to image
393396
BufferedImage image;
394397

395-
// Use global maximum DPI setting, fallback to 300 if not set
396-
int renderDpi = 300; // Default fallback
398+
// maxDPI is a safety ceiling for user-supplied values, not a target
399+
int renderDpi = OCR_RENDER_DPI;
397400
if (applicationProperties != null
398401
&& applicationProperties.getSystem() != null) {
399-
renderDpi = applicationProperties.getSystem().getMaxDPI();
402+
renderDpi =
403+
Math.min(
404+
renderDpi,
405+
applicationProperties.getSystem().getMaxDPI());
400406
}
401407
final int dpi = renderDpi;
402408
final int currentPageNum = pageNum;

0 commit comments

Comments
 (0)