From 5f45ee53bd214515530b5217f247ca9e66e84ec4 Mon Sep 17 00:00:00 2001 From: Eliot Jones Date: Sun, 29 Mar 2020 16:43:52 +0100 Subject: [PATCH] #127 add basic pdf/a-1b level compliance to the document builder adds color profiles/output intents and an xmp metadata stream to the document in order to be compliant with pdf/a-1b (basic). this compliance level is toggled on the builder since it will generate larger files and set to 'off/none' by default. pdf/a documents are also not able to use standard fonts so using a font when the compliance level is not none will throw. --- .../PublicApiScannerTests.cs | 1 + .../Writer/PdfDocumentBuilderTests.cs | 28 +++ .../Resources/ICC/sRGB2014.icc | Bin 0 -> 3024 bytes src/UglyToad.PdfPig/UglyToad.PdfPig.csproj | 4 +- .../Writer/Colors/OutputIntentsFactory.cs | 46 ++++ .../Writer/Colors/ProfileStreamReader.cs | 34 +++ src/UglyToad.PdfPig/Writer/PdfAStandard.cs | 17 ++ .../Writer/PdfDocumentBuilder.cs | 34 ++- src/UglyToad.PdfPig/Writer/Xmp/XmpWriter.cs | 220 ++++++++++++++++++ 9 files changed, 378 insertions(+), 6 deletions(-) create mode 100644 src/UglyToad.PdfPig/Resources/ICC/sRGB2014.icc create mode 100644 src/UglyToad.PdfPig/Writer/Colors/OutputIntentsFactory.cs create mode 100644 src/UglyToad.PdfPig/Writer/Colors/ProfileStreamReader.cs create mode 100644 src/UglyToad.PdfPig/Writer/PdfAStandard.cs create mode 100644 src/UglyToad.PdfPig/Writer/Xmp/XmpWriter.cs diff --git a/src/UglyToad.PdfPig.Tests/PublicApiScannerTests.cs b/src/UglyToad.PdfPig.Tests/PublicApiScannerTests.cs index 4e682e83..00a8ff0f 100644 --- a/src/UglyToad.PdfPig.Tests/PublicApiScannerTests.cs +++ b/src/UglyToad.PdfPig.Tests/PublicApiScannerTests.cs @@ -195,6 +195,7 @@ "UglyToad.PdfPig.Util.Adler32Checksum", "UglyToad.PdfPig.Util.IWordExtractor", "UglyToad.PdfPig.Util.DefaultWordExtractor", + "UglyToad.PdfPig.Writer.PdfAStandard", "UglyToad.PdfPig.Writer.PdfDocumentBuilder", "UglyToad.PdfPig.Writer.PdfMerger", "UglyToad.PdfPig.Writer.PdfPageBuilder", diff --git a/src/UglyToad.PdfPig.Tests/Writer/PdfDocumentBuilderTests.cs b/src/UglyToad.PdfPig.Tests/Writer/PdfDocumentBuilderTests.cs index 887cd3c6..b1d1e6a2 100644 --- a/src/UglyToad.PdfPig.Tests/Writer/PdfDocumentBuilderTests.cs +++ b/src/UglyToad.PdfPig.Tests/Writer/PdfDocumentBuilderTests.cs @@ -494,6 +494,34 @@ } } + [Fact] + public void CanGeneratePdfA1BFile() + { + var builder = new PdfDocumentBuilder + { + ArchiveStandard = PdfAStandard.A1B + }; + + var page = builder.AddPage(PageSize.A4); + + var font = builder.AddTrueTypeFont(TrueTypeTestHelper.GetFileBytes("Roboto-Regular.ttf")); + + page.AddText("Howdy!", 12, new PdfPoint(25, 670), font); + + var bytes = builder.Build(); + + WriteFile(nameof(CanGeneratePdfA1BFile), bytes); + + using (var pdf = PdfDocument.Open(bytes, ParsingOptions.LenientParsingOff)) + { + Assert.Equal(1, pdf.NumberOfPages); + + Assert.True(pdf.TryGetXmpMetadata(out var xmp)); + + Assert.NotNull(xmp.GetXDocument()); + } + } + private static void WriteFile(string name, byte[] bytes) { try diff --git a/src/UglyToad.PdfPig/Resources/ICC/sRGB2014.icc b/src/UglyToad.PdfPig/Resources/ICC/sRGB2014.icc new file mode 100644 index 0000000000000000000000000000000000000000..49afbfef10f22a1832590b68369d2f248ea553b9 GIT binary patch literal 3024 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 literal 0 HcmV?d00001 diff --git a/src/UglyToad.PdfPig/UglyToad.PdfPig.csproj b/src/UglyToad.PdfPig/UglyToad.PdfPig.csproj index e537c748..c624a058 100644 --- a/src/UglyToad.PdfPig/UglyToad.PdfPig.csproj +++ b/src/UglyToad.PdfPig/UglyToad.PdfPig.csproj @@ -1,4 +1,4 @@ - + netstandard2.0;net45;net451;net452;net46;net461;net462;net47 latest @@ -12,9 +12,11 @@ + + diff --git a/src/UglyToad.PdfPig/Writer/Colors/OutputIntentsFactory.cs b/src/UglyToad.PdfPig/Writer/Colors/OutputIntentsFactory.cs new file mode 100644 index 00000000..0a785af5 --- /dev/null +++ b/src/UglyToad.PdfPig/Writer/Colors/OutputIntentsFactory.cs @@ -0,0 +1,46 @@ +namespace UglyToad.PdfPig.Writer.Colors +{ + using System; + using System.Collections.Generic; + using Tokens; + + internal static class OutputIntentsFactory + { + private const string SrgbIec61966OutputCondition = "sRGB IEC61966-2.1"; + private const string RegistryName = "http://www.color.org"; + + public static ArrayToken GetOutputIntentsArray(Func objectWriter) + { + var rgbColorCondition = new StringToken(SrgbIec61966OutputCondition); + + var profileBytes = ProfileStreamReader.GetSRgb2014(); + + var compressedBytes = DataCompresser.CompressBytes(profileBytes); + + var profileStreamDictionary = new Dictionary + { + {NameToken.Length, new NumericToken(compressedBytes.Length)}, + {NameToken.N, new NumericToken(3)}, + {NameToken.Filter, NameToken.FlateDecode} + }; + + var stream = new StreamToken(new DictionaryToken(profileStreamDictionary), compressedBytes); + + var written = objectWriter(stream); + + return new ArrayToken(new IToken[] + { + new DictionaryToken(new Dictionary + { + {NameToken.Type, NameToken.OutputIntent }, + {NameToken.S, NameToken.GtsPdfa1}, + {NameToken.OutputCondition, rgbColorCondition}, + {NameToken.OutputConditionIdentifier, rgbColorCondition}, + {NameToken.RegistryName, new StringToken(RegistryName)}, + {NameToken.Info, rgbColorCondition}, + {NameToken.DestOutputProfile, new IndirectReferenceToken(written.Number)} + }), + }); + } + } +} diff --git a/src/UglyToad.PdfPig/Writer/Colors/ProfileStreamReader.cs b/src/UglyToad.PdfPig/Writer/Colors/ProfileStreamReader.cs new file mode 100644 index 00000000..43389164 --- /dev/null +++ b/src/UglyToad.PdfPig/Writer/Colors/ProfileStreamReader.cs @@ -0,0 +1,34 @@ +namespace UglyToad.PdfPig.Writer.Colors +{ + using System; + using System.IO; + using System.Linq; + using PdfFonts.Parser; + + internal static class ProfileStreamReader + { + public static byte[] GetSRgb2014() + { + var resources = typeof(ProfileStreamReader).Assembly.GetManifestResourceNames(); + + var resource = resources.FirstOrDefault(x => + x.EndsWith("sRGB2014.icc", StringComparison.InvariantCultureIgnoreCase)); + + if (resource == null) + { + throw new InvalidOperationException("Could not find the sRGB ICC color profile stream."); + } + + byte[] bytes; + using (var stream = typeof(CMapParser).Assembly.GetManifestResourceStream(resource)) + using (var memoryStream = new MemoryStream()) + { + stream?.CopyTo(memoryStream); + + bytes = memoryStream.ToArray(); + } + + return bytes; + } + } +} diff --git a/src/UglyToad.PdfPig/Writer/PdfAStandard.cs b/src/UglyToad.PdfPig/Writer/PdfAStandard.cs new file mode 100644 index 00000000..b22f28ef --- /dev/null +++ b/src/UglyToad.PdfPig/Writer/PdfAStandard.cs @@ -0,0 +1,17 @@ +namespace UglyToad.PdfPig.Writer +{ + /// + /// The standard of PDF/A compliance for generated documents. + /// + public enum PdfAStandard + { + /// + /// No PDF/A compliance. + /// + None = 0, + /// + /// Compliance with PDF/A1-B. Level B (basic) conformance are standards necessary for the reliable reproduction of a document's visual appearance. + /// + A1B = 1 + } +} \ No newline at end of file diff --git a/src/UglyToad.PdfPig/Writer/PdfDocumentBuilder.cs b/src/UglyToad.PdfPig/Writer/PdfDocumentBuilder.cs index 280ea91b..c1a79ed4 100644 --- a/src/UglyToad.PdfPig/Writer/PdfDocumentBuilder.cs +++ b/src/UglyToad.PdfPig/Writer/PdfDocumentBuilder.cs @@ -1,4 +1,5 @@ -namespace UglyToad.PdfPig.Writer + +namespace UglyToad.PdfPig.Writer { using System; using System.Collections.Generic; @@ -12,6 +13,9 @@ using PdfPig.Fonts.Standard14Fonts; using PdfPig.Fonts.TrueType.Parser; using Tokens; + using Colors; + using Xmp; + using Util.JetBrains.Annotations; /// @@ -24,6 +28,11 @@ private readonly Dictionary fonts = new Dictionary(); private readonly Dictionary images = new Dictionary(); + /// + /// The standard of PDF/A compliance of the generated document. Defaults to . + /// + public PdfAStandard ArchiveStandard { get; set; } = PdfAStandard.None; + /// /// Whether to include the document information dictionary in the produced document. /// @@ -128,6 +137,11 @@ /// An identifier which can be passed to . public AddedFont AddStandard14Font(Standard14Font type) { + if (ArchiveStandard != PdfAStandard.None) + { + throw new NotSupportedException($"PDF/A {ArchiveStandard} requires the font to be embedded in the file, only {nameof(AddTrueTypeFont)} is supported."); + } + var id = Guid.NewGuid(); var name = NameToken.Create($"F{fonts.Count}"); var added = new AddedFont(id, name); @@ -321,11 +335,21 @@ var pagesRef = context.WriteObject(memory, pagesDictionary, reserved); - var catalog = new DictionaryToken(new Dictionary + var catalogDictionary = new Dictionary { - { NameToken.Type, NameToken.Catalog }, - { NameToken.Pages, new IndirectReferenceToken(pagesRef.Number) } - }); + {NameToken.Type, NameToken.Catalog}, + {NameToken.Pages, new IndirectReferenceToken(pagesRef.Number)} + }; + + if (ArchiveStandard != PdfAStandard.None) + { + catalogDictionary[NameToken.OutputIntents] = OutputIntentsFactory.GetOutputIntentsArray(x => context.WriteObject(memory, x)); + var xmpStream = XmpWriter.GenerateXmpStream(DocumentInformation, 1.7m, ArchiveStandard); + var xmpObj = context.WriteObject(memory, xmpStream); + catalogDictionary[NameToken.Metadata] = new IndirectReferenceToken(xmpObj.Number); + } + + var catalog = new DictionaryToken(catalogDictionary); var catalogRef = context.WriteObject(memory, catalog); diff --git a/src/UglyToad.PdfPig/Writer/Xmp/XmpWriter.cs b/src/UglyToad.PdfPig/Writer/Xmp/XmpWriter.cs new file mode 100644 index 00000000..84fd9d0d --- /dev/null +++ b/src/UglyToad.PdfPig/Writer/Xmp/XmpWriter.cs @@ -0,0 +1,220 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Xml.Linq; +using UglyToad.PdfPig.Tokens; + +namespace UglyToad.PdfPig.Writer.Xmp +{ + internal static class XmpWriter + { + private const string Xmptk = "Adobe XMP Core 5.6-c014 79.156797, 2014/08/20-09:53:02 "; + private const string RdfNamespace = "http://www.w3.org/1999/02/22-rdf-syntax-ns#"; + + private const string XmpMetaPrefix = "x"; + private const string XmpMetaNamespace = "adobe:ns:meta/"; + + + private const string DublinCorePrefix = "dc"; + private const string DublinCoreNamespace = "http://purl.org/dc/elements/1.1/"; + + private const string XmpBasicPrefix = "xmp"; + private const string XmpBasicNamespace = "http://ns.adobe.com/xap/1.0/"; + + // ReSharper disable UnusedMember.Local + private const string XmpRightsManagementPrefix = "xmpRights"; + private const string XmpRightsManagementNamespace = "http://ns.adobe.com/xap/1.0/rights/"; + + private const string XmpMediaManagementPrefix = "xmpMM"; + private const string XmpMediaManagementNamespace = "http://ns.adobe.com/xap/1.0/mm/"; + // ReSharper restore UnusedMember.Local + + private const string AdobePdfPrefix = "pdf"; + private const string AdobePdfNamespace = "http://ns.adobe.com/pdf/1.3/"; + + private const string PdfAIdentificationExtensionPrefix = "pdfaid"; + private const string PdfAIdentificationExtensionNamespace = "http://www.aiim.org/pdfa/ns/id/"; + + public static StreamToken GenerateXmpStream(PdfDocumentBuilder.DocumentInformationBuilder builder, decimal version, + PdfAStandard standard) + { + XNamespace xmpMeta = XmpMetaNamespace; + XNamespace rdf = RdfNamespace; + + var emptyRdfAbout = new XAttribute(rdf + "about", string.Empty); + + var rdfDescriptionElement = new XElement(rdf + "Description", emptyRdfAbout); + + // Dublin Core Schema + AddElementsForSchema(rdfDescriptionElement, DublinCorePrefix, DublinCoreNamespace, builder, + new List + { + new SchemaMapper("format", b => "application/pdf"), + new SchemaMapper("creator", b => b.Author), + new SchemaMapper("description", b => b.Subject), + new SchemaMapper("title", b => b.Title) + }); + + // XMP Basic Schema + AddElementsForSchema(rdfDescriptionElement, XmpBasicPrefix, XmpBasicNamespace, builder, + new List + { + new SchemaMapper("CreatorTool", b => b.Creator) + }); + + // Adobe PDF Schema + AddElementsForSchema(rdfDescriptionElement, AdobePdfPrefix, AdobePdfNamespace, builder, + new List + { + new SchemaMapper("PDFVersion", b => "1.7"), + new SchemaMapper("Producer", b => b.Producer) + }); + + var pdfAIdContainer = GetVersionAndConformanceLevelIdentificationElement(rdf, emptyRdfAbout, standard); + + var document = new XDocument( + new XElement(xmpMeta + "xmpmeta", GetNamespaceAttribute(XmpMetaPrefix, XmpMetaNamespace), + new XAttribute(xmpMeta + "xmptk", Xmptk), + new XElement(rdf + "RDF", + GetNamespaceAttribute("rdf", rdf), + rdfDescriptionElement, + pdfAIdContainer + ) + ) + ); + + var xml = document.ToString(SaveOptions.None).Replace("\r\n", "\n"); + xml = $"\n{xml}\n"; + + var bytes = Encoding.UTF8.GetBytes(xml); + + return new StreamToken(new DictionaryToken(new Dictionary + { + {NameToken.Type, NameToken.Metadata}, + {NameToken.Subtype, NameToken.Xml}, + {NameToken.Length, new NumericToken(bytes.Length)} + }), bytes); + } + + private static XAttribute GetNamespaceAttribute(string prefix, XNamespace ns) => new XAttribute(XNamespace.Xmlns + prefix, ns); + + private static void AddElementsForSchema(XElement parent, string prefix, string ns, PdfDocumentBuilder.DocumentInformationBuilder builder, + List mappers) + { + var xns = XNamespace.Get(ns); + parent.Add(GetNamespaceAttribute(prefix, xns)); + + foreach (var mapper in mappers) + { + var value = mapper.ValueFunc(builder); + + if (value == null) + { + continue; + } + + parent.Add(new XElement(xns + mapper.Name, value)); + } + } + + private static XElement GetVersionAndConformanceLevelIdentificationElement(XNamespace rdf, XAttribute emptyRdfAbout, PdfAStandard standard) + { + /* + * The only mandatory XMP entries are those which indicate that the file is a PDF/A-1 file and its conformance level. + * The PDF/A version and conformance level of a file shall be specified using the PDF/A Identification extension schema. + */ + XNamespace pdfaid = PdfAIdentificationExtensionNamespace; + var pdfAidContainer = new XElement(rdf + "Description", emptyRdfAbout, GetNamespaceAttribute(PdfAIdentificationExtensionPrefix, pdfaid)); + + int part; + string conformance; + switch (standard) + { + case PdfAStandard.A1B: + part = 1; + conformance = "B"; + break; + default: + throw new ArgumentOutOfRangeException(nameof(standard), standard, null); + } + + pdfAidContainer.Add(new XElement(pdfaid + "part", part)); + pdfAidContainer.Add(new XElement(pdfaid + "conformance", conformance)); + + return pdfAidContainer; + } + + // Potentially required for further PDF/A versions. + // ReSharper disable once UnusedMember.Local + private static XElement GetExtensionSchemasElement(XNamespace rdf, XAttribute emptyRdfAbout) + { + const string pdfAExtensionSchemaContainerSchemaPrefix = "pdfaExtension"; + const string pdfAExtensionSchemaContainerSchemaUri = "http://www.aiim.org/pdfa/ns/extension/"; + const string pdfASchemaValueTypePrefix = "pdfaSchema"; + const string pdfASchemaValueTypeUri = "http://www.aiim.org/pdfa/ns/schema#"; + const string pdfAPropertyValueTypePrefix = "pdfaProperty"; + const string pdfAPropertyValueTypeUri = "http://www.aiim.org/pdfa/ns/property#"; + + XNamespace pdfaExtension = pdfAExtensionSchemaContainerSchemaUri; + XNamespace pdfaSchema = pdfASchemaValueTypeUri; + XNamespace pdfaProperty = pdfAPropertyValueTypeUri; + + var pdfaSchemaContainer = new XElement(rdf + "Description", emptyRdfAbout, + GetNamespaceAttribute(pdfAExtensionSchemaContainerSchemaPrefix, pdfaExtension), + GetNamespaceAttribute(pdfASchemaValueTypePrefix, pdfaSchema), + GetNamespaceAttribute(pdfAPropertyValueTypePrefix, pdfaProperty)); + + var schemaBag = new XElement(pdfaExtension + "schemas", + new XElement(rdf + "Bag")); + + var individualSchemaContainer = new XElement(rdf + "li", new XAttribute(rdf + "parseType", "Resource")); + + individualSchemaContainer.Add(new XElement(pdfaSchema + "namespaceURI", PdfAIdentificationExtensionNamespace)); + individualSchemaContainer.Add(new XElement(pdfaSchema + "prefix", PdfAIdentificationExtensionPrefix)); + individualSchemaContainer.Add(new XElement(pdfaSchema + "schema", "PDF/A ID Schema")); + + var seqContainer = new XElement(pdfaSchema + "property", new XElement(rdf + "Seq")); + + var seq = seqContainer.Elements().Last(); + + seq.Add(GetSchemaPropertyListItem(rdf, pdfaProperty, "part", "Part of PDF/A standard", "internal", "Integer")); + seq.Add(GetSchemaPropertyListItem(rdf, pdfaProperty, "amd", "Amendment of PDF/A standard")); + seq.Add(GetSchemaPropertyListItem(rdf, pdfaProperty, "conformance", "Conformance level of PDF/A standard")); + + individualSchemaContainer.Add(seqContainer); + + schemaBag.Elements().Last().Add(individualSchemaContainer); + + pdfaSchemaContainer.Add(schemaBag); + + return pdfaSchemaContainer; + } + + private static XElement GetSchemaPropertyListItem(XNamespace rdfNs, + XNamespace pdfaPropertyNs, string name, string description, string category = "internal", string valueType = "Text") + { + var li = new XElement(rdfNs + "li", new XAttribute(rdfNs + "parseType", "Resource")); + + li.Add(new XElement(pdfaPropertyNs + "category", category)); + li.Add(new XElement(pdfaPropertyNs + "description", description)); + li.Add(new XElement(pdfaPropertyNs + "name", name)); + li.Add(new XElement(pdfaPropertyNs + "valueType", valueType)); + + return li; + } + + private class SchemaMapper + { + public string Name { get; } + + public Func ValueFunc { get; } + + public SchemaMapper(string name, Func valueFunc) + { + Name = name; + ValueFunc = valueFunc; + } + } + } +}