// ===========================================================================
// ©2013-2026 WebSupergoo. All rights reserved.
//
// This source code is for use exclusively with the ABCpdf product with
// which it is distributed, under the terms of the license for that
// product. Details can be found at
//
// http://www.websupergoo.com/
//
// This copyright notice must not be deleted and must be reproduced alongside
// any sections of code extracted from this module.
// ===========================================================================
using System;
using System.Drawing;
using System.Collections;
using System.Collections.Generic;
using System.ComponentModel;
using System.Windows.Forms;
using System.Data;
using System.Runtime;
using System.Diagnostics;
using System.IO;
using System.Text;
using System.Xml;
using System.Net;
using WebSupergoo.ABCpdf14;
using WebSupergoo.ABCpdf14.Objects;
using WebSupergoo.ABCpdf14.Operations;
using WebSupergoo.ABCpdf14.Atoms;
namespace OptionalContent {
/// <summary>
/// This class represents the Optional Content Properties Dictionary in a PDF document.
/// This is the top level container of all Optional Content Groups (OCGs)
/// in a document. OCGs are referenced at the top level in a global dictionary and their
/// visibility states are held in a global configuration dictionary. These OCGs may then
/// be referenced by one or more pages in the document.
/// Here we refer to OCGs simply as Groups.
/// </summary>
class Properties {
/// <summary>
/// Create a Properties object for the Doc.
/// </summary>
/// <param name="doc">The Doc on which to operate.</param>
/// <param name="create">Whether to create a new properties entry if one is not already present.</param>
/// <returns>The Properties object. This may be null if layers were not present in the document and the create parameter is false.</returns>
public static Properties FromDoc(Doc doc, bool create) {
Catalog catalog = doc.ObjectSoup.Catalog;
Atom atom = catalog.Resolve(Atom.GetItem(catalog.Atom, "OCProperties"));
if ((atom == null) && (create)) {
int id = doc.AddObject("<< /OCGs [] /D << /BaseState /ON >> >>");
IndirectObject io = doc.ObjectSoup[id];
Atom.SetItem(catalog.Atom, "OCProperties", new RefAtom(io));
atom = io.Atom;
}
if (atom == null)
return null;
return new Properties(catalog, atom);
}
private Catalog _catalog;
private Atom _atom;
private Properties(Catalog catalog, Atom atom) {
_catalog = catalog;
_atom = atom;
}
/// <summary>
/// The Catalog from the Doc.
/// </summary>
public Catalog Catalog { get { return _catalog; } }
/// <summary>
/// Get the Groups for the Doc.
/// </summary>
/// <returns>A list of the Groups.</returns>
public List<Group> GetGroups() {
List<Group> list = new List<Group>();
ArrayAtom array = EntryOCGs;
if (array != null) {
foreach (Atom item in array) {
IndirectObject io = _catalog.ResolveObj(item);
Group group = Group.FromIndirectObject(this, io);
if (group != null)
list.Add(group);
}
}
return list;
}
/// <summary>
/// Get the Optional Content Groups (OCGs) for the Page.
/// </summary>
/// <param name="page">The page from which OCGs should be found.</param>
/// <returns>A list of the OCGs.</returns>
public List<Group> GetGroups(Page page) {
var ocs = new HashSet<IndirectObject>();
var props = page.GetResourcesByType("Properties", true, true, true, true, null);
foreach (var atom in props) {
IndirectObject oc = page.ResolveObj(atom);
if (oc != null)
ocs.Add(oc);
}
foreach (var io in GetOptionalObjects(page))
ocs.Add(GetOptionalObjectOC(io));
var list = new List<Group>();
foreach (var oc in ocs)
AddGroups(oc, list);
return list;
}
/// <summary>
/// Adds any Optional Content Groups (OCGs) referenced by an OCG or OCG Membership Dictionary.
/// </summary>
/// <param name="io">The OCG or OCGMD</param>
/// <param name="groups">The list to which the items should be added.</param>
public void AddGroups(IndirectObject io, List<Group> groups) {
if (io == null)
return;
Group ocg = Group.FromIndirectObject(this, io);
if (ocg != null) {
groups.Add(ocg);
return;
}
MembershipGroup mg = MembershipGroup.FromIndirectObject(this, io);
if (mg != null) {
foreach (var ocg2 in mg.PolicyGroups)
groups.Add(ocg2);
}
}
/// <summary>
/// Find all the document level groups that are not used on pages.
/// </summary>
/// <returns>A list of groups.</returns>
public HashSet<Group> RemoveUnusedGroups() {
HashSet<Group> groups = GetUnusedGroups();
if (groups.Count > 0) {
HashSet<int> ids = new HashSet<int>();
foreach (Group g in groups)
ids.Add(g.IndirectObject.ID);
RemoveRefsByID(_atom, ids, new HashSet<int>(), 0);
RemoveAppSettings();
}
return groups;
}
private HashSet<Group> GetUnusedGroups() {
HashSet<Group> all = new HashSet<Group>(GetGroups());
foreach (Page page in Catalog.Pages.GetPageArrayAll()) {
foreach (Group g in GetGroups(page))
all.Remove(g);
// We add these OC entries in because our page scanning only references objects which are used.
// So if there's an image which isn't used on the page but is referenced in the resources we need to
// see if it has an OC entry. Yes it's redundant and could perhaps be removed from the resources but then
// it might be there for a reason. Difficult to second guess what might be intended.
List<Group> ocs = new List<Group>();
foreach (var io in GetOptionalObjects(page))
AddGroups(GetOptionalObjectOC(io), ocs);
foreach (Group g in ocs)
all.Remove(g);
}
return all;
}
internal List<IndirectObject> GetOptionalObjects(Page page) {
List<IndirectObject> ocs = new List<IndirectObject>();
foreach (Atom atom in page.GetResourcesByType("XObject", true, true, true, true, null)) {
IndirectObject oc = GetOptionalObjectOC(page.ResolveObj(atom));
if (oc == null) continue;
IndirectObject io = page.ResolveObj(atom);
if (io == null) continue;
ocs.Add(io);
}
foreach (Annotation annot in page.GetAnnotations()) {
IndirectObject oc = GetOptionalObjectOC(annot);
if (oc == null) continue;
ocs.Add(annot);
}
return ocs;
}
internal IndirectObject GetOptionalObjectOC(IndirectObject io) {
return io != null ? io.ResolveObj(MakeIndirect(io, io.Atom, "OC")) : null;
}
private void RemoveRefsByID(Atom atom, HashSet<int> ids, HashSet<int> seen, int depth) {
if (depth > 100)
throw new Exception("Atom nesting depth unfeasibly large.");
Atom resAtom = _catalog.Resolve(atom);
ArrayAtom array = resAtom as ArrayAtom;
if (array != null) {
int n = array.Count;
for (int i = n - 1; i >= 0; i--) {
Atom item = array[i];
IndirectObject io = _catalog.ResolveObj(item);
if (io != null) {
if (ids.Contains(io.ID)) {
array.RemoveAt(i);
continue;
}
if (seen.Contains(io.ID))
continue;
seen.Add(io.ID);
}
Atom resItem = _catalog.Resolve(item);
if (resItem is ArrayAtom || resItem is DictAtom)
RemoveRefsByID(resItem, ids, seen, depth + 1);
}
return;
}
DictAtom dict = resAtom as DictAtom;
if (dict != null) {
foreach (string key in dict.GetKeys()) {
Atom item = dict[key];
IndirectObject io = _catalog.ResolveObj(item);
if (io != null) {
if (ids.Contains(io.ID)) {
dict.Remove(key);
continue;
}
if (seen.Contains(io.ID))
continue;
seen.Add(io.ID);
}
Atom resItem = _catalog.Resolve(item);
if (resItem is ArrayAtom || resItem is DictAtom)
RemoveRefsByID(resItem, ids, seen, depth + 1);
}
return;
}
}
private void RemoveAppSettings() {
// Adobe Illustrator holds separate layer info in the PieceInfo structure
// and indeed other applications may also hold layer information here.
// It isn't ideal removing the entire thing because it will remove all
// settings but then there isn't a public spec for AI so it's difficult
// to know what else one would do which would be better.
Atom.RemoveItem(Catalog.Atom, "PieceInfo");
foreach (Page page in Catalog.Pages.GetPageArrayAll()) {
Atom.RemoveItem(page.Atom, "PieceInfo");
foreach (Atom atom in page.GetResourcesByType("XObject", true, true, true, true, null))
Atom.RemoveItem(Catalog.Resolve(atom), "PieceInfo");
}
}
/// <summary>
/// Sort Groups for presentation in a UI. Remove any Groups which should not appear in UI.
/// Construct a list of indents for those Groups that should be presented nested.
/// </summary>
/// <param name="groups">The OCGs to order.</param>
/// <param name="indents">A list of indents to indicate nestedness.</param>
public void SortGroupsForPresentation(List<Group> groups, List<int> indents) {
Dictionary<int, Group> lookup = new Dictionary<int, Group>();
foreach (Group group in groups)
lookup[group.IndirectObject.ID] = group;
groups.Clear();
indents.Clear();
Configuration config = GetDefault();
ArrayAtom order = config != null ? config.EntryOrder : null;
if (order == null)
return;
SortGroupsForPresentation(order, lookup, groups, indents, 0);
}
private void SortGroupsForPresentation(ArrayAtom order, Dictionary<int, Group> lookup, List<Group> groups, List<int> indents, int depth) {
if (depth > 100)
throw new Exception("OCG order nesting depth unfeasibly large.");
foreach (Atom atom in order) {
Atom resAtom = _catalog.Resolve(atom);
if (resAtom is ArrayAtom) { // OCG group
SortGroupsForPresentation((ArrayAtom)resAtom, lookup, groups, indents, depth + 1);
}
else if (resAtom is DictAtom) { // OCG
RefAtom refAtom = _catalog.ResolveRef(atom);
if (refAtom != null) {
Group group = null;
lookup.TryGetValue(refAtom.ID, out group);
if (group != null) {
groups.Add(group);
indents.Add(depth);
}
}
}
}
}
/// <summary>
/// Add a Group to the Doc.
/// </summary>
/// <param name="name">The name for the Group</param>
/// <param name="parent">The parent for the Group to indicate nested visibility. This may be null if nested visibility is not required.</param>
/// <returns>The newly added Group.</returns>
public Group AddGroup(string name, Group parent) {
Group ocg = Group.NewGroup(this);
ocg.EntryName = new StringAtom(name);
if (true) { // we need to list the ocg in the global database
ArrayAtom array = EntryOCGs;
if (array == null) {
array = new ArrayAtom();
EntryOCGs = array;
}
array.Add(new RefAtom(ocg.IndirectObject));
}
if (true) { // we also need to list it in the visible entries
Configuration config = GetDefault();
ArrayAtom array = config.EntryOrder;
if (array == null) {
array = new ArrayAtom();
config.EntryOrder = array;
}
if (parent != null) {
Tuple<ArrayAtom, int> entry = FindArrayEntry(parent.IndirectObject, array, 0);
if (entry == null)
throw new ArgumentException("Parent OCG not present in configuration dictionary.");
array = entry.Item1;
int index = entry.Item2;
ArrayAtom next = index < array.Count - 1 ? _catalog.Resolve(array[index + 1]) as ArrayAtom : null;
if (next == null) {
next = new ArrayAtom();
array.Insert(index + 1, next);
}
array = next;
}
array.Add(new RefAtom(ocg.IndirectObject));
}
return ocg;
}
/// <summary>
/// Add a Membership Group to the Doc.
/// </summary>
/// <returns>The newly added Membership Group.</returns>
public MembershipGroup AddMembershipGroup() {
return MembershipGroup.New(this);
}
/// <summary>
/// Get the default Configuration to indicate which layers are visible or invisible.
/// </summary>
/// <returns>The Configuration.</returns>
public Configuration GetDefault() {
Atom defaultConfig = EntryD;
return defaultConfig != null ? Configuration.FromAtom(this, defaultConfig) : null;
}
/// <summary>
/// Get the alternate Configurations that may be used to indicate which layers are visible or invisible.
/// Alternate configurations are indended for use under different circumstances - they represent alternate
/// views of the pages.
/// </summary>
/// <returns>A list of Configurations.</returns>
public List<Configuration> GetConfigs() {
List<Configuration> list = new List<Configuration>();
ArrayAtom configs = EntryConfigs;
if (configs != null) {
foreach (Atom item in configs) {
Configuration occd = Configuration.FromAtom(this, item);
list.Add(occd);
}
}
return list;
}
/// <summary>
/// The Optional Content Properties Dictionary OCG entry.
/// </summary>
public ArrayAtom EntryOCGs {
get { return _catalog.Resolve(Atom.GetItem(_atom, "OCGs")) as ArrayAtom; }
set { Atom.SetItem(_atom, "OCGs", value); }
}
/// <summary>
/// The Optional Content Properties Dictionary D entry.
/// </summary>
public DictAtom EntryD {
get { return _catalog.Resolve(Atom.GetItem(_atom, "D")) as DictAtom; }
set { Atom.SetItem(_atom, "D", value); }
}
/// <summary>
/// The Optional Content Properties Dictionary Configs entry.
/// </summary>
public ArrayAtom EntryConfigs {
get { return _catalog.Resolve(Atom.GetItem(_atom, "Configs")) as ArrayAtom; }
set { Atom.SetItem(_atom, "Configs", value); }
}
private static Tuple<ArrayAtom, int> FindArrayEntry(IndirectObject io, ArrayAtom array, int depth) {
if (depth > 100)
return null;
int n = array.Count;
for (int i = 0; i < n; i++) {
Atom atom = array[i];
ArrayAtom subArray = io.Resolve(atom) as ArrayAtom;
if (subArray != null) {
Tuple<ArrayAtom, int> result = FindArrayEntry(io, subArray, depth + 1);
if (result != null)
return result;
}
else {
RefAtom refAtom = io.ResolveRef(atom);
if ((refAtom != null) && (refAtom.ID == io.ID))
return new Tuple<ArrayAtom, int>(array, i);
}
}
return null;
}
public static RefAtom MakeResourceIndirect(Page page, IndirectObject container, string resourceType, string resourceName, Atom resourceAtom, ref Atom cache) {
RefAtom refAtom = container.ResolveRef(resourceAtom);
if (refAtom == null) {
if (cache == null)
cache = GetResourceDict(page, container, resourceType, true);
IndirectObject prop = new IndirectObject();
prop.Atom = resourceAtom;
container.Doc.ObjectSoup.Add(prop);
refAtom = (RefAtom)Atom.SetItem(cache, resourceName, new RefAtom(prop));
}
return refAtom;
}
public static RefAtom MakeIndirect(IndirectObject something, Atom container, string name) {
container = something.Resolve(container);
Atom value = Atom.GetItem(container, name);
if (value == null || value is RefAtom)
return (RefAtom)value;
IndirectObject prop = new IndirectObject();
prop.Atom = value;
something.Doc.ObjectSoup.Add(prop);
RefAtom refAtom = new RefAtom(prop);
Atom.SetItem(container, name, refAtom);
return refAtom;
}
public static DictAtom GetResourceDict(Page page, IndirectObject container, string resourceType, bool create) {
DictAtom dict = null;
Atom resAtom = null;
if (container is FormXObject) {
resAtom = container.Resolve(Atom.GetItem(container.Atom, "Resources"));
if (resAtom == null)
resAtom = Atom.SetItem(container.Atom, "Resources", new DictAtom());
}
else {
resAtom = container.Resolve(Atom.GetItem(page.Atom, "Resources"));
Debug.Assert(resAtom != null, "Page does not contain Resources.");
}
dict = container.Resolve(Atom.GetItem(resAtom, resourceType)) as DictAtom;
if ((dict == null) && (create))
dict = (DictAtom)Atom.SetItem(resAtom, resourceType, new DictAtom());
return dict;
}
}
/// <summary>
/// This class represents an Optional Content Group (OCG) in a PDF document.
/// An OCG is a layer-like object that may be visible or invisible. Items on the page
/// may belong to one or more than one OCG. Only if all OCGs that they belong to are visible
/// are the items visible. OCGs are held at the Doc level in a global dictionary
/// and then referenced at a Page level.
/// </summary>
class Group : IEquatable<Group> {
/// <summary>
/// Create a new Group for the document.
/// </summary>
/// <param name="oc">The Properties object for the Doc.</param>
/// <returns>The new Group.</returns>
public static Group NewGroup(Properties oc) {
IndirectObject io = IndirectObject.FromString("<< /Type /OCG /Name () >>");
oc.Catalog.Doc.ObjectSoup.Add(io);
return FromIndirectObject(oc, io);
}
/// <summary>
/// Create a list of Groups from a list of atoms that reference Optional Content Groups (OCGs) already existing in the Doc.
/// Only those atoms that reference OCGs will be included in the list. Other atoms will be ignored.
/// </summary>
/// <param name="oc">The Properties object for the Doc.</param>
/// <param name="atoms">The list of atoms.</param>
/// <returns>A list of Groups.</returns>
public static List<Group> FromAtoms(Properties oc, IEnumerable<Atom> atoms) {
List<Group> list = new List<Group>();
foreach (Atom atom in atoms) {
IndirectObject io = oc.Catalog.ResolveObj(atom);
Group ocg = Group.FromIndirectObject(oc, io);
if (ocg != null)
list.Add(ocg);
}
return list;
}
/// <summary>
/// Create a Group from an IndirectObject that already exists in the Doc.
/// If the IndirectObject is not an Optional Content Group then null will be returned.
/// </summary>
/// <param name="oc">The Properties object for the Doc.</param>
/// <param name="io">The IndirectObject.</param>
/// <returns>The Group.</returns>
public static Group FromIndirectObject(Properties oc, IndirectObject io) {
if (io == null)
return null;
Group ocg = new Group(oc, io);
return ocg.AllOk() ? ocg : null;
}
private Properties _oc;
private IndirectObject _io;
private Group(Properties oc, IndirectObject io) {
_oc = oc;
_io = io;
}
public override bool Equals(object obj) {
return Equals(obj as Group);
}
public bool Equals(Group other) {
return other != null && _io.ID == other._io.ID;
}
public override int GetHashCode() {
return _io.ID.GetHashCode();
}
private bool AllOk() {
// check type
NameAtom type = _io.Resolve(Atom.GetItem(_io.Atom, "Type")) as NameAtom;
if ((type == null) || (type.Text != "OCG"))
return false; // malformed entry - corrupt PDF
// The optional intent can be either a name or an array of names.
// To keep things simple we always use the array form.
Atom intent = _io.Resolve(Atom.GetItem(_io.Atom, "Intent"));
if (intent is NameAtom) {
ArrayAtom array = new ArrayAtom();
array.Add(intent);
Atom.SetItem(_io.Atom, "Intent", array);
intent = array;
}
return true;
}
/// <summary>
/// The IndirectObject representing the Group.
/// </summary>
public IndirectObject IndirectObject {
get { return _io; }
}
/// <summary>
/// Indicates whether the Group is visible or not.
/// </summary>
public bool Visible {
get { return _oc.GetDefault().GetVisibility(true, this); }
set { _oc.GetDefault().SetVisibility(value, this); }
}
/// <summary>
/// Adds the Group to a page.
/// </summary>
/// <param name="page"></param>
/// <returns></returns>
public string AddToPage(Page page) {
return page.AddResource(_io, "Properties", "OC" + _io.ID.ToString());
}
/// <summary>
/// The Optional Content Group Dictionary Name entry.
/// </summary>
public StringAtom EntryName {
get { return _io.Resolve(Atom.GetItem(_io.Atom, "Name")) as StringAtom; }
set { Atom.SetItem(_io.Atom, "Name", value); }
}
/// <summary>
/// The Optional Content Group Dictionary Intent entry.
/// </summary>
public ArrayAtom EntryIntent {
get { return _io.Resolve(Atom.GetItem(_io.Atom, "Intent")) as ArrayAtom; }
set { Atom.SetItem(_io.Atom, "Intent", value); }
}
/// <summary>
/// The Optional Content Group Dictionary Usage entry.
/// </summary>
public DictAtom EntryUsage {
get { return _io.Resolve(Atom.GetItem(_io.Atom, "Usage")) as DictAtom; }
set { Atom.SetItem(_io.Atom, "Usage", value); }
}
}
/// <summary>
/// This class represents an Optional Content Membership Dictionary (OCMD) in a PDF document.
/// This is a bit of a mouthful and doesn't actually express what it is, which is a
/// kind of metagroup. It expresses visibility dependent on the visibility of a
/// set of groups rather than directly being switched on and off by itself. OCMDs are not
/// referenced at the document level - only at a page level. However of course the OCMD references
/// OCGs which are themselves referenced at a document level.
/// Membership Groups may express visibility either using a Policy or using a Visibility Expression.
/// Policies are simpler to implement but limited in scope. Visibility Expressions are more complex
/// but also more powerful.
/// OCGs always have to be IndirectObjects since they are referenced from more than one place.
/// OCMGs do not have to be IndirectObject - they can be simple Atoms - since they are only referenced from
/// the Resources of the Page. However to make things simple we convert them to IndirectObjects when we
/// find them.
/// </summary>
class MembershipGroup : IEquatable<MembershipGroup> {
/// <summary>
/// An enumeration representing the possibilities for a Policy based Membership Group.
/// </summary>
public enum PolicyEnum { AllOn, AnyOn, AllOff, AnyOff };
/// <summary>
/// An enumeration representing the possibilities for a Logic based Membership Group.
/// </summary>
public enum LogicEnum { And, Or, Not };
/// <summary>
/// Create a new Membership Group.
/// </summary>
/// <param name="oc">The Properties object for the Doc</param>
/// <returns>The Membership Group</returns>
public static MembershipGroup New(Properties oc) {
IndirectObject io = IndirectObject.FromString("<< /Type /OCMD >>");
oc.Catalog.Doc.ObjectSoup.Add(io);
return FromIndirectObject(oc, io);
}
/// <summary>
/// Create a list of Membership Groups from a list of atoms that reference Optional Content Membership Groups (OCMGs) already existing in the Doc.
/// Only those atoms that reference OCMGs will be included in the list. Other atoms will be ignored.
/// </summary>
/// <param name="oc">The Properties object for the Doc.</param>
/// <param name="atoms">The list of atoms.</param>
/// <returns>A list of Membership Groups.</returns>
public static List<MembershipGroup> FromAtoms(Properties oc, IEnumerable<Atom> atoms) {
// Here we assume that all atoms passed in are references to an OCMG IndirectObject.
// In our code this is always the case. However if you modify the code to take Atoms
// read from another PDF then you need to tak account of the fact that OCMGs (unlike
// OCGs) need not be IndirectObjects - they can be plain DictAtoms. If this is the case
// you will need to use Properties.MakeResourceIndirect to convert these Atoms into
// IndirectObjects.
List<MembershipGroup> list = new List<MembershipGroup>();
foreach (Atom atom in atoms) {
IndirectObject io = oc.Catalog.ResolveObj(atom);
MembershipGroup ocmg = MembershipGroup.FromIndirectObject(oc, io);
if (ocmg != null)
list.Add(ocmg);
}
return list;
}
/// <summary>
/// Create a new Membership Group from an IndirectObject that already exists in the Doc.
/// If the IndirectObject is not a Membership Group then null will be returned.
/// </summary>
/// <param name="oc">The Properties object for the Doc</param>
/// <param name="io">The IndirectObject.</param>
/// <returns>The Group.</returns>
public static MembershipGroup FromIndirectObject(Properties oc, IndirectObject io) {
if (io == null)
return null;
MembershipGroup ocm = new MembershipGroup(oc, io);
return ocm.AllOk() ? ocm : null;
}
private Properties _oc;
private IndirectObject _io;
private MembershipGroup(Properties oc, IndirectObject io) {
_oc = oc;
_io = io;
}
public override bool Equals(object obj) {
return Equals(obj as MembershipGroup);
}
public bool Equals(MembershipGroup other) {
return other != null && _io.ID == other._io.ID;
}
public override int GetHashCode() {
return _io.ID.GetHashCode();
}
private bool AllOk() {
// check type
NameAtom type = _io.Resolve(Atom.GetItem(_io.Atom, "Type")) as NameAtom;
if ((type == null) || (type.Text != "OCMD"))
return false; // malformed entry - corrupt PDF
// OCGs are always IndirectObjects because the OCProperties entry
// in the document Catalog requires it to be so.
// The optional OCGs can be either a dict or an array of dicts.
// To keep things simple we always use the array form.
Atom ocgs = Atom.GetItem(_io.Atom, "OCGs");
if (_io.Resolve(ocgs) is DictAtom) {
ArrayAtom array = new ArrayAtom();
array.Add(ocgs);
Atom.SetItem(_io.Atom, "OCGs", array);
}
return true;
}
/// <summary>
/// The IndirectObject representing the Membership Group.
/// </summary>
public IndirectObject IndirectObject {
get { return _io; }
}
/// <summary>
/// Indicates whether the Membership Group is visible or not.
/// </summary>
public bool Visible {
get { return EntryVE != null ? EvaluateVisibility(EntryVE, 0) : EvaluateVisibility(Policy, PolicyGroups); }
}
private bool EvaluateVisibility(Atom ve, int depth) {
if (depth > 100)
return true;
ve = _oc.Catalog.Resolve(ve);
if (ve is ArrayAtom) {
ArrayAtom array = (ArrayAtom)ve;
int n = array.Count;
string op = n > 0 ? Atom.GetName(_oc.Catalog.Resolve(Atom.GetItem(array, 0))) : "";
if (op == LogicEnum.And.ToString()) {
for(int i = 1; i < n; i++) {
IndirectObject io = _oc.Catalog.ResolveObj(array[i]);
if (io != null) {
if (!EvaluateVisibility(io))
return false;
}
else {
if (!EvaluateVisibility(array[i], depth + 1))
return false;
}
}
return true;
}
else if (op == LogicEnum.Or.ToString()) {
for(int i = 1; i < n; i++) {
IndirectObject io = _oc.Catalog.ResolveObj(array[i]);
if (io != null) {
if (EvaluateVisibility(io))
return true;
}
else {
if (EvaluateVisibility(array[i], depth + 1))
return true;
}
}
return false;
}
else if ((op == LogicEnum.Not.ToString()) && (n > 1)) {
IndirectObject io = _oc.Catalog.ResolveObj(array[1]);
if (io != null)
return !EvaluateVisibility(io);
else
return !EvaluateVisibility(array[1], depth + 1);
}
}
return true;
}
private bool EvaluateVisibility(PolicyEnum? policy, IEnumerable<Group> groups) {
bool visible = true;
if (policy == null)
policy = PolicyEnum.AnyOn;
if (groups == null)
groups = new Group[0];
if (policy == PolicyEnum.AnyOn) {
visible = false;
foreach (Group group in groups) {
if (group.Visible) {
visible = true;
break;
}
}
}
else if (policy == PolicyEnum.AnyOff) {
visible = false;
foreach (Group group in groups) {
if (!group.Visible) {
visible = true;
break;
}
}
}
else if (policy == PolicyEnum.AllOn) {
visible = true;
foreach (Group group in groups) {
if (!group.Visible) {
visible = false;
break;
}
}
}
else if (policy == PolicyEnum.AllOff) {
visible = true;
foreach (Group group in groups) {
if (group.Visible) {
visible = false;
break;
}
}
}
return visible;
}
private bool EvaluateVisibility(IndirectObject io) {
Group group = Group.FromIndirectObject(_oc, io);
return group != null ? group.Visible : true;
}
/// <summary>
/// The Policy for the Membership Group. Null indicates that there is no Policy.
/// </summary>
public PolicyEnum? Policy {
get { NameAtom a = EntryP; return a != null ? (PolicyEnum?)Enum.Parse(typeof(PolicyEnum), a.Text) : null; }
set { EntryP = value.HasValue ? new NameAtom(value.ToString()) : null; }
}
/// <summary>
/// The Groups on which the Policy will operate.
/// </summary>
public IEnumerable<Group> PolicyGroups {
get {
ArrayAtom array = EntryOCGs;
return array != null ? Group.FromAtoms(_oc, EntryOCGs) : new List<Group>();
}
set {
ArrayAtom array = new ArrayAtom();
foreach (Group item in value)
array.Add(new RefAtom(item.IndirectObject));
EntryOCGs = array;
}
}
/// <summary>
/// Create a Visibility Expression showing how the visibility of a set of Groups should be combined.
/// </summary>
/// <param name="op">The logic operator used to combine the visibility.</param>
/// <param name="groups">The set of Groups on which to operate. Visible Groups are true and invisible ones are false.</param>
/// <returns>An ArrayAtom containing the Visibility Expression.</returns>
public ArrayAtom MakeVisibilityExpression(LogicEnum op, IEnumerable<Group> groups) {
ArrayAtom array = new ArrayAtom();
array.Add(new NameAtom(op.ToString()));
foreach (var group in groups)
array.Add(new RefAtom(group.IndirectObject));
return array;
}
/// <summary>
/// Create a Visibility Expression showing how the visibility of a set of Visibility Expressions should be combined.
/// </summary>
/// <param name="op">The logic operator used to combine the visibility.</param>
/// <param name="atoms">The set of groups on which to operate.</param>
/// <returns>An ArrayAtom containing the Visibility Expression.</returns>
public ArrayAtom MakeVisibilityExpression(LogicEnum op, IEnumerable<ArrayAtom> atoms) {
ArrayAtom array = new ArrayAtom();
array.Add(new NameAtom(op.ToString()));
foreach (var atom in atoms)
array.Add(atom);
return array;
}
/// <summary>
/// All the Groups on which the Membership Group depends.
/// </summary>
public IEnumerable<Atom> GetGroupReferences() {
List<Atom> list = new List<Atom>();
GetGroupReferences(EntryVE, list, 0);
GetGroupReferences(EntryOCGs, list, 0);
return list;
}
private IEnumerable<Atom> GetGroupReferences(ArrayAtom array, List<Atom> list, int depth) {
if ((array == null) || (depth > 100))
return list;
foreach (Atom atom in array) {
Atom item = _oc.Catalog.Resolve(atom);
if (item is ArrayAtom)
GetGroupReferences((ArrayAtom)item, list, depth + 1);
else {
RefAtom refAtom =_oc.Catalog.ResolveRef(atom);
if (refAtom != null)
list.Add(refAtom);
}
}
return list;
}
/// <summary>
/// Add the Membership Group to a Page.
/// </summary>
/// <param name="page">The Page to which this should be added.</param>
/// <returns>The resource name used for this Membership Group.</returns>
public string AddToPage(Page page) {
return page.AddResource(_io, "Properties", "OC" + _io.ID.ToString());
}
/// <summary>
/// The Optional Content Membership Dictionary OCGs entry.
/// </summary>
public ArrayAtom EntryOCGs {
get { return _io.Resolve(Atom.GetItem(_io.Atom, "OCGs")) as ArrayAtom; }
set { Atom.SetItem(_io.Atom, "OCGs", value); }
}
/// <summary>
/// The Optional Content Membership Dictionary P entry.
/// </summary>
public NameAtom EntryP {
get { return _io.Resolve(Atom.GetItem(_io.Atom, "P")) as NameAtom; }
set { Atom.SetItem(_io.Atom, "P", value); }
}
/// <summary>
/// The Optional Content Membership Dictionary VE entry.
/// </summary>
public ArrayAtom EntryVE {
get { return _io.Resolve(Atom.GetItem(_io.Atom, "VE")) as ArrayAtom; }
set { Atom.SetItem(_io.Atom, "VE", value); }
}
}
/// <summary>
/// This class represents an Optional Content Configuration Dictionary (OCCD) in a PDF document.
/// An OCCD expresses the visibility of the Groups in the document and also how they should be
/// presented in a User Interface (UI).
/// </summary>
class Configuration {
/// <summary>
/// Create a Configuration from an Optional Content Configuration Dictionary (OCCD) Atom already existing in the Doc.
/// If the Atom is not an OCCD then null will be returned.
/// </summary>
/// <param name="oc">The Properties object for the Doc.</param>
/// <param name="atom">The Atom.</param>
/// <returns>The Configuration.</returns>
public static Configuration FromAtom(Properties oc, Atom atom) {
if ((atom == null) || (oc == null))
return null;
return new Configuration(oc, atom);
}
private Properties _oc;
private Atom _atom;
private Configuration(Properties oc, Atom atom) {
_oc = oc;
_atom = atom;
}
/// <summary>
/// The Atom representing the Membership Group.
/// </summary>
public Atom Atom {
get { return _atom; }
}
private enum Visibility { On, Off, Unchanged };
/// <summary>
/// Evaluate the visibility of the Group.
/// </summary>
/// <param name="isDefault">Whether this is the default configuration.</param>
/// <param name="layer">The Group for which visibility should be evaluated.</param>
/// <returns>Whether visible.</returns>
public bool GetVisibility(bool isDefault, Group layer) {
Visibility visibility = Visibility.On;
NameAtom baseState = isDefault ? null : EntryBaseState;
if (baseState != null) {
switch (baseState.Text) {
case "OFF": visibility = Visibility.Off; break;
case "Unchanged": visibility = Visibility.Unchanged; break;
}
}
if (visibility != Visibility.On) {
ArrayAtom array = EntryON;
if (array != null) {
foreach (Atom item in array) {
IndirectObject io = _oc.Catalog.ResolveObj(item);
if ((io != null) && (io.ID == layer.IndirectObject.ID)) {
visibility = Visibility.On;
break;
}
}
}
}
else if (visibility != Visibility.Off) {
ArrayAtom array = EntryOFF;
if (array != null) {
foreach (Atom item in array) {
IndirectObject io = _oc.Catalog.ResolveObj(item);
if ((io != null) && (io.ID == layer.IndirectObject.ID)) {
visibility = Visibility.Off;
break;
}
}
}
}
return visibility != Visibility.Off;
}
/// <summary>
/// Set the visibility of a Group.
/// </summary>
/// <param name="visible">Whether the Group should be visible.</param>
/// <param name="layer">The Group for which visibility should be evaluated.</param>
public void SetVisibility(bool visible, Group layer) {
RemoveEntries(EntryOFF, layer.IndirectObject.ID);
RemoveEntries(EntryON, layer.IndirectObject.ID);
if (visible) {
if (EntryON == null)
EntryON = new ArrayAtom();
EntryON.Add(new RefAtom(layer.IndirectObject));
}
else {
if (EntryOFF == null)
EntryOFF = new ArrayAtom();
EntryOFF.Add(new RefAtom(layer.IndirectObject));
}
//layer.IndirectObject.Atom = layer.IndirectObject.Atom.Clone();
}
private void RemoveEntries(ArrayAtom array, int id) {
int n = array != null ? array.Count : 0;
for (int i = 0; i < n; i++) {
IndirectObject item = _oc.Catalog.ResolveObj(array[i]);
if ((item != null) && (item.ID == id)) {
array.RemoveAt(i);
i--;
n--;
}
}
}
/// <summary>
/// The Optional Content Configuration Dictionary Name entry.
/// </summary>
public StringAtom EntryName {
get { return _oc.Catalog.Resolve(Atom.GetItem(_atom, "Name")) as StringAtom; }
set { Atom.SetItem(_atom, "Name", value); }
}
/// <summary>
/// The Optional Content Configuration Dictionary Creator entry.
/// </summary>
public StringAtom EntryCreator {
get { return _oc.Catalog.Resolve(Atom.GetItem(_atom, "Creator")) as StringAtom; }
set { Atom.SetItem(_atom, "Creator", value); }
}
/// <summary>
/// The Optional Content Configuration Dictionary BaseState entry.
/// </summary>
public NameAtom EntryBaseState {
get { return _oc.Catalog.Resolve(Atom.GetItem(_atom, "BaseState")) as NameAtom; }
set { Atom.SetItem(_atom, "BaseState", value); }
}
/// <summary>
/// The Optional Content Configuration Dictionary ON entry.
/// </summary>
public ArrayAtom EntryON {
get { return _oc.Catalog.Resolve(Atom.GetItem(_atom, "ON")) as ArrayAtom; }
set { Atom.SetItem(_atom, "ON", value); }
}
/// <summary>
/// The Optional Content Configuration Dictionary OFF entry.
/// </summary>
public ArrayAtom EntryOFF {
get { return _oc.Catalog.Resolve(Atom.GetItem(_atom, "OFF")) as ArrayAtom; }
set { Atom.SetItem(_atom, "OFF", value); }
}
/// <summary>
/// The Optional Content Configuration Dictionary Order entry.
/// </summary>
public ArrayAtom EntryOrder {
get { return _oc.Catalog.Resolve(Atom.GetItem(_atom, "Order")) as ArrayAtom; }
set { Atom.SetItem(_atom, "Order", value); }
}
}
/// <summary>
/// Class for evaluating which parts of a page are visible and which parts are members
/// of which Optional Content Groups or Membership dictionaries. Includes facilities
/// for redacting elements that are invisible.
/// </summary>
class Reader {
/// <summary>
/// Create a Reader for a particular Page.
/// </summary>
/// <param name="oc">The Properties object for the Doc.</param>
/// <param name="page">The page to operate on.</param>
/// <returns></returns>
public static Reader FromPage(Properties oc, Page page) {
Reader content = new Reader(oc, page);
return content.AllOk() ? content : null;
}
[DebuggerDisplay("\\{ Offset = {Offset} Length = {Length} Command = {Command} \\}")]
private class Op : IComparable {
public Op(int offset, int length, string command, string textWidth) {
Offset = offset;
Length = length;
Command = command;
TextWidth = textWidth;
}
public int CompareTo(Object obj) {
return this.Offset - ((Op)obj).Offset;
}
public int Offset;
public int Length;
public string Command;
public string TextWidth;
}
private Properties _oc;
private Page _page;
private Dictionary<int, List<Op>> _ops;
private Dictionary<int, Dictionary<int, Atom[]>> _states; // first int is stream ID, second is position in stream
private Dictionary<int, IDictionary<string, Atom>> _rezMap;
private Dictionary<int, List<int>> _lookups; // fast lookup list
private Reader() {
Debug.Assert(false);
}
private Reader(Properties oc, Page page) {
_oc = oc;
_page = page;
_ops = null;
_states = null;
_rezMap = null;
_lookups = null;
}
private bool AllOk() {
_page.DeInline(true);
_page.Flatten(true, false);
// Theoretically a FormXObject could have different visibilty
// at different times it is drawn. So you should really call
// _page.StampFormXObjects(true) to separate them all out. However
// this might have quite an impact on size and it seems unlikely
// so we don't currently do this.
// This could be more efficiently done using the ContentStreamScanner class,
// but we would have to take care of OC properties in XObjects and Annotations ourselves.
ScanPage(_page.GetText(Page.TextType.SvgPlus2, true));
return true;
}
private void ScanPage(string svg) {
_ops = new Dictionary<int, List<Op>>();
_states = new Dictionary<int, Dictionary<int, Atom[]>>();
_rezMap = new Dictionary<int, IDictionary<string, Atom>>();
_lookups = new Dictionary<int, List<int>>();
Stack<Atom> names = new Stack<Atom>();
Stack<int> depths = new Stack<int>();
depths.Push(0);
using (StringReader reader = new StringReader(svg)) {
for (string item; (item = reader.ReadLine()) != null; ) {
if (item.StartsWith("<", StringComparison.InvariantCultureIgnoreCase)) {
// get object ID
string pdf_Op = GetAttribute(item, "pdf_Op");
string op = WebUtility.HtmlDecode(pdf_Op);
if (op == null)
continue;
int streamID = Int32.Parse(GetAttribute(item, "pdf_StreamID"));
int streamOffset = Int32.Parse(GetAttribute(item, "pdf_StreamOffset"));
int streamLength = Int32.Parse(GetAttribute(item, "pdf_StreamLength"));
string textWidth = GetAttribute(item, "pdf_w1000");
List<Op> ops = GetOpsFromStreamID(streamID);
// FormXObjects may be drawn multiple times on page. Don't need to keep
// track of all the times this happens. Once is enough.
if ((ops.Count == 0) || (ops[ops.Count - 1].Offset < streamOffset))
ops.Add(new Op(streamOffset, streamLength, op, textWidth));
bool isBDC = (op.EndsWith("BDC"));
bool isBMC = (op.EndsWith("BMC"));
bool isEMC = (op.EndsWith("EMC"));
if (isBDC || isBMC || isEMC) {
if (isBDC) {
ArrayAtom array = (ArrayAtom)Atom.FromString("[" + op + "]");
NameAtom type = array.Count > 0 ? (NameAtom)array[0] : null; // this must be a NameAtom
NameAtom name = array.Count > 1 ? array[1] as NameAtom : null; // this may be NameAtom or DictAtom but if OC, NameAtom only
if ((type != null) && (name != null) && (type.Text == "OC")) { // optional content
depths.Push(0);
IDictionary<string, Atom> map = GetResourceMapFromStreamID(streamID);
names.Push(map[name.Text]);
Dictionary<int, Atom[]> state = GetStateFromStreamID(streamID);
state.Add(streamOffset, names.Count > 0 ? names.ToArray() : new Atom[0]);
}
else { // some other kind of marked content
depths.Push(depths.Pop() + 1);
}
}
else if (isBMC) { // some other kind of marked content
depths.Push(depths.Pop() + 1);
}
else if (isEMC) {
if (depths.Peek() == 0) {
names.Pop(); // exception here indicates too many EMC without corresponding BDC
Dictionary<int, Atom[]> state = GetStateFromStreamID(streamID);
state.Add(streamOffset, names.Count > 0 ? names.ToArray() : new Atom[0]);
depths.Pop();
}
else {
depths.Push(depths.Pop() - 1);
}
}
}
}
}
}
}
/// <summary>
/// Get all the Layers referenced on the current page.
/// </summary>
/// <returns>A list of Layers.</returns>
public List<Layer> GetLayers() {
HashSet<Atom> set = new HashSet<Atom>();
foreach (KeyValuePair<int, Dictionary<int, Atom[]>> state1 in _states) {
foreach (KeyValuePair<int, Atom[]> state2 in state1.Value) {
foreach (Atom atom in state2.Value) {
set.Add(atom);
}
}
}
foreach (var io in _oc.GetOptionalObjects(_page))
set.Add(Atom.GetItem(io.Atom, "OC"));
List<Layer> list = new List<Layer>();
foreach (Atom atom in set) {
IndirectObject io = _oc.Catalog.ResolveObj(atom);
Group group = Group.FromIndirectObject(_oc, io);
if (group != null) {
list.Add(new Layer(group, null));
continue;
}
MembershipGroup membershipGroup = MembershipGroup.FromIndirectObject(_oc, io);
if (membershipGroup != null) {
list.Add(new Layer(membershipGroup, null));
continue;
}
}
return list;
}
/// <summary>
/// Get the Groups active at a particular point in a particular content stream.
/// You can use this to establish the visibility of a PDF operator at a particular
/// point in a stream.
/// </summary>
/// <param name="streamID">The ID of the StreamObject.</param>
/// <param name="position">The offset in the stream.</param>
/// <returns>A list of Layers</returns>
public List<Layer> GetLayersFromStreamAndPosition(int streamID, int position) {
List<Layer> list = new List<Layer>();
Atom[] atoms = GetStateFromStreamAndPosition(streamID, position);
foreach (Group group in Group.FromAtoms(_oc, atoms))
list.Add(new Layer(group, null));
foreach (MembershipGroup group in MembershipGroup.FromAtoms(_oc, atoms))
list.Add(new Layer(group, null));
return list;
}
/// <summary>
/// Get the Groups active for an Image XObject, Form XObject or Annotation.
/// You can use this to establish the visibility of particular object.
/// </summary>
/// <param name="io">The IndirectObject to check against.</param>
/// <returns>A list of Layers</returns>
public List<Layer> GetLayersFromObject(IndirectObject io) {
List<Layer> list = new List<Layer>();
if (io is FormXObject || io is PixMap) {
var oc = _oc.GetOptionalObjectOC(io);
if (oc != null) {
Group group = Group.FromIndirectObject(_oc, oc);
if (group != null)
list.Add(new Layer(group, null));
MembershipGroup membership = MembershipGroup.FromIndirectObject(_oc, oc);
if (group != null)
list.Add(new Layer(membership, null));
}
}
return list;
}
/// <summary>
/// Delete all content associated with the specified Group. After this the Reader will
/// no longer be valid and will need to be recreated if further operations are required.
/// </summary>
/// <param name="reader">The reader. This will be set to null on exit to ensure that an invalid reader is not used.</param>
/// <param name="layer">The Layer whos content should be deleted.</param>
static public void Redact(ref Reader reader, Layer layer) {
HashSet<Atom> kept = reader.Redact(layer);
reader.RemoveResources(kept);
reader = null; // invalid
}
private HashSet<Atom> Redact(Layer layer) {
HashSet<Atom> kept = new HashSet<Atom>();
foreach (KeyValuePair<int, Dictionary<int, Atom[]>> pair in _states) {
List<Op> ops = _ops[pair.Key];
Dictionary<int, Atom[]> state = GetStateFromStreamID(pair.Key);
List<int> lookup = GetLookupFromStreamID(pair.Key);
int n = lookup.Count, pos = 0;
bool keeping = true;
for (int i = 0; i < n; i++) {
bool match = false;
Atom[] ocs = state[lookup[i]]; // OCMG or OCMD
foreach (Atom atom in ocs) {
IndirectObject io = _page.ResolveObj(atom);
if (io.ID == layer.IndirectObject.ID) {
match = true;
break;
}
}
if (!match)// keep track of the ones left
foreach (Atom atom in ocs)
kept.Add(_oc.Catalog.Resolve(atom));
if (match && keeping) {
pos = lookup[i];
keeping = false;
}
else if (!match && !keeping) {
Redact(ops, pos, lookup[i]);
keeping = true;
}
}
if ((!keeping) && (ops.Count > 0)) // flush
Redact(ops, pos, ops[ops.Count - 1].Offset + 1);
StringBuilder sb = new StringBuilder();
foreach (Op op in ops)
sb.AppendLine(op.Command);
StreamObject so = (StreamObject)_page.Doc.ObjectSoup[pair.Key];
so.ClearData();
so.SetText(sb.ToString());
so.CompressFlate();
}
foreach (var io in _oc.GetOptionalObjects(_page)) {
IndirectObject oc = _oc.GetOptionalObjectOC(io);
if (oc.ID == layer.IndirectObject.ID) {
if (io is Annotation) {
ArrayAtom annots = _page.Resolve(Atom.GetItem(_page.Atom, "Annotations")) as ArrayAtom;
for (int i = annots.Count - 1; i >= 0; i--) {
IndirectObject a = _page.ResolveObj(Atom.GetItem(annots, i));
if (a != null && a.ID == io.ID)
annots.RemoveAt(i);
}
}
// If an XObject is invisible on the page, it is invisible
// everywhere, so we can just clear it.
// As an alternative we could identify the 'Do' elements
// during ScanPage and then remove those from the stream.
// However then we would need to ensure we also removed
// the resource and that is a more complex operation.
else if (io is StreamObject) {
((StreamObject)io).ClearData();
Atom.RemoveItem(io.Atom, "Resources");
Atom.RemoveItem(io.Atom, "OC");
Atom.RemoveItem(io.Atom, "Metadata");
}
}
}
_states = null; // invalid
_lookups = null; // invalid
return kept;
}
private void RemoveResources(HashSet<Atom> kept) {
foreach (KeyValuePair<int, IDictionary<string, Atom>> pair1 in _rezMap) {
foreach (KeyValuePair<string, Atom> pair2 in pair1.Value) {
Atom atom = _oc.Catalog.Resolve(pair2.Value);
if (!kept.Contains(atom)) {
IndirectObject resObj = _oc.Catalog.Doc.ObjectSoup[pair1.Key];
DictAtom dict = Properties.GetResourceDict(_page, resObj, "Properties", false);
Atom.RemoveItem(dict, pair2.Key);
}
}
}
}
private void Redact(List<Op> ops, int pos1, int pos2) {
HashSet<string> cmdMakePath = new HashSet<string>(new string[] { "m", "l", "c", "v", "y", "h", "re" });
HashSet<string> cmdPaintPath = new HashSet<string>(new string[] { "S", "s", "F", "f", "f*", "B", "B*", "b", "b*" });
HashSet<string> cmdClipPath = new HashSet<string>(new string[] { "W" }); // ignore "n"
HashSet<string> cmdShow = new HashSet<string>(new string[] { "Do", "sh" });
int depth = 0;
Stack<int> depths = new Stack<int>();
List<int> paints = new List<int>();
// there should always be an exact match for the binary search
int start = ops.BinarySearch(new Op(pos1, 0, null, null));
int n = ops.Count;
for (int i = start; i < n; i++) {
Op op = ops[i];
Debug.Assert((i != start) || (op.Command.EndsWith("BDC")));
if (op.Offset > pos2)
break;
Debug.Assert(!((i == n - 1) || (ops[i + 1].Offset > pos2)) || (op.Command == "EMC"));
string cmd = op.Command.TrimEnd();
int p = cmd.Length - 1;
while (p >= 0) {
if ((char.IsWhiteSpace(cmd[p])) || (cmd[p] == ')') || (cmd[p] == ']') || (cmd[p] == '>')) {
cmd = cmd.Substring(p + 1);
break;
}
p--;
}
// here we remove any displayed content
if (cmdShow.Contains(cmd))
op.Command = "";
// here we remove any path painting but keep clipping
else if (cmdMakePath.Contains(cmd)) {
paints.Add(i);
}
else if (cmdPaintPath.Contains(cmd)) {
op.Command = "n"; // needed because of bugs in Acrobat
foreach (int j in paints)
ops[j].Command = "";
paints.Clear();
}
else if (cmdClipPath.Contains(cmd)) {
paints.Clear();
}
// here we remove the BDC / EMC entries
else if (cmd == "BMC") {
depth++;
}
else if (cmd == "BDC") {
if (op.Command.StartsWith("/OC")) {
op.Command = "";
depths.Push(depth);
}
depth++;
}
else if (cmd == "EMC") {
depth--;
if (depths.Peek() == depth) {
op.Command = "";
depths.Pop();
}
}
// here we remove the text showing commands but in a way that preserves
// the offset of the text position albeit with invisible text
else if ((cmd == "TJ") || (cmd == "Tj"))
op.Command = "[" + op.TextWidth + "] TJ";
else if (cmd == "\'")
op.Command = "T* [" + op.TextWidth + "] TJ";
else if (cmd == "\"") {
int p1 = 0, p2 = 0;
while (p1 < op.Command.Length - 1) {
if (!char.IsWhiteSpace(op.Command[p1]))
break;
p1++;
}
while (p1 < op.Command.Length - 1) {
if (char.IsWhiteSpace(op.Command[p1]))
break;
p1++;
}
p2 = p1;
while (p2 < op.Command.Length - 1) {
if (!char.IsWhiteSpace(op.Command[p2]))
break;
p2++;
}
while (p2 < op.Command.Length - 1) {
if (char.IsWhiteSpace(op.Command[p2]))
break;
p2++;
}
string tw = p1 > 0 ? op.Command.Substring(0, p1) : "0";
string tc = p2 > p1 ? op.Command.Substring(p1, p2 - p1) : "0";
op.Command = tw + " Tw " + tc + " Tc T* [" + op.TextWidth + "] TJ";
}
}
}
private Atom[] GetStateFromStreamAndPosition(int streamID, int position) {
Dictionary<int, Atom[]> state = GetStateFromStreamID(streamID);
List<int> lookup = GetLookupFromStreamID(streamID);
int pos = lookup.BinarySearch(position);
if (pos >= 0) { // exact match
return state[lookup[pos]];
}
else { // exact key not found
pos = ~pos; // Bitwise complement of the first index in the list larger than the target.
// But we want the one before that - so move back one. For that reason we shouldn't need to pin the top
if (pos == 0) // no state
return new Atom[0];
pos = pos >= 0 ? pos - 1 : pos;
return state[lookup[pos]];
}
}
private static string GetAttribute(string item, string name) {
int p1 = item.IndexOf(name);
if (p1 < 0) return null;
p1 = item.IndexOf('=', p1) + 1;
if (p1 < 0) return null;
p1 = item.IndexOf('\"', p1) + 1;
if (p1 < 0) return null;
int p2 = item.IndexOf('\"', p1);
if (p2 < 0) return null;
string attr = item.Substring(p1, p2 - p1);
return attr;
}
private List<Op> GetOpsFromStreamID(int streamID) {
List<Op> op = null;
_ops.TryGetValue(streamID, out op);
if (op == null) {
op = new List<Op>();
_ops[streamID] = op;
}
return op;
}
private Dictionary<int, Atom[]> GetStateFromStreamID(int streamID) {
Dictionary<int, Atom[]> state = null;
_states.TryGetValue(streamID, out state);
if (state == null) {
state = new Dictionary<int, Atom[]>();
_states[streamID] = state;
}
return state;
}
private IDictionary<string, Atom> GetResourceMapFromStreamID(int streamID) {
IDictionary<string, Atom> map = null;
_rezMap.TryGetValue(streamID, out map);
if (map == null) {
IndirectObject io = _page.Doc.ObjectSoup[streamID];
int id = io is FormXObject ? streamID : _page.ID;
map = _page.GetResourceMap(id, "Properties");
Atom cache = null; // Convert OCMD Atoms to IndirectObjects where necessary
List<string> keys = new List<string>(map.Keys);
foreach (string key in keys) {
Atom atom = map[key];
if (_oc.Catalog.ResolveRef(atom) == null) {
RefAtom refAtom = Properties.MakeResourceIndirect(_page, io, "Properties", key, atom, ref cache);
map[key] = refAtom;
}
}
_rezMap[streamID] = map;
}
return map;
}
private List<int> GetLookupFromStreamID(int streamID) {
List<int> lookup = null;
_lookups.TryGetValue(streamID, out lookup);
if (lookup == null) {
Dictionary<int, Atom[]> state = GetStateFromStreamID(streamID);
lookup = new List<int>(state.Keys);
lookup.Sort();
_lookups[streamID] = lookup;
}
return lookup;
}
}
/// <summary>
/// Class to represent a Layer on the page. A Layer represents a reference to a visibility
/// object from within a Page. The visibilty object may be either a Group or
/// on a Membership Group.
/// </summary>
class Layer {
private Layer() { ResourceName = ""; }
/// <summary>
/// Create a Layer object from a Group.
/// </summary>
/// <param name="group">The Group.</param>
/// <param name="name">The resource name for the Group.</param>
public Layer(Group group, string name) { Group = group; ResourceName = name; }
/// <summary>
/// Create a Layer object from a Membership Group.
/// </summary>
/// <param name="group">The Membership Group.</param>
/// <param name="name">The resource name for the Membership Group.</param>
public Layer(MembershipGroup group, string name) { MembershipGroup = group; ResourceName = name; }
/// <summary>
/// The Group. Either this or the MembershipGroup property will be null. Only one can be populated.
/// </summary>
public Group Group { get; set; }
/// <summary>
/// The Membership Group. Either this or the Group property will be null. Only one can be populated.
/// </summary>
public MembershipGroup MembershipGroup { get; set; }
/// <summary>
/// The resource name used for this object.
/// </summary>
public string ResourceName { get; set; }
/// <summary>
/// The IndirectObject for this Layer.
/// </summary>
public IndirectObject IndirectObject { get { return Group != null ? Group.IndirectObject : (MembershipGroup != null ? MembershipGroup.IndirectObject : null); } }
/// <summary>
/// Indicates whether the Group is visible or not.
/// </summary>
public bool Visible { get { return Group != null ? Group.Visible : (MembershipGroup != null ? MembershipGroup.Visible : true); } }
}
/// <summary>
/// Class for creating layers on a Page.
/// </summary>
class Writer {
private Properties _oc;
private Page _page;
private int _depth = 0;
private Writer() {
Debug.Assert(false);
}
/// <summary>
/// Create a new Writer for the Page.
/// </summary>
/// <param name="oc">The Properties object for the Doc.</param>
/// <param name="page">The Page on which to operate.</param>
public Writer(Properties oc, Page page) {
_oc = oc;
_page = page;
}
~Writer() {
Debug.Assert(_depth == 0, "Unbalanced StartLayer and EndLayer calls.");
}
/// <summary>
/// The depth of nested calls to StartLayer and EndLayer.
/// </summary>
public int Depth { get { return _depth; } }
/// <summary>
/// Add a reference to a Group for the current Page.
/// </summary>
/// <param name="group">The Group for which a reference should be added.</param>
/// <returns>The Layer.</returns>
public Layer AddGroup(Group group) {
string resource = group.AddToPage(_page);
return new Layer(group, resource);
}
/// <summary>
/// Add a reference to a Membership Group for the current Page.
/// </summary>
/// <param name="group">The Membership Group for which a reference should be added.</param>
/// <returns>The Layer.</returns>
public Layer AddGroup(MembershipGroup group) {
string resource = group.AddToPage(_page);
return new Layer(group, resource);
}
/// <summary>
/// Start writing content for a particular layer. Calls to StartLayer may be nested
/// but each call to StartLayer must be balanced by a corresponding call to EndLayer.
/// </summary>
/// <param name="layer">The Layer to start.</param>
public void StartLayer(Layer layer) {
_page.AddLayer(MakeLayerStart(_page, layer.ResourceName));
_depth++;
}
/// <summary>
/// End writing content for a particular layer. Calls to StartLayer may be nested
/// but each call to StartLayer must be balanced by a corresponding call to EndLayer.
/// </summary>
public void EndLayer() {
_page.AddLayer(MakeLayerEnd(_page));
_depth--;
}
private static StreamObject MakeLayerStart(Page page, string name) {
byte[] data = ASCIIEncoding.UTF7.GetBytes("/OC /" + name + " BDC\r\n");
return new StreamObject(page.Doc.ObjectSoup, data);
}
private static StreamObject MakeLayerEnd(Page page) {
byte[] data = ASCIIEncoding.UTF7.GetBytes("EMC\r\n");
return new StreamObject(page.Doc.ObjectSoup, data);
}
}
}