This is an automated email from the ASF dual-hosted git repository.
tballison pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/tika.git
The following commit(s) were added to refs/heads/main by this push:
new a584c4e052 TIKA-4898: scan for startxref in blocks (#3188)
a584c4e052 is described below
commit a584c4e0525fffd7ed595734f949349f5aa89eca
Author: Tim Allison <[email protected]>
AuthorDate: Thu Sep 17 13:20:54 2026 -0400
TIKA-4898: scan for startxref in blocks (#3188)
---
CHANGES.txt | 3 +
.../tika/parser/pdf/updates/StartXRefScanner.java | 109 ++++++++++++++-------
.../tika/parser/pdf/PDFIncrementalUpdatesTest.java | 27 +++++
3 files changed, 105 insertions(+), 34 deletions(-)
diff --git a/CHANGES.txt b/CHANGES.txt
index 05fe851477..7a90a8a12f 100644
--- a/CHANGES.txt
+++ b/CHANGES.txt
@@ -1,5 +1,8 @@
Release 4.1.0 - unreleased
+ * PDF incremental-update scanning reads the file in blocks instead of one
+ byte at a time; same output, about 4x less CPU (TIKA-4898).
+
* Metadata never stores an unpaired UTF-16 surrogate: every write replaces
one with U+FFFD (StringUtils.wellFormed), so no reader meets one; the
Smile encoder on the pipes channel rejected them and the parent counted
diff --git
a/tika-parsers/tika-parsers-standard/tika-parsers-standard-modules/tika-parser-pdf-module/src/main/java/org/apache/tika/parser/pdf/updates/StartXRefScanner.java
b/tika-parsers/tika-parsers-standard/tika-parsers-standard-modules/tika-parser-pdf-module/src/main/java/org/apache/tika/parser/pdf/updates/StartXRefScanner.java
index 51d7506670..816fc96edd 100644
---
a/tika-parsers/tika-parsers-standard/tika-parsers-standard-modules/tika-parser-pdf-module/src/main/java/org/apache/tika/parser/pdf/updates/StartXRefScanner.java
+++
b/tika-parsers/tika-parsers-standard/tika-parsers-standard-modules/tika-parser-pdf-module/src/main/java/org/apache/tika/parser/pdf/updates/StartXRefScanner.java
@@ -44,7 +44,10 @@ public class StartXRefScanner {
static final int MAX_LENGTH_LONG = Long.toString(Long.MAX_VALUE).length();
- private static final char[] STARTXREF = new char[]{'s', 't', 'a', 'r',
't', 'x', 'r', 'e', 'f'};
+ private static final byte[] STARTXREF =
+ new byte[]{'s', 't', 'a', 'r', 't', 'x', 'r', 'e', 'f'};
+
+ private static final int BLOCK_SIZE = 1 << 16;
private static final char[] EOF_MARKER = new char[]{'%', '%', 'E', 'O',
'F'};
@@ -65,19 +68,46 @@ public class StartXRefScanner {
}
public List<StartXRefOffset> scan() throws IOException {
+ long length = source.length();
+ if (length >= Integer.MAX_VALUE) {
+ throw new IOException("can't read more than " + Integer.MAX_VALUE
+ " bytes");
+ }
List<StartXRefOffset> offsets = new ArrayList<>();
+ byte[] block = new byte[BLOCK_SIZE];
+ long blockStart = source.getPosition();
try {
- int b = source.read();
- while (b > -1) {
- if (b == STARTXREF[0]) {
- tryStartXRef(offsets);
+ blocks:
+ while (blockStart < length) {
+ source.seek(blockStart);
+ int read = fill(block);
+ if (read <= 0) {
+ break;
+ }
+ //a candidate that runs off the end of a block is found by the
next block, which
+ //starts STARTXREF.length - 1 bytes before this one ends
+ int limit = blockStart + read >= length ? read : read -
STARTXREF.length + 1;
+ int i = 0;
+ while (i < limit) {
+ while (i < limit && block[i] != STARTXREF[0]) {
+ i++;
+ }
+ if (i >= limit || !startsWithStartXRef(block, i, read)) {
+ i++;
+ continue;
+ }
+ source.seek(blockStart + i + STARTXREF.length);
+ readStartXRef(offsets, blockStart + i);
+ //the number and the %%EOF marker have been consumed;
resume after them
+ long resume = Math.max(source.getPosition(), blockStart +
i + STARTXREF.length);
+ if (resume >= blockStart + limit) {
+ blockStart = resume;
+ continue blocks;
+ }
+ i = (int) (resume - blockStart);
}
- b = source.read();
+ blockStart += limit;
}
} finally {
- if (source.getPosition() >= Integer.MAX_VALUE) {
- throw new IOException("read more than " + Integer.MAX_VALUE +
" bytes");
- }
//TODO: if we're opening a new file for the source
//we shouldn't bother with this.
source.rewind((int) source.getPosition());
@@ -85,32 +115,43 @@ public class StartXRefScanner {
return offsets;
}
- private void tryStartXRef(List<StartXRefOffset> offsets) throws
IOException {
- int match = 1;
- int read = 0;
- int b = source.read();
- while (b > -1) {
- if (b == STARTXREF[match]) {
- ++match;
- if (match == STARTXREF.length) {
- try {
- long startXREFOffset = source.getPosition() -
STARTXREF.length;
- long startxref = readLong();
- boolean hasEof = readEOF();
- long endOfEOFOffset = source.getPosition();
- offsets.add(new StartXRefOffset(startxref,
startXREFOffset, endOfEOFOffset,
- hasEof));
- return;
- } catch (IOException e) {
- //swallow
- return;
- }
- }
- } else {
- source.rewind(1);
- return;
+ /**
+ * A single read stops at the source's internal page boundary; keep going
until the block
+ * is full or the source is exhausted.
+ */
+ private int fill(byte[] block) throws IOException {
+ int off = 0;
+ while (off < block.length) {
+ int read = source.read(block, off, block.length - off);
+ if (read <= 0) {
+ break;
}
- b = source.read();
+ off += read;
+ }
+ return off;
+ }
+
+ private boolean startsWithStartXRef(byte[] block, int i, int read) {
+ if (i + STARTXREF.length > read) {
+ return false;
+ }
+ for (int j = 1; j < STARTXREF.length; j++) {
+ if (block[i + j] != STARTXREF[j]) {
+ return false;
+ }
+ }
+ return true;
+ }
+
+ private void readStartXRef(List<StartXRefOffset> offsets, long
startXREFOffset)
+ throws IOException {
+ try {
+ long startxref = readLong();
+ boolean hasEof = readEOF();
+ offsets.add(new StartXRefOffset(startxref, startXREFOffset,
source.getPosition(),
+ hasEof));
+ } catch (IOException e) {
+ //swallow
}
}
diff --git
a/tika-parsers/tika-parsers-standard/tika-parsers-standard-modules/tika-parser-pdf-module/src/test/java/org/apache/tika/parser/pdf/PDFIncrementalUpdatesTest.java
b/tika-parsers/tika-parsers-standard/tika-parsers-standard-modules/tika-parser-pdf-module/src/test/java/org/apache/tika/parser/pdf/PDFIncrementalUpdatesTest.java
index f0f70231a6..2cd7968e8b 100644
---
a/tika-parsers/tika-parsers-standard/tika-parsers-standard-modules/tika-parser-pdf-module/src/test/java/org/apache/tika/parser/pdf/PDFIncrementalUpdatesTest.java
+++
b/tika-parsers/tika-parsers-standard/tika-parsers-standard-modules/tika-parser-pdf-module/src/test/java/org/apache/tika/parser/pdf/PDFIncrementalUpdatesTest.java
@@ -19,6 +19,7 @@ package org.apache.tika.parser.pdf;
import static org.junit.jupiter.api.Assertions.assertArrayEquals;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertNull;
+import static org.junit.jupiter.api.Assertions.assertTrue;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
@@ -128,6 +129,32 @@ public class PDFIncrementalUpdatesTest extends TikaTest {
assertEquals(1, getOffsets(s).size());
}
+ @Test
+ public void testStartXRefAcrossBlockBoundary() throws Exception {
+ //the scanner reads in blocks; a startxref that straddles a block
boundary must still
+ //be found, at its true offset
+ int blockSize = 1 << 16;
+ for (int pad = blockSize - 20; pad < blockSize + 20; pad++) {
+ String s = "x".repeat(pad) + "startxref 123456\n%%EOF\n";
+ List<StartXRefOffset> offsets = getOffsets(s);
+ assertEquals(1, offsets.size(), "pad " + pad);
+ assertEquals(pad, offsets.get(0).getStartXrefOffset(), "pad " +
pad);
+ assertEquals(123456, offsets.get(0).getStartxref(), "pad " + pad);
+ assertTrue(offsets.get(0).isHasEof(), "pad " + pad);
+ }
+ }
+
+ @Test
+ public void testMultipleStartXRefsAcrossBlockBoundary() throws Exception {
+ int blockSize = 1 << 16;
+ String update = "startxref 123456\n%%EOF\n";
+ StringBuilder sb = new StringBuilder();
+ for (int i = 0; i < 6; i++) {
+ sb.append("x".repeat(blockSize / 4)).append(update);
+ }
+ assertEquals(6, getOffsets(sb.toString()).size());
+ }
+
private List<StartXRefOffset> getOffsets(String s) throws IOException {
try (RandomAccessRead randomAccessRead =
new
RandomAccessReadBuffer(s.getBytes(StandardCharsets.US_ASCII))) {