diff --git a/fontbox/src/main/java/org/apache/fontbox/ttf/GlyphTable.java b/fontbox/src/main/java/org/apache/fontbox/ttf/GlyphTable.java index 66b3533375f..cbb058f6931 100644 --- a/fontbox/src/main/java/org/apache/fontbox/ttf/GlyphTable.java +++ b/fontbox/src/main/java/org/apache/fontbox/ttf/GlyphTable.java @@ -18,8 +18,6 @@ import java.io.IOException; -import org.apache.pdfbox.io.RandomAccessReadBuffer; - /** * This 'glyf'-table is a required table in a TrueType font. * @@ -78,12 +76,9 @@ void read(TrueTypeFont ttf, TTFDataStream data) throws IOException } // we don't actually read the complete table here because it can contain tens of thousands of glyphs - // cache the relevant part of the font data so that the data stream can be closed if it is no longer needed - byte[] dataBytes = data.read((int) getLength()); - try (RandomAccessReadBuffer read = new RandomAccessReadBuffer(dataBytes)) - { - this.data = new RandomAccessReadDataStream(read); - } + // keep the relevant part of the font data so that the data stream can be closed if it is + // no longer needed + this.data = data.createSubStream((int) getLength()); // PDFBOX-5460: read hmtx table early to avoid deadlock if getGlyph() locks "data" // and then locks TrueTypeFont to read this table, while another thread diff --git a/fontbox/src/main/java/org/apache/fontbox/ttf/OTFParser.java b/fontbox/src/main/java/org/apache/fontbox/ttf/OTFParser.java index 3ac562d03b2..7f4055d9172 100644 --- a/fontbox/src/main/java/org/apache/fontbox/ttf/OTFParser.java +++ b/fontbox/src/main/java/org/apache/fontbox/ttf/OTFParser.java @@ -18,6 +18,7 @@ package org.apache.fontbox.ttf; import java.io.IOException; +import java.nio.ByteBuffer; import org.apache.pdfbox.io.RandomAccessRead; @@ -50,6 +51,12 @@ public OpenTypeFont parse(RandomAccessRead randomAccessRead) throws IOException return (OpenTypeFont) super.parse(randomAccessRead); } + @Override + public OpenTypeFont parse(ByteBuffer buffer) throws IOException + { + return (OpenTypeFont) super.parse(buffer); + } + @Override OpenTypeFont parse(TTFDataStream raf) throws IOException { diff --git a/fontbox/src/main/java/org/apache/fontbox/ttf/RandomAccessReadDataStream.java b/fontbox/src/main/java/org/apache/fontbox/ttf/RandomAccessReadDataStream.java index 67fc2b15dc6..edfef4261b4 100644 --- a/fontbox/src/main/java/org/apache/fontbox/ttf/RandomAccessReadDataStream.java +++ b/fontbox/src/main/java/org/apache/fontbox/ttf/RandomAccessReadDataStream.java @@ -16,28 +16,22 @@ */ package org.apache.fontbox.ttf; -import java.io.ByteArrayInputStream; import java.io.IOException; import java.io.InputStream; +import java.nio.ByteBuffer; -import org.apache.logging.log4j.LogManager; -import org.apache.logging.log4j.Logger; - +import org.apache.pdfbox.io.RandomAccessInputStream; import org.apache.pdfbox.io.RandomAccessRead; import org.apache.pdfbox.io.RandomAccessReadBuffer; /** - * An implementation of the TTFDataStream using RandomAccessRead as source. - * The underlying RandomAccessRead can be any length, but this implementation supports - * only bufferLengths upto Integer.MAX_VALUE. + * An implementation of the TTFDataStream that holds the font data in memory. + * The data can be any length up to Integer.MAX_VALUE. */ class RandomAccessReadDataStream extends TTFDataStream { - private static final Logger LOG = LogManager.getLogger(RandomAccessReadDataStream.class); - private final long length; - private final byte[] data; - private int currentPosition = 0; + private final ByteBuffer data; /** * Constructor. @@ -54,14 +48,15 @@ class RandomAccessReadDataStream extends TTFDataStream // PDFBOX-5991 throw new IOException("Stream is too long, size: " + length); } - data = new byte[(int) length]; - int remainingBytes = data.length; + byte[] bytes = new byte[(int) length]; + int remainingBytes = bytes.length; int amountRead; - while ((amountRead = randomAccessRead.read(data, data.length - remainingBytes, + while ((amountRead = randomAccessRead.read(bytes, bytes.length - remainingBytes, remainingBytes)) > 0) { remainingBytes -= amountRead; } + data = ByteBuffer.wrap(bytes); } /** @@ -73,8 +68,20 @@ class RandomAccessReadDataStream extends TTFDataStream */ RandomAccessReadDataStream(InputStream inputStream) throws IOException { - data = inputStream.readAllBytes(); - length = data.length; + this(ByteBuffer.wrap(inputStream.readAllBytes())); + } + + /** + * Constructor. The data is not copied. This stream has its own position, so several streams + * may read the same buffer at the same time. + * + * @param buffer the data from its position to its limit. It must not be changed while this + * stream is in use. + */ + RandomAccessReadDataStream(ByteBuffer buffer) + { + data = buffer.slice(); + length = data.limit(); } /** @@ -85,7 +92,7 @@ class RandomAccessReadDataStream extends TTFDataStream @Override public long getCurrentPosition() throws IOException { - return currentPosition; + return data.position(); } /** @@ -107,11 +114,11 @@ public void close() throws IOException @Override public int read() throws IOException { - if (currentPosition >= length) + if (!data.hasRemaining()) { return -1; } - return data[currentPosition++] & 0xff; + return data.get() & 0xff; } /** @@ -143,7 +150,7 @@ private int readInt() throws IOException /** * Seek into the datasource. * When the requested pos is < 0, an IOException() is fired. - * When the requested pos is ≥ {@link #length}, the {@link #currentPosition} + * When the requested pos is ≥ {@link #length}, the position * is set to the first byte after the {@link #data}! * * @param pos The position to seek to. @@ -156,7 +163,7 @@ public void seek(long pos) throws IOException { throw new IOException("Invalid position " + pos); } - currentPosition = pos < length ? (int) pos : (int) length; + data.position(pos < length ? (int) pos : (int) length); } /** @@ -173,31 +180,39 @@ public void seek(long pos) throws IOException @Override public int read(byte[] b, int off, int len) throws IOException { - if (currentPosition >= length) + if (!data.hasRemaining()) { return -1; } - int remainingBytes = (int) (length - currentPosition); - int bytesToRead = Math.min(remainingBytes, len); - System.arraycopy(data, currentPosition, b, off, bytesToRead); - currentPosition += bytesToRead; + int bytesToRead = Math.min(data.remaining(), len); + data.get(b, off, bytesToRead); return bytesToRead; } @Override - @SuppressWarnings("java:S2095") // buffer is intentionally not closed, must remain alive for the lifetime of the view public RandomAccessRead createSubView(long length) { - try - { - RandomAccessReadBuffer buffer = new RandomAccessReadBuffer(data); - return buffer.createView(currentPosition, length); - } - catch (IOException e) + ByteBuffer view = data.slice(); + view.limit((int) Math.min(length, view.limit())); + return new RandomAccessReadBuffer(view); + } + + /** + * {@inheritDoc} + *

+ * This implementation doesn't copy the bytes. + */ + @Override + TTFDataStream createSubStream(int length) throws IOException + { + if (length > data.remaining()) { - LOG.warn("Could not create a SubView", e); - return null; + throw new IOException("Unexpected end of TTF stream reached"); } + ByteBuffer view = data.slice(); + view.limit(length); + data.position(data.position() + length); + return new RandomAccessReadDataStream(view); } /** @@ -206,7 +221,9 @@ public RandomAccessRead createSubView(long length) @Override public InputStream getOriginalData() throws IOException { - return new ByteArrayInputStream(data); + ByteBuffer view = data.duplicate(); + view.rewind(); + return new RandomAccessInputStream(new RandomAccessReadBuffer(view)); } /** diff --git a/fontbox/src/main/java/org/apache/fontbox/ttf/TTCDataStream.java b/fontbox/src/main/java/org/apache/fontbox/ttf/TTCDataStream.java index dff61d8977b..946c3fac34a 100644 --- a/fontbox/src/main/java/org/apache/fontbox/ttf/TTCDataStream.java +++ b/fontbox/src/main/java/org/apache/fontbox/ttf/TTCDataStream.java @@ -89,4 +89,10 @@ public RandomAccessRead createSubView(long length) { return stream.createSubView(length); } + + @Override + TTFDataStream createSubStream(int length) throws IOException + { + return stream.createSubStream(length); + } } diff --git a/fontbox/src/main/java/org/apache/fontbox/ttf/TTFDataStream.java b/fontbox/src/main/java/org/apache/fontbox/ttf/TTFDataStream.java index 92cf95286d7..e4a85f2118a 100644 --- a/fontbox/src/main/java/org/apache/fontbox/ttf/TTFDataStream.java +++ b/fontbox/src/main/java/org/apache/fontbox/ttf/TTFDataStream.java @@ -20,6 +20,7 @@ import java.io.EOFException; import java.io.IOException; import java.io.InputStream; +import java.nio.ByteBuffer; import java.nio.charset.Charset; import java.nio.charset.StandardCharsets; import java.util.Calendar; @@ -291,6 +292,21 @@ public RandomAccessRead createSubView(long length) return null; } + /** + * Creates a stream over the next {@code length} bytes. It has its own position and stays + * readable after this stream is closed. This stream's position moves past those bytes. + *

+ * This implementation copies the bytes. + * + * @param length the number of bytes. + * @return A new stream. + * @throws IOException If there is an error reading the data. + */ + TTFDataStream createSubStream(int length) throws IOException + { + return new RandomAccessReadDataStream(ByteBuffer.wrap(read(length))); + } + /** * Get the current position in the stream. * diff --git a/fontbox/src/main/java/org/apache/fontbox/ttf/TTFParser.java b/fontbox/src/main/java/org/apache/fontbox/ttf/TTFParser.java index 5a8de6d5fff..194573e73a7 100644 --- a/fontbox/src/main/java/org/apache/fontbox/ttf/TTFParser.java +++ b/fontbox/src/main/java/org/apache/fontbox/ttf/TTFParser.java @@ -19,6 +19,7 @@ import java.io.Closeable; import java.io.IOException; import java.io.InputStream; +import java.nio.ByteBuffer; import org.apache.logging.log4j.Logger; import org.apache.logging.log4j.LogManager; @@ -66,6 +67,21 @@ public TrueTypeFont parse(RandomAccessRead randomAccessRead) throws IOException return parse(randomAccessRead, new RandomAccessReadDataStream(randomAccessRead)); } + /** + * Parse a buffer and return a TrueType font. The buffer is not copied, so many fonts can be + * parsed from one buffer, e.g. a font file read once or mapped into memory. Each font reads + * the buffer with its own position, so they can be used in different threads. + * + * @param buffer The font data from its position to its limit. It must not be changed while + * the font is in use. Its position is not changed. + * @return A TrueType font. + * @throws IOException If there is an error parsing the TrueType font. + */ + public TrueTypeFont parse(ByteBuffer buffer) throws IOException + { + return parse(new RandomAccessReadDataStream(buffer)); + } + /** * Parse an input stream and return a TrueType font that is to be embedded. * diff --git a/fontbox/src/test/java/org/apache/fontbox/ttf/RandomAccessReadBufferDataStreamTest.java b/fontbox/src/test/java/org/apache/fontbox/ttf/RandomAccessReadBufferDataStreamTest.java index 5d7a7d5ea74..8c74e53c7dc 100644 --- a/fontbox/src/test/java/org/apache/fontbox/ttf/RandomAccessReadBufferDataStreamTest.java +++ b/fontbox/src/test/java/org/apache/fontbox/ttf/RandomAccessReadBufferDataStreamTest.java @@ -236,4 +236,29 @@ void testReadBuffer() throws IOException Files.delete(path); } + /** + * A sub stream reads its part of the data with its own position, and stays readable after the + * parent is closed. + * + * @throws IOException If something went wrong + */ + @Test + void testCreateSubStream() throws IOException + { + byte[] byteArray = "0123456789".getBytes(StandardCharsets.US_ASCII); + TTFDataStream subStream; + try (RandomAccessReadDataStream dataStream = new RandomAccessReadDataStream( + new RandomAccessReadBuffer(byteArray))) + { + dataStream.seek(2); + subStream = dataStream.createSubStream(5); + assertEquals(7, dataStream.getCurrentPosition()); + assertThrows(IOException.class, () -> dataStream.createSubStream(4)); + } + assertEquals(5, subStream.getOriginalDataSize()); + assertEquals("23456", subStream.readString(5, StandardCharsets.US_ASCII)); + assertEquals(-1, subStream.read()); + subStream.seek(1); + assertEquals('3', subStream.read()); + } } diff --git a/fontbox/src/test/java/org/apache/fontbox/ttf/TestTTFParser.java b/fontbox/src/test/java/org/apache/fontbox/ttf/TestTTFParser.java index f3f0fef998a..45f41529180 100644 --- a/fontbox/src/test/java/org/apache/fontbox/ttf/TestTTFParser.java +++ b/fontbox/src/test/java/org/apache/fontbox/ttf/TestTTFParser.java @@ -15,6 +15,7 @@ */ package org.apache.fontbox.ttf; +import static org.junit.jupiter.api.Assertions.assertArrayEquals; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertFalse; import static org.junit.jupiter.api.Assertions.assertNotNull; @@ -24,6 +25,8 @@ import java.io.File; import java.io.IOException; import java.io.InputStream; +import java.nio.ByteBuffer; +import java.nio.file.Files; import java.util.Calendar; import java.util.TimeZone; @@ -201,4 +204,36 @@ void testParseHeaders() throws IOException assertEquals(ttf.getOS2Windows().getSuperscriptYSize(),headers.getOS2Windows().getSuperscriptYSize()); } + /** + * Parse from a ByteBuffer: the buffer isn't changed, so it can be parsed again, and the font + * reads the same as one parsed from a RandomAccessRead. + * + * @throws IOException If something went wrong + */ + @Test + void testParseByteBuffer() throws IOException + { + final File testFile = new File("src/test/resources/ttf/LiberationSans-Regular.ttf"); + byte[] fontBytes = Files.readAllBytes(testFile.toPath()); + // the font starts after a prefix, so the buffer must be read from its position + ByteBuffer buffer = ByteBuffer.allocate(fontBytes.length + 3); + buffer.position(3); + buffer.put(fontBytes); + buffer.position(3); + buffer = buffer.asReadOnlyBuffer(); + + TrueTypeFont expected = new TTFParser().parse(new RandomAccessReadBufferedFile(testFile)); + TrueTypeFont ttf1 = new TTFParser().parse(buffer); + TrueTypeFont ttf2 = new TTFParser().parse(buffer); + assertEquals(3, buffer.position()); + assertEquals(expected.getName(), ttf2.getName()); + assertEquals(expected.getNumberOfGlyphs(), ttf2.getNumberOfGlyphs()); + assertEquals(expected.getPath("A").getBounds2D(), ttf1.getPath("A").getBounds2D()); + assertEquals(expected.getPath("Aacute").getBounds2D(), ttf2.getPath("Aacute").getBounds2D()); + assertEquals(fontBytes.length, ttf1.getOriginalDataSize()); + try (InputStream is = ttf1.getOriginalData()) + { + assertArrayEquals(fontBytes, is.readAllBytes()); + } + } }