PdfSimpleFontTest.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.FontEncoding;
import com.itextpdf.io.font.FontProgram;
import com.itextpdf.io.font.FontProgramFactory;
import com.itextpdf.io.font.constants.StandardFonts;
import com.itextpdf.io.font.otf.Glyph;
import com.itextpdf.io.font.otf.GlyphLine;
import com.itextpdf.kernel.pdf.PdfDictionary;
import com.itextpdf.kernel.pdf.PdfOutputStream;
import com.itextpdf.test.ExtendedITextTest;

import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.util.ArrayList;
import java.util.List;
import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.BeforeAll;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.Tag;

@Tag("UnitTest")
public class PdfSimpleFontTest extends ExtendedITextTest {

    private static final byte T_CODE = 116;
    private static final byte E_CODE = 101;
    private static final byte E_CUSTOM_CODE = 103;
    private static final byte OPEN_BRACKET_CODE = 40;
    private static final byte CLOSE_BRACKET_CODE = 41;

    private static Glyph E_GLYPH_FONT_SPECIFIC;
    private static Glyph T_GLYPH_FONT_SPECIFIC;
    private static Glyph E_GLYPH_CUSTOM_MAPPED;

    @BeforeAll
    public static void init() {
        T_GLYPH_FONT_SPECIFIC = new Glyph(T_CODE, 278, 116, new int[]{14, -7, 257, 669});
        T_GLYPH_FONT_SPECIFIC.setChars(new char[]{'t'});

        E_GLYPH_FONT_SPECIFIC = new Glyph(E_CODE, 556, 101, new int[]{40, -15, 516, 538});
        E_GLYPH_FONT_SPECIFIC.setChars(new char[]{'e'});

        E_GLYPH_CUSTOM_MAPPED = new Glyph(E_CUSTOM_CODE, 44, 103);
        E_GLYPH_CUSTOM_MAPPED.setChars(new char[]{'e'});
    }

    @Test
    public void createGlyphLineWithSpecificEncodingTest() throws IOException {
        PdfSimpleFont<FontProgram> fontToTest = new TestSimpleFont(FontEncoding.createFontSpecificEncoding());
        GlyphLine glyphLine = fontToTest.createGlyphLine("te");

        List<Glyph> glyphs = new ArrayList<>();
        glyphs.add(T_GLYPH_FONT_SPECIFIC);
        glyphs.add(E_GLYPH_FONT_SPECIFIC);
        GlyphLine expected = new GlyphLine(glyphs, 0, 2);

        Assertions.assertEquals(expected, glyphLine);
    }

    @Test
    public void createGlyphLineWithEmptyEncodingTest() throws IOException {
        PdfSimpleFont<FontProgram> fontToTest = new TestSimpleFont(FontEncoding.createEmptyFontEncoding());
        GlyphLine glyphLine = fontToTest.createGlyphLine("te");

        List<Glyph> glyphs = new ArrayList<>();
        glyphs.add(E_GLYPH_CUSTOM_MAPPED);
        GlyphLine expected = new GlyphLine(glyphs, 0, 1);

        Assertions.assertEquals(expected, glyphLine);
    }

    @Test
    public void appendGlyphsWithSpecificEncodingTest() throws IOException {
        PdfSimpleFont<FontProgram> fontToTest = new TestSimpleFont(FontEncoding.createFontSpecificEncoding());
        List<Glyph> toAppend = new ArrayList<>();
        int processed = fontToTest.appendGlyphs("te", 0, 1, toAppend);
        Assertions.assertEquals(2, processed);

        List<Glyph> glyphs = new ArrayList<>();
        glyphs.add(T_GLYPH_FONT_SPECIFIC);
        glyphs.add(E_GLYPH_FONT_SPECIFIC);

        Assertions.assertEquals(glyphs, toAppend);
    }

    @Test
    public void appendGlyphsWithEmptyEncodingTest() throws IOException {
        PdfSimpleFont<FontProgram> fontToTest = new TestSimpleFont(FontEncoding.createEmptyFontEncoding());
        List<Glyph> toAppend = new ArrayList<>();
        int processed = fontToTest.appendGlyphs("e ete", 0, 4, toAppend);
        Assertions.assertEquals(3, processed);

        List<Glyph> glyphs = new ArrayList<>();
        glyphs.add(E_GLYPH_CUSTOM_MAPPED);
        glyphs.add(E_GLYPH_CUSTOM_MAPPED);

        Assertions.assertEquals(glyphs, toAppend);
    }

    @Test
    public void appendAnyGlyphWithSpecificEncodingTest() throws IOException {
        PdfSimpleFont<FontProgram> fontToTest = new TestSimpleFont(FontEncoding.createFontSpecificEncoding());
        List<Glyph> toAppend = new ArrayList<>();
        int processed = fontToTest.appendAnyGlyph("te", 0, toAppend);
        Assertions.assertEquals(1, processed);

        List<Glyph> glyphs = new ArrayList<>();
        glyphs.add(T_GLYPH_FONT_SPECIFIC);

        Assertions.assertEquals(glyphs, toAppend);
    }

    @Test
    public void appendAnyGlyphWithEmptyEncodingTest() throws IOException {
        PdfSimpleFont<FontProgram> fontToTest = new TestSimpleFont(FontEncoding.createEmptyFontEncoding());
        List<Glyph> toAppend = new ArrayList<>();
        int processed = fontToTest.appendAnyGlyph("e ete", 0, toAppend);
        Assertions.assertEquals(1, processed);

        List<Glyph> glyphs = new ArrayList<>();
        glyphs.add(E_GLYPH_CUSTOM_MAPPED);

        Assertions.assertEquals(glyphs, toAppend);
    }

    @Test
    public void convertGlyphLineToBytesWithSpecificEncodingTest() throws IOException {
        PdfSimpleFont<FontProgram> fontToTest = new TestSimpleFont(FontEncoding.createFontSpecificEncoding());

        List<Glyph> glyphs = new ArrayList<>();
        glyphs.add(T_GLYPH_FONT_SPECIFIC);
        glyphs.add(E_GLYPH_FONT_SPECIFIC);
        GlyphLine glyphLine = new GlyphLine(glyphs, 0, 2);

        byte[] bytes = fontToTest.convertToBytes(glyphLine);

        Assertions.assertArrayEquals(new byte[]{T_CODE, E_CODE}, bytes);
    }

    @Test
    public void convertGlyphLineToBytesWithEmptyEncodingTest() throws IOException {
        PdfSimpleFont<FontProgram> fontToTest = new TestSimpleFont(FontEncoding.createEmptyFontEncoding());

        List<Glyph> glyphs = new ArrayList<>();
        glyphs.add(T_GLYPH_FONT_SPECIFIC);
        glyphs.add(E_GLYPH_FONT_SPECIFIC);
        GlyphLine glyphLine = new GlyphLine(glyphs, 0, 2);

        byte[] bytes = fontToTest.convertToBytes(glyphLine);

        Assertions.assertArrayEquals(new byte[0], bytes);
    }

    @Test
    public void convertToBytesWithNullEntry() throws IOException {
        PdfSimpleFont<FontProgram> fontToTest = new TestSimpleFont(FontEncoding.createEmptyFontEncoding());

        byte[] bytes = fontToTest.convertToBytes((GlyphLine) null);

        Assertions.assertArrayEquals(new byte[0], bytes);
    }

    @Test
    public void convertGlyphToBytesWithSpecificEncodingTest() throws IOException {
        PdfSimpleFont<FontProgram> fontToTest = new TestSimpleFont(FontEncoding.createFontSpecificEncoding());

        byte[] bytes = fontToTest.convertToBytes(E_GLYPH_FONT_SPECIFIC);

        Assertions.assertArrayEquals(new byte[]{E_CODE}, bytes);
    }

    @Test
    public void convertGlyphToBytesWithCustomEncodingTest() throws IOException {
        FontEncoding emptyFontEncoding = FontEncoding.createEmptyFontEncoding();
        emptyFontEncoding.addSymbol(E_CUSTOM_CODE, E_CODE);
        PdfSimpleFont<FontProgram> fontToTest = new TestSimpleFont(emptyFontEncoding);

        byte[] bytes = fontToTest.convertToBytes(E_GLYPH_FONT_SPECIFIC);

        Assertions.assertArrayEquals(new byte[]{E_CUSTOM_CODE}, bytes);
    }

    @Test
    public void convertGlyphToBytesWithEmptyEncodingTest() throws IOException {
        PdfSimpleFont<FontProgram> fontToTest = new TestSimpleFont(FontEncoding.createEmptyFontEncoding());

        byte[] bytes = fontToTest.convertToBytes(E_GLYPH_FONT_SPECIFIC);

        Assertions.assertArrayEquals(new byte[0], bytes);
    }

    @Test
    public void writeTextGlyphLineWithSpecificEncodingTest() throws IOException {
        PdfSimpleFont<FontProgram> fontToTest = new TestSimpleFont(FontEncoding.createFontSpecificEncoding());

        List<Glyph> glyphs = new ArrayList<>();
        glyphs.add(T_GLYPH_FONT_SPECIFIC);
        glyphs.add(E_GLYPH_FONT_SPECIFIC);
        GlyphLine glyphLine = new GlyphLine(glyphs, 0, 2);

        ByteArrayOutputStream bos = new ByteArrayOutputStream();
        try (PdfOutputStream pos = new PdfOutputStream(bos)) {
            fontToTest.writeText(glyphLine, 0, 1, pos);
        }

        Assertions.assertArrayEquals(new byte[]{OPEN_BRACKET_CODE, T_CODE, E_CODE, CLOSE_BRACKET_CODE}, bos.toByteArray());
    }

    @Test
    public void writeTextGlyphLineWithCustomEncodingTest() throws IOException {
        FontEncoding fontEncoding = FontEncoding.createEmptyFontEncoding();
        fontEncoding.addSymbol(E_CUSTOM_CODE, E_CODE);
        PdfSimpleFont<FontProgram> fontToTest = new TestSimpleFont(fontEncoding);

        List<Glyph> glyphs = new ArrayList<>();
        glyphs.add(E_GLYPH_FONT_SPECIFIC);
        glyphs.add(T_GLYPH_FONT_SPECIFIC);
        GlyphLine glyphLine = new GlyphLine(glyphs, 0, 2);

        ByteArrayOutputStream bos = new ByteArrayOutputStream();
        try (PdfOutputStream pos = new PdfOutputStream(bos)) {
            fontToTest.writeText(glyphLine, 0, 1, pos);
        }

        Assertions.assertArrayEquals(new byte[]{OPEN_BRACKET_CODE, E_CUSTOM_CODE, CLOSE_BRACKET_CODE}, bos.toByteArray());
    }

    @Test
    public void writeTextGlyphLineWithEmptyEncodingTest() throws IOException {
        PdfSimpleFont<FontProgram> fontToTest = new TestSimpleFont(FontEncoding.createEmptyFontEncoding());

        List<Glyph> glyphs = new ArrayList<>();
        glyphs.add(E_GLYPH_FONT_SPECIFIC);
        glyphs.add(T_GLYPH_FONT_SPECIFIC);
        GlyphLine glyphLine = new GlyphLine(glyphs, 0, 2);

        ByteArrayOutputStream bos = new ByteArrayOutputStream();
        try (PdfOutputStream pos = new PdfOutputStream(bos)) {
            fontToTest.writeText(glyphLine, 0, 1, pos);
        }

        Assertions.assertArrayEquals(new byte[]{OPEN_BRACKET_CODE, CLOSE_BRACKET_CODE}, bos.toByteArray());
    }

    private static class TestSimpleFont extends PdfSimpleFont<FontProgram> {

        public TestSimpleFont(FontEncoding fontEncoding) throws IOException {
            this.fontEncoding = fontEncoding;
            setFontProgram(FontProgramFactory.createFont(StandardFonts.HELVETICA));
        }

        @Override
        public Glyph getGlyph(int unicode) {
            if (unicode == E_CODE) {
                return E_GLYPH_CUSTOM_MAPPED;
            }
            return null;
        }

        @Override
        protected void addFontStream(PdfDictionary fontDescriptor) {

        }
    }

}