PdfFontUnitTest.java
/*
This file is part of the iText (R) project.
Copyright (c) 1998-2025 Apryse Group NV
Authors: Apryse Software.
This program is offered under a commercial and under the AGPL license.
For commercial licensing, contact us at https://itextpdf.com/sales. For AGPL licensing, see below.
AGPL licensing:
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU Affero General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU Affero General Public License for more details.
You should have received a copy of the GNU Affero General Public License
along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package com.itextpdf.kernel.font;
import com.itextpdf.io.font.FontMetrics;
import com.itextpdf.io.font.FontProgram;
import com.itextpdf.io.font.otf.Glyph;
import com.itextpdf.io.font.otf.GlyphLine;
import com.itextpdf.kernel.exceptions.KernelExceptionMessageConstant;
import com.itextpdf.kernel.exceptions.PdfException;
import com.itextpdf.kernel.pdf.PdfDictionary;
import com.itextpdf.kernel.pdf.PdfDocument;
import com.itextpdf.kernel.pdf.PdfName;
import com.itextpdf.kernel.pdf.PdfOutputStream;
import com.itextpdf.kernel.pdf.PdfStream;
import com.itextpdf.kernel.pdf.PdfString;
import com.itextpdf.kernel.pdf.PdfWriter;
import com.itextpdf.test.ExtendedITextTest;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.util.List;
import java.util.regex.Pattern;
import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.Tag;
@Tag("UnitTest")
public class PdfFontUnitTest extends ExtendedITextTest {
public static final int FONT_METRICS_DESCENT = -40;
public static final int FONT_METRICS_ASCENT = 700;
public static final int FONT_SIZE = 50;
public static class TestFont extends PdfFont {
public static final int SIMPLE_GLYPH = 97;
public static final int SIMPLE_GLYPH_WITHOUT_BBOX = 98;
public static final int SIMPLE_GLYPH_WITH_POSITIVE_DESCENT = 99;
public static final int COMPLEX_GLYPH = 119070;
public static final int ZERO_CODE_GLYPH = 0;
// these are two parts of G-clef glyph
public static final char[] COMPLEX_GLYPH_AS_CHARS = new char[]{'\ud834', '\udd1e'};
public static final int SIMPLE_GLYPH_WIDTH = 100;
public static final int COMPLEX_GLYPH_WIDTH = 200;
public TestFont() {
super();
}
public TestFont(PdfDictionary dictionary) {
super(dictionary);
}
public void setFontProgram(FontProgram fontProgram) {
this.fontProgram = fontProgram;
}
@Override
public Glyph getGlyph(int unicode) {
if (unicode == SIMPLE_GLYPH) {
return new Glyph(1, SIMPLE_GLYPH_WIDTH, SIMPLE_GLYPH, new int[]{10, -20, 200, 600});
} else if (unicode == SIMPLE_GLYPH_WITHOUT_BBOX) {
return new Glyph(2, SIMPLE_GLYPH_WIDTH, SIMPLE_GLYPH);
} else if (unicode == SIMPLE_GLYPH_WITH_POSITIVE_DESCENT) {
return new Glyph(3, SIMPLE_GLYPH_WIDTH, SIMPLE_GLYPH, new int[]{10, 10, 200, 600});
} else if (unicode == COMPLEX_GLYPH) {
return new Glyph(4, COMPLEX_GLYPH_WIDTH, COMPLEX_GLYPH, new int[]{20, -100, 400, 800});
} else if (unicode == ZERO_CODE_GLYPH) {
return new Glyph(0, 0, 0);
}
return null;
}
@Override
public GlyphLine createGlyphLine(String content) {
return null;
}
@Override
public int appendGlyphs(String text, int from, int to, List<Glyph> glyphs) {
return 0;
}
@Override
public int appendAnyGlyph(String text, int from, List<Glyph> glyphs) {
return 0;
}
@Override
public byte[] convertToBytes(String text) {
return new byte[0];
}
@Override
public byte[] convertToBytes(GlyphLine glyphLine) {
return new byte[0];
}
@Override
public String decode(PdfString content) {
return null;
}
@Override
public GlyphLine decodeIntoGlyphLine(PdfString content) {
return null;
}
@Override
public float getContentWidth(PdfString content) {
return 0;
}
@Override
public byte[] convertToBytes(Glyph glyph) {
return new byte[0];
}
@Override
public void writeText(GlyphLine text, int from, int to, PdfOutputStream stream) {
}
@Override
public void writeText(String text, PdfOutputStream stream) {
}
@Override
protected PdfDictionary getFontDescriptor(String fontName) {
return null;
}
}
public static class TestFontProgram extends FontProgram {
@Override
public int getPdfFontFlags() {
return 0;
}
@Override
public int getKerning(Glyph first, Glyph second) {
return 0;
}
@Override
public FontMetrics getFontMetrics() {
return new TestFontMetrics();
}
@Override
public boolean isFontSpecific() {
return true;
}
}
public static class TestFontMetrics extends FontMetrics {
public TestFontMetrics() {
setTypoDescender(FONT_METRICS_DESCENT);
setTypoAscender(FONT_METRICS_ASCENT);
}
}
@Test
public void constructorWithoutParamsTest() {
TestFont font = new TestFont();
Assertions.assertEquals(PdfName.Font, font.getPdfObject().get(PdfName.Type));
}
@Test
public void constructorWithDictionaryTest() {
PdfDictionary dictionary = new PdfDictionary();
dictionary.put(PdfName.A, PdfName.B);
TestFont font = new TestFont(dictionary);
Assertions.assertEquals(PdfName.Font, font.getPdfObject().get(PdfName.Type));
Assertions.assertEquals(PdfName.B, font.getPdfObject().get(PdfName.A));
}
@Test
public void containsGlyphTest() {
TestFont font = new TestFont();
Assertions.assertTrue(font.containsGlyph(TestFont.SIMPLE_GLYPH));
Assertions.assertFalse(font.containsGlyph(111));
}
@Test
public void zeroGlyphIsAllowedOnlyIfFontIsSymbolicTest() {
TestFont font = new TestFont();
Assertions.assertFalse(font.containsGlyph(TestFont.ZERO_CODE_GLYPH));
font.setFontProgram(new TestFontProgram());
Assertions.assertTrue(font.containsGlyph(TestFont.ZERO_CODE_GLYPH));
}
@Test
public void getWidthUnicodeTest() {
TestFont font = new TestFont();
Assertions.assertEquals(TestFont.SIMPLE_GLYPH_WIDTH, font.getWidth(TestFont.SIMPLE_GLYPH));
Assertions.assertEquals(0, font.getWidth(111));
}
@Test
public void getWidthFontSizeTest() {
TestFont font = new TestFont();
double expectedValue = TestFont.SIMPLE_GLYPH_WIDTH * FONT_SIZE / (double) FontProgram.UNITS_NORMALIZATION;
Assertions.assertEquals(expectedValue, font.getWidth(TestFont.SIMPLE_GLYPH, FONT_SIZE), 0.1);
Assertions.assertEquals(0, font.getWidth(111));
}
@Test
public void getWidthOfStringTest() {
TestFont font = new TestFont();
char[] text = getSentence(3);
String textAsString = new String(text);
Assertions.assertEquals(3 * TestFont.SIMPLE_GLYPH_WIDTH, font.getWidth(textAsString));
}
@Test
public void getWidthOfSurrogatePairTest() {
TestFont font = new TestFont();
char[] text = new char[] {
TestFont.COMPLEX_GLYPH_AS_CHARS[0],
TestFont.COMPLEX_GLYPH_AS_CHARS[1],
(char) TestFont.SIMPLE_GLYPH,
};
String textAsString = new String(text);
Assertions.assertEquals(TestFont.COMPLEX_GLYPH_WIDTH + TestFont.SIMPLE_GLYPH_WIDTH,
font.getWidth(textAsString));
}
@Test
public void getWidthOfUnknownGlyphsTest() {
TestFont font = new TestFont();
char[] text = new char[] {
(char) 111,
(char) 222,
(char) 333,
};
String textAsString = new String(text);
Assertions.assertEquals(0, font.getWidth(textAsString));
}
@Test
public void getWidthOfStringWithFontSizeTest() {
TestFont font = new TestFont();
char[] text = getSentence(3);
String textAsString = new String(text);
double expectedValue = 3 * TestFont.SIMPLE_GLYPH_WIDTH * FONT_SIZE / (double) FontProgram.UNITS_NORMALIZATION;
Assertions.assertEquals(expectedValue, font.getWidth(textAsString, FONT_SIZE), 0.1);
}
@Test
public void getDescentOfGlyphTest() {
TestFont font = new TestFont();
int expectedDescent = font.getGlyph(TestFont.SIMPLE_GLYPH).getBbox()[1];
float expectedValue = (float) (expectedDescent * FONT_SIZE / (float) FontProgram.UNITS_NORMALIZATION);
Assertions.assertEquals(expectedValue, font.getDescent(TestFont.SIMPLE_GLYPH, FONT_SIZE), 0.1);
}
@Test
public void descentCannotBePositiveTest() {
TestFont font = new TestFont();
Assertions.assertEquals(0, font.getDescent(TestFont.SIMPLE_GLYPH_WITH_POSITIVE_DESCENT, 50), 0.1);
}
@Test
public void getDescentOfUnknownGlyphTest() {
TestFont font = new TestFont();
Assertions.assertEquals(0, font.getDescent(111, 50), 0.1);
}
@Test
public void getDescentOfGlyphWithoutBBoxTest() {
TestFont font = new TestFont();
font.setFontProgram(new TestFontProgram());
float expectedValue = (float) (FONT_METRICS_DESCENT * FONT_SIZE / (double) FontProgram.UNITS_NORMALIZATION);
Assertions.assertEquals(expectedValue, font.getDescent(TestFont.SIMPLE_GLYPH_WITHOUT_BBOX, FONT_SIZE), 0.1);
}
@Test
public void getDescentOfTextTest() {
TestFont font = new TestFont();
char[] text = new char[] {
(char) TestFont.SIMPLE_GLYPH,
TestFont.COMPLEX_GLYPH_AS_CHARS[0],
TestFont.COMPLEX_GLYPH_AS_CHARS[1],
};
String textAsString = new String(text);
int expectedMinDescent = Math.min(font.getGlyph(TestFont.SIMPLE_GLYPH).getBbox()[1],
font.getGlyph(TestFont.COMPLEX_GLYPH).getBbox()[1]);
float expectedValue = (float) (expectedMinDescent * FONT_SIZE / (double) FontProgram.UNITS_NORMALIZATION);
Assertions.assertEquals(expectedValue, font.getDescent(textAsString, FONT_SIZE), 0.1);
}
@Test
public void getDescentOfTextWithGlyphWithoutBBoxTest() {
TestFont font = new TestFont();
font.setFontProgram(new TestFontProgram());
char[] text = new char[] {
(char) TestFont.SIMPLE_GLYPH,
(char) TestFont.SIMPLE_GLYPH_WITHOUT_BBOX
};
String textAsString = new String(text);
int expectedMinDescent = Math.min(font.getGlyph(TestFont.SIMPLE_GLYPH).getBbox()[1],
FONT_METRICS_DESCENT);
float expectedValue = (float) (expectedMinDescent * FONT_SIZE / (double) FontProgram.UNITS_NORMALIZATION);
Assertions.assertEquals(expectedValue, font.getDescent(textAsString, FONT_SIZE), 0.1);
}
@Test
public void getAscentOfGlyphTest() {
TestFont font = new TestFont();
int expectedAscent = font.getGlyph(TestFont.SIMPLE_GLYPH).getBbox()[3];
float expectedValue = (float) (expectedAscent * FONT_SIZE / (double) FontProgram.UNITS_NORMALIZATION);
Assertions.assertEquals(expectedValue, font.getAscent(TestFont.SIMPLE_GLYPH, FONT_SIZE), 0.1);
}
@Test
public void getAscentOfGlyphWithoutBBoxTest() {
TestFont font = new TestFont();
font.setFontProgram(new TestFontProgram());
float expectedValue = (float) (FONT_METRICS_ASCENT * FONT_SIZE / (double) FontProgram.UNITS_NORMALIZATION);
Assertions.assertEquals(expectedValue, font.getAscent(TestFont.SIMPLE_GLYPH_WITHOUT_BBOX, FONT_SIZE), 0.1);
}
@Test
public void getAscentOfTextTest() {
TestFont font = new TestFont();
char[] text = new char[] {
(char) TestFont.SIMPLE_GLYPH,
TestFont.COMPLEX_GLYPH_AS_CHARS[0],
TestFont.COMPLEX_GLYPH_AS_CHARS[1],
};
String textAsString = new String(text);
int expectedMaxAscent = Math.max(
font.getGlyph(TestFont.SIMPLE_GLYPH).getBbox()[3],
font.getGlyph(TestFont.COMPLEX_GLYPH).getBbox()[3]);
float expectedValue = (float) (expectedMaxAscent * FONT_SIZE / (double) FontProgram.UNITS_NORMALIZATION);
Assertions.assertEquals(expectedValue, font.getAscent(textAsString, FONT_SIZE), 0.1);
}
@Test
public void getAscentOfTextWithGlyphWithoutBBoxTest() {
TestFont font = new TestFont();
font.setFontProgram(new TestFontProgram());
char[] text = new char[] {
(char) TestFont.SIMPLE_GLYPH,
(char) TestFont.SIMPLE_GLYPH_WITHOUT_BBOX
};
String textAsString = new String(text);
int expectedMaxAscent = Math.max(
font.getGlyph(TestFont.SIMPLE_GLYPH).getBbox()[3],
FONT_METRICS_ASCENT);
float expectedValue = (float) (expectedMaxAscent * FONT_SIZE / (double) FontProgram.UNITS_NORMALIZATION);
Assertions.assertEquals(expectedValue, font.getAscent(textAsString, FONT_SIZE), 0.1);
}
@Test
public void isEmbeddedTest() {
TestFont font = new TestFont();
Assertions.assertFalse(font.isEmbedded());
font.embedded = true;
Assertions.assertTrue(font.isEmbedded());
}
@Test
public void isSubsetTest() {
TestFont font = new TestFont();
Assertions.assertTrue(font.isSubset());
font.setSubset(false);
Assertions.assertFalse(font.isSubset());
}
@Test
public void addSubsetRangeTest() {
TestFont font = new TestFont();
font.setSubset(false);
final int[] range1 = {1, 2};
final int[] range2 = {10, 20};
font.addSubsetRange(range1);
font.addSubsetRange(range2);
Assertions.assertTrue(font.isSubset());
Assertions.assertEquals(2, font.subsetRanges.size());
Assertions.assertArrayEquals(range1, font.subsetRanges.get(0));
Assertions.assertArrayEquals(range2, font.subsetRanges.get(1));
}
@Test
public void splitSentenceFitMaxWidthTest() {
TestFont font = new TestFont();
char[] words = getSentence(3, 3);
String wordsAsString = new String(words);
double width = 6 * font.getWidth(TestFont.SIMPLE_GLYPH, FONT_SIZE);
List<String> result = font.splitString(wordsAsString, FONT_SIZE, (float) width + 0.01f);
Assertions.assertEquals(1, result.size());
Assertions.assertEquals(wordsAsString, result.get(0));
}
@Test
public void splitSentenceWordFitMaxWidthTest() {
TestFont font = new TestFont();
char[] words = getSentence(3, 4, 2);
String wordsAsString = new String(words);
double width = 4 * font.getWidth(TestFont.SIMPLE_GLYPH, FONT_SIZE);
List<String> result = font.splitString(wordsAsString, FONT_SIZE, (float) width + 0.01f);
Assertions.assertEquals(3, result.size());
Assertions.assertEquals(new String(getSentence(3)), result.get(0));
Assertions.assertEquals(new String(getSentence(4)), result.get(1));
Assertions.assertEquals(new String(getSentence(2)), result.get(2));
}
@Test
public void splitSentenceWordDoesNotFitMaxWidthCase_PartIsCombinedWithTheFollowingWordTest() {
TestFont font = new TestFont();
char[] words = getSentence(3, 4, 2);
String wordsAsString = new String(words);
double width = 3 * font.getWidth(TestFont.SIMPLE_GLYPH, FONT_SIZE);
List<String> result = font.splitString(wordsAsString, FONT_SIZE, (float) width + 0.01f);
Assertions.assertEquals(3, result.size());
Assertions.assertEquals(new String(getSentence(3)), result.get(0));
Assertions.assertEquals(new String(getSentence(3)), result.get(1));
Assertions.assertEquals(new String(getSentence(1, 2)), result.get(2));
}
@Test
public void splitSentenceWordDoesNotFitMaxWidthCase_PartIsOnTheSeparateLineTest() {
TestFont font = new TestFont();
char[] words = getSentence(2, 4, 3);
String wordsAsString = new String(words);
double width = 3 * font.getWidth(TestFont.SIMPLE_GLYPH, FONT_SIZE);
List<String> result = font.splitString(wordsAsString, FONT_SIZE, (float) width + 0.01f);
Assertions.assertEquals(4, result.size());
Assertions.assertEquals(new String(getSentence(2)), result.get(0));
Assertions.assertEquals(new String(getSentence(3)), result.get(1));
Assertions.assertEquals(new String(getSentence(1)), result.get(2));
Assertions.assertEquals(new String(getSentence(3)), result.get(3));
}
@Test
public void splitSentenceSymbolDoesNotFitLineTest() {
TestFont font = new TestFont();
char[] words = getSentence(3);
String wordsAsString = new String(words);
double width = font.getWidth(TestFont.SIMPLE_GLYPH, FONT_SIZE) / 2.;
List<String> result = font.splitString(wordsAsString, FONT_SIZE, (float) width + 0.01f);
Assertions.assertEquals(4, result.size());
Assertions.assertEquals(new String(getSentence(1)), result.get(0));
Assertions.assertEquals(new String(getSentence(1)), result.get(1));
Assertions.assertEquals(new String(getSentence(1)), result.get(2));
Assertions.assertEquals(new String(getSentence(0)), result.get(3));
}
@Test
public void splitSentenceWithLineBreakTest() {
TestFont font = new TestFont();
char[] words = new char[] {
(char) TestFont.SIMPLE_GLYPH,
'\n',
(char) TestFont.SIMPLE_GLYPH
};
String wordsAsString = new String(words);
double width = 10 * font.getWidth(TestFont.SIMPLE_GLYPH, FONT_SIZE);
List<String> result = font.splitString(wordsAsString, FONT_SIZE, (float) width + 0.01f);
Assertions.assertEquals(2, result.size());
Assertions.assertEquals(new String(getSentence(1)), result.get(0));
Assertions.assertEquals(new String(getSentence(1)), result.get(1));
}
@Test
public void isBuiltWithTest() {
TestFont font = new TestFont();
Assertions.assertFalse(font.isBuiltWith("Any String Here", "Any Encoding"));
}
@Test
public void isWrappedObjectMustBeIndirectTest() {
TestFont font = new TestFont();
Assertions.assertTrue(font.isWrappedObjectMustBeIndirect());
}
@Test
public void updateEmbeddedSubsetPrefixTest() {
final String fontName = "FontTest";
String embeddedSubsetFontName = TestFont.updateSubsetPrefix(fontName, true, true);
String onlySubsetFontName = TestFont.updateSubsetPrefix(fontName, true, false);
String onlyEmbeddedFontName = TestFont.updateSubsetPrefix(fontName, false, true);
String justFontName = TestFont.updateSubsetPrefix(fontName, false, false);
Assertions.assertEquals(fontName, onlySubsetFontName);
Assertions.assertEquals(fontName, onlyEmbeddedFontName);
Assertions.assertEquals(fontName, justFontName);
Pattern prefixPattern = Pattern.compile("^[A-Z]{6}\\+FontTest$");
Assertions.assertTrue(prefixPattern.matcher(embeddedSubsetFontName).matches());
}
@Test
public void getEmptyPdfStreamTest() {
TestFont font = new TestFont();
Exception e = Assertions.assertThrows(PdfException.class,
() -> font.getPdfFontStream(null, null)
);
Assertions.assertEquals(KernelExceptionMessageConstant.FONT_EMBEDDING_ISSUE, e.getMessage());
}
@Test
public void getPdfStreamTest() {
TestFont font = new TestFont();
byte[] data = new byte[10];
for (int i = 0; i < 10; i++) {
data[i] = (byte) i;
}
int[] fontStreamLength = new int[] {10, 20, 30};
PdfStream stream = font.getPdfFontStream(data, fontStreamLength);
Assertions.assertArrayEquals(data, stream.getBytes());
Assertions.assertEquals(10, stream.getAsNumber(new PdfName("Length1")).intValue());
Assertions.assertEquals(20, stream.getAsNumber(new PdfName("Length2")).intValue());
Assertions.assertEquals(30, stream.getAsNumber(new PdfName("Length3")).intValue());
}
@Test
public void getFontProgramTest() {
TestFont font = new TestFont();
TestFontProgram program = new TestFontProgram();
Assertions.assertNull(font.getFontProgram());
font.setFontProgram(program);
Assertions.assertEquals(program, font.getFontProgram());
}
@Test
public void toStringTest() {
TestFont font = new TestFont();
Assertions.assertEquals("PdfFont{fontProgram=" + font.fontProgram + "}", font.toString());
}
@Test
public void makeObjectIndirectWhileFontIsIndirectTest() {
try (PdfDocument document = new PdfDocument(new PdfWriter(new ByteArrayOutputStream()))) {
// to avoid an exception
document.addNewPage();
TestFont font = new TestFont();
font.getPdfObject().makeIndirect(document);
PdfDictionary dictionary = new PdfDictionary();
Assertions.assertTrue(font.makeObjectIndirect(dictionary));
Assertions.assertNotNull(dictionary.getIndirectReference());
Assertions.assertEquals(document, dictionary.getIndirectReference().getDocument());
}
}
@Test
public void makeObjectIndirectWhileFontIsDirectTest() {
TestFont font = new TestFont();
PdfDictionary dictionary = new PdfDictionary();
Assertions.assertFalse(font.makeObjectIndirect(dictionary));
Assertions.assertNull(dictionary.getIndirectReference());
}
private char[] getSentence(int... lengthsOfWords) {
int length = 0;
for (int lengthOfWord : lengthsOfWords) {
length += lengthOfWord;
}
int numberOfSpaces = lengthsOfWords.length - 1;
length += numberOfSpaces;
char[] sentence = new char[length];
int index = 0;
for (int lengthOfWord : lengthsOfWords) {
for (int i = 0; i < lengthOfWord; i++) {
sentence[index] = (char) TestFont.SIMPLE_GLYPH;
index++;
}
if (index < length) {
sentence[index] = ' ';
index++;
}
}
return sentence;
}
@Test
public void cannotGetFontStreamForNullBytesTest() throws IOException {
PdfFont pdfFont = PdfFontFactory.createFont();
Exception exception = Assertions.assertThrows(PdfException.class,
() -> pdfFont.getPdfFontStream(null, new int[] {1}));
Assertions.assertEquals(KernelExceptionMessageConstant.FONT_EMBEDDING_ISSUE, exception.getMessage());
}
@Test
public void cannotGetFontStreamForNullLengthsTest() throws IOException {
PdfFont pdfFont = PdfFontFactory.createFont();
Exception exception = Assertions.assertThrows(PdfException.class,
() -> pdfFont.getPdfFontStream(new byte[] {1}, null));
Assertions.assertEquals(KernelExceptionMessageConstant.FONT_EMBEDDING_ISSUE, exception.getMessage());
}
@Test
public void cannotGetFontStreamForNullBytesAndLengthsTest() throws IOException {
PdfFont pdfFont = PdfFontFactory.createFont();
Exception exception = Assertions.assertThrows(PdfException.class,
() -> pdfFont.getPdfFontStream(null, null));
Assertions.assertEquals(KernelExceptionMessageConstant.FONT_EMBEDDING_ISSUE, exception.getMessage());
}
}