#region PDFsharp - A .NET library for processing PDF // // Authors: // Stefan Lange // // Copyright (c) 2005-2017 empira Software GmbH, Cologne Area (Germany) // // http://www.pdfsharp.com // http://sourceforge.net/projects/pdfsharp // // Permission is hereby granted, free of charge, to any person obtaining a // copy of this software and associated documentation files (the "Software"), // to deal in the Software without restriction, including without limitation // the rights to use, copy, modify, merge, publish, distribute, sublicense, // and/or sell copies of the Software, and to permit persons to whom the // Software is furnished to do so, subject to the following conditions: // // The above copyright notice and this permission notice shall be included // in all copies or substantial portions of the Software. // // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL // THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING // FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER // DEALINGS IN THE SOFTWARE. #endregion using System; using System.Collections.Generic; using System.Diagnostics; using System.Collections; using PdfSharp.Pdf.IO; using PdfSharp.Pdf.Advanced; using PdfSharp.Pdf.Annotations; namespace PdfSharp.Pdf { /// /// Represents the pages of the document. /// [DebuggerDisplay("(PageCount={Count})")] public sealed class PdfPages : PdfDictionary, IEnumerable { internal PdfPages(PdfDocument document) : base(document) { Elements.SetName(Keys.Type, "/Pages"); Elements[Keys.Count] = new PdfInteger(0); } internal PdfPages(PdfDictionary dictionary) : base(dictionary) { } /// /// Gets the number of pages. /// public int Count { get { return PagesArray.Elements.Count; } } /// /// Gets the page with the specified index. /// public PdfPage this[int index] { get { if (index < 0 || index >= Count) throw new ArgumentOutOfRangeException("index", index, PSSR.PageIndexOutOfRange); PdfDictionary dict = (PdfDictionary)((PdfReference)PagesArray.Elements[index]).Value; if (!(dict is PdfPage)) dict = new PdfPage(dict); return (PdfPage)dict; } } /// /// Finds a page by its id. Transforms it to PdfPage if necessary. /// internal PdfPage FindPage(PdfObjectID id) // TODO: public? { PdfPage page = null; foreach (PdfItem item in PagesArray) { PdfReference reference = item as PdfReference; if (reference != null) { PdfDictionary dictionary = reference.Value as PdfDictionary; if (dictionary != null && dictionary.ObjectID == id) { page = dictionary as PdfPage ?? new PdfPage(dictionary); break; } } } return page; } /// /// Creates a new PdfPage, adds it to the end of this document, and returns it. /// public PdfPage Add() { PdfPage page = new PdfPage(); Insert(Count, page); return page; } /// /// Adds the specified PdfPage to the end of this document and maybe returns a new PdfPage object. /// The value returned is a new object if the added page comes from a foreign document. /// public PdfPage Add(PdfPage page) { return Insert(Count, page); } /// /// Creates a new PdfPage, inserts it at the specified position into this document, and returns it. /// public PdfPage Insert(int index) { PdfPage page = new PdfPage(); Insert(index, page); return page; } /// /// Inserts the specified PdfPage at the specified position to this document and maybe returns a new PdfPage object. /// The value returned is a new object if the inserted page comes from a foreign document. /// public PdfPage Insert(int index, PdfPage page) { if (page == null) throw new ArgumentNullException("page"); // Is the page already owned by this document? if (page.Owner == Owner) { // Case: Page is first removed and than inserted again, maybe at another position. int count = Count; // Check if page is not already part of the document. for (int idx = 0; idx < count; idx++) { if (ReferenceEquals(this[idx], page)) throw new InvalidOperationException(PSSR.MultiplePageInsert); } // TODO: check this case // Because the owner of the inserted page is this document we assume that the page was former part of it // and it is therefore well-defined. Owner._irefTable.Add(page); Debug.Assert(page.Owner == Owner); // Insert page in array. PagesArray.Elements.Insert(index, page.Reference); // Update page count. Elements.SetInteger(Keys.Count, PagesArray.Elements.Count); return page; } // All new page insertions come here. if (page.Owner == null) { // Case: New page was newly created and inserted now. page.Document = Owner; Owner._irefTable.Add(page); Debug.Assert(page.Owner == Owner); PagesArray.Elements.Insert(index, page.Reference); Elements.SetInteger(Keys.Count, PagesArray.Elements.Count); } else { // Case: Page is from an external document -> import it. PdfPage importPage = page; page = ImportExternalPage(importPage); Owner._irefTable.Add(page); // Add page substitute to importedObjectTable. PdfImportedObjectTable importedObjectTable = Owner.FormTable.GetImportedObjectTable(importPage); importedObjectTable.Add(importPage.ObjectID, page.Reference); PagesArray.Elements.Insert(index, page.Reference); Elements.SetInteger(Keys.Count, PagesArray.Elements.Count); PdfAnnotations.FixImportedAnnotation(page); } if (Owner.Settings.TrimMargins.AreSet) page.TrimMargins = Owner.Settings.TrimMargins; return page; } /// /// Inserts pages of the specified document into this document. /// /// The index in this document where to insert the page . /// The document to be inserted. /// The index of the first page to be inserted. /// The number of pages to be inserted. public void InsertRange(int index, PdfDocument document, int startIndex, int pageCount) { if (document == null) throw new ArgumentNullException("document"); if (index < 0 || index > Count) throw new ArgumentOutOfRangeException("index", "Argument 'index' out of range."); int importDocumentPageCount = document.PageCount; if (startIndex < 0 || startIndex + pageCount > importDocumentPageCount) throw new ArgumentOutOfRangeException("startIndex", "Argument 'startIndex' out of range."); if (pageCount > importDocumentPageCount) throw new ArgumentOutOfRangeException("pageCount", "Argument 'pageCount' out of range."); PdfPage[] insertPages = new PdfPage[pageCount]; PdfPage[] importPages = new PdfPage[pageCount]; // 1st create all new pages. for (int idx = 0, insertIndex = index, importIndex = startIndex; importIndex < startIndex + pageCount; idx++, insertIndex++, importIndex++) { PdfPage importPage = document.Pages[importIndex]; PdfPage page = ImportExternalPage(importPage); insertPages[idx] = page; importPages[idx] = importPage; Owner._irefTable.Add(page); // Add page substitute to importedObjectTable. PdfImportedObjectTable importedObjectTable = Owner.FormTable.GetImportedObjectTable(importPage); importedObjectTable.Add(importPage.ObjectID, page.Reference); PagesArray.Elements.Insert(insertIndex, page.Reference); if (Owner.Settings.TrimMargins.AreSet) page.TrimMargins = Owner.Settings.TrimMargins; } Elements.SetInteger(Keys.Count, PagesArray.Elements.Count); // 2nd copy link annotations that are in the range of the imported pages. for (int idx = 0, importIndex = startIndex; importIndex < startIndex + pageCount; idx++, importIndex++) { PdfPage importPage = document.Pages[importIndex]; PdfPage page = insertPages[idx]; // Get annotations. PdfArray annots = importPage.Elements.GetArray(PdfPage.Keys.Annots); if (annots != null) { PdfAnnotations annotations = new PdfAnnotations(Owner); // Loop through annotations. int count = annots.Elements.Count; for (int idxAnnotation = 0; idxAnnotation < count; idxAnnotation++) { PdfDictionary annot = annots.Elements.GetDictionary(idxAnnotation); if (annot != null) { string subtype = annot.Elements.GetString(PdfAnnotation.Keys.Subtype); if (subtype == "/Link") { bool addAnnotation = false; PdfLinkAnnotation newAnnotation = new PdfLinkAnnotation(Owner); PdfName[] importAnnotationKeyNames = annot.Elements.KeyNames; foreach (PdfName pdfItem in importAnnotationKeyNames) { PdfItem impItem; switch (pdfItem.Value) { case "/BS": newAnnotation.Elements.Add("/BS", new PdfLiteral("<>")); break; case "/F": // /F 4 impItem = annot.Elements.GetValue("/F"); Debug.Assert(impItem is PdfInteger); newAnnotation.Elements.Add("/F", impItem.Clone()); break; case "/Rect": // /Rect [68.6 681.08 145.71 702.53] impItem = annot.Elements.GetValue("/Rect"); Debug.Assert(impItem is PdfArray); newAnnotation.Elements.Add("/Rect", impItem.Clone()); break; case "/StructParent": // /StructParent 3 impItem = annot.Elements.GetValue("/StructParent"); Debug.Assert(impItem is PdfInteger); newAnnotation.Elements.Add("/StructParent", impItem.Clone()); break; case "/Subtype": // Already set. break; case "/Dest": // /Dest [30 0 R /XYZ 68 771 0] impItem = annot.Elements.GetValue("/Dest"); impItem = impItem.Clone(); // Is value an array with 5 elements where the first one is an iref? PdfArray destArray = impItem as PdfArray; if (destArray != null && destArray.Elements.Count == 5) { PdfReference iref = destArray.Elements[0] as PdfReference; if (iref != null) { iref = RemapReference(insertPages, importPages, iref); if (iref != null) { destArray.Elements[0] = iref; newAnnotation.Elements.Add("/Dest", destArray); addAnnotation = true; } } } break; default: #if DEBUG_ Debug-Break.Break(true); #endif break; } } // Add newAnnotations only it points to an imported page. if (addAnnotation) annotations.Add(newAnnotation); } } } // At least one link annotation found? if (annotations.Count > 0) { //Owner._irefTable.Add(annotations); page.Elements.Add(PdfPage.Keys.Annots, annotations); } } } } /// /// Inserts all pages of the specified document into this document. /// /// The index in this document where to insert the page . /// The document to be inserted. public void InsertRange(int index, PdfDocument document) { if (document == null) throw new ArgumentNullException("document"); InsertRange(index, document, 0, document.PageCount); } /// /// Inserts all pages of the specified document into this document. /// /// The index in this document where to insert the page . /// The document to be inserted. /// The index of the first page to be inserted. public void InsertRange(int index, PdfDocument document, int startIndex) { if (document == null) throw new ArgumentNullException("document"); InsertRange(index, document, startIndex, document.PageCount - startIndex); } /// /// Removes the specified page from the document. /// public void Remove(PdfPage page) { PagesArray.Elements.Remove(page.Reference); Elements.SetInteger(Keys.Count, PagesArray.Elements.Count); } /// /// Removes the specified page from the document. /// public void RemoveAt(int index) { PagesArray.Elements.RemoveAt(index); Elements.SetInteger(Keys.Count, PagesArray.Elements.Count); } /// /// Moves a page within the page sequence. /// /// The page index before this operation. /// The page index after this operation. public void MovePage(int oldIndex, int newIndex) { if (oldIndex < 0 || oldIndex >= Count) throw new ArgumentOutOfRangeException("oldIndex"); if (newIndex < 0 || newIndex >= Count) throw new ArgumentOutOfRangeException("newIndex"); if (oldIndex == newIndex) return; //PdfPage page = (PdfPage)pagesArray.Elements[oldIndex]; PdfReference page = (PdfReference)_pagesArray.Elements[oldIndex]; _pagesArray.Elements.RemoveAt(oldIndex); _pagesArray.Elements.Insert(newIndex, page); } /// /// Imports an external page. The elements of the imported page are cloned and added to this document. /// Important: In contrast to PdfFormXObject adding an external page always make a deep copy /// of their transitive closure. Any reuse of already imported objects is not intended because /// any modification of an imported page must not change another page. /// PdfPage ImportExternalPage(PdfPage importPage) { if (importPage.Owner._openMode != PdfDocumentOpenMode.Import) throw new InvalidOperationException("A PDF document must be opened with PdfDocumentOpenMode.Import to import pages from it."); PdfPage page = new PdfPage(_document); // ReSharper disable AccessToStaticMemberViaDerivedType for a better code readability. CloneElement(page, importPage, PdfPage.Keys.Resources, false); CloneElement(page, importPage, PdfPage.Keys.Contents, false); CloneElement(page, importPage, PdfPage.Keys.MediaBox, true); CloneElement(page, importPage, PdfPage.Keys.CropBox, true); CloneElement(page, importPage, PdfPage.Keys.Rotate, true); CloneElement(page, importPage, PdfPage.Keys.BleedBox, true); CloneElement(page, importPage, PdfPage.Keys.TrimBox, true); CloneElement(page, importPage, PdfPage.Keys.ArtBox, true); #if true // Do not deep copy annotations. CloneElement(page, importPage, PdfPage.Keys.Annots, false); #else // Deep copy annotations. CloneElement(page, importPage, PdfPage.Keys.Annots, true); #endif // ReSharper restore AccessToStaticMemberViaDerivedType // TODO more elements? return page; } /// /// Helper function for ImportExternalPage. /// void CloneElement(PdfPage page, PdfPage importPage, string key, bool deepcopy) { Debug.Assert(page != null); Debug.Assert(page.Owner == _document); Debug.Assert(importPage.Owner != null); Debug.Assert(importPage.Owner != _document); PdfItem item = importPage.Elements[key]; if (item != null) { PdfImportedObjectTable importedObjectTable = null; if (!deepcopy) importedObjectTable = Owner.FormTable.GetImportedObjectTable(importPage); // The item can be indirect. If so, replace it by its value. if (item is PdfReference) item = ((PdfReference)item).Value; if (item is PdfObject) { PdfObject root = (PdfObject)item; if (deepcopy) { Debug.Assert(root.Owner != null, "See 'else' case for details"); root = DeepCopyClosure(_document, root); } else { // The owner can be null if the item is not a reference. if (root.Owner == null) root.Document = importPage.Owner; root = ImportClosure(importedObjectTable, page.Owner, root); } if (root.Reference == null) page.Elements[key] = root; else page.Elements[key] = root.Reference; } else { // Simple items are just cloned. page.Elements[key] = item.Clone(); } } } static PdfReference RemapReference(PdfPage[] newPages, PdfPage[] impPages, PdfReference iref) { // Directs the iref to a one of the imported pages? for (int idx = 0; idx < newPages.Length; idx++) { if (impPages[idx].Reference == iref) return newPages[idx].Reference; } return null; } /// /// Gets a PdfArray containing all pages of this document. The array must not be modified. /// public PdfArray PagesArray { get { if (_pagesArray == null) _pagesArray = (PdfArray)Elements.GetValue(Keys.Kids, VCF.Create); return _pagesArray; } } PdfArray _pagesArray; /// /// Replaces the page tree by a flat array of indirect references to the pages objects. /// internal void FlattenPageTree() { // Acrobat creates a balanced tree if the number of pages is roughly more than ten. This is // not difficult but obviously also not necessary. I created a document with 50000 pages with // PDF4NET and Acrobat opened it in less than 2 seconds. //PdfReference xrefRoot = Document.Catalog.Elements[PdfCatalog.Keys.Pages] as PdfReference; //PdfDictionary[] pages = GetKids(xrefRoot, null); // Promote inheritable values down the page tree PdfPage.InheritedValues values = new PdfPage.InheritedValues(); PdfPage.InheritValues(this, ref values); PdfDictionary[] pages = GetKids(Reference, values, null); // Replace /Pages in catalog by this object // xrefRoot.Value = this; PdfArray array = new PdfArray(Owner); foreach (PdfDictionary page in pages) { // Fix the parent page.Elements[PdfPage.Keys.Parent] = Reference; array.Elements.Add(page.Reference); } Elements.SetName(Keys.Type, "/Pages"); #if true // Direct array. Elements.SetValue(Keys.Kids, array); #else // Indirect array. Document.xrefTable.Add(array); Elements.SetValue(Keys.Kids, array.XRef); #endif Elements.SetInteger(Keys.Count, array.Elements.Count); } /// /// Recursively converts the page tree into a flat array. /// PdfDictionary[] GetKids(PdfReference iref, PdfPage.InheritedValues values, PdfDictionary parent) { // TODO: inherit inheritable keys... PdfDictionary kid = (PdfDictionary)iref.Value; #if true string type = kid.Elements.GetName(Keys.Type); if (type == "/Page") { PdfPage.InheritValues(kid, values); return new PdfDictionary[] { kid }; } if (string.IsNullOrEmpty(type)) { // Type is required. If type is missing, assume it is "/Page" and hope it will work. // TODO Implement a "Strict" mode in PDFsharp and don't do this in "Strict" mode. PdfPage.InheritValues(kid, values); return new PdfDictionary[] { kid }; } #else if (kid.Elements.GetName(Keys.Type) == "/Page") { PdfPage.InheritValues(kid, values); return new PdfDictionary[] { kid }; } #endif Debug.Assert(kid.Elements.GetName(Keys.Type) == "/Pages"); PdfPage.InheritValues(kid, ref values); List list = new List(); PdfArray kids = kid.Elements["/Kids"] as PdfArray; if (kids == null) { PdfReference xref3 = kid.Elements["/Kids"] as PdfReference; if (xref3 != null) kids = xref3.Value as PdfArray; } foreach (PdfReference xref2 in kids) list.AddRange(GetKids(xref2, values, kid)); int count = list.Count; Debug.Assert(count == kid.Elements.GetInteger("/Count")); return list.ToArray(); } /// /// Prepares the document for saving. /// internal override void PrepareForSave() { // TODO: Close all open content streams // TODO: Create the page tree. // Arrays have a limit of 8192 entries, but I successfully tested documents // with 50000 pages and no page tree. // ==> wait for bug report. int count = _pagesArray.Elements.Count; for (int idx = 0; idx < count; idx++) { PdfPage page = this[idx]; page.PrepareForSave(); } } /// /// Gets the enumerator. /// public new IEnumerator GetEnumerator() { return new PdfPagesEnumerator(this); } class PdfPagesEnumerator : IEnumerator { internal PdfPagesEnumerator(PdfPages list) { _list = list; _index = -1; } public bool MoveNext() { if (_index < _list.Count - 1) { _index++; _currentElement = _list[_index]; return true; } _index = _list.Count; return false; } public void Reset() { _currentElement = null; _index = -1; } object IEnumerator.Current { get { return Current; } } public PdfPage Current { get { if (_index == -1 || _index >= _list.Count) throw new InvalidOperationException(PSSR.ListEnumCurrentOutOfRange); return _currentElement; } } public void Dispose() { // Nothing to do. } PdfPage _currentElement; int _index; readonly PdfPages _list; } /// /// Predefined keys of this dictionary. /// internal sealed class Keys : PdfPage.InheritablePageKeys { /// /// (Required) The type of PDF object that this dictionary describes; /// must be Pages for a page tree node. /// [KeyInfo(KeyType.Name | KeyType.Required, FixedValue = "Pages")] public const string Type = "/Type"; /// /// (Required except in root node; must be an indirect reference) /// The page tree node that is the immediate parent of this one. /// [KeyInfo(KeyType.Dictionary | KeyType.Required)] public const string Parent = "/Parent"; /// /// (Required) An array of indirect references to the immediate children of this node. /// The children may be page objects or other page tree nodes. /// [KeyInfo(KeyType.Array | KeyType.Required)] public const string Kids = "/Kids"; /// /// (Required) The number of leaf nodes (page objects) that are descendants of this node /// within the page tree. /// [KeyInfo(KeyType.Integer | KeyType.Required)] public const string Count = "/Count"; /// /// Gets the KeysMeta for these keys. /// public static DictionaryMeta Meta { get { return _meta ?? (_meta = CreateMeta(typeof(Keys))); } } static DictionaryMeta _meta; } /// /// Gets the KeysMeta of this dictionary type. /// internal override DictionaryMeta Meta { get { return Keys.Meta; } } } }