/*
 * Licensed to the Apache Software Foundation (ASF) under one or more
 * contributor license agreements.  See the NOTICE file distributed with
 * this work for additional information regarding copyright ownership.
 * The ASF licenses this file to You under the Apache License, Version 2.0
 * (the "License"); you may not use this file except in compliance with
 * the License.  You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

/* $Id: TTFReader.java 1805173 2017-08-16 10:50:04Z ssteiner $ */

package org.apache.fop.fonts.apps;

import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.util.Map;
import java.util.Set;

import javax.xml.parsers.DocumentBuilderFactory;

import org.w3c.dom.Document;
import org.w3c.dom.Element;
import org.xml.sax.Attributes;
import org.xml.sax.SAXException;

import org.apache.fop.Version;
import org.apache.fop.fonts.CMapSegment;
import org.apache.fop.fonts.FontUtil;
import org.apache.fop.fonts.truetype.FontFileReader;
import org.apache.fop.fonts.truetype.OFFontLoader;
import org.apache.fop.fonts.truetype.TTFFile;

/**
 * A tool which reads TTF files and generates
 * XML font metrics file for use in FOP.
 */
public class TTFReader extends AbstractFontReader {

    /** Used to detect incompatible versions of the generated XML files */
    public static final String METRICS_VERSION_ATTR = "metrics-version";
    /** Current version number for the metrics file */
    public static final int METRICS_VERSION = 2;

    /**
     * Main constructor.
     */
    public TTFReader() {
        super();
    }

    private static void displayUsage() {
        System.out.println(
                "java " + TTFReader.class.getName() + " [options] fontfile.ttf xmlfile.xml");
        System.out.println();
        System.out.println("where options can be:");
        System.out.println("-t  Trace mode");
        System.out.println("-d  Debug mode");
        System.out.println("-q  Quiet mode");
        System.out.println("-enc ansi");
        System.out.println("    With this option you create a WinAnsi encoded font.");
        System.out.println("    The default is to create a CID keyed font.");
        System.out.println("    If you're not going to use characters outside the");
        System.out.println("    pdfencoding range (almost the same as iso-8889-1)");
        System.out.println("    you can add this option.");
        System.out.println("-ttcname <fontname>");
        System.out.println("    If you're reading data from a TrueType Collection");
        System.out.println("    (.ttc file) you must specify which font from the");
        System.out.println("    collection you will read metrics from. If you read");
        System.out.println("    from a .ttc file without this option, the fontnames");
        System.out.println("    will be listed for you.");
        System.out.println(" -fn <fontname>");
        System.out.println("    default is to use the fontname in the .ttf file, but");
        System.out.println("    you can override that name to make sure that the");
        System.out.println("    embedded font is used (if you're embedding fonts)");
        System.out.println("    instead of installed fonts when viewing documents ");
        System.out.println("    with Acrobat Reader.");
    }


    /**
     * The main method for the TTFReader tool.
     *
     * @param  args Command-line arguments: [options] fontfile.ttf xmlfile.xml
     * where options can be:
     * -fn fontname
     * default is to use the fontname in the .ttf file, but you can override
     * that name to make sure that the embedded font is used instead of installed
     * fonts when viewing documents with Acrobat Reader.
     * -cn classname
     * default is to use the fontname
     * -ef path to the truetype fontfile
     * will add the possibility to embed the font. When running fop, fop will look
     * for this file to embed it
     * -er path to truetype fontfile relative to org/apache/fop/render/pdf/fonts
     * you can also include the fontfile in the fop.jar file when building fop.
     * You can use both -ef and -er. The file specified in -ef will be searched first,
     * then the -er file.
     * -nocs
     * if complex script features are disabled
     */
    public static void main(String[] args) {
        String embFile = null;
        String embResource = null;
        String className = null;
        String fontName = null;
        String ttcName = null;
        boolean isCid = true;

        Map options = new java.util.HashMap();
        String[] arguments = parseArguments(options, args);

        determineLogLevel(options);

        TTFReader app = new TTFReader();

        log.info("TTF Reader for Apache FOP " + Version.getVersion() + "\n");

        if (options.get("-enc") != null) {
            String enc = (String)options.get("-enc");
            if ("ansi".equals(enc)) {
                isCid = false;
            }
        }

        if (options.get("-ttcname") != null) {
            ttcName = (String)options.get("-ttcname");
        }

        if (options.get("-ef") != null) {
            embFile = (String)options.get("-ef");
        }

        if (options.get("-er") != null) {
            embResource = (String)options.get("-er");
        }

        if (options.get("-fn") != null) {
            fontName = (String)options.get("-fn");
        }

        if (options.get("-cn") != null) {
            className = (String)options.get("-cn");
        }

        boolean useKerning = true;
        boolean useAdvanced = true;
        if (options.get("-nocs") != null) {
            useAdvanced = false;
        }

        if (arguments.length != 2 || options.get("-h") != null
            || options.get("-help") != null || options.get("--help") != null) {
            displayUsage();
        } else {
            try {
                log.info("Parsing font...");
                TTFFile ttf = app.loadTTF(arguments[0], ttcName, useKerning, useAdvanced);
                if (ttf != null) {
                    org.w3c.dom.Document doc = app.constructFontXML(ttf,
                            fontName, className, embResource, embFile, isCid,
                            ttcName);

                    if (isCid) {
                        log.info("Creating CID encoded metrics...");
                    } else {
                        log.info("Creating WinAnsi encoded metrics...");
                    }

                    if (doc != null) {
                        app.writeFontXML(doc, arguments[1]);
                    }

                    if (ttf.isEmbeddable()) {
                        log.info("This font contains no embedding license restrictions.");
                    } else {
                        log.info("** Note: This font contains license retrictions for\n"
                               + "         embedding. This font shouldn't be embedded.");
                    }
                }
                log.info("");
                log.info("XML font metrics file successfully created.");
            } catch (Exception e) {
                log.error("Error while building XML font metrics file.", e);
                System.exit(-1);
            }
        }
    }

    /**
     * Read a TTF file and returns it as an object.
     *
     * @param  fileName The filename of the TTF file.
     * @param  fontName The name of the font
     * @param  useKerning true if should load kerning data
     * @param  useAdvanced true if should load advanced typographic table data
     * @return The TTF as an object, null if the font is incompatible.
     * @throws IOException In case of an I/O problem
     */
    public TTFFile loadTTF(String fileName, String fontName, boolean useKerning, boolean useAdvanced)
            throws IOException {
        TTFFile ttfFile = new TTFFile(useKerning, useAdvanced);
        log.info("Reading " + fileName + "...");
        InputStream stream = new FileInputStream(fileName);
        try {
            FontFileReader reader = new FontFileReader(stream);
            String header = OFFontLoader.readHeader(reader);
            boolean supported = ttfFile.readFont(reader, header, fontName);
            if (!supported) {
                return null;
            }
        } finally {
            stream.close();
        }

        log.info("Font Family: " + ttfFile.getFamilyNames());
        if (ttfFile.isCFF()) {
            throw new UnsupportedOperationException(
                    "OpenType fonts with CFF data are not supported, yet");
        }
        return ttfFile;
    }


    /**
     * Generates the font metrics file from the TTF/TTC file.
     *
     * @param ttf The PFM file to generate the font metrics from.
     * @param fontName Name of the font
     * @param className Class name for the font
     * @param resource path to the font as embedded resource
     * @param file path to the font as file
     * @param isCid True if the font is CID encoded
     * @param ttcName Name of the TrueType Collection
     * @return The DOM document representing the font metrics file.
     */
    public org.w3c.dom.Document constructFontXML(TTFFile ttf,
            String fontName, String className, String resource, String file,
            boolean isCid, String ttcName) {
        log.info("Creating xml font file...");

        Document doc;
        try {
            DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance();
            doc = factory.newDocumentBuilder().newDocument();
        } catch (javax.xml.parsers.ParserConfigurationException e) {
            log.error("Can't create DOM implementation", e);
            return null;
        }
        Element root = doc.createElement("font-metrics");
        doc.appendChild(root);
        root.setAttribute(METRICS_VERSION_ATTR, String.valueOf(METRICS_VERSION));
        if (isCid) {
            root.setAttribute("type", "TYPE0");
        } else {
            root.setAttribute("type", "TRUETYPE");
        }

        Element el = doc.createElement("font-name");
        root.appendChild(el);

        // Note that the PostScript name usually is something like
        // "Perpetua-Bold", but the TrueType spec says that in the ttf file
        // it should be "Perpetua,Bold".

        String s = FontUtil.stripWhiteSpace(ttf.getPostScriptName());

        if (fontName != null) {
            el.appendChild(doc.createTextNode(FontUtil.stripWhiteSpace(fontName)));
        } else {
            el.appendChild(doc.createTextNode(s));
        }
        if (ttf.getFullName() != null) {
            el = doc.createElement("full-name");
            root.appendChild(el);
            el.appendChild(doc.createTextNode(ttf.getFullName()));
        }
        Set<String> familyNames = ttf.getFamilyNames();
        if (familyNames.size() > 0) {
            String familyName = familyNames.iterator().next();
            el = doc.createElement("family-name");
            root.appendChild(el);
            el.appendChild(doc.createTextNode(familyName));
        }

        el = doc.createElement("embed");
        root.appendChild(el);
        if (file != null && ttf.isEmbeddable()) {
            el.setAttribute("file", file);
        }
        if (resource != null && ttf.isEmbeddable()) {
            el.setAttribute("class", resource);
        }

        el = doc.createElement("cap-height");
        root.appendChild(el);
        el.appendChild(doc.createTextNode(String.valueOf(ttf.getCapHeight())));

        el = doc.createElement("x-height");
        root.appendChild(el);
        el.appendChild(doc.createTextNode(String.valueOf(ttf.getXHeight())));

        el = doc.createElement("ascender");
        root.appendChild(el);
        el.appendChild(doc.createTextNode(String.valueOf(ttf.getLowerCaseAscent())));

        el = doc.createElement("descender");
        root.appendChild(el);
        el.appendChild(doc.createTextNode(String.valueOf(ttf.getLowerCaseDescent())));

        Element bbox = doc.createElement("bbox");
        root.appendChild(bbox);
        int[] bb = ttf.getFontBBox();
        final String[] names = {"left", "bottom", "right", "top"};
        for (int i = 0; i < names.length; i++) {
            el = doc.createElement(names[i]);
            bbox.appendChild(el);
            el.appendChild(doc.createTextNode(String.valueOf(bb[i])));
        }

        el = doc.createElement("flags");
        root.appendChild(el);
        el.appendChild(doc.createTextNode(String.valueOf(ttf.getFlags())));

        el = doc.createElement("stemv");
        root.appendChild(el);
        el.appendChild(doc.createTextNode(ttf.getStemV()));

        el = doc.createElement("italicangle");
        root.appendChild(el);
        el.appendChild(doc.createTextNode(ttf.getItalicAngle()));

        if (ttcName != null) {
            el = doc.createElement("ttc-name");
            root.appendChild(el);
            el.appendChild(doc.createTextNode(ttcName));
        }

        el = doc.createElement("subtype");
        root.appendChild(el);

        // Fill in extras for CID keyed fonts
        if (isCid) {
            el.appendChild(doc.createTextNode("TYPE0"));

            generateDOM4MultiByteExtras(root, ttf, isCid);
        } else {
            // Fill in extras for singlebyte fonts
            el.appendChild(doc.createTextNode("TRUETYPE"));

            generateDOM4SingleByteExtras(root, ttf, isCid);
        }

        generateDOM4Kerning(root, ttf, isCid);

        return doc;
    }

    private void generateDOM4MultiByteExtras(Element parent, TTFFile ttf, boolean isCid) {
        Element el;
        Document doc = parent.getOwnerDocument();

        Element mel = doc.createElement("multibyte-extras");
        parent.appendChild(mel);

        el = doc.createElement("cid-type");
        mel.appendChild(el);
        el.appendChild(doc.createTextNode("CIDFontType2"));

        el = doc.createElement("default-width");
        mel.appendChild(el);
        el.appendChild(doc.createTextNode("0"));

        el = doc.createElement("bfranges");
        mel.appendChild(el);
        for (CMapSegment ce : ttf.getCMaps()) {
            Element el2 = doc.createElement("bf");
            el.appendChild(el2);
            el2.setAttribute("us", String.valueOf(ce.getUnicodeStart()));
            el2.setAttribute("ue", String.valueOf(ce.getUnicodeEnd()));
            el2.setAttribute("gi", String.valueOf(ce.getGlyphStartIndex()));
        }

        el = doc.createElement("cid-widths");
        el.setAttribute("start-index", "0");
        mel.appendChild(el);

        int[] wx = ttf.getWidths();
        for (int i = 0; i < wx.length; i++) {
            Element wxel = doc.createElement("wx");
            wxel.setAttribute("w", String.valueOf(wx[i]));
            int[] bbox = ttf.getBBox(i);
            wxel.setAttribute("xMin", String.valueOf(bbox[0]));
            wxel.setAttribute("yMin", String.valueOf(bbox[1]));
            wxel.setAttribute("xMax", String.valueOf(bbox[2]));
            wxel.setAttribute("yMax", String.valueOf(bbox[3]));
            el.appendChild(wxel);
        }
    }

    private void generateDOM4SingleByteExtras(Element parent, TTFFile ttf, boolean isCid) {
        Element el;
        Document doc = parent.getOwnerDocument();

        Element sel = doc.createElement("singlebyte-extras");
        parent.appendChild(sel);

        el = doc.createElement("encoding");
        sel.appendChild(el);
        el.appendChild(doc.createTextNode(ttf.getCharSetName()));

        el = doc.createElement("first-char");
        sel.appendChild(el);
        el.appendChild(doc.createTextNode(String.valueOf(ttf.getFirstChar())));

        el = doc.createElement("last-char");
        sel.appendChild(el);
        el.appendChild(doc.createTextNode(String.valueOf(ttf.getLastChar())));

        Element widths = doc.createElement("widths");
        sel.appendChild(widths);

        for (short i = ttf.getFirstChar(); i <= ttf.getLastChar(); i++) {
            el = doc.createElement("char");
            widths.appendChild(el);
            el.setAttribute("idx", String.valueOf(i));
            el.setAttribute("wdt", String.valueOf(ttf.getCharWidth(i)));
        }
    }

    private void generateDOM4Kerning(Element parent, TTFFile ttf, boolean isCid) {
        Element el;
        Document doc = parent.getOwnerDocument();

        // Get kerning
        Set<Integer> kerningKeys;
        if (isCid) {
            kerningKeys = ttf.getKerning().keySet();
        } else {
            kerningKeys = ttf.getAnsiKerning().keySet();
        }

        for (Integer kpx1 : kerningKeys) {

            el = doc.createElement("kerning");
            el.setAttribute("kpx1", kpx1.toString());
            parent.appendChild(el);
            Element el2 = null;

            Map<Integer, Integer> h2;
            if (isCid) {
                h2 = ttf.getKerning().get(kpx1);
            } else {
                h2 = ttf.getAnsiKerning().get(kpx1);
            }

            for (Map.Entry<Integer, Integer> e : h2.entrySet()) {
                Integer kpx2 = e.getKey();
                if (isCid || kpx2 < 256) {
                    el2 = doc.createElement("pair");
                    el2.setAttribute("kpx2", kpx2.toString());
                    Integer val = e.getValue();
                    el2.setAttribute("kern", val.toString());
                    el.appendChild(el2);
                }
            }
        }
    }

    /**
     * Bugzilla 40739, check that attr has a metrics-version attribute
     * compatible with ours.
     * @param attr attributes read from the root element of a metrics XML file
     * @throws SAXException if incompatible
     */
    public static void checkMetricsVersion(Attributes attr) throws SAXException {
        String err = null;
        final String str = attr.getValue(METRICS_VERSION_ATTR);
        if (str == null) {
            err = "Missing " + METRICS_VERSION_ATTR + " attribute";
        } else {
            int version = 0;
            try {
                version = Integer.parseInt(str);
                if (version < METRICS_VERSION) {
                    err = "Incompatible " + METRICS_VERSION_ATTR
                        + " value (" + version + ", should be " + METRICS_VERSION
                        + ")";
                }
            } catch (NumberFormatException e) {
                err = "Invalid " + METRICS_VERSION_ATTR
                    + " attribute value (" + str + ")";
            }
        }

        if (err != null) {
            throw new SAXException(
                err
                + " - please regenerate the font metrics file with "
                + "a more recent version of FOP."
            );
        }
    }

}

