Description
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:
- Allow access to an
IXpsFixedDocumentSequenceWriter
for anXpsDocument
's existingFixedDocumentSequence
. At present, the only way to access anIXpsFixedDocumentSequenceWriter
is to create one - and only one can be created perXpsDocument
, so it's not possible to amend existingXpsDocument
s. - Allow navigation from a
FixedDocumentSequence
,FixedDocument
andFixedPage
'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 returnthis
. - Improve the writer APIs, ensuring that every property which can be read from
IXps*Reader
can be modified fromIXps*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