Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 3 additions & 8 deletions fontbox/src/main/java/org/apache/fontbox/ttf/GlyphTable.java
Original file line number Diff line number Diff line change
Expand Up @@ -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.
*
Expand Down Expand Up @@ -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
Expand Down
7 changes: 7 additions & 0 deletions fontbox/src/main/java/org/apache/fontbox/ttf/OTFParser.java
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@
package org.apache.fontbox.ttf;

import java.io.IOException;
import java.nio.ByteBuffer;

import org.apache.pdfbox.io.RandomAccessRead;

Expand Down Expand Up @@ -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
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand All @@ -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);
}

/**
Expand All @@ -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();
}

/**
Expand All @@ -85,7 +92,7 @@ class RandomAccessReadDataStream extends TTFDataStream
@Override
public long getCurrentPosition() throws IOException
{
return currentPosition;
return data.position();
}

/**
Expand All @@ -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;
}

/**
Expand Down Expand Up @@ -143,7 +150,7 @@ private int readInt() throws IOException
/**
* Seek into the datasource.
* When the requested <code>pos</code> is &lt; 0, an IOException() is fired.
* When the requested <code>pos</code> is &ge; {@link #length}, the {@link #currentPosition}
* When the requested <code>pos</code> is &ge; {@link #length}, the position
* is set to the first byte <i>after</i> the {@link #data}!
*
* @param pos The position to seek to.
Expand All @@ -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);
}

/**
Expand All @@ -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}
* <p>
* 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);
}

/**
Expand All @@ -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));
}

/**
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -89,4 +89,10 @@ public RandomAccessRead createSubView(long length)
{
return stream.createSubView(length);
}

@Override
TTFDataStream createSubStream(int length) throws IOException
{
return stream.createSubStream(length);
}
}
16 changes: 16 additions & 0 deletions fontbox/src/main/java/org/apache/fontbox/ttf/TTFDataStream.java
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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.
* <p>
* 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.
*
Expand Down
16 changes: 16 additions & 0 deletions fontbox/src/main/java/org/apache/fontbox/ttf/TTFParser.java
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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.
*
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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());
}
}
35 changes: 35 additions & 0 deletions fontbox/src/test/java/org/apache/fontbox/ttf/TestTTFParser.java
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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;

Expand Down Expand Up @@ -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());
}
}
}