This is an automated email from the ASF dual-hosted git repository. tballison pushed a commit to branch pdfiumtoppm-renderer in repository https://gitbox.apache.org/repos/asf/tika.git
commit be3b902cfcfb190be48b29632ce52c8315dd3a98 Author: tallison <[email protected]> AuthorDate: Fri Aug 28 20:05:35 2026 -0400 WIP - add PdfiumToPpmRenderer --- CHANGES.txt | 7 + .../org/apache/tika/parser/pdf/PDFParserTest.java | 11 + .../configs/tika-rendering-pdfiumtoppm-config.json | 17 + .../pdf/pdfiumtoppm/PdfiumToPpmRenderer.java | 374 +++++++++++++++++++++ .../pdf/pdfiumtoppm/PdfiumToPpmRendererTest.java | 221 ++++++++++++ 5 files changed, 630 insertions(+) diff --git a/CHANGES.txt b/CHANGES.txt index c4d49ce9cb..e11c32f596 100644 --- a/CHANGES.txt +++ b/CHANGES.txt @@ -1,5 +1,12 @@ Release 4.1.0 - unreleased + * New pdfiumtoppm-renderer for PDF page rendering and OCR: drives the + external pdfiumtoppm command (PDFium behind a pdftoppm-compatible + command line) with -max-pixels and -max-memory limits for untrusted + input; exit 4 surfaces as TikaMemoryLimitException. Output is PNG, or + PPM/PGM with png=false (no encode/decode; pair with the OCR engine's + explicit DPI, e.g. tesseract user_defined_dpi). + * New exception-reporting parse-context config controls how much of an exception is written to tk:exception:* metadata for the container and embedded documents alike: FULL (default), MESSAGE_REDACTED (stack trace diff --git a/tika-parsers/tika-parsers-standard/tika-parsers-standard-integration-tests/src/test/java/org/apache/tika/parser/pdf/PDFParserTest.java b/tika-parsers/tika-parsers-standard/tika-parsers-standard-integration-tests/src/test/java/org/apache/tika/parser/pdf/PDFParserTest.java index 0c8160aced..f04b64311b 100644 --- a/tika-parsers/tika-parsers-standard/tika-parsers-standard-integration-tests/src/test/java/org/apache/tika/parser/pdf/PDFParserTest.java +++ b/tika-parsers/tika-parsers-standard/tika-parsers-standard-integration-tests/src/test/java/org/apache/tika/parser/pdf/PDFParserTest.java @@ -467,6 +467,17 @@ public class PDFParserTest extends TikaTest { assertContains("Happy", text.trim()); } + @Test + public void testPdfiumToPpmInOCR() throws Exception { + assumeTrue(canRunOCR(), "can't run OCR"); + assumeTrue(ProcessUtils.checkCommand(new String[]{"pdfiumtoppm", "-v"}), + "does not have pdfiumtoppm"); + Parser p = TikaLoaderHelper.getLoader("tika-rendering-pdfiumtoppm-config.json") + .loadAutoDetectParser(); + String text = getText(getResourceAsStream("/test-documents/testOCR.pdf"), p); + assertContains("Happy", text.trim()); + } + @Test public void testIncrementalUpdatesInAnAttachedPDF() throws Exception { PDFParserConfig pdfParserConfig = new PDFParserConfig(); diff --git a/tika-parsers/tika-parsers-standard/tika-parsers-standard-integration-tests/src/test/resources/configs/tika-rendering-pdfiumtoppm-config.json b/tika-parsers/tika-parsers-standard/tika-parsers-standard-integration-tests/src/test/resources/configs/tika-rendering-pdfiumtoppm-config.json new file mode 100644 index 0000000000..5fc725f876 --- /dev/null +++ b/tika-parsers/tika-parsers-standard/tika-parsers-standard-integration-tests/src/test/resources/configs/tika-rendering-pdfiumtoppm-config.json @@ -0,0 +1,17 @@ +{ + "parsers": [ + { + "default-parser":{} + }, + { + "tesseract-ocr-parser": { + "pageSegMode": 3 + } + } + ], + "renderers": [ + { + "pdfiumtoppm-renderer": {} + } + ] +} diff --git a/tika-parsers/tika-parsers-standard/tika-parsers-standard-modules/tika-parser-pdf-module/src/main/java/org/apache/tika/renderer/pdf/pdfiumtoppm/PdfiumToPpmRenderer.java b/tika-parsers/tika-parsers-standard/tika-parsers-standard-modules/tika-parser-pdf-module/src/main/java/org/apache/tika/renderer/pdf/pdfiumtoppm/PdfiumToPpmRenderer.java new file mode 100644 index 0000000000..66dfe90a44 --- /dev/null +++ b/tika-parsers/tika-parsers-standard/tika-parsers-standard-modules/tika-parser-pdf-module/src/main/java/org/apache/tika/renderer/pdf/pdfiumtoppm/PdfiumToPpmRenderer.java @@ -0,0 +1,374 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.tika.renderer.pdf.pdfiumtoppm; + +import java.io.Closeable; +import java.io.File; +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import java.util.Set; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +import org.apache.tika.annotation.TikaComponent; +import org.apache.tika.exception.TikaException; +import org.apache.tika.exception.TikaMemoryLimitException; +import org.apache.tika.exception.TikaTimeoutException; +import org.apache.tika.io.TemporaryResources; +import org.apache.tika.io.TikaInputStream; +import org.apache.tika.metadata.Metadata; +import org.apache.tika.metadata.TikaCoreProperties; +import org.apache.tika.metadata.TikaPagedText; +import org.apache.tika.mime.MediaType; +import org.apache.tika.parser.ParseContext; +import org.apache.tika.renderer.PageBasedRenderResults; +import org.apache.tika.renderer.PageRangeRequest; +import org.apache.tika.renderer.RenderRequest; +import org.apache.tika.renderer.RenderResult; +import org.apache.tika.renderer.RenderResults; +import org.apache.tika.renderer.Renderer; +import org.apache.tika.renderer.RenderingTracker; +import org.apache.tika.utils.FileProcessResult; +import org.apache.tika.utils.ProcessUtils; + +/** + * Renderer that uses the {@code pdfiumtoppm} command + * (<a href="https://github.com/tballison/pdfiumtoppm">github.com/tballison/pdfiumtoppm</a>), + * a {@code pdftoppm}-compatible renderer built on PDFium, to convert PDF pages + * to PNG images. + * <p> + * Compared with {@code pdftoppm} it renders faster with a shorter tail on + * pathological files and adds hard limits for untrusted input: + * {@code -max-pixels} (downscale only, never enlarge) and {@code -max-memory} + * (an address-space limit the process enforces on itself; exit code 4 when + * hit). The binary and a matching {@code libpdfium.so} ship together in the + * release tarball; keep them in the same directory. + * <p> + * Configuration key: {@code "pdfiumtoppm-renderer"} + * + * @since Apache Tika 4.1 + */ +@TikaComponent(name = "pdfiumtoppm-renderer", spi = false) +public class PdfiumToPpmRenderer implements Renderer { + + /** pdfiumtoppm exits 4 when a page could not fit under {@code -max-memory}. */ + static final int EXIT_MEMORY = 4; + + private static final Set<MediaType> SUPPORTED_TYPES = + Collections.singleton(MediaType.application("pdf")); + + private static final Pattern PAGE_FILE_PATTERN = + Pattern.compile("tika-pdfium-(\\d+)\\.(png|ppm|pgm)"); + + private String pdfiumToPpmPath = "pdfiumtoppm"; + private String pdfiumLibraryDir = null; + private int dpi = 300; + private boolean gray = true; + + /** + * Write PNG (default) or, when false, binary PPM/PGM: no encode or decode, but no DPI + * header either, so tell the OCR engine the resolution (tesseract: {@code user_defined_dpi}). + */ + private boolean png = true; + private int timeoutMillis = 120000; + + /** + * Longest edge in pixels; maps to {@code -scale-to}. Like {@code pdftoppm}, + * this scales every page to exactly this size, enlarging small pages and + * ignoring {@code dpi}. Off by default; {@link #maxPixels} is the cap. + */ + private int maxScaleTo = -1; + + /** + * Maximum width × height of a rendered page; maps to + * {@code -max-pixels}. Pages that would exceed it are downscaled to fit; + * smaller pages are untouched. Default 16,777,216 (4096 × 4096). + */ + private long maxPixels = 4096L * 4096L; + + /** + * Address-space limit in MiB for the render process; maps to + * {@code -max-memory}. {@code -1} leaves pdfiumtoppm's own default + * (4096 MiB or half of RAM, whichever is lower); {@code 0} disables the + * limit. + */ + private int maxMemoryMb = -1; + + @Override + public Set<MediaType> getSupportedTypes(ParseContext context) { + return SUPPORTED_TYPES; + } + + @Override + public RenderResults render(TikaInputStream tis, Metadata metadata, + ParseContext parseContext, + RenderRequest... requests) + throws IOException, TikaException { + TemporaryResources tmp = new TemporaryResources(); + PageBasedRenderResults results = new PageBasedRenderResults(tmp); + Path path = tis.getPath(); + for (RenderRequest request : requests) { + renderRequest(path, parseContext, request, results, tmp); + } + return results; + } + + private void renderRequest(Path pdf, ParseContext parseContext, + RenderRequest request, + PageBasedRenderResults results, + TemporaryResources tmp) + throws TikaException, IOException { + if (!(request instanceof PageRangeRequest)) { + throw new TikaException( + "I regret that this renderer can only handle " + + "PageRangeRequests, not " + request.getClass()); + } + PageRangeRequest rangeRequest = (PageRangeRequest) request; + + RenderingTracker tracker = parseContext.get(RenderingTracker.class); + if (tracker == null) { + tracker = new RenderingTracker(); + parseContext.set(RenderingTracker.class, tracker); + } + + Path dir = Files.createTempDirectory("tika-render-"); + tmp.addResource(new Closeable() { + @Override + public void close() throws IOException { + Files.delete(dir); + } + }); + + String[] args = createCommandLine(pdf, dir, rangeRequest); + + ProcessBuilder builder = new ProcessBuilder(); + builder.command(args); + FileProcessResult result = ProcessUtils.execute( + builder, parseContext, timeoutMillis, 10, 1000); + if (result.isTimeout()) { + throw new TikaTimeoutException("pdfiumtoppm timed out", + result.getRequestedTimeoutMillis(), result.getGrantedTimeoutMillis()); + } else if (result.getExitValue() == EXIT_MEMORY) { + throw new TikaMemoryLimitException( + "pdfiumtoppm hit its -max-memory limit: " + result.getStderr()); + } else if (result.getExitValue() != 0) { + throw new TikaException( + "pdfiumtoppm failed (exit " + result.getExitValue() + + "): " + result.getStderr()); + } + + Matcher m = PAGE_FILE_PATTERN.matcher(""); + File[] files = dir.toFile().listFiles(); + if (files == null) { + return; + } + for (File f : files) { + if (m.reset(f.getName()).find()) { + int pageNumber = Integer.parseInt(m.group(1)); + Metadata renderMetadata = Metadata.newInstance(parseContext); + renderMetadata.set(TikaPagedText.PAGE_NUMBER, pageNumber); + renderMetadata.set(TikaCoreProperties.EMBEDDED_RESOURCE_TYPE, + TikaCoreProperties.EmbeddedResourceType.RENDERING + .name()); + results.add(new RenderResult( + RenderResult.STATUS.SUCCESS, + tracker.getNextId(), + f.toPath(), + renderMetadata)); + } + } + } + + String[] createCommandLine(Path pdf, Path dir, + PageRangeRequest request) { + List<String> args = new ArrayList<>(); + args.add(pdfiumToPpmPath); + if (pdfiumLibraryDir != null) { + args.add("-pdfium"); + args.add(ProcessUtils.escapeCommandLine(pdfiumLibraryDir)); + } + if (png) { + args.add("-png"); + } + args.add("-r"); + args.add(String.valueOf(dpi)); + if (maxScaleTo > 0) { + args.add("-scale-to"); + args.add(String.valueOf(maxScaleTo)); + } + if (maxPixels > 0) { + args.add("-max-pixels"); + args.add(String.valueOf(maxPixels)); + } + if (maxMemoryMb >= 0) { + args.add("-max-memory"); + args.add(String.valueOf(maxMemoryMb)); + } + if (gray) { + args.add("-gray"); + } + if (request != PageRangeRequest.RENDER_ALL) { + args.add("-f"); + args.add(String.valueOf(request.getFrom())); + args.add("-l"); + args.add(String.valueOf(request.getTo())); + } + args.add(ProcessUtils.escapeCommandLine( + pdf.toAbsolutePath().toString())); + args.add(ProcessUtils.escapeCommandLine( + dir.toAbsolutePath().toString() + "/tika-pdfium")); + return args.toArray(new String[0]); + } + + // ---- config getters/setters ------------------------------------------- + + public String getPdfiumToPpmPath() { + return pdfiumToPpmPath; + } + + /** + * Path to the {@code pdfiumtoppm} executable. Defaults to + * {@code "pdfiumtoppm"} (on the system path). + */ + public void setPdfiumToPpmPath(String pdfiumToPpmPath) { + this.pdfiumToPpmPath = pdfiumToPpmPath; + } + + public String getPdfiumLibraryDir() { + return pdfiumLibraryDir; + } + + /** + * Directory containing {@code libpdfium.so}; maps to {@code -pdfium}. + * Optional: by default pdfiumtoppm looks in {@code $PDFIUM_PATH}, its + * own directory, then the system library path. + */ + public void setPdfiumLibraryDir(String pdfiumLibraryDir) { + this.pdfiumLibraryDir = pdfiumLibraryDir; + } + + public int getDpi() { + return dpi; + } + + /** + * Rendering resolution in DPI. Defaults to 300. Ignored when + * {@link #setMaxScaleTo(int)} is set. + */ + public void setDpi(int dpi) { + if (dpi < 1) { + throw new IllegalArgumentException("dpi must be at least 1, got: " + dpi); + } + this.dpi = dpi; + } + + public boolean isPng() { + return png; + } + + /** + * If true (the default), pages are written as PNG. If false, as binary PPM (PGM with + * {@link #setGray(boolean)}), which skips PNG encode/decode; the images then carry no DPI, + * so set the OCR engine's resolution explicitly. + */ + public void setPng(boolean png) { + this.png = png; + } + + public boolean isGray() { + return gray; + } + + /** + * If true (the default), render in grayscale. + */ + public void setGray(boolean gray) { + this.gray = gray; + } + + public int getTimeoutMillis() { + return timeoutMillis; + } + + /** + * Timeout in milliseconds for the pdfiumtoppm process. Defaults to + * 120000 (2 minutes). pdfiumtoppm bounds memory but not time; this is + * the only time limit. + */ + public void setTimeoutMillis(int timeoutMillis) { + this.timeoutMillis = timeoutMillis; + } + + public int getMaxScaleTo() { + return maxScaleTo; + } + + /** + * Scale every page's longest edge to exactly this many pixels + * ({@code -scale-to}); enlarges small pages and overrides {@code dpi}. + * {@code -1} (the default) disables it. Prefer {@link #setMaxPixels(long)} + * for a cap that never enlarges. + */ + public void setMaxScaleTo(int maxScaleTo) { + if (maxScaleTo < 1 && maxScaleTo != -1) { + throw new IllegalArgumentException( + "maxScaleTo must be -1 (disabled) or at least 1, got: " + + maxScaleTo); + } + this.maxScaleTo = maxScaleTo; + } + + public long getMaxPixels() { + return maxPixels; + } + + /** + * Maximum width × height of a rendered page ({@code -max-pixels}); + * larger pages are downscaled to fit. Default 16,777,216. {@code -1} + * disables it (not recommended). + */ + public void setMaxPixels(long maxPixels) { + if (maxPixels < 1 && maxPixels != -1) { + throw new IllegalArgumentException( + "maxPixels must be -1 (disabled) or at least 1, got: " + maxPixels); + } + this.maxPixels = maxPixels; + } + + public int getMaxMemoryMb() { + return maxMemoryMb; + } + + /** + * Address-space limit in MiB for the render process ({@code -max-memory}). + * {@code -1} (the default) keeps pdfiumtoppm's own default of 4096 MiB or + * half of RAM, whichever is lower; {@code 0} disables the limit. When the + * limit is hit the render fails with a {@link TikaMemoryLimitException}. + */ + public void setMaxMemoryMb(int maxMemoryMb) { + if (maxMemoryMb < -1) { + throw new IllegalArgumentException( + "maxMemoryMb must be -1 (tool default), 0 (no limit) or positive, got: " + + maxMemoryMb); + } + this.maxMemoryMb = maxMemoryMb; + } +} diff --git a/tika-parsers/tika-parsers-standard/tika-parsers-standard-modules/tika-parser-pdf-module/src/test/java/org/apache/tika/renderer/pdf/pdfiumtoppm/PdfiumToPpmRendererTest.java b/tika-parsers/tika-parsers-standard/tika-parsers-standard-modules/tika-parser-pdf-module/src/test/java/org/apache/tika/renderer/pdf/pdfiumtoppm/PdfiumToPpmRendererTest.java new file mode 100644 index 0000000000..398a425626 --- /dev/null +++ b/tika-parsers/tika-parsers-standard/tika-parsers-standard-modules/tika-parser-pdf-module/src/test/java/org/apache/tika/renderer/pdf/pdfiumtoppm/PdfiumToPpmRendererTest.java @@ -0,0 +1,221 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.tika.renderer.pdf.pdfiumtoppm; + +import static org.junit.jupiter.api.Assertions.assertArrayEquals; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.junit.jupiter.api.Assumptions.assumeTrue; + +import java.io.InputStream; +import java.nio.file.Path; +import java.util.HashSet; +import java.util.List; +import java.util.Set; + +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Test; + +import org.apache.tika.exception.TikaMemoryLimitException; +import org.apache.tika.io.TikaInputStream; +import org.apache.tika.metadata.Metadata; +import org.apache.tika.metadata.TikaCoreProperties; +import org.apache.tika.metadata.TikaPagedText; +import org.apache.tika.parser.ParseContext; +import org.apache.tika.renderer.PageBasedRenderResults; +import org.apache.tika.renderer.PageRangeRequest; +import org.apache.tika.renderer.RenderResult; +import org.apache.tika.utils.ProcessUtils; + +public class PdfiumToPpmRendererTest { + + private static boolean hasPdfiumToPpm; + + @BeforeAll + static void checkPdfiumToPpm() { + hasPdfiumToPpm = ProcessUtils.checkCommand(new String[]{"pdfiumtoppm", "-v"}); + } + + @Test + void testCommandLineDefaults() { + PdfiumToPpmRenderer renderer = new PdfiumToPpmRenderer(); + String[] args = renderer.createCommandLine(Path.of("/in/a.pdf"), Path.of("/out"), + PageRangeRequest.RENDER_ALL); + assertArrayEquals(new String[]{"pdfiumtoppm", "-png", "-r", "300", + "-max-pixels", "16777216", "-gray", "/in/a.pdf", "/out/tika-pdfium"}, args); + } + + @Test + void testCommandLineAllOptions() { + PdfiumToPpmRenderer renderer = new PdfiumToPpmRenderer(); + renderer.setPdfiumToPpmPath("/opt/pdfiumtoppm/pdfiumtoppm"); + renderer.setPdfiumLibraryDir("/opt/pdfiumtoppm"); + renderer.setDpi(72); + renderer.setGray(false); + renderer.setMaxScaleTo(2048); + renderer.setMaxPixels(-1); + renderer.setMaxMemoryMb(1024); + String[] args = renderer.createCommandLine(Path.of("/in/a.pdf"), Path.of("/out"), + new PageRangeRequest(2, 3)); + assertArrayEquals(new String[]{"/opt/pdfiumtoppm/pdfiumtoppm", "-pdfium", + "/opt/pdfiumtoppm", "-png", "-r", "72", "-scale-to", "2048", "-max-memory", + "1024", "-f", "2", "-l", "3", "/in/a.pdf", "/out/tika-pdfium"}, args); + } + + @Test + void testPpmOutput() throws Exception { + PdfiumToPpmRenderer renderer = new PdfiumToPpmRenderer(); + renderer.setPng(false); + List<String> args = List.of(renderer.createCommandLine(Path.of("/in/a.pdf"), + Path.of("/out"), PageRangeRequest.RENDER_ALL)); + assertTrue(!args.contains("-png"), args.toString()); + + assumeTrue(hasPdfiumToPpm, "pdfiumtoppm not available"); + try (InputStream is = getClass().getResourceAsStream("/test-documents/testPDF_bookmarks.pdf"); + TikaInputStream tis = TikaInputStream.get(is)) { + PageBasedRenderResults results = (PageBasedRenderResults) renderer.render( + tis, new Metadata(), new ParseContext(), new PageRangeRequest(1, 1)); + assertEquals(1, results.getResults().size()); + try (TikaInputStream img = results.getResults().get(0).getInputStream()) { + byte[] head = img.readNBytes(2); + assertEquals("P5", new String(head, java.nio.charset.StandardCharsets.US_ASCII), "gray -> PGM"); + } + results.close(); + } + } + + @Test + void testMemoryZeroDisablesLimit() { + PdfiumToPpmRenderer renderer = new PdfiumToPpmRenderer(); + renderer.setMaxMemoryMb(0); + List<String> args = List.of(renderer.createCommandLine(Path.of("/in/a.pdf"), + Path.of("/out"), PageRangeRequest.RENDER_ALL)); + assertEquals("0", args.get(args.indexOf("-max-memory") + 1)); + } + + @Test + void testSetterValidation() { + PdfiumToPpmRenderer renderer = new PdfiumToPpmRenderer(); + assertThrows(IllegalArgumentException.class, () -> renderer.setDpi(0)); + assertThrows(IllegalArgumentException.class, () -> renderer.setMaxScaleTo(0)); + assertThrows(IllegalArgumentException.class, () -> renderer.setMaxPixels(0)); + assertThrows(IllegalArgumentException.class, () -> renderer.setMaxMemoryMb(-2)); + } + + @Test + void testRenderAllPages() throws Exception { + assumeTrue(hasPdfiumToPpm, "pdfiumtoppm not available"); + + PdfiumToPpmRenderer renderer = new PdfiumToPpmRenderer(); + + try (InputStream is = getClass().getResourceAsStream( + "/test-documents/testPDF_bookmarks.pdf")) { + assertNotNull(is, "test PDF not found"); + + try (TikaInputStream tis = TikaInputStream.get(is)) { + PageBasedRenderResults results = + (PageBasedRenderResults) renderer.render( + tis, new Metadata(), new ParseContext(), + PageRangeRequest.RENDER_ALL); + + List<RenderResult> allResults = results.getResults(); + assertEquals(2, allResults.size(), + "testPDF_bookmarks.pdf has 2 pages"); + + Set<Integer> pageNumbers = new HashSet<>(); + for (RenderResult rr : allResults) { + assertEquals(RenderResult.STATUS.SUCCESS, rr.getStatus()); + + Metadata rm = rr.getMetadata(); + Integer page = rm.getInt(TikaPagedText.PAGE_NUMBER); + assertNotNull(page, "page number should be set"); + pageNumbers.add(page); + + assertEquals( + TikaCoreProperties.EmbeddedResourceType.RENDERING + .name(), + rm.get(TikaCoreProperties.EMBEDDED_RESOURCE_TYPE)); + + try (TikaInputStream imageTis = rr.getInputStream()) { + byte[] imageBytes = imageTis.readAllBytes(); + assertTrue(imageBytes.length > 100, + "rendered page should be a non-trivial PNG"); + assertEquals((byte) 0x89, imageBytes[0]); + assertEquals((byte) 'P', imageBytes[1]); + assertEquals((byte) 'N', imageBytes[2]); + assertEquals((byte) 'G', imageBytes[3]); + } + } + + assertEquals(Set.of(1, 2), pageNumbers, + "should have pages 1 and 2"); + + results.close(); + } + } + } + + @Test + void testRenderPageRange() throws Exception { + assumeTrue(hasPdfiumToPpm, "pdfiumtoppm not available"); + + PdfiumToPpmRenderer renderer = new PdfiumToPpmRenderer(); + + try (InputStream is = getClass().getResourceAsStream( + "/test-documents/testPDF_bookmarks.pdf")) { + assertNotNull(is, "test PDF not found"); + + try (TikaInputStream tis = TikaInputStream.get(is)) { + PageBasedRenderResults results = + (PageBasedRenderResults) renderer.render( + tis, new Metadata(), new ParseContext(), + new PageRangeRequest(2, 2)); + + List<RenderResult> allResults = results.getResults(); + assertEquals(1, allResults.size(), + "should render exactly 1 page"); + + assertEquals(2, + allResults.get(0).getMetadata() + .getInt(TikaPagedText.PAGE_NUMBER)); + + results.close(); + } + } + } + + @Test + void testMemoryLimitHit() throws Exception { + assumeTrue(hasPdfiumToPpm, "pdfiumtoppm not available"); + + PdfiumToPpmRenderer renderer = new PdfiumToPpmRenderer(); + // 300 dpi letter needs ~8 MB of bitmap on top of the ~64 MB baseline + renderer.setMaxMemoryMb(32); + + try (InputStream is = getClass().getResourceAsStream( + "/test-documents/testPDF_bookmarks.pdf")) { + assertNotNull(is, "test PDF not found"); + try (TikaInputStream tis = TikaInputStream.get(is)) { + assertThrows(TikaMemoryLimitException.class, () -> renderer.render( + tis, new Metadata(), new ParseContext(), + PageRangeRequest.RENDER_ALL)); + } + } + } +}
