Merge XPS documents

Basicly I want to merge 1-n xps-documents into a new package, either just add all source-documents or just add all the pages of the source-documents into the new package...

I've tried out with following code, which writes all the source-documents-pages into a new xps-document, however this is very very slow!!!

public void CreateXPSStreamPages(string targetDocument, List<string> list)
{
Package container = Package.Open(targetDocument, FileMode.Create);
XpsDocument xpsDoc = new XpsDocument(container);
XpsDocumentWriter writer = XpsDocument.CreateXpsDocumentWriter(xpsDoc);

SerializerWriterCollator vxpsd = writer.CreateVisualsCollator();
vxpsd.BeginBatchWrite();
foreach (string sourceDocument in list)
{
AddXPSDocument(sourceDocument, vxpsd);
}
vxpsd.EndBatchWrite();
container.Close();
}

public void AddXPSDocument(string sourceDocument, SerializerWriterCollator vxpsd)
{
XpsDocument xpsOld = new XpsDocument(sourceDocument, FileAccess.Read);
FixedDocumentSequence seqOld = xpsOld.GetFixedDocumentSequence();
foreach (DocumentReference r in seqOld.References)
{
FixedDocument d = r.GetDocument(false);
foreach (PageContent pc in d.Pages)
{
FixedPage fixedPage = pc.GetPageRoot(false);
double width = fixedPage.Width;
double height = fixedPage.Height;
Size sz = new Size(width, height);
fixedPage.Width = width;
fixedPage.Height = height;
fixedPage.Measure(sz);
fixedPage.Arrange(new Rect(new Point(), sz));
//fixedPage.UpdateLayout();

ContainerVisual newPage = new ContainerVisual();
newPage.Children.Add(fixedPage);
//test: add Watermark from Feng Yuan sample
//newPage.Children.Add(CreateWatermark(width, height, "Watermark"));

vxpsd.Write(newPage);
}
}
xpsOld.Close();
}


guess it's slow due the following method calls:

fixedPage.Measure(sz);
fixedPage.Arrange(new Rect(new Point(), sz));

So what is the best/fastest way to merge 1-n XPS-documents into a new xps-document/package

any help/ideas/sample code would be fine...
thanx in advance for help...

kind regards,
Jo


Answer this question

Merge XPS documents

  • vcboy

    Hi,

    I am having a set of fixeddocuments which are generated from individual xaml files. I am adding each fixed document to a dictionary collection. From the dictionay i need to get some of the fixeddocuments and print that or save as a XPS document. I have to iterate through the dictionary a number of times to generate the fixeddocumentsequnce since each time i have to form a fixeddocumentsequnce differenly. Mostly it will contain repeated documents. Once if i add a fixeddocument from the dictionary to a fixeddocumentsequnce and not able to add the same document again. There are no clonable or copy options available for this. When i try to add that document it throws an exception "Specifiied element is already logical of other" .But the conversion of XAML to a fixeddocument takes time so for each iteration and the performance is decreasing.

    Can anybody suggest how to make a copy of the fixeddocument



  • Spanglishone

    Hi,

    I would assume that merging the documents in a single package will be more efficient, than extracting every page and inserting them in a single doc. Xps has the ability to contain multiple docs.

    Do not know how to do this with the WPF api, I'm not an expert on that. I could help you doing this with C/C++, contact me direct if this would interest you.

    regards,
    nixps


  • Tom Janssen

    Code for images recourse... Copying the images from old document to new document..

    #region Copy Image resources to target

    foreach (XpsImage image in page.Images)

    {

    // check if we have already added that image...

    imageUri = image.Uri.ToString();

    if (!imageList.ContainsKey(imageUri))

    {

    // ...no, then add the image

    XpsImage newImage = pageWriter.AddImage(XpsImageType.JpegImageType);

    newImageUri = newImage.Uri.ToString();

    CopyStream(image.GetStream(), newImage.GetStream());

    newImage.Commit();

    imageList.Add(imageUri, newImageUri);

    }

    else

    {

    //yes, so just add a reference to the image resource we already have added

    newImageUri = (string)imageList[imageUri];

    pageWriter.AddResource(typeof(XpsImage), new Uri(newImageUri, UriKind.RelativeOrAbsolute));

    }

    // update image resource in source-page

    pageContent = pageContent.Replace(imageUri, newImageUri);

    }

    #endregion



  • smilertoo

    As it was helpful for some others, I'll post it here again to keep the MergeXPS-Samples in one thread...

    This one is much faster than my other merge-samples, as it's just adding original documents without modifying page/document-content, but should keep original documents...


    public void CreateXPSStream(string targetDocument, List<string> list)
    {
    if (File.Exists(targetDocument))
    {
    File.Delete(targetDocument);
    }

    Package container = Package.Open(targetDocument, FileMode.Create);
    XpsDocument xpsDoc = new XpsDocument(container);

    FixedDocumentSequence seqNew = new FixedDocumentSequence();

    foreach (string sourceDocument in list)
    {
    AddXPSDocuments(sourceDocument, seqNew);
    }

    XpsDocumentWriter xpsWriter = XpsDocument.CreateXpsDocumentWriter(xpsDoc);
    xpsWriter.Write(seqNew);
    xpsDoc.Close();
    container.Close();
    }

    public void AddXPSDocuments(string sourceDocument, FixedDocumentSequence seqNew)
    {
    XpsDocument xpsOld = new XpsDocument(sourceDocument, FileAccess.Read);
    FixedDocumentSequence seqOld = xpsOld.GetFixedDocumentSequence();

    foreach (DocumentReference r in seqOld.References)
    {
    DocumentReference newRef = new DocumentReference();
    (newRef as IUriContext).BaseUri = (r as IUriContext).BaseUri;
    newRef.Source = r.Source;
    seqNew.References.Add(newRef);
    }
    }


  • Siddartha Pal

    how do you add the FixedDocument to your new FixedDocumentSequence

    If you add document references it shouldn't be a problem to add the same references several times to a FixedDocumentSequence...


    Code Snippet

    // create a new FixedDocumentSequence
    FixedDocumentSequence myFixedDocSeq = new FixedDocumentSequence();

    // open a test-document for this example...
    using (XpsDocument xpsDoc = new XpsDocument("20pages.xps", FileAccess.Read))
    {
    // and get the FixedDocumentSequence of it...
    FixedDocumentSequence fixedDocSeq = xpsDoc.GetFixedDocumentSequence();

    // get the DocumentReference to first document (or just iterate over all)
    DocumentReference docRef = fixedDocSeq.References[0];
    // create a new documentReference and set BaseUr/Source from original document reference
    DocumentReference newRef = new DocumentReference();
    (newRef as IUriContext).BaseUri = (docRef as IUriContext).BaseUri;
    newRef.Source = docRef.Source;

    // add DocumentReference to our new fixedDocumentSequence
    myFixedDocSeq.References.Add(newRef);
    // and some more to prove it works using same document references in the FixedDocumentSequence
    myFixedDocSeq.References.Add(newRef);
    myFixedDocSeq.References.Add(newRef);
    }

    // get default PrintQueue
    PrintQueue printQueue = LocalPrintServer.GetDefaultPrintQueue();
    // get a XpsDocumentWriter
    XpsDocumentWriter xpsWriter = PrintQueue.CreateXpsDocumentWriter(printQueue);
    // and print out our new FixedDocumentSequence, which holds 3 references to the same
    // document now, hence it prints the document 3 times also... :P
    xpsWriter.Write(myFixedDocSeq);




  • Mosesm

    well, I know XPS has ability to contain multiple docs... the code above was just one of my tests I did and it just creates a package containing a single document with ALL the pages from the source-documents...

    I've also tried to create a package which contains multiple documents (hence all the source-documents) instead of a single document, however the merging was in both ways really slow and though I'm not sure if I've done correctly and if it's best way to use WPF-API for it !

    Ofcourse I'm interested in how you do that without WPF-API, all help is welcome and I'll for sure will contact you! :)

    Anyone got some ideas how to merge complete docs (and it's resources) to a new package (without recreating each page)

    Anyone got some more ideas (c;

  • calvinkwoo

    Ok, I have done some more tests and post another method for streaming xps-documents into a new package, maybe it's helpful for others also... feedback would be nice!

    However this was just a quick hack and could be optimized for sure, but it shows that this way is a bit faster than my first try... The trickiest part was moving an obfuscated font... (c;
    Also do note that moving other resources like images is not implemented in that example, but could be done the same way...

    Here are some results on my test-machine (which isn't the fastest) and the documents only contained font-resources and I used same documents (so this is not a "real, serious" measurment to rely on, but shows it's faster - just test on your machine with your own document):

    7 documents, total of 35 pages:

    method #1 : 2634ms / avg. 75,25ms/page
    method #2 : 801ms / avg. 22,88ms/page

    70 documents, total of 350 pages:

    method #1 : 21s / avg. 0,06s/page
    method #2 : 5s / avg. 0,014s/page

    700 documents, total of 3500 pages:

    method #1 : 421s / avg. 0,12s/page
    method #2 : 214s / avg. 0,06s/page

    so it seems that the 2nd method is faster, because it doesn't need to instantiate drawing primitivies and rendering etc...

    Still I have a few questions about, which I hope someone of the Microsoft XPS-Team (or any other) could answer:

    - Is this a "legitim" way of merging documents
    - Is there a way to find out which font a XpsFont-resource is I only saw GUIDS... This would be helpful for optimizing resources in the new stream... (why add an Arial-font x-times when for example the same font is used by several documents... )
    - Same for images

    any ideas, feedback etc. would be nice...
    regards from Germany
    -Jo

    _____________________

    public void CreateStream(string targetDocument, List<string> list)
    {
    if (File.Exists(targetDocument))
    {
    File.Delete(targetDocument);
    }

    // create global fontList to keep track of added font-resources
    fontList = new Hashtable();

    XpsDocument newXpsDoc = new XpsDocument(targetDocument, FileAccess.ReadWrite);
    IXpsFixedDocumentSequenceWriter docSeqWriter = newXpsDoc.AddFixedDocumentSequence();

    foreach (string sourceDocument in list)
    {
    AddToStream(sourceDocument, docSeqWriter);
    }

    docSeqWriter.Commit();
    newXpsDoc.Close();
    }

    public void AddToStream(string sourceDocument, IXpsFixedDocumentSequenceWriter docSeqWriter)
    {
    string fontUri;
    string newFontUri;

    // Open original document for reading...
    XpsDocument xpsOld = new XpsDocument(sourceDocument, FileAccess.Read);
    IXpsFixedDocumentSequenceReader fixedDocSeqReader = xpsOld.FixedDocumentSequenceReader;

    foreach (IXpsFixedDocumentReader docReader in fixedDocSeqReader.FixedDocuments)
    {
    // Add new document to target stream
    IXpsFixedDocumentWriter docWriter = docSeqWriter.AddFixedDocument();

    foreach (IXpsFixedPageReader fixedPageReader in docReader.FixedPages)
    {
    // read xml of source-page
    fixedPageReader.XmlReader.Read();
    string pageContent = fixedPageReader.XmlReader.ReadOuterXml();

    // Add new page to target stream
    IXpsFixedPageWriter pageWriter = docWriter.AddFixedPage();

    #region Copy Font resources to target
    // Add Font Resources...
    foreach (XpsFont font in fixedPageReader.Fonts)
    {
    // check if we have already added that font...
    fontUri = font.Uri.ToString();
    if (!fontList.ContainsKey(fontUri))
    {
    // ...no, then add the font
    XpsFont newFont = pageWriter.AddFont(font.IsObfuscated, font.IsRestricted);
    newFontUri = newFont.Uri.ToString();
    if (font.IsObfuscated)
    {
    CopyObfuscatedStream(fontUri, newFontUri, font.GetStream(), newFont.GetStream());
    }
    else
    {
    CopyStream(font.GetStream(), newFont.GetStream());
    }
    newFont.Commit();

    fontList.Add(fontUri, newFontUri);
    }
    else
    {
    //yes, so just add a reference to the font resource we already have added
    newFontUri = (string) fontList[fontUri];
    pageWriter.AddResource(typeof(XpsFont), new Uri(newFontUri, UriKind.RelativeOrAbsolute));
    }

    // update font resource in source-page
    pageContent = pageContent.Replace(fontUri, newFontUri);
    }
    #endregion

    // ---------------------------------------------------------------
    // at this point do the same with other resources like images etc.
    // ---------------------------------------------------------------

    // now when done, write updated xml to new page
    pageWriter.XmlWriter.WriteRaw(pageContent);
    pageWriter.Commit();
    }
    docWriter.Commit();
    }

    }

    private byte[] ConvertGuidToByteArray(string resourceName)
    {
    // Get the GUID byte from the resource name. Typical Font name:
    // /Resources/Fonts/xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx.ODTTF
    int startPos = resourceName.LastIndexOf('/') + 1;
    int length = resourceName.LastIndexOf('.') - startPos;
    string g = resourceName.Substring(startPos, length);

    Guid guid = new Guid(g);
    string guidString = guid.ToString("N");

    // Parsing the guid string and coverted into byte value
    byte[] guidBytes = new byte[16];
    for (int i = 0; i < guidBytes.Length; i++)
    {
    guidBytesIdea = Convert.ToByte(guidString.Substring(i * 2, 2), 16);
    }

    return guidBytes;
    }

    private void CopyObfuscatedStream(string srcResource, string dstResource, Stream srcStream, Stream dstSteam)
    {
    int bufSize = 0x1000;
    int guidByteSize = 16;
    int obfuscatedByte = 32;

    // extract Guids from resourceName and convert guid to byte array
    byte[] obfuscatedBytes = ConvertGuidToByteArray(srcResource);
    byte[] guidBytes = ConvertGuidToByteArray(dstResource);

    // XOR the first 32 byte of the source resource stream with guid byte
    byte[] buf = new byte[obfuscatedByte];
    srcStream.Read(buf, 0, obfuscatedByte);

    for (int i = 0; i < obfuscatedByte; i++)
    {
    int guidBytesPos = guidByteSize - (i % guidBytes.Length) - 1;
    // xor with old guid to get original
    bufIdea ^= obfuscatedBytes[guidBytesPos];
    // xor with new guid to obfuscate
    bufIdea ^= guidBytes[guidBytesPos];
    }
    dstSteam.Write(buf, 0, obfuscatedByte);

    // copy remaining stream from source without obfuscation
    buf = new byte[bufSize];

    int bytesRead = 0;
    while ((bytesRead = srcStream.Read(buf, 0, bufSize)) > 0)
    {
    dstSteam.Write(buf, 0, bytesRead);
    }
    }

    public static void CopyStream(Stream srcStream, Stream dstSteam)
    {
    int arraySz = 1024 * 1024;
    byte[] buffer = new byte[arraySz]; //1MB
    long bytesRemaining = srcStream.Length;

    while (bytesRemaining > 0)
    {
    long copyLng = (bytesRemaining > arraySz) arraySz : bytesRemaining;
    srcStream.Read(buffer, 0, (int)copyLng);
    dstSteam.Write(buffer, 0, (int)copyLng);
    bytesRemaining -= copyLng;
    }
    }


  • Merge XPS documents