Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
643 changes: 643 additions & 0 deletions PdfSharp.sln

Large diffs are not rendered by default.

2 changes: 2 additions & 0 deletions src/Directory.Packages.props
Original file line number Diff line number Diff line change
Expand Up @@ -40,5 +40,7 @@
<!-- Other packages -->
<PackageVersion Include="System.Resources.Extensions" Version="10.0.5" />
<!-- Needed for PDFsharp-GDI. -->
<!-- Used by samples to decode COSE/CBOR structures inside C2PA manifests. -->
<PackageVersion Include="System.Formats.Cbor" Version="8.0.0" />
</ItemGroup>
</Project>
Original file line number Diff line number Diff line change
Expand Up @@ -264,6 +264,8 @@
<Compile Include="..\PdfSharp\Pdf.Attachments\PdfAFRelationship.cs" Link="Pdf.Attachments\PdfAFRelationship.cs" />
<Compile Include="..\PdfSharp\Pdf.Attachments\PdfEmbeddedFiles.cs" Link="Pdf.Attachments\PdfEmbeddedFiles.cs" />
<Compile Include="..\PdfSharp\Pdf.Attachments\PdfFileSpecification.cs" Link="Pdf.Attachments\PdfFileSpecification.cs" />
<Compile Include="..\PdfSharp\Pdf.C2pa\C2paManager.cs" Link="Pdf.C2pa\C2paManager.cs" />
<Compile Include="..\PdfSharp\Pdf.C2pa\C2paManifestInfo.cs" Link="Pdf.C2pa\C2paManifestInfo.cs" />
<Compile Include="..\PdfSharp\Pdf.Content.Objects\CObjects.cs" Link="Pdf.Content.Objects\CObjects.cs" />
<Compile Include="..\PdfSharp\Pdf.Content.Objects\enum\OpCodeFlags.cs" Link="Pdf.Content.Objects\enum\OpCodeFlags.cs" />
<Compile Include="..\PdfSharp\Pdf.Content.Objects\enum\OpCodeName.cs" Link="Pdf.Content.Objects\enum\OpCodeName.cs" />
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -259,6 +259,8 @@
<Compile Include="..\PdfSharp\Pdf.Attachments\PdfAFRelationship.cs" Link="Pdf.Attachments\PdfAFRelationship.cs" />
<Compile Include="..\PdfSharp\Pdf.Attachments\PdfEmbeddedFiles.cs" Link="Pdf.Attachments\PdfEmbeddedFiles.cs" />
<Compile Include="..\PdfSharp\Pdf.Attachments\PdfFileSpecification.cs" Link="Pdf.Attachments\PdfFileSpecification.cs" />
<Compile Include="..\PdfSharp\Pdf.C2pa\C2paManager.cs" Link="Pdf.C2pa\C2paManager.cs" />
<Compile Include="..\PdfSharp\Pdf.C2pa\C2paManifestInfo.cs" Link="Pdf.C2pa\C2paManifestInfo.cs" />
<Compile Include="..\PdfSharp\Pdf.Content.Objects\CObjects.cs" Link="Pdf.Content.Objects\CObjects.cs" />
<Compile Include="..\PdfSharp\Pdf.Content.Objects\enum\OpCodeFlags.cs" Link="Pdf.Content.Objects\enum\OpCodeFlags.cs" />
<Compile Include="..\PdfSharp\Pdf.Content.Objects\enum\OpCodeName.cs" Link="Pdf.Content.Objects\enum\OpCodeName.cs" />
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,11 @@ public static class PdfAFRelationship // TODO: clean up #US322
/// shall be used if this file specification is a schema definition for the associated object (e.g. an XML schema associated with a metadata stream).
/// </summary>
public const string Schema = nameof(Schema);

/// <summary>
/// shall be used if this file specification denotes a C2PA manifest (content credentials) associated with this PDF document.
/// </summary>
public const string C2paManifest = "C2PA_Manifest";
}
}

125 changes: 125 additions & 0 deletions src/foundation/src/PDFsharp/src/PdfSharp/Pdf.C2pa/C2paManager.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,125 @@
// PDFsharp - A .NET library for processing PDF
// See the LICENSE file in the solution root for more information.

using PdfSharp.Pdf.Advanced;
using PdfSharp.Pdf.Attachments;
using PdfSharp.Pdf.Internal;

namespace PdfSharp.Pdf.C2pa
{
/// <summary>
/// Provides functionality to read C2PA content credentials manifests embedded in a PDF document.
/// </summary>
public class C2paManager : ManagerBase
{
C2paManager(PdfDocument document) : base(document)
{ }

/// <summary>
/// Gets a value indicating whether the document contains at least one C2PA manifest.
/// </summary>
public bool HasManifests => Manifests.Count > 0;

/// <summary>
/// Gets the number of C2PA manifests found in the document.
/// </summary>
public int ManifestCount => Manifests.Count;

/// <summary>
/// Gets information about all C2PA manifests embedded in the document.
/// </summary>
public IReadOnlyList<C2paManifestInfo> Manifests => _manifests ??= ScanManifests();

List<C2paManifestInfo>? _manifests;

List<C2paManifestInfo> ScanManifests()
{
var result = new List<C2paManifestInfo>();
var seen = new HashSet<PdfObjectID>();
var catalog = Document.Catalog;

// The C2PA spec requires the manifest to be listed in the catalog's /AF array, but some
// producers only add it to the /Names/EmbeddedFiles name tree. The name tree is the only
// place we can get the /Names key from, so scan it first.
if (catalog.HasNames)
{
var names = catalog.Names;
if (names.HasEmbeddedFiles)
{
var namesNode = names.GetEmbeddedFiles()?.Names;
if (namesNode != null)
{
var count = namesNode.Count;
for (var idx = 0; idx < count; idx++)
{
var entry = namesNode[idx];
TryAddManifest(entry.Value, entry.Key.Value, result, seen);
}
}
}
}

// Scan /AF as well to catch manifests that are not listed in the name tree.
if (catalog.Elements.TryGetArray<PdfArrayOfDictionaries>(PdfCatalog.Keys.AF, out var afArray))
{
var count = afArray.Elements.Count;
for (var idx = 0; idx < count; idx++)
TryAddManifest(afArray.Elements[idx], "", result, seen);
}

return result;
}

static void TryAddManifest(PdfItem item, string namesKey, List<C2paManifestInfo> result, HashSet<PdfObjectID> seen)
{
PdfReference.Dereference(ref item);
if (item is not PdfDictionary fileSpec)
return;

if (!fileSpec.Elements.TryGetString(PdfFileSpecification.Keys.AFRelationship, out var relationship))
return;
// /AFRelationship is written as a PDF name, whose value includes the leading slash.
var relationshipName = relationship.Length > 0 && relationship[0] == '/' ? relationship[1..] : relationship;
if (relationshipName != PdfAFRelationship.C2paManifest)
return;

var ef = fileSpec.Elements.GetDictionary(PdfFileSpecification.Keys.EF);
var stream = ef?.Elements.GetDictionary("/F") ?? ef?.Elements.GetDictionary("/UF");
if (stream?.Stream == null)
return;

// The same manifest can be reachable through both the name tree and /AF.
if (!seen.Add(stream.ObjectID))
return;

if (!fileSpec.Elements.TryGetString(PdfFileSpecification.Keys.UF, out var fileName) || fileName.Length == 0)
fileSpec.Elements.TryGetString(PdfFileSpecification.Keys.F, out fileName);
fileName ??= "";

fileSpec.Elements.TryGetString(PdfFileSpecification.Keys.Desc, out var description);
description ??= "";

// /Subtype is not a defined file specification key, but producers put the mime type here anyway.
if (!fileSpec.Elements.TryGetString("/Subtype", out var mimeType) || mimeType.Length == 0)
stream.Elements.TryGetString("/Subtype", out mimeType);
mimeType ??= "";

result.Add(new C2paManifestInfo
{
NamesKey = namesKey,
FileName = fileName,
Description = description,
MimeType = mimeType,
AFRelationship = relationshipName,
Data = stream.Stream.UnfilteredValue,
ObjectID = stream.ObjectID
});
}

/// <summary>
/// Gets or creates the C2paManager for the specified document.
/// </summary>
public static C2paManager ForDocument(PdfDocument document)
=> document.C2paManager ??= new(document);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
// PDFsharp - A .NET library for processing PDF
// See the LICENSE file in the solution root for more information.

namespace PdfSharp.Pdf.C2pa
{
/// <summary>
/// Sums up all relevant information of a C2PA content credentials manifest embedded in a PDF file.
/// </summary>
public class C2paManifestInfo
{
/// <summary>
/// The key of the file specification in the /Names array of the /EmbeddedFiles name tree,
/// or the empty string if the manifest was only found via the catalog's /AF array.
/// </summary>
public string NamesKey { get; internal set; } = "";

/// <summary>
/// Gets the name of the file, taken from the file specification's /UF entry, falling back to /F.
/// </summary>
public string FileName { get; internal set; } = "";

/// <summary>
/// Gets the optional description of the file specification.
/// </summary>
public string Description { get; internal set; } = "";

/// <summary>
/// Gets the mime type of the manifest, taken from the file specification's /Subtype entry,
/// falling back to the /Subtype entry of the embedded file stream.
/// </summary>
public string MimeType { get; internal set; } = "";

/// <summary>
/// Gets the /AFRelationship value of the file specification.
/// </summary>
// ReSharper disable once InconsistentNaming
public string AFRelationship { get; internal set; } = "";

/// <summary>
/// Gets the raw bytes of the C2PA manifest (JUMBF box), i.e. the unfiltered content
/// of the embedded file stream.
/// </summary>
public byte[] Data { get; internal set; } = [];

/// <summary>
/// The object ID of the embedded file stream, used to detect duplicates when the
/// same manifest is referenced from both the name tree and the /AF array.
/// </summary>
internal PdfObjectID ObjectID { get; init; }
}
}
47 changes: 46 additions & 1 deletion src/foundation/src/PDFsharp/src/PdfSharp/Pdf.IO/Parser.cs
Original file line number Diff line number Diff line change
Expand Up @@ -422,7 +422,7 @@ void ReadDictionaryStream(PdfDictionary dict, SuppressExceptions? suppressObject
int GetStreamLength(PdfDictionary dict, SuppressExceptions? suppressObjectOrderExceptions)
{
//if (dict.Elements["/F"] != null) // TODO #US373 Just a null check.
if (dict.Elements.HasValue("/F")) // #US373
if (IsExternalFileStream(dict)) // #US373
throw new NotImplementedException("File streams are not yet implemented.");
#if TEST_CODE_
// By uncommenting this and the label below,
Expand Down Expand Up @@ -528,6 +528,51 @@ int GetStreamLength(PdfDictionary dict, SuppressExceptions? suppressObjectOrderE
return lenStream;
}

/// <summary>
/// Determines whether the stream dictionary's /F entry denotes an external file stream
/// (PDF 2.0 §7.3.8.2), i.e. the stream data is stored outside the PDF file and the bytes
/// between 'stream' and 'endstream' must be ignored.
/// Some producers misuse /F for unrelated purposes (e.g. embedded file parameters), so we
/// only treat it as an external file stream if its value actually looks like a file specification.
/// </summary>
static bool IsExternalFileStream(PdfDictionary dict)
{
switch (dict.Elements["/F"])
{
case null:
return false;

// A file specification can be a simple string containing the file name.
case PdfString or PdfStringObject:
return true;

// A dictionary is only a file specification if it looks like one.
case PdfDictionary fileSpec:
return IsFileSpecificationDictionary(fileSpec);

case PdfReference { Value: PdfDictionary fileSpec }:
return IsFileSpecificationDictionary(fileSpec);

// An indirect object not yet read. We assume it is a file specification.
case PdfReference:
return true;

default:
return false;
}
}

static bool IsFileSpecificationDictionary(PdfDictionary dict)
{
if (dict.Elements.TryGetName("/Type", out var type) && type == "/Filespec")
return true;

// The /Type entry is only required if /EF or /RF is present, so we also check
// for the entries a file specification dictionary is made of.
return dict.Elements.HasValue("/FS") || dict.Elements.HasValue("/F") || dict.Elements.HasValue("/UF")
|| dict.Elements.HasValue("/DOS") || dict.Elements.HasValue("/Mac") || dict.Elements.HasValue("/Unix");
}

/// <summary>
/// Try to read 'endstream' after reading the stream content. Sometimes the Length is not exact
/// and ReadSymbol fails. In this case we search the token 'endstream' in the
Expand Down
6 changes: 6 additions & 0 deletions src/foundation/src/PDFsharp/src/PdfSharp/Pdf/PdfDocument.cs
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
using PdfSharp.Logging;
using PdfSharp.Pdf.Advanced;
using PdfSharp.Pdf.Attachments;
using PdfSharp.Pdf.C2pa;
using PdfSharp.Pdf.Filters;
using PdfSharp.Pdf.Forms;
using PdfSharp.Pdf.Internal;
Expand Down Expand Up @@ -884,6 +885,11 @@ internal EmbeddedFilesManager GetEmbeddedFilesManager()
internal PdfAManager GetPdfAManager()
=> PdfAManager ??= PdfAManager.ForDocument(this);

internal C2paManager? C2paManager { get; set; }

internal C2paManager GetC2paManager()
=> C2paManager ??= C2paManager.ForDocument(this);

// ReSharper disable once InconsistentNaming
internal UAManager? UAManager { get; set; }

Expand Down
Loading