Skip to content

[API Proposal]: Lightweight in-place XpsDocument modification APIs #10361

Open
@edwardneal

Description

@edwardneal

Background and motivation

Relates to #4000, providing the API proposal for the ability to read and rewrite the structure of an XPS document.

There are three objectives with this proposal:

  1. Allow access to an IXpsFixedDocumentSequenceWriter for an XpsDocument's existing FixedDocumentSequence. At present, the only way to access an IXpsFixedDocumentSequenceWriter is to create one - and only one can be created per XpsDocument, so it's not possible to amend existing XpsDocuments.
  2. Allow navigation from a FixedDocumentSequence, FixedDocument and FixedPage's reader APIs to their writer APIs. This should be simple enough: the underlying implementation implements both interfaces, so I expect these three properties could potentially just return this.
  3. Improve the writer APIs, ensuring that every property which can be read from IXps*Reader can be modified from IXps*Writer.

The wider goal is to allow an XPS document's XML structure to be inspected in a way which doesn't involve instantiating full WPF controls (with the thread affinity this implies) or using STA threads in the way that the existing XpsDocumentWriter API requires. With that said, this enables some functionality too - being able to navigate from a page's IXpsFixedPageReader to its IXpsFixedPageWriter means that we can get to the raw XmlWriter APIs, so any future XPS support can build on these types.

Although #4000 relates to modifying and printing XPS documents, this proposal only addresses the modification aspect - I think that's where the largest speed/memory usage improvement lies.

API Proposal

namespace System.Windows.Xps.Packaging;

public interface IXpsFixedDocumentSequenceReader
{
    // Existing
    ReadOnlyCollection<IXpsFixedDocumentReader> FixedDocuments { get; }
    PrintTicket PrintTicket { get; }
    XpsThumbnail Thumbnail { get; }
    Uri Uri { get; }

    IXpsFixedDocumentReader GetFixedDocument (Uri documentSource);

    // New
+   IXpsFixedDocumentSequenceWriter FixedDocumentSequenceWriter { get { throw new NotImplementedException(); } }
}

public interface IXpsFixedDocumentReader : IDocumentStructureProvider
{
    // Existing
    int DocumentNumber { get; }
    XpsStructure DocumentStructure { get; }
    ReadOnlyCollection<IXpsFixedPageReader> FixedPages { get; }
    PrintTicket PrintTicket { get; }
    ICollection<XpsSignatureDefinition> SignatureDefinitions { get; }
    XpsThumbnail Thumbnail { get; }
    Uri Uri { get; }
    
    void AddSignatureDefinition (XpsSignatureDefinition signatureDefinition);
    void CommitSignatureDefinition ();
    IXpsFixedPageReader GetFixedPage (Uri pageSource);
    RemoveSignatureDefinition (XpsSignatureDefinition signatureDefinition);

    // New
+   IXpsFixedDocumentWriter FixedDocumentWriter { get { throw new NotImplementedException(); } }
}

public interface IXpsFixedPageReader : IStoryFragmentProvider
{
    // Existing
    ICollection<XpsColorContext> ColorContexts { get; }
    ICollection<XpsFont> Fonts { get; }
    ICollection<XpsImage> Images { get; }
    int PageNumber { get; }
    PrintTicket PrintTicket { get; }
    ICollection<XpsResourceDictionary> ResourceDictionaries { get; }
    XpsStructure StoryFragment { get; }
    XpsThumbnail Thumbnail { get; }
    Uri Uri { get; }
    XmlReader XmlReader { get; }

    XpsColorContext GetColorContext (Uri uri);
    XpsFont GetFont (Uri uri);
    XpsImage GetImage (Uri uri);
    XpsResource GetResource (Uri resourceUri);
    XpsResourceDictionary GetResourceDictionary (Uri uri);

    // New
+   IXpsFixedPageWriter FixedPageWriter { get { throw new NotImplementedException(); } }
}

public interface IXpsFixedDocumentSequenceWriter
{
    // Existing
    PrintTicket PrintTicket { set; }
    Uri Uri { get; }

    IXpsFixedDocumentWriter AddFixedDocument ();
    XpsThumbnail AddThumbnail (XpsImageType imageType);
    void Commit ();

    // New
+   void RemoveFixedDocument (IXpsFixedDocumentWriter document) => throw new NotImplementedException();
+   void RemoveThumbnail (XpsThumbnail thumbnail) => throw new NotImplementedException();
}

public interface IXpsFixedDocumentWriter : IDocumentStructureProvider
{
    // Existing
    int DocumentNumber { get; }
    PrintTicket PrintTicket { set; }
    Uri Uri { get; }

    IXpsFixedPageWriter AddFixedPage ();
    XpsThumbnail AddThumbnail (XpsImageType imageType);
    void Commit ();

    // New
+   void RemovePage (IXpsFixedPageWriter page) => throw new NotImplementedException();
+   void RemoveThumbnail (XpsThumbnail thumbnail) => throw new NotImplementedException();
}

public interface IXpsFixedPageWriter : IStoryFragmentProvider
{
    // Existing
    IList<string> LinkTargetStream { get; }
    int PageNumber { get; }
    PrintTicket PrintTicket { set; }
    Uri Uri { get; }
    XmlWriter XmlWriter { get; }

    XpsColorContext AddColorContext ();
    XpsFont AddFont ();
    XpsFont AddFont (bool obfuscate);
    XpsFont AddFont (bool obfuscate, bool addRestrictedRelationship);
    XpsImage AddImage (string mimeType);
    XpsImage AddImage (XpsImageType imageType);
    XpsResource AddResource (Type resourceType, Uri resourceUri);
    XpsResourceDictionary AddResourceDictionary ();
    XpsThumbnail AddThumbnail (XpsImageType imageType);
    void Commit ();

    // New
+   void RemoveColorContext (XpsColorContext colorContext) => throw new NotImplementedException();
+   void RemoveFont (XpsFont font) => throw new NotImplementedException();
+   void RemoveImage (XpsImage image) => throw new NotImplementedException();
+   void RemoveResource (XpsResource resource) => throw new NotImplementedException();
+   void RemoveResourceDictionary (XpsResourceDictionary resourceDictionary) => throw new NotImplementedException();
+   void RemoveThumbnail (XpsThumbnail thumbnail) => throw new NotImplementedException();
}

public interface IDocumentStructureProvider
{
    // Existing
    XpsStructure AddDocumentStructure ();

    // New
+   void RemoveDocumentStructure (XpsStructure documentStructure) => throw new NotImplementedException();
}

public interface IStoryFragmentProvider
{
    // Existing
    XpsStructure AddStoryFragment ();

    // New
+   void RemoveStoryFragment (XpsStructure storyFragment) => throw new NotImplementedException();
}

All of these members have default interface members which throw a NotImplementedException; this is to avoid introducing source breaking changes.

API Usage

const string XpsPath = "...";
const string LastPageUrl = "...";
using var existingDocument = new XpsDocument(XpsPath, FileAccess.ReadWrite, CompressionOption.Normal);

var fdsReader = existingDocument.FixedDocumentSequenceReader;
var fdReader = fdsReader.FixedDocuments[0];

ReplaceLastPage(fdReader);
AttachDocumentThumbnail(existingDocument);

void ReplaceLastPage(IXpsFixedDocumentReader fixedDocument)
{
    var fdWriter = fixedDocument.FixedDocumentWriter;
    var lastPage = fixedDocument.GetFixedPage(new Uri(LastPageUrl, UriKind.Relative));
    var lastPageWriter = lastPage.FixedPageWriter;

    fdWriter.RemovePage(lastPageWriter);
    lastPageWriter = fdWriter.AddFixedPage();

    AttachPageThumbnail(lastPageWriter);
}

void AttachPageThumbnail(IXpsFixedPageWriter newPage)
{
    var pgThumbnail = newPage.AddThumbnail(XpsImageType.PngImageType);

    using var newThumbnail = GenerateThumbnail(newPage);
    SetThumbnail(pgThumbnail, newThumbnail);
}

void AttachDocumentThumbnail(XpsDocument document)
{
    var fdsReader = document.FixedDocumentSequenceReader;
    var fdsWriter = fdsReader.FixedDocumentSequenceWriter;
    var fdsThumbnail = fdsReader.Thumbnail ?? fdsWriter.AddThumbnail(XpsImageType.PngImageType);

    using var newThumbnail = GenerateThumbnail(document);
    SetThumbnail(fdsThumbnail, newThumbnail);
}

void SetThumbnail(XpsThumbnail thumbnail, Stream thumbnailStream)
{
    using var xpsThumbnailStream = thumbnail.OpenStream();

    xpsThumbnailStream.SetLength(0);
    thumbnailStream.CopyTo(xpsThumbnailStream);
}

Stream GenerateThumbnail(XpsDocument document) => null;
Stream GenerateThumbnail(IXpsFixedPageWriter newPage) => null;

Alternative Designs

XPS support could be moved into its own library, separate to WPF and similar to OpenXML. This could make XPS support cross-platform, but it'd need a much larger effort and it'd need someone to maintain that library.

For the moment, the key obstacle is the inability to read and update XPS documents without instantiating WPF controls and by using multiple threads. I expect that once it becomes possible to open an XPS document, iterate through its pages and access each page's XmlReader and XmlWriter, this functionality can be used as building blocks if somebody wants to build a strongly-typed API later.

Risks

No response

Metadata

Metadata

Assignees

No one assigned

    Labels

    API suggestionEarly API idea and discussion, it is NOT ready for implementation

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions