FontUtil.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.CjkResourceLoader;
import com.itextpdf.io.font.PdfEncodings;
import com.itextpdf.io.font.cmap.CMapContentParser;
import com.itextpdf.io.font.cmap.CMapLocationFromBytes;
import com.itextpdf.io.font.cmap.CMapLocationResource;
import com.itextpdf.io.font.cmap.CMapParser;
import com.itextpdf.io.font.cmap.CMapToUnicode;
import com.itextpdf.io.font.cmap.CMapUniCid;
import com.itextpdf.io.font.cmap.ICMapLocation;
import com.itextpdf.io.font.otf.Glyph;
import com.itextpdf.io.logs.IoLogMessageConstant;
import com.itextpdf.io.source.ByteArrayOutputStream;
import com.itextpdf.io.source.HighPrecisionOutputStream;
import com.itextpdf.io.util.IntHashtable;
import com.itextpdf.kernel.pdf.PdfArray;
import com.itextpdf.kernel.pdf.PdfName;
import com.itextpdf.kernel.pdf.PdfNumber;
import com.itextpdf.kernel.pdf.PdfObject;
import com.itextpdf.kernel.pdf.PdfStream;

import java.security.SecureRandom;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

/**
 * Utility class for font processing.
 */
public class FontUtil {
    private static final SecureRandom NUMBER_GENERATOR = new SecureRandom();

    private static final HashMap<String, CMapToUnicode> uniMaps = new HashMap<>();

    private static final Logger LOGGER = LoggerFactory.getLogger(FontUtil.class);

    private static final String UNIVERSAL_CMAP_DIR = "toUnicode/";

    private static final Set<String> UNIVERSAL_CMAP_ORDERINGS = new HashSet<>(Arrays.asList(
            "CNS1", "GB1", "Japan1", "Korea1", "KR"));

    private FontUtil() {}

    /**
     * Adds random subset prefix (+ and 6 upper case letters) to passed font name.
     *
     * @param fontName the font add prefix to
     *
     * @return the font name with added prefix.
     */
    public static String addRandomSubsetPrefixForFontName(final String fontName) {
        final StringBuilder newFontName = getRandomFontPrefix(6);
        newFontName.append('+').append(fontName);
        return newFontName.toString();
    }

    /**
     * Processes passed {@code ToUnicode} object to {@link CMapToUnicode} instance.
     *
     * @param toUnicode the {@code ToUnicode} object
     *
     * @return parsed {@link CMapToUnicode} instance
     */
    public static CMapToUnicode processToUnicode(PdfObject toUnicode) {
        CMapToUnicode cMapToUnicode = null;
        if (toUnicode instanceof PdfStream) {
            try {
                byte[] uniBytes = ((PdfStream) toUnicode).getBytes();
                ICMapLocation lb = new CMapLocationFromBytes(uniBytes);
                cMapToUnicode = new CMapToUnicode();
                CMapParser.parseCid("", cMapToUnicode, lb);
            } catch (Exception e) {
                LOGGER.error(IoLogMessageConstant.UNKNOWN_ERROR_WHILE_PROCESSING_CMAP, e);
                cMapToUnicode = CMapToUnicode.EMPTY_CMAP;
            }
        } else if (PdfName.IdentityH.equals(toUnicode)) {
            cMapToUnicode = CMapToUnicode.getIdentity();
        }
        return cMapToUnicode;
    }

    /**
     * Converts passed {@code W} array to integer table.
     *
     * @param widthsArray the {@code W} array to convert
     *
     * @return converted {@code W} array as an integer table
     */
    public static IntHashtable convertCompositeWidthsArray(PdfArray widthsArray) {
        IntHashtable res = new IntHashtable();
        if (widthsArray == null) {
            return res;
        }

        for (int k = 0; k < widthsArray.size(); ++k) {
            int c1 = widthsArray.getAsNumber(k).intValue();
            PdfObject obj = widthsArray.get(++k);
            if (obj.isArray()) {
                PdfArray subWidths = (PdfArray)obj;
                for (int j = 0; j < subWidths.size(); ++j) {
                    int c2 = subWidths.getAsNumber(j).intValue();
                    res.put(c1++, c2);
                }
            } else {
                int c2 = ((PdfNumber)obj).intValue();
                int w = widthsArray.getAsNumber(++k).intValue();
                for (; c1 <= c2; ++c1) {
                    res.put(c1, w);
                }
            }
        }
        return res;
    }

    /**
     * Gets a {@code ToUnicode} {@link PdfStream} from passed glyphs.
     *
     * @param glyphs the glyphs {@code ToUnicode} will be based on
     *
     * @return the created {@code ToUnicode} {@link PdfStream}
     */
    public static PdfStream getToUnicodeStream(Set<Glyph> glyphs) {
        HighPrecisionOutputStream<ByteArrayOutputStream> stream =
                new HighPrecisionOutputStream<>(new ByteArrayOutputStream());
        stream.writeString("/CIDInit /ProcSet findresource begin\n" +
                "12 dict begin\n" +
                "begincmap\n" +
                "/CIDSystemInfo\n" +
                "<< /Registry (Adobe)\n" +
                "/Ordering (UCS)\n" +
                "/Supplement 0\n" +
                ">> def\n" +
                "/CMapName /Adobe-Identity-UCS def\n" +
                "/CMapType 2 def\n" +
                "1 begincodespacerange\n" +
                "<0000><FFFF>\n" +
                "endcodespacerange\n");

        //accumulate long tag into a subset and write it.
        List<Glyph> glyphGroup = new ArrayList<>(100);

        int bfranges = 0;
        for (Glyph glyph : glyphs) {
            if (glyph.getChars() != null) {
                glyphGroup.add(glyph);
                if (glyphGroup.size() == 100) {
                    bfranges += writeBfrange(stream, glyphGroup);
                }
            }
        }
        //flush leftovers
        bfranges += writeBfrange(stream, glyphGroup);

        if (bfranges == 0) {
            return null;
        }

        stream.writeString("endcmap\n" +
                "CMapName currentdict /CMap defineresource pop\n" +
                "end end\n");
        return new PdfStream(((ByteArrayOutputStream)stream.getOutputStream()).toByteArray());
    }

    private static int writeBfrange(HighPrecisionOutputStream<ByteArrayOutputStream> stream, List<Glyph> range) {
        if (range.isEmpty()) {
            return 0;
        }
        stream.writeInteger(range.size());
        stream.writeString(" beginbfrange\n");
        for (Glyph glyph: range) {
            String fromTo = CMapContentParser.toHex(glyph.getCode());
            stream.writeString(fromTo);
            stream.writeString(fromTo);
            stream.writeByte('<');
            for (char ch : glyph.getChars()) {
                stream.writeString(toHex4(ch));
            }
            stream.writeByte('>');
            stream.writeByte('\n');
        }
        stream.writeString("endbfrange\n");
        range.clear();
        return 1;
    }

    private static String toHex4(char ch) {
        String s = "0000" + Integer.toHexString(ch);
        return s.substring(s.length() - 4);
    }

    static CMapToUnicode parseUniversalToUnicodeCMap(String ordering) {
        if (!UNIVERSAL_CMAP_ORDERINGS.contains(ordering)) {
            return null;
        }
        String cmapRelPath = UNIVERSAL_CMAP_DIR + "Adobe-" + ordering + "-UCS2";
        CMapToUnicode cMapToUnicode = new CMapToUnicode();
        try {
            CMapParser.parseCid(cmapRelPath, cMapToUnicode, new CMapLocationResource());
        } catch (Exception e) {
            LOGGER.error(IoLogMessageConstant.UNKNOWN_ERROR_WHILE_PROCESSING_CMAP, e);
            return null;
        }
        return cMapToUnicode;
    }

    static CMapToUnicode getToUnicodeFromUniMap(String uniMap) {
        if (uniMap == null)
            return null;
        synchronized (uniMaps) {
            if (uniMaps.containsKey(uniMap)) {
                return uniMaps.get(uniMap);
            }
            CMapToUnicode toUnicode;
            if (PdfEncodings.IDENTITY_H.equals(uniMap)) {
                toUnicode = CMapToUnicode.getIdentity();
            } else {
                CMapUniCid uni = CjkResourceLoader.getUni2CidCmap(uniMap);
                toUnicode = uni.exportToUnicode();
            }
            uniMaps.put(uniMap, toUnicode);
            return toUnicode;
        }
    }

    static String createRandomFontName() {
        return getRandomFontPrefix(7).toString();
    }

    static int[] convertSimpleWidthsArray(PdfArray widthsArray, int first, int missingWidth) {
        int[] res = new int[256];
        Arrays.fill(res, missingWidth);
        if (widthsArray == null) {
            Logger logger = LoggerFactory.getLogger(FontUtil.class);
            logger.warn(IoLogMessageConstant.FONT_DICTIONARY_WITH_NO_WIDTHS);
            return res;
        }

        for (int i = 0; i < widthsArray.size() && first + i < 256; i++) {
            PdfNumber number = widthsArray.getAsNumber(i);
            res[first + i] = number != null ? number.intValue() : missingWidth;
        }
        return res;
    }

    private static StringBuilder getRandomFontPrefix(int length) {
        final StringBuilder stringBuilder = new StringBuilder();
        final byte[] randomByte = new byte[length];
        NUMBER_GENERATOR.nextBytes(randomByte);
        for (int k = 0; k < length; ++k) {
            stringBuilder.append((char) (Math.abs(randomByte[k] % 26) + 'A'));
        }
        return stringBuilder;
    }
}