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()); + } + } }