mirror of
https://github.com/UglyToad/PdfPig.git
synced 2025-10-14 02:44:58 +08:00
#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.
This commit is contained in:
@@ -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",
|
||||
|
@@ -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
|
||||
|
BIN
src/UglyToad.PdfPig/Resources/ICC/sRGB2014.icc
Normal file
BIN
src/UglyToad.PdfPig/Resources/ICC/sRGB2014.icc
Normal file
Binary file not shown.
@@ -1,4 +1,4 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
<PropertyGroup>
|
||||
<TargetFrameworks>netstandard2.0;net45;net451;net452;net46;net461;net462;net47</TargetFrameworks>
|
||||
<LangVersion>latest</LangVersion>
|
||||
@@ -12,9 +12,11 @@
|
||||
</PropertyGroup>
|
||||
<ItemGroup>
|
||||
<None Remove="Resources\CMap\*" />
|
||||
<None Remove="Resources\ICC\*" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<EmbeddedResource Include="Resources\CMap\*" />
|
||||
<EmbeddedResource Include="Resources\ICC\*" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Microsoft.SourceLink.GitHub" Version="1.0.0">
|
||||
|
46
src/UglyToad.PdfPig/Writer/Colors/OutputIntentsFactory.cs
Normal file
46
src/UglyToad.PdfPig/Writer/Colors/OutputIntentsFactory.cs
Normal file
@@ -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<IToken, ObjectToken> objectWriter)
|
||||
{
|
||||
var rgbColorCondition = new StringToken(SrgbIec61966OutputCondition);
|
||||
|
||||
var profileBytes = ProfileStreamReader.GetSRgb2014();
|
||||
|
||||
var compressedBytes = DataCompresser.CompressBytes(profileBytes);
|
||||
|
||||
var profileStreamDictionary = new Dictionary<NameToken, IToken>
|
||||
{
|
||||
{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, IToken>
|
||||
{
|
||||
{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)}
|
||||
}),
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
34
src/UglyToad.PdfPig/Writer/Colors/ProfileStreamReader.cs
Normal file
34
src/UglyToad.PdfPig/Writer/Colors/ProfileStreamReader.cs
Normal file
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
17
src/UglyToad.PdfPig/Writer/PdfAStandard.cs
Normal file
17
src/UglyToad.PdfPig/Writer/PdfAStandard.cs
Normal file
@@ -0,0 +1,17 @@
|
||||
namespace UglyToad.PdfPig.Writer
|
||||
{
|
||||
/// <summary>
|
||||
/// The standard of PDF/A compliance for generated documents.
|
||||
/// </summary>
|
||||
public enum PdfAStandard
|
||||
{
|
||||
/// <summary>
|
||||
/// No PDF/A compliance.
|
||||
/// </summary>
|
||||
None = 0,
|
||||
/// <summary>
|
||||
/// Compliance with PDF/A1-B. Level B (basic) conformance are standards necessary for the reliable reproduction of a document's visual appearance.
|
||||
/// </summary>
|
||||
A1B = 1
|
||||
}
|
||||
}
|
@@ -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;
|
||||
|
||||
/// <summary>
|
||||
@@ -24,6 +28,11 @@
|
||||
private readonly Dictionary<Guid, FontStored> fonts = new Dictionary<Guid, FontStored>();
|
||||
private readonly Dictionary<Guid, ImageStored> images = new Dictionary<Guid, ImageStored>();
|
||||
|
||||
/// <summary>
|
||||
/// The standard of PDF/A compliance of the generated document. Defaults to <see cref="PdfAStandard.None"/>.
|
||||
/// </summary>
|
||||
public PdfAStandard ArchiveStandard { get; set; } = PdfAStandard.None;
|
||||
|
||||
/// <summary>
|
||||
/// Whether to include the document information dictionary in the produced document.
|
||||
/// </summary>
|
||||
@@ -128,6 +137,11 @@
|
||||
/// <returns>An identifier which can be passed to <see cref="PdfPageBuilder.AddText"/>.</returns>
|
||||
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<NameToken, IToken>
|
||||
var catalogDictionary = new Dictionary<NameToken, IToken>
|
||||
{
|
||||
{ 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);
|
||||
|
||||
|
220
src/UglyToad.PdfPig/Writer/Xmp/XmpWriter.cs
Normal file
220
src/UglyToad.PdfPig/Writer/Xmp/XmpWriter.cs
Normal file
@@ -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<SchemaMapper>
|
||||
{
|
||||
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<SchemaMapper>
|
||||
{
|
||||
new SchemaMapper("CreatorTool", b => b.Creator)
|
||||
});
|
||||
|
||||
// Adobe PDF Schema
|
||||
AddElementsForSchema(rdfDescriptionElement, AdobePdfPrefix, AdobePdfNamespace, builder,
|
||||
new List<SchemaMapper>
|
||||
{
|
||||
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 = $"<?xpacket begin=\"\ufeff\" id=\"W5M0MpCehiHzreSzNTczkc9d\"?>\n{xml}\n<?xpacket end=\"r\"?>";
|
||||
|
||||
var bytes = Encoding.UTF8.GetBytes(xml);
|
||||
|
||||
return new StreamToken(new DictionaryToken(new Dictionary<NameToken, IToken>
|
||||
{
|
||||
{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<SchemaMapper> 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<PdfDocumentBuilder.DocumentInformationBuilder, string> ValueFunc { get; }
|
||||
|
||||
public SchemaMapper(string name, Func<PdfDocumentBuilder.DocumentInformationBuilder, string> valueFunc)
|
||||
{
|
||||
Name = name;
|
||||
ValueFunc = valueFunc;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
Reference in New Issue
Block a user