net.padaf.preflight.font.CompositeFontValidator.java Source code

Java tutorial

Introduction

Here is the source code for net.padaf.preflight.font.CompositeFontValidator.java

Source

/*******************************************************************************
 * Copyright 2010 Atos Worldline SAS
 * 
 * Licensed by Atos Worldline SAS under one or more
 * contributor license agreements.  See the NOTICE file distributed with
 * this work for additional information regarding copyright ownership.
 * Atos Worldline SAS 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.
 ******************************************************************************/
package net.padaf.preflight.font;

import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.util.LinkedHashMap;
import java.util.List;

import net.padaf.preflight.DocumentHandler;
import net.padaf.preflight.ValidationConstants;
import net.padaf.preflight.ValidationException;
import net.padaf.preflight.ValidationResult;
import net.padaf.preflight.ValidationResult.ValidationError;
import net.padaf.preflight.utils.COSUtils;

import org.apache.fontbox.cff.CFFFont;
import org.apache.fontbox.cff.CFFParser;
import org.apache.fontbox.cmap.CMap;
import org.apache.fontbox.cmap.CMapParser;
import org.apache.fontbox.ttf.CIDFontType2Parser;
import org.apache.fontbox.ttf.TrueTypeFont;
import org.apache.pdfbox.cos.COSArray;
import org.apache.pdfbox.cos.COSBase;
import org.apache.pdfbox.cos.COSDictionary;
import org.apache.pdfbox.cos.COSDocument;
import org.apache.pdfbox.cos.COSName;
import org.apache.pdfbox.cos.COSObject;
import org.apache.pdfbox.cos.COSStream;
import org.apache.pdfbox.pdmodel.common.PDStream;
import org.apache.pdfbox.pdmodel.font.PDFontDescriptorDictionary;

public class CompositeFontValidator extends AbstractFontValidator {
    protected String basefont;
    protected COSBase descendantFonts;
    protected COSDictionary cidFont;
    protected COSBase encoding;
    protected COSStream cmap;
    protected COSBase toUnicode;

    protected CMap cidToGidMap = null;

    protected boolean isIdentityCMap = false;

    public CompositeFontValidator(DocumentHandler handler, COSObject obj) throws ValidationException {
        super(handler, obj);
    }

    /**
     * This methods extracts from the Font dictionary all mandatory fields. If a
     * mandatory field is missing, the list of ValidationError in the
     * FontContainer is updated. On error, the method returns false.
     * 
     * @return
     */
    protected boolean checkMandatoryFields() {
        String type = fDictionary.getNameAsString(COSName.getPDFName(DICTIONARY_KEY_TYPE));
        String subtype = fDictionary.getNameAsString(COSName.getPDFName(DICTIONARY_KEY_SUBTYPE));

        // ---- just check if they are present because of the Helper has already
        // checked them.
        if ((type == null || "".equals(type)) || (subtype == null || "".equals(subtype))) {
            this.fontContainer.addError(
                    new ValidationError(ERROR_FONTS_DICTIONARY_INVALID, "Type and/or Subtype keys are missing"));
            return false;
        }

        // ---- Check presence of baseFont, CMap and CIDFont
        this.basefont = fDictionary.getNameAsString(COSName.getPDFName(FONT_DICTIONARY_KEY_BASEFONT));
        this.descendantFonts = fDictionary.getItem(COSName.getPDFName(FONT_DICTIONARY_KEY_DESCENDANT_FONTS));
        this.encoding = fDictionary.getItem(COSName.getPDFName(FONT_DICTIONARY_KEY_ENCODING));

        if ((basefont == null || "".equals(basefont)) || descendantFonts == null || encoding == null) {
            // ---- baseFont syntax isn't checked because of it is a convention not a
            // rule
            this.fontContainer.addError(new ValidationError(ERROR_FONTS_DICTIONARY_INVALID,
                    "BaseFont, Encoding or DescendantFonts keys are missing"));
            return false;
        }

        // ---- toUnicode is optional, but keep the value if present.
        this.toUnicode = fDictionary.getItem(COSName.getPDFName(FONT_DICTIONARY_KEY_TOUNICODE));
        return true;
    }

    /**
     * This method validates the CIDFont dictionary.
     * 
     * This method returns false and updates the list of errors in the
     * FontContainer if some mandatory fields are missing.
     * 
     * This method calls the processCIDFontTypeX method to check if the font is
     * damaged or not. If the font is damaged, the errors list is updated and the
     * method return false.
     * 
     * @return
     * @throws ValidationException
     */
    protected boolean checkCIDFont() throws ValidationException {

        // ---- a CIDFont is contained in the DescendantFonts array
        COSDocument cDoc = this.handler.getDocument().getDocument();
        COSArray array = COSUtils.getAsArray(descendantFonts, cDoc);

        if (array == null) {
            this.fontContainer.addError(new ValidationError(ERROR_FONTS_CIDKEYED_INVALID,
                    "CIDFont is missing from the DescendantFonts array"));
            return false;
        }
        // ---- in PDF 1.4, this array must contain only one element,
        // because of a PDF/A should be a PDF 1.4, this method returns an error if
        // the array
        // has more than one element.
        if (array.size() != 1) {
            this.fontContainer.addError(new ValidationError(ERROR_FONTS_CIDKEYED_INVALID,
                    "The DescendantFonts array should have one element."));
            return false;
        }

        this.cidFont = COSUtils.getAsDictionary(array.get(0), cDoc);
        if (this.cidFont == null) {
            this.fontContainer.addError(new ValidationError(ERROR_FONTS_CIDKEYED_INVALID,
                    "The DescendantFonts array should have one element with is a dictionary."));
            return false;
        }

        String type = cidFont.getNameAsString(COSName.getPDFName(DICTIONARY_KEY_TYPE));
        String subtype = cidFont.getNameAsString(COSName.getPDFName(DICTIONARY_KEY_SUBTYPE));

        if ((type == null || "".equals(type)) || (subtype == null || "".equals(subtype))) {
            this.fontContainer.addError(
                    new ValidationError(ERROR_FONTS_DICTIONARY_INVALID, "Type and/or Subtype keys are missing"));
            return false;
        }

        boolean isT0 = FONT_DICTIONARY_VALUE_TYPE0.equals(subtype);
        boolean isT2 = FONT_DICTIONARY_VALUE_TYPE2.equals(subtype);
        // ---- Even if these entries are present, values must be checked.
        if (!FONT_DICTIONARY_VALUE_FONT.equals(type) || !(isT0 || isT2)) {
            this.fontContainer.addError(
                    new ValidationError(ERROR_FONTS_DICTIONARY_INVALID, "Type and/or Subtype keys are missing"));
            return false;
        }

        // ---- BaseFont is mandatory
        String bf = cidFont.getNameAsString(COSName.getPDFName(FONT_DICTIONARY_KEY_BASEFONT));
        if (bf == null || "".equals(bf)) {
            this.fontContainer.addError(new ValidationError(ERROR_FONTS_DICTIONARY_INVALID, "BaseFont is missing"));
            return false;
        }

        // ---- checks others mandatory fields
        COSBase sysinfo = cidFont.getItem(COSName.getPDFName(FONT_DICTIONARY_KEY_CID_SYSINFO));
        COSBase fontDesc = cidFont.getItem(COSName.getPDFName(FONT_DICTIONARY_KEY_FONT_DESC));
        COSBase cidToGid = cidFont.getItem(COSName.getPDFName(FONT_DICTIONARY_KEY_CID_GIDMAP));

        boolean result = checkCIDSystemInfo(sysinfo, cDoc);
        if (isT0) {
            result = result && checkCIDToGIDMap(cidToGid, cDoc, false);
            result = result && processCIDFontType0(fontDesc);
        } else {
            result = result && checkCIDToGIDMap(cidToGid, cDoc, true);
            result = result && processCIDFontType2(fontDesc);
        }
        return result;
    }

    /**
     * Check the content of the CIDSystemInfo dictionary. A CIDSystemInfo
     * dictionary must contain :
     * <UL>
     * <li>a Name - Registry
     * <li>a Name - Ordering
     * <li>a Integer - Supplement
     * </UL>
     * 
     * @param sysinfo
     * @param cDoc
     * @return
     */
    private boolean checkCIDSystemInfo(COSBase sysinfo, COSDocument cDoc) {
        COSDictionary cidSysInfo = COSUtils.getAsDictionary(sysinfo, cDoc);
        if (cidSysInfo == null) {
            this.fontContainer.addError(new ValidationError(ERROR_FONTS_CIDKEYED_SYSINFO));
            return false;
        }

        COSBase reg = cidSysInfo.getItem(COSName.getPDFName(FONT_DICTIONARY_KEY_SYSINFO_REGISTRY));
        COSBase ord = cidSysInfo.getItem(COSName.getPDFName(FONT_DICTIONARY_KEY_SYSINFO_ORDERING));
        COSBase sup = cidSysInfo.getItem(COSName.getPDFName(FONT_DICTIONARY_KEY_SYSINFO_SUPPLEMENT));

        if (!(COSUtils.isString(reg, cDoc) && COSUtils.isString(ord, cDoc) && COSUtils.isInteger(sup, cDoc))) {
            this.fontContainer.addError(new ValidationError(ERROR_FONTS_CIDKEYED_SYSINFO));
            return false;
        }

        return true;
    }

    /**
     * This method checks the CIDtoGIDMap entry of the Font dictionary. This
     * element must be a Stream or a Name. If it is a name, it must be "Identity"
     * otherwise, the PDF file isn't a PDF/A-1b.
     * 
     * If the validation fails, the method returns false and the list of error in
     * the FontContainer is updated.
     * 
     * If the CIDtoGIDMap is a Stream, it is parsed as a CMap and the result is
     * kept in the cidToGidMap attribute.
     * 
     * @param ctog
     * @param cDoc
     * @param mandatory
     * @return
     */
    private boolean checkCIDToGIDMap(COSBase ctog, COSDocument cDoc, boolean mandatory) {
        if (COSUtils.isString(ctog, cDoc)) {
            // ---- valid only if the string is Identity
            String ctogStr = COSUtils.getAsString(ctog, cDoc);
            if (!FONT_DICTIONARY_VALUE_CMAP_IDENTITY.equals(ctogStr)) {
                this.fontContainer.addError(
                        new ValidationError(ERROR_FONTS_CIDKEYED_CIDTOGID, "The CIDToGID entry is invalid"));
                return false;
            }
        } else if (COSUtils.isStream(ctog, cDoc)) {
            try {
                COSStream ctogMap = COSUtils.getAsStream(ctog, cDoc);
                this.cidToGidMap = new CMapParser().parse(null, ctogMap.getUnfilteredStream());
            } catch (IOException e) {
                // ---- map can be invalid, return a Validation Error
                this.fontContainer.addError(new ValidationError(ERROR_FONTS_CIDKEYED_CIDTOGID));
                return false;
            }
        } else if (mandatory) {
            this.fontContainer.addError(new ValidationError(ERROR_FONTS_CIDKEYED_CIDTOGID));
            return false;
        }
        return true;
    }

    /**
     * Check the CMap entry.
     * 
     * The CMap entry must be a dictionary in a PDF/A. This entry can be a String
     * only if the String value is Identity-H or Identity-V
     * 
     * @param errors
     * @return
     */
    protected boolean checkCMap(COSBase aEncoding) {
        COSDocument cDoc = this.handler.getDocument().getDocument();

        if (COSUtils.isString(aEncoding, cDoc)) {
            // ---- if encoding is a string, only 2 values are allowed
            String str = COSUtils.getAsString(aEncoding, cDoc);
            if (!(FONT_DICTIONARY_VALUE_CMAP_IDENTITY_V.equals(str)
                    || FONT_DICTIONARY_VALUE_CMAP_IDENTITY_H.equals(str))) {
                this.fontContainer.addError(new ValidationError(ERROR_FONTS_CIDKEYED_INVALID,
                        "The CMap is a string but it isn't an Identity-H/V"));
                return false;
            }
            isIdentityCMap = true;
        } else if (COSUtils.isStream(aEncoding, cDoc)) {
            // ---- If the CMap is a stream, some fields are mandatory
            // and the CIDSytemInfo must be compared with the CIDSystemInfo
            // entry of the CIDFont.
            return processCMapAsStream(COSUtils.getAsStream(aEncoding, cDoc));
        } else {
            // ---- CMap type is invalid
            this.fontContainer.addError(
                    new ValidationError(ERROR_FONTS_CIDKEYED_CMAP_INVALID_OR_MISSING, "The CMap type is invalid"));
            return false;
        }
        return true;
    }

    /**
     * Standard information of a stream element will be checked by the
     * StreamHelper.
     * 
     * This method checks mandatory fields of the CMap stream. This method checks
     * too if the CMap stream is damaged using the CMapParser of the fontbox api.
     * 
     * @param aCMap
     * @return
     */
    private boolean processCMapAsStream(COSStream aCMap) {
        COSDocument cDoc = handler.getDocument().getDocument();

        String type = aCMap.getNameAsString(COSName.getPDFName(DICTIONARY_KEY_TYPE));
        String cmapName = aCMap.getNameAsString(COSName.getPDFName(FONT_DICTIONARY_KEY_CMAP_NAME));
        COSBase sysinfo = aCMap.getItem(COSName.getPDFName(FONT_DICTIONARY_KEY_CID_SYSINFO));
        int wmode = aCMap.getInt(COSName.getPDFName(FONT_DICTIONARY_KEY_CMAP_WMODE));
        COSBase cmapUsed = aCMap.getItem(COSName.getPDFName(FONT_DICTIONARY_KEY_CMAP_USECMAP));

        if (!FONT_DICTIONARY_VALUE_TYPE_CMAP.equals(type)) {
            // ---- CMap type is invalid
            this.fontContainer.addError(
                    new ValidationError(ERROR_FONTS_CIDKEYED_CMAP_INVALID_OR_MISSING, "The CMap type is invalid"));
            return false;
        }

        // ---- check the content of the CIDSystemInfo
        if (!checkCIDSystemInfo(sysinfo, cDoc)) {
            return false;
        }

        if (cmapName == null || "".equals(cmapName) || wmode > 1) {
            this.fontContainer.addError(new ValidationError(ERROR_FONTS_CIDKEYED_CMAP_INVALID_OR_MISSING,
                    "Some elements in the CMap dictionary are missing or invalid"));
            return false;
        }

        try {

            CMap fontboxCMap = new CMapParser().parse(null, aCMap.getUnfilteredStream());
            int wmValue = fontboxCMap.getWMode();
            String cmnValue = fontboxCMap.getName(); //getCmapEntry("CMapName");

            if (wmValue != wmode) {

                this.fontContainer.addError(
                        new ValidationError(ERROR_FONTS_CIDKEYED_CMAP_INVALID_OR_MISSING, "WMode is inconsistent"));
                return false;
            }

            if (!cmnValue.equals(cmapName)) {

                this.fontContainer.addError(new ValidationError(ERROR_FONTS_CIDKEYED_CMAP_INVALID_OR_MISSING,
                        "CMapName is inconsistent"));
                return false;
            }

        } catch (IOException e) {
            this.fontContainer
                    .addError(new ValidationError(ERROR_FONTS_CID_CMAP_DAMAGED, "The CMap type is damaged"));
            return false;
        }

        if (cmapUsed != null) {
            return checkCMap(cmapUsed);
        }

        return true;
    }

    /**
     * The CIDSystemInfo must have the same Registry and Ordering for CMap and
     * CIDFont. This control is useless if CMap is Identity-H or Identity-V so
     * this method is called by the checkCMap method.
     * 
     * @param errors
     * @return
     */
    private boolean compareCIDSystemInfo() {
        COSDocument cDoc = handler.getDocument().getDocument();
        if (!isIdentityCMap) {
            COSDictionary cmsi = COSUtils
                    .getAsDictionary(this.cmap.getItem(COSName.getPDFName(FONT_DICTIONARY_KEY_CID_SYSINFO)), cDoc);
            COSDictionary cfsi = COSUtils.getAsDictionary(
                    this.cidFont.getItem(COSName.getPDFName(FONT_DICTIONARY_KEY_CID_SYSINFO)), cDoc);

            String regCM = COSUtils
                    .getAsString(cmsi.getItem(COSName.getPDFName(FONT_DICTIONARY_KEY_SYSINFO_REGISTRY)), cDoc);
            String ordCM = COSUtils
                    .getAsString(cmsi.getItem(COSName.getPDFName(FONT_DICTIONARY_KEY_SYSINFO_ORDERING)), cDoc);

            String regCF = COSUtils
                    .getAsString(cfsi.getItem(COSName.getPDFName(FONT_DICTIONARY_KEY_SYSINFO_REGISTRY)), cDoc);
            String ordCF = COSUtils
                    .getAsString(cfsi.getItem(COSName.getPDFName(FONT_DICTIONARY_KEY_SYSINFO_ORDERING)), cDoc);

            if (!regCF.equals(regCM) || !ordCF.equals(ordCM)) {
                this.fontContainer.addError(
                        new ValidationError(ERROR_FONTS_CIDKEYED_SYSINFO, "The CIDSystemInfo is inconsistent"));
                return false;
            }
        } // else cmap is null because it is a Identity-H/V
        return true;
    }

    /*
     * (non-Javadoc)
     * 
     * @see
     * net.awl.edoc.pdfa.validation.font.FontValidator#validate(java.util.List)
     */
    public boolean validate() throws ValidationException {
        boolean result = checkMandatoryFields();
        result = result && checkCIDFont();
        result = result && checkCMap(encoding);
        if (result) {
            this.cmap = COSUtils.getAsStream(encoding, handler.getDocument().getDocument());
        }
        result = result && compareCIDSystemInfo();
        return result;
    }

    /**
     * Check if all required fields are present in the PDF file to describe the
     * Font Descriptor.
     * 
     * @param handler
     * @param fontDescriptor
     * @param result
     */
    protected boolean checkFontDescriptorMandatoryFields(PDFontDescriptorDictionary pdFontDesc) {
        boolean fname = false, flags = false, itangle = false, cheight = false;
        boolean fbbox = false, asc = false, desc = false, stemv = false;

        COSDictionary fontDescDictionary = pdFontDesc.getCOSDictionary();
        for (Object key : fontDescDictionary.keySet()) {

            if (!(key instanceof COSName)) {
                this.fontContainer.addError(new ValidationResult.ValidationError(
                        ValidationConstants.ERROR_SYNTAX_DICTIONARY_KEY_INVALID,
                        "Invalid key in The font descriptor"));
                return false;
            }

            String cosName = ((COSName) key).getName();
            if (cosName.equals(FONT_DICTIONARY_KEY_FONTNAME)) {
                fname = true;
            }
            if (cosName.equals(FONT_DICTIONARY_KEY_FLAGS)) {
                flags = true;
            }
            if (cosName.equals(FONT_DICTIONARY_KEY_ITALICANGLE)) {
                itangle = true;
            }
            if (cosName.equals(FONT_DICTIONARY_KEY_CAPHEIGHT)) {
                cheight = true;
            }
            if (cosName.equals(FONT_DICTIONARY_KEY_FONTBBOX)) {
                fbbox = true;
            }
            if (cosName.equals(FONT_DICTIONARY_KEY_ASCENT)) {
                asc = true;
            }
            if (cosName.equals(FONT_DICTIONARY_KEY_DESCENT)) {
                desc = true;
            }
            if (cosName.equals(FONT_DICTIONARY_KEY_STEMV)) {
                stemv = true;
            }

        }

        if (!(fname && flags && itangle && cheight && fbbox && asc && desc && stemv)) {
            this.fontContainer.addError(
                    new ValidationError(ERROR_FONTS_DESCRIPTOR_INVALID, "Some mandatory fields are missing"));
            return false;
        }

        return true;
    }

    /**
     * Process the CIDFontType0 validation.
     * 
     * @param fontDesc
     *          The FontDescriptor which contains the Font Program
     * @return
     * @throws ValidationException
     */
    protected boolean processCIDFontType0(COSBase fontDesc) throws ValidationException {
        COSDictionary fontDescDic = COSUtils.getAsDictionary(fontDesc, handler.getDocument().getDocument());
        if (fontDescDic == null) {
            throw new ValidationException(
                    "Unable to process CIDFontType0 because of the font descriptor is invalid.");
        }
        PDFontDescriptorDictionary pfDescriptor = new PDFontDescriptorDictionary(fontDescDic);
        boolean isValid = checkFontDescriptorMandatoryFields(pfDescriptor);
        isValid = isValid && checkCIDKeyedFontName(pfDescriptor, true);
        isValid = isValid && checkFontFileElement_CIDFontType0(pfDescriptor);
        isValid = isValid && checkCIDSet(pfDescriptor);
        return isValid;
    }

    /**
     * Checks if the FontName contained in the FontDescriptor dictionary of the
     * CID-Keyed Font is present. (The FontName is mandatory according to the PDF
     * Reference.) If the consistency must be checked, the FontName contained in
     * the FontDescriptor is consistent with the BaseName of the font dictionary.
     * If font name is invalid, the list of validation errors in the FontContainer
     * is updated.
     * 
     * @param pfDescriptor
     *          The FontDescriptor dictionary which contains the FontName to check
     * @param checkConsistency
     *          true if the font name must be consistent with the BaseName of the
     *          Font dictionary
     * @return
     */
    protected boolean checkCIDKeyedFontName(PDFontDescriptorDictionary pfDescriptor, boolean checkConsistency) {
        String fontName = pfDescriptor.getFontName();
        String baseName = this.pFont.getBaseFont();

        if (fontName == null) {
            this.fontContainer.addError(new ValidationResult.ValidationError(ERROR_FONTS_DESCRIPTOR_INVALID,
                    "The FontName in font descriptor is missing"));
            return false;
        }

        if (checkConsistency
                && !(fontName.equals(baseName) || fontName.contains(baseName) || baseName.contains(fontName))) {
            this.fontContainer.addError(new ValidationResult.ValidationError(ERROR_FONTS_DESCRIPTOR_INVALID,
                    "The FontName in font descriptor isn't the same as the BaseFont in the Font dictionary"));
            return false;
        }
        return true;
    }

    /**
     * This method return false and updates the FontContainer if the Composite
     * Font TYPE 0 or TYPE1C is damaged. This method checks the Widths
     * consistency.
     * 
     * @param pfDescriptor
     * @return
     */
    boolean checkFontFileElement_CIDFontType0(PDFontDescriptorDictionary pfDescriptor) throws ValidationException {
        // ---- FontFile Validation
        PDStream ff1 = pfDescriptor.getFontFile();
        PDStream ff2 = pfDescriptor.getFontFile2();
        PDStream ff3 = pfDescriptor.getFontFile3();

        boolean onlyOne = (ff1 != null && ff2 == null && ff3 == null) || (ff1 == null && ff2 != null && ff3 == null)
                || (ff1 == null && ff2 == null && ff3 != null);

        if ((ff3 == null) || !onlyOne) {
            this.fontContainer.addError(new ValidationResult.ValidationError(ERROR_FONTS_FONT_FILEX_INVALID,
                    "The FontFile is invalid"));
            return false;
        }

        // ---- Stream validation should be done by the StreamValidateHelper.
        // ---- Process font specific check
        COSStream stream = ff3.getStream();
        if (stream == null) {
            this.fontContainer.addError(new ValidationResult.ValidationError(ERROR_FONTS_FONT_FILEX_INVALID,
                    "The FontFile is missing"));
            this.fontContainer.setFontProgramEmbedded(false);
            return false;
        }

        // ---- Lengthx aren't mandatory for this type of font
        // ---- But the Subtype is a mandatory field with specific values
        String st = stream.getNameAsString(COSName.getPDFName(DICTIONARY_KEY_SUBTYPE));
        if (!(FONT_DICTIONARY_VALUE_TYPE0C.equals(st) || FONT_DICTIONARY_VALUE_TYPE1C.equals(st))) {
            this.fontContainer.addError(new ValidationResult.ValidationError(ERROR_FONTS_FONT_FILEX_INVALID,
                    "The FontFile3 stream doesn't have the right Subtype"));
            return false;
        }

        // ---- try to load the font using the java.awt.font object.
        // ---- if the font is invalid, an exception will be thrown
        try {
            CFFParser cffParser = new CFFParser();
            List<CFFFont> lCFonts = cffParser.parse(ff3.getByteArray());

            if (lCFonts == null || lCFonts.isEmpty()) {
                this.fontContainer.addError(new ValidationResult.ValidationError(ERROR_FONTS_CID_DAMAGED,
                        "The FontFile can't be read"));
                return false;
            }

            return checkCIDFontWidths(lCFonts) && checkFontFileMetaData(pfDescriptor, ff3);
        } catch (IOException e) {
            this.fontContainer.addError(
                    new ValidationResult.ValidationError(ERROR_FONTS_CID_DAMAGED, "The FontFile can't be read"));
            return false;
        }
    }

    /**
     * This method checks the metric consistency of a CIDFontType2.
     * 
     * @param ttf
     *          The TrueTypeFont object which represent the CIDFontType2 Font
     *          Program.
     * @return
     * @throws ValidationException
     */
    protected boolean checkTTFontMetrics(TrueTypeFont ttf) throws ValidationException {
        LinkedHashMap<Integer, Integer> widths = getWidthsArray();
        int defaultWidth = this.cidFont.getInt("DW", 1000);
        int unitsPerEm = ttf.getHeader().getUnitsPerEm();
        int[] glyphWidths = ttf.getHorizontalMetrics().getAdvanceWidth();
        /* In a Mono space font program, the length of the AdvanceWidth array must be one.
         * According to the TrueType font specification, the Last Value of the AdvanceWidth array
         * is apply to the subsequent glyphs. So if the GlyphId is greater than the length of the array
         * the last entry is used.
         */
        int numberOfLongHorMetrics = ttf.getHorizontalHeader().getNumberOfHMetrics();
        CFFType2FontContainer type2FontContainer = ((CompositeFontContainer) this.fontContainer).getCFFType2();
        type2FontContainer.setPdfWidths(widths);
        type2FontContainer.setCmap(this.cidToGidMap);
        type2FontContainer.setDefaultGlyphWidth(defaultWidth);
        type2FontContainer.setFontObject(ttf);
        type2FontContainer.setGlyphWidths(glyphWidths);
        type2FontContainer.setNumberOfLongHorMetrics(numberOfLongHorMetrics);
        type2FontContainer.setUnitsPerEm(unitsPerEm);

        return true;
    }

    /**
     * This method check Metrics consistency of the CIDFontType0.
     * 
     * @param lCFonts
     * @return
     * @throws ValidationException
     */
    protected boolean checkCIDFontWidths(List<CFFFont> lCFonts) throws ValidationException {
        // ---- Extract Widths and default Width from the CIDFont dictionary
        LinkedHashMap<Integer, Integer> widths = getWidthsArray();
        int defaultWidth = this.cidFont.getInt("DW", 1000);
        CFFType0FontContainer type0FontContainer = ((CompositeFontContainer) this.fontContainer).getCFFType0();
        type0FontContainer.setFontObject(lCFonts);
        type0FontContainer.setDefaultGlyphWidth(defaultWidth);
        type0FontContainer.setWidthsArray(widths);

        return true;
    }

    /**
     * This method return false and updates the FontContainer if the Composite
     * Font TYPE 2 is damaged or missing.
     * 
     * @param pfDescriptor
     * @return
     */
    boolean checkFontFileElement_CIDFontType2(PDFontDescriptorDictionary pfDescriptor) throws ValidationException {

        // ---- FontFile Validation
        PDStream ff1 = pfDescriptor.getFontFile();
        PDStream ff2 = pfDescriptor.getFontFile2();
        PDStream ff3 = pfDescriptor.getFontFile3();

        boolean onlyOne = (ff1 != null && ff2 == null && ff3 == null) || (ff1 == null && ff2 != null && ff3 == null)
                || (ff1 == null && ff2 == null && ff3 != null);

        if ((ff2 == null) || !onlyOne) {
            this.fontContainer.addError(new ValidationResult.ValidationError(ERROR_FONTS_FONT_FILEX_INVALID,
                    "The FontFile is invalid"));
            return false;
        }

        // ---- Stream validation should be done by the StreamValidateHelper.
        // ---- Process font specific check
        COSStream stream = ff2.getStream();
        if (stream == null) {
            this.fontContainer.addError(new ValidationResult.ValidationError(ERROR_FONTS_FONT_FILEX_INVALID,
                    "The FontFile is missing"));
            this.fontContainer.setFontProgramEmbedded(false);
            return false;
        }

        boolean hasLength1 = stream.getInt(COSName.getPDFName(FONT_DICTIONARY_KEY_LENGTH1)) > 0;
        if (!hasLength1) {
            this.fontContainer.addError(new ValidationResult.ValidationError(
                    ValidationConstants.ERROR_FONTS_FONT_FILEX_INVALID, "The FontFile is invalid"));
            return false;
        }

        // ---- try to load the font using the java.awt.font object.
        // ---- if the font is invalid, an exception will be thrown
        TrueTypeFont ttf = null;
        try {
            // ---- According to PDF Reference, CIDFontType2 is a TrueType font.
            // ---- Remark : Java.awt.Font throws exception when a CIDFontType2 is
            // parsed even if it is valid.
            ttf = new CIDFontType2Parser(true).parseTTF(new ByteArrayInputStream(ff2.getByteArray()));
        } catch (Exception e) {
            // ---- Exceptionally, Exception is catched Here because of damaged font
            // can throw NullPointer Exception...
            this.fontContainer.addError(
                    new ValidationResult.ValidationError(ERROR_FONTS_CID_DAMAGED, "The FontFile can't be read"));
            return false;
        }

        return checkTTFontMetrics(ttf) && checkFontFileMetaData(pfDescriptor, ff2);
    }

    /**
     * For a CIDFont the width array, there are two formats of width array :
     * <UL>
     * <li>C [W1...Wn] : C is an integer specifying a starting CID value and the
     * array of n numbers specify widths for n consecutive CIDs.
     * <li>Cf Cl W : Defines the same width W for the range Cf to Cl
     * </UL>
     * This method gets a linked hash map of width where the key is a CID and the
     * value is the Width.
     * 
     * @return
     * @throws ValidationException
     */
    protected LinkedHashMap<Integer, Integer> getWidthsArray() throws ValidationException {
        LinkedHashMap<Integer, Integer> widthsMap = new LinkedHashMap<Integer, Integer>();
        COSDocument cDoc = handler.getDocument().getDocument();
        COSBase cBase = this.cidFont.getItem(COSName.getPDFName("W"));
        COSArray wArr = COSUtils.getAsArray(cBase, cDoc);

        for (int i = 0; i < wArr.size();) {

            int firstCid = wArr.getInt(i);

            if (i + 1 >= wArr.size()) {
                throw new ValidationException("Invalid format of the W entry");
            }

            COSBase cb = wArr.getObject(i + 1);
            if (COSUtils.isArray(cb, cDoc)) {

                // ---- First Format
                COSArray seqWidths = COSUtils.getAsArray(cb, cDoc);
                widthsMap.put(firstCid, seqWidths.getInt(0));
                for (int jw = 1; jw < seqWidths.size(); jw++) {
                    widthsMap.put((firstCid + jw), seqWidths.getInt(jw));
                }

                i = i + 2;

            } else {

                // ---- Second Format
                if (i + 2 >= wArr.size()) {
                    throw new ValidationException("Invalid format of the W entry");
                }

                int lastCid = wArr.getInt(i + 1);
                int commonWidth = wArr.getInt(i + 2);
                for (int jw = firstCid; jw <= lastCid; ++jw) {
                    widthsMap.put((firstCid + jw), commonWidth);
                }

                i = i + 3;

            }

        }

        return widthsMap;
    }

    /**
     * If the embedded font is a subset, the CIDSet entry is mandatory and must be
     * a Stream. This method returns true if the CIDSet entry respects conditions,
     * otherwise the method returns false and the FontContainer is updated.
     * 
     * @param pfDescriptor
     * @return
     */
    protected boolean checkCIDSet(PDFontDescriptorDictionary pfDescriptor) {
        if (isSubSet(pfDescriptor.getFontName())) {
            COSBase cidset = pfDescriptor.getCOSDictionary()
                    .getItem(COSName.getPDFName(FONT_DICTIONARY_KEY_CIDSET));
            if (cidset == null || !COSUtils.isStream(cidset, this.handler.getDocument().getDocument())) {
                this.fontContainer
                        .addError(new ValidationResult.ValidationError(ERROR_FONTS_CIDSET_MISSING_FOR_SUBSET,
                                "The CIDSet entry is missing for the Composite Subset"));
                return false;
            }
        }
        return true;
    }

    /**
     * 
     * @param fontDesc
     * @return
     * @throws ValidationException
     */
    protected boolean processCIDFontType2(COSBase fontDesc) throws ValidationException {
        COSDictionary fontDescDic = COSUtils.getAsDictionary(fontDesc, handler.getDocument().getDocument());
        if (fontDescDic == null) {
            throw new ValidationException(
                    "Unable to process CIDFontType2 because of the font descriptor is invalid.");
        }
        PDFontDescriptorDictionary pfDescriptor = new PDFontDescriptorDictionary(fontDescDic);
        boolean isValid = checkFontDescriptorMandatoryFields(pfDescriptor);
        isValid = isValid && checkCIDKeyedFontName(pfDescriptor, false);
        isValid = isValid && checkFontFileElement_CIDFontType2(pfDescriptor);
        isValid = isValid && checkCIDSet(pfDescriptor);
        return isValid;
    }
}