PdfConformance.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.pdf;

import com.itextpdf.commons.utils.StringNormalizer;
import com.itextpdf.kernel.xmp.XMPConst;
import com.itextpdf.kernel.xmp.XMPException;
import com.itextpdf.kernel.xmp.XMPMeta;
import com.itextpdf.kernel.xmp.XMPMetaFactory;
import com.itextpdf.kernel.xmp.XMPUtils;
import com.itextpdf.kernel.xmp.options.PropertyOptions;
import com.itextpdf.kernel.xmp.properties.XMPProperty;

/**
 * The class represents possible PDF document conformance.
 */
public class PdfConformance {
    public static final String PDF_A_4_REVISION = "2020";

    public static final PdfConformance PDF_A_1A = new PdfConformance(PdfAConformance.PDF_A_1A);
    public static final PdfConformance PDF_A_1B = new PdfConformance(PdfAConformance.PDF_A_1B);
    public static final PdfConformance PDF_A_2A = new PdfConformance(PdfAConformance.PDF_A_2A);
    public static final PdfConformance PDF_A_2B = new PdfConformance(PdfAConformance.PDF_A_2B);
    public static final PdfConformance PDF_A_2U = new PdfConformance(PdfAConformance.PDF_A_2U);
    public static final PdfConformance PDF_A_3A = new PdfConformance(PdfAConformance.PDF_A_3A);
    public static final PdfConformance PDF_A_3B = new PdfConformance(PdfAConformance.PDF_A_3B);
    public static final PdfConformance PDF_A_3U = new PdfConformance(PdfAConformance.PDF_A_3U);
    public static final PdfConformance PDF_A_4 = new PdfConformance(PdfAConformance.PDF_A_4);
    public static final PdfConformance PDF_A_4E = new PdfConformance(PdfAConformance.PDF_A_4E);
    public static final PdfConformance PDF_A_4F = new PdfConformance(PdfAConformance.PDF_A_4F);

    public static final PdfConformance PDF_UA_1 = new PdfConformance(PdfUAConformance.PDF_UA_1);
    public static final PdfConformance PDF_UA_2 = new PdfConformance(PdfUAConformance.PDF_UA_2);

    public static final PdfConformance PDF_NONE_CONFORMANCE = new PdfConformance();

    private final PdfAConformance aConformance;
    private final PdfUAConformance uaConformance;

    /**
     * Creates a new {@link PdfConformance} instance based on PDF/A and PDF/UA conformance.
     *
     * @param aConformance the PDF/A conformance
     * @param uaConformance the PDF/UA conformance
     */
    public PdfConformance(PdfAConformance aConformance, PdfUAConformance uaConformance) {
        this.aConformance = aConformance;
        this.uaConformance = uaConformance;
    }

    /**
     * Creates a new {@link PdfConformance} instance based on only PDF/A conformance.
     *
     * @param aConformance the PDF/A conformance
     */
    public PdfConformance(PdfAConformance aConformance) {
        this.aConformance = aConformance;
        this.uaConformance = null;
    }

    /**
     * Creates a new {@link PdfConformance} instance based on only PDF/UA conformance.
     *
     * @param uaConformance the PDF/UA conformance
     */
    public PdfConformance(PdfUAConformance uaConformance) {
        this.uaConformance = uaConformance;
        this.aConformance = null;
    }

    /**
     * Creates a new {@link PdfConformance} instance without PDF/A or PDF/UA conformance.
     */
    public PdfConformance() {
        this.aConformance = null;
        this.uaConformance = null;
    }

    /**
     * Checks if any PDF/A conformance is specified.
     *
     * @return {@code true} if PDF/A conformance is specified, otherwise {@code false}
     */
    public boolean isPdfA() {
        return aConformance != null;
    }

    /**
     * Checks if any PDF/UA conformance is specified.
     *
     * @return {@code true} if PDF/UA conformance is specified, otherwise {@code false}
     */
    public boolean isPdfUA() {
        return uaConformance != null;
    }

    /**
     * Checks if any PDF/A or PDF/UA conformance is specified.
     *
     * @return {@code true} if PDF/A or PDF/UA conformance is specified, otherwise {@code false}
     */
    public boolean isPdfAOrUa() {
        return isPdfA() || isPdfUA();
    }

    /**
     * Gets the {@link PdfAConformance} instance if specified.
     *
     * @return the specified {@link PdfAConformance} instance or {@code null}.
     */
    public PdfAConformance getAConformance() {
        return aConformance;
    }

    /**
     * Gets the {@link PdfUAConformance} instance if specified.
     *
     * @return the specified {@link PdfUAConformance} instance or {@code null}.
     */
    public PdfUAConformance getUAConformance() {
        return uaConformance;
    }

    @Override
    public boolean equals(Object o) {
        if (this == o) {
            return true;
        }
        if (o == null || getClass() != o.getClass()) {
            return false;
        }
        PdfConformance that = (PdfConformance) o;
        return aConformance == that.aConformance && uaConformance == that.uaConformance;
    }

    @Override
    public int hashCode() {
        int result = aConformance == null ? 0 : aConformance.hashCode();
        result = 31 * result + (uaConformance == null ? 0 : uaConformance.hashCode());
        return result;
    }

    /**
     * Gets {@link PdfConformance} instance from {@link XMPMeta}.
     *
     * @param meta the meta data to parse
     *
     * @return the {@link PdfConformance} instance
     */
    public static PdfConformance getConformance(XMPMeta meta) {
        if (meta == null) {
            return PdfConformance.PDF_NONE_CONFORMANCE;
        }
        XMPProperty conformanceAXmpProperty = null;
        XMPProperty partAXmpProperty = null;
        PdfAConformance aLevel = null;
        try {
            conformanceAXmpProperty = meta.getProperty(XMPConst.NS_PDFA_ID, XMPConst.CONFORMANCE);
            partAXmpProperty = meta.getProperty(XMPConst.NS_PDFA_ID, XMPConst.PART);
        } catch (XMPException ignored) {
        }
        if (partAXmpProperty != null && (conformanceAXmpProperty != null || "4".equals(partAXmpProperty.getValue()))) {
            aLevel = getAConformance(partAXmpProperty.getValue(),
                    conformanceAXmpProperty == null ? null : conformanceAXmpProperty.getValue());
        }

        XMPProperty partUAXmpProperty = null;
        PdfUAConformance uaLevel = null;
        try {
            partUAXmpProperty = meta.getProperty(XMPConst.NS_PDFUA_ID, XMPConst.PART);
        } catch (XMPException ignored) {
        }
        if (partUAXmpProperty != null) {
            uaLevel = getUAConformance(partUAXmpProperty.getValue());
        }

        return new PdfConformance(aLevel, uaLevel);
    }

    /**
     * Sets required fields into XMP metadata according to passed PDF conformance.
     *
     * @param xmpMeta the xmp metadata to which required PDF conformance fields will be set
     * @param conformance the PDF conformance according to which XMP will be updated
     *
     * @throws XMPException if the file is not well-formed XML or if the parsing fails
     */
    public static void setConformanceToXmp(XMPMeta xmpMeta, PdfConformance conformance) throws XMPException {
        if (conformance == null) {
            return;
        }
        // Don't set any property if property value was set, so if
        // smth was invalid in source document, it will be left as is.
        // But if e.g. for PDF/A-4 revision wasn't specified, we will fix it.
        if (conformance.isPdfUA()) {
            if (xmpMeta.getProperty(XMPConst.NS_PDFUA_ID, XMPConst.PART) == null) {
                xmpMeta.setPropertyInteger(XMPConst.NS_PDFUA_ID, XMPConst.PART,
                        Integer.parseInt(conformance.getUAConformance().getPart()),
                        new PropertyOptions(PropertyOptions.SEPARATE_NODE));
            }
            if (conformance.getUAConformance() == PdfUAConformance.PDF_UA_2 &&
                    xmpMeta.getProperty(XMPConst.NS_PDFUA_ID, XMPConst.REV) == null) {
                xmpMeta.setPropertyInteger(XMPConst.NS_PDFUA_ID, XMPConst.REV, 2024);
            }
        }
        if (conformance.isPdfA()) {
            final PdfAConformance aLevel = conformance.getAConformance();
            if (xmpMeta.getProperty(XMPConst.NS_PDFA_ID, XMPConst.PART) == null) {
                xmpMeta.setProperty(XMPConst.NS_PDFA_ID, XMPConst.PART, aLevel.getPart());
            }
            if (aLevel.getLevel() != null && xmpMeta.getProperty(XMPConst.NS_PDFA_ID, XMPConst.CONFORMANCE) == null) {
                xmpMeta.setProperty(XMPConst.NS_PDFA_ID, XMPConst.CONFORMANCE, aLevel.getLevel());
            }
            if ("4".equals(aLevel.getPart()) && xmpMeta.getProperty(XMPConst.NS_PDFA_ID, XMPConst.REV) == null) {
                xmpMeta.setProperty(XMPConst.NS_PDFA_ID, XMPConst.REV, PdfConformance.PDF_A_4_REVISION);
            }

            if (xmpMeta.getPropertyInteger(XMPConst.NS_PDFUA_ID, XMPConst.PART) != null) {
                XMPMeta taggedExtensionMeta = XMPMetaFactory.parseFromString(PDF_UA_EXTENSION);
                XMPUtils.appendProperties(taggedExtensionMeta, xmpMeta, true, false);
            }
        }
    }

    /**
     * Gets an instance of {@link PdfAConformance} based on passed part and level.
     *
     * @param part the part of PDF/A conformance
     * @param level the level of PDF/A conformance
     *
     * @return the {@link PdfAConformance} instance or {@code null} if there is no PDF/A conformance for passed parameters
     */
    public static PdfAConformance getAConformance(String part, String level) {
        String lowLetter = StringNormalizer.toUpperCase(level);
        boolean aLevel = "A".equals(lowLetter);
        boolean bLevel = "B".equals(lowLetter);
        boolean uLevel = "U".equals(lowLetter);
        boolean eLevel = "E".equals(lowLetter);
        boolean fLevel = "F".equals(lowLetter);

        switch (part) {
            case "1":
                if (aLevel) {
                    return PdfAConformance.PDF_A_1A;
                }
                if (bLevel) {
                    return PdfAConformance.PDF_A_1B;
                }
                break;
            case "2":
                if (aLevel) {
                    return PdfAConformance.PDF_A_2A;
                }
                if (bLevel) {
                    return PdfAConformance.PDF_A_2B;
                }
                if (uLevel) {
                    return PdfAConformance.PDF_A_2U;
                }
                break;
            case "3":
                if (aLevel) {
                    return PdfAConformance.PDF_A_3A;
                }
                if (bLevel) {
                    return PdfAConformance.PDF_A_3B;
                }
                if (uLevel) {
                    return PdfAConformance.PDF_A_3U;
                }
                break;
            case "4":
                if (eLevel) {
                    return PdfAConformance.PDF_A_4E;
                }
                if (fLevel) {
                    return PdfAConformance.PDF_A_4F;
                }
                return PdfAConformance.PDF_A_4;
        }
        return null;
    }

    private static PdfUAConformance getUAConformance(String part) {
        if ("1".equals(part)) {
            return PdfUAConformance.PDF_UA_1;
        }
        if ("2".equals(part)) {
            return PdfUAConformance.PDF_UA_2;
        }
        return null;
    }

    private static final String PDF_UA_EXTENSION =
            "    <x:xmpmeta xmlns:x=\"adobe:ns:meta/\">\n" +
                    "      <rdf:RDF xmlns:rdf=\"http://www.w3.org/1999/02/22-rdf-syntax-ns#\">\n" +
                    "        <rdf:Description rdf:about=\"\" xmlns:pdfaExtension=\"http://www.aiim.org/pdfa/ns/extension/\" xmlns:pdfaSchema=\"http://www.aiim.org/pdfa/ns/schema#\" xmlns:pdfaProperty=\"http://www.aiim.org/pdfa/ns/property#\">\n" +
                    "          <pdfaExtension:schemas>\n" +
                    "            <rdf:Bag>\n" +
                    "              <rdf:li rdf:parseType=\"Resource\">\n" +
                    "                <pdfaSchema:namespaceURI rdf:resource=\"http://www.aiim.org/pdfua/ns/id/\"/>\n" +
                    "                <pdfaSchema:prefix>pdfuaid</pdfaSchema:prefix>\n" +
                    "                <pdfaSchema:schema>PDF/UA identification schema</pdfaSchema:schema>\n" +
                    "                <pdfaSchema:property>\n" +
                    "                  <rdf:Seq>\n" +
                    "                    <rdf:li rdf:parseType=\"Resource\">\n" +
                    "                      <pdfaProperty:category>internal</pdfaProperty:category>\n" +
                    "                      <pdfaProperty:description>PDF/UA version identifier</pdfaProperty:description>\n" +
                    "                      <pdfaProperty:name>part</pdfaProperty:name>\n" +
                    "                      <pdfaProperty:valueType>Integer</pdfaProperty:valueType>\n" +
                    "                    </rdf:li>\n" +
                    "                    <rdf:li rdf:parseType=\"Resource\">\n" +
                    "                      <pdfaProperty:category>internal</pdfaProperty:category>\n" +
                    "                      <pdfaProperty:description>PDF/UA amendment identifier</pdfaProperty:description>\n" +
                    "                      <pdfaProperty:name>amd</pdfaProperty:name>\n" +
                    "                      <pdfaProperty:valueType>Text</pdfaProperty:valueType>\n" +
                    "                    </rdf:li>\n" +
                    "                    <rdf:li rdf:parseType=\"Resource\">\n" +
                    "                      <pdfaProperty:category>internal</pdfaProperty:category>\n" +
                    "                      <pdfaProperty:description>PDF/UA corrigenda identifier</pdfaProperty:description>\n" +
                    "                      <pdfaProperty:name>corr</pdfaProperty:name>\n" +
                    "                      <pdfaProperty:valueType>Text</pdfaProperty:valueType>\n" +
                    "                    </rdf:li>\n" +
                    "                  </rdf:Seq>\n" +
                    "                </pdfaSchema:property>\n" +
                    "              </rdf:li>\n" +
                    "            </rdf:Bag>\n" +
                    "          </pdfaExtension:schemas>\n" +
                    "        </rdf:Description>\n" +
                    "      </rdf:RDF>\n" +
                    "    </x:xmpmeta>";

}