// ===========================================================================
// ©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.IO;
using System.Text;
using System.Diagnostics;
using WebSupergoo.ABCpdf14;
using WebSupergoo.ABCpdf14.Objects;
using WebSupergoo.ABCpdf14.Atoms;
using WebSupergoo.ABCpdf14.Operations;
namespace Redaction {
// In this example, redactions operate on an atomic level of one text operator.
// So if, for example, your text operator displays the text "the cat sat on the mat"
// and you redact the "cat" fragment, then this will also redact the other words too.
// This kind of redaction is often appropriate when looking at areas of a document rather
// than particular bits of text on a document.
class SimpleRedaction {
public static void RedactTextOps(Doc doc, IList<TextFragment> fragments) {
if (fragments.Count == 0)
return;
ResourceTracker tracker = new ResourceTracker(doc);
Dictionary<int, List<TextFragment>> streams = new Dictionary<int, List<TextFragment>>();
foreach (TextFragment fragment in fragments) {
List<TextFragment> redactions = null;
int id = tracker.CopyOnWrite(fragment.StreamID, fragment.PageID);
streams.TryGetValue(id, out redactions);
if (redactions == null) {
redactions = new List<TextFragment>();
streams[id] = redactions;
}
redactions.Add(fragment);
}
foreach (KeyValuePair<int, List<TextFragment>> pair in streams) {
// get data
StreamObject stream = doc.ObjectSoup[pair.Key] as StreamObject;
Debug.Assert(stream != null); // should never happen
stream.Decompress(); // shouldn't really be necessary
Debug.Assert(stream.Compressed == false); // should never happen
byte[] data = stream.GetData();
// construct new stream
List<TextFragment> redactions = pair.Value;
redactions.Sort((x, y) => x.StreamOffset.CompareTo(y.StreamOffset));
MemoryStream mem = new MemoryStream(data.Length);
int p = 0;
foreach (TextFragment redaction in redactions) {
if (p < redaction.StreamOffset) {
mem.Write(data, p, redaction.StreamOffset - p);
IDictionary<char, int> widths = redaction.Font.Widths;
int defaultWidth = doc.GetInfoInt(redaction.Font.ID, "/DescendantFonts[0]*/DW*");
if (defaultWidth == 0)
defaultWidth = 1000;
int width1000ths = 0;
foreach (char c in redaction.Text) {
int width = 0;
if (!widths.TryGetValue(c, out width))
width = defaultWidth;
width1000ths += width;
}
string tj = string.Format("[{0}] TJ\r\n", width1000ths);
byte[] tjBytes = ASCIIEncoding.ASCII.GetBytes(tj);
mem.Write(tjBytes, 0, tjBytes.Length);
}
p = redaction.StreamOffset + redaction.StreamLength;
}
mem.Write(data, p, data.Length - p);
stream.SetData(mem.ToArray());
}
}
}
// In this example, redactions operate on an atomic level of one character. This
// requires more complex code and there is a greater overhead but it is often more
// appropriate when dealing with small bits of text.
class FineRedaction {
public static void RedactCharacters(Doc doc, IList<TextFragment> fragments) {
if (fragments.Count == 0)
return;
ResourceTracker tracker = new ResourceTracker(doc);
Dictionary<int, List<TextFragment>> streams = new Dictionary<int, List<TextFragment>>();
foreach (TextFragment fragment in fragments) {
List<TextFragment> redactions = null;
int id = tracker.CopyOnWrite(fragment.StreamID, fragment.PageID);
streams.TryGetValue(id, out redactions);
if (redactions == null) {
redactions = new List<TextFragment>();
streams[id] = redactions;
}
redactions.Add(fragment);
}
foreach (KeyValuePair<int, List<TextFragment>> pair in streams) {
// get data
StreamObject stream = doc.ObjectSoup[pair.Key] as StreamObject;
Debug.Assert(stream != null); // should never happen
stream.Decompress(); // shouldn't really be necessary
Debug.Assert(stream.Compressed == false); // should never happen
byte[] data = stream.GetData();
// construct new stream
List<TextFragment> redactions = pair.Value;
redactions.Sort((x, y) => x.StreamOffset.CompareTo(y.StreamOffset));
MemoryStream mem = new MemoryStream(data.Length);
int p = 0;
for (int i = 0; i < redactions.Count; i++) {
TextFragment redaction = redactions[i];
RedactionGroup group = new RedactionGroup(data);
for (int j = i; j < redactions.Count; j++) {
if (!group.Add(redactions[j])) {
i = j - 1;
break;
}
}
if (p < redaction.StreamOffset) {
mem.Write(data, p, redaction.StreamOffset - p);
byte[] tjBytes = group.GetTextOperator().GetData();
mem.Write(tjBytes, 1, tjBytes.Length - 2);
}
p = redaction.StreamOffset + redaction.StreamLength;
}
mem.Write(data, p, data.Length - p);
stream.SetData(mem.ToArray());
}
}
private class RedactionGroup {
private byte[] _data;
private Dictionary<int, Tuple<string, bool[]>> _redactions;
private TextFragment _text;
public RedactionGroup(byte[] data) {
_data = data;
_redactions = new Dictionary<int, Tuple<string, bool[]>>();
}
public bool Add(TextFragment fragment) {
if ((_text != null) && (_text.StreamOffset != fragment.StreamOffset))
return false;
_text = fragment;
Tuple<string, bool[]> redaction = null;
_redactions.TryGetValue(fragment.TextSpanIndex, out redaction);
int pos = fragment.TextOffset;
int len = fragment.Text.Length;
Debug.Assert(fragment.RawText.Substring(pos).StartsWith(fragment.Text));
if (redaction == null) {
bool[] redacted = new bool[fragment.RawText.Length];
for (int i = 0; i < len; i++)
redacted[i + pos] = true;
redaction = new Tuple<string, bool[]>(fragment.RawText, redacted);
_redactions[fragment.TextSpanIndex] = redaction;
}
else {
Debug.Assert(fragment.RawText == redaction.Item1);
bool[] redacted = redaction.Item2;
for (int i = 0; i < len; i++)
redacted[i + pos] = true;
}
return true;
}
public ArrayAtom GetTextOperator() {
Debug.Assert(_text != null);
// get data and widths
var section = new byte[_text.StreamLength];
Array.Copy(_data, _text.StreamOffset, section, 0, _text.StreamLength);
var widths = _text.Font.Widths;
var defaultWidth = _text.Font.DefaultWidth;
// convert to atoms
var textOp = ArrayAtom.FromContentStream(section);
string srcOp = ((OpAtom)textOp[1]).Text;
int numParams = srcOp == "\"" ? 4 : 2;
Debug.Assert(textOp.Count == numParams);
var srcParams = textOp[numParams == 4 ? 2 : 0] as ArrayAtom;
if (srcParams == null) {
Debug.Assert(srcOp != "TJ");
srcParams = new ArrayAtom();
srcParams.Add(textOp[0]);
}
var dstParams = new ArrayAtom();
// copy items
int textSpanIndex = 0;
for (int i = 0; i < srcParams.Count; i++) {
var item = srcParams[i];
if (item is StringAtom) {
var str = (StringAtom)item;
Tuple<string, bool[]> redaction = null;
_redactions.TryGetValue(textSpanIndex++, out redaction);
if (redaction == null) {
dstParams.Add(item.Clone());
}
else {
string encoded = str.Text;
string whole = redaction.Item1;
bool[] redacted = redaction.Item2;
Debug.Assert((encoded.Length == whole.Length) || (encoded.Length == whole.Length * 2));
int bytesPerChar = encoded.Length / whole.Length;
int p1 = 0;
while (p1 < redacted.Length) {
bool hidden = redacted[p1];
int width1000ths = 0;
int p2;
for (p2 = p1; p2 < redacted.Length; p2++) {
if (hidden != redacted[p2])
break;
if (hidden) {
if (!widths.TryGetValue(whole[p2], out var width))
width = defaultWidth;
width1000ths += width;
}
}
if (hidden) {
dstParams.Add(new NumAtom(-width1000ths));
}
else {
string sub1 = encoded.Substring(p1 * bytesPerChar, (p2 - p1) * bytesPerChar);
if (sub1.Length > 0)
dstParams.Add(new StringAtom(sub1));
}
p1 = p2;
}
}
}
else if (item is NumAtom) {
NumAtom num = (NumAtom)item;
dstParams.Add(item.Clone());
}
else {
Debug.Assert(false);
}
}
// make new operators
var newTextOp = new ArrayAtom();
if (srcOp == "\'") {
newTextOp.Add(new OpAtom("T*"));
}
else if (srcOp == "\"") {
newTextOp.Add(textOp[0].Clone());
newTextOp.Add(new OpAtom("Tw"));
newTextOp.Add(textOp[1].Clone());
newTextOp.Add(new OpAtom("Tc"));
newTextOp.Add(new OpAtom("T*"));
}
newTextOp.Add(dstParams);
newTextOp.Add(new OpAtom("TJ"));
return newTextOp;
}
}
}
class FontTracker {
public Doc _doc;
private HashSet<int> _fonts;
private HashSet<int> _containers;
private FontTracker() { }
public FontTracker(Doc doc) {
_doc = doc;
_fonts = new HashSet<int>();
_containers = new HashSet<int>();
}
public void Keep(TextFragment fragment) {
_fonts.Add(fragment.Font.ID);
_containers.Add(fragment.PageID);
_containers.Add(fragment.StreamID);
}
public void Purge() {
_doc.PageNumber = 1;
foreach (int id in _containers) {
IndirectObject io = _doc.ObjectSoup[id];
while (io != null) {
Atom rez = io.Resolve(Atom.GetItem(io.Atom, "Resources"));
DictAtom fonts = io.Resolve(Atom.GetItem(rez, "Font")) as DictAtom;
if ((fonts != null) && (fonts.Count > 0)) {
List<string> fontsToRemove = new List<string>();
foreach (KeyValuePair<string, Atom> pair in fonts) {
IndirectObject font = io.ResolveObj(pair.Value);
if (font == null)
continue; // shouldn't ever happen
if (!_fonts.Contains(font.ID))
fontsToRemove.Add(pair.Key);
}
foreach (string key in fontsToRemove)
fonts.Remove(key);
}
io = io.ResolveObj(Atom.GetItem(io.Atom, "Parent"));
}
}
}
public void PurgeForm() {
Catalog cat = _doc.ObjectSoup.Catalog;
Atom form = cat.Resolve(Atom.GetItem(cat.Atom, "AcroForm"));
if (form == null)
return;
// look at default appearance for documents and widgets
HashSet<string> fontsToKeep = new HashSet<string>();
Atom docDA = cat.Resolve(Atom.GetItem(form, "DA"));
bool needsDocDA = false;
for (int i = 0; i < _doc.PageNumber; i++) {
_doc.PageNumber = i + 1;
Page page = (Page)_doc.ObjectSoup[_doc.Page];
foreach (Annotation annot in page.GetAnnotations()) {
bool needsDA = false;
if ((annot.FieldType == FieldType.Text) || (annot.FieldType == FieldType.Signature) || (annot.FieldType == FieldType.List) || (annot.FieldType == FieldType.Combo))
needsDA = true;
else {
string subtype = Atom.GetName(cat.Resolve(Atom.GetItem(annot.Atom, "Subtype")));
if ((subtype == "FreeText") || ((subtype == "Redact") && (Atom.GetItem(annot.Atom, "OverlayText") != null)))
needsDA = true;
}
if (!needsDA)
continue; // we can ignore this annotation
Atom da = cat.Resolve(Atom.GetItem(annot.Atom, "DA"));
if (da == null) {
needsDocDA = true;
continue; // we add in the doc DA at the end
}
KeepFonts(fontsToKeep, da);
}
}
Debug.Assert((needsDocDA == false) || (docDA != null), "Both Widgets and Document fail to define required DA entry.");
if (!needsDocDA) {
Atom.RemoveItem(form, "DA");
docDA = null;
}
if (docDA != null)
KeepFonts(fontsToKeep, docDA);
HashSet<string> fontsToRemove = new HashSet<string>();
Atom rez = cat.Resolve(Atom.GetItem(form, "DR"));
DictAtom fonts = cat.Resolve(Atom.GetItem(rez, "Font")) as DictAtom;
if (fonts != null) {
foreach (KeyValuePair<string, Atom> pair in fonts) {
if (!fontsToKeep.Contains(pair.Key))
fontsToRemove.Add(pair.Key);
}
foreach (string key in fontsToRemove)
fonts.Remove(key);
}
}
private static void KeepFonts(HashSet<string> fontsToKeep, Atom atomDA) {
string da = Atom.GetText(atomDA);
ArrayAtom array = ArrayAtom.FromContentStream(ASCIIEncoding.ASCII.GetBytes(da));
var items = OpAtom.Find(array, new string[] { "Tf" });
foreach (var pair in items) {
Atom[] args = OpAtom.GetParameters(array, pair.Item2);
Debug.Assert(args != null); // shouldn't really happen
if (args != null)
fontsToKeep.Add(Atom.GetName(args[0]));
}
}
}
class ImageRedaction {
private Doc _doc;
private ICollection<ImageProperties> _images;
private bool _imagesCoverWholeDoc;
private Dictionary<int, List<ImageRendition>> _redactions;
private HashSet<ImageRendition> _redactionSet;
private ImageRedaction() { }
public ImageRedaction(Doc doc, ICollection<ImageProperties> images, bool wholeDoc) {
_doc = doc;
_images = images;
_imagesCoverWholeDoc = wholeDoc;
_redactions = new Dictionary<int, List<ImageRendition>>();
_redactionSet = new HashSet<ImageRendition>();
}
public void MarkForRedaction(ImageRendition rendition) {
List<ImageRendition> list = null;
_redactions.TryGetValue(rendition.StreamID, out list);
if (list == null) {
list = new List<ImageRendition>();
_redactions[rendition.StreamID] = list;
}
list.Add(rendition);
_redactionSet.Add(rendition);
}
public void Redact() {
Delete();
Purge();
}
public void Delete() {
foreach (KeyValuePair<int, List<ImageRendition>> pair in _redactions) {
StreamObject so = (StreamObject)_doc.ObjectSoup[pair.Key];
List<ImageRendition> renditions = pair.Value;
renditions.Sort((i1, i2) => { return i1.StreamOffset.CompareTo(i2.StreamOffset); });
if (!so.Decompress())
throw new ArgumentException("Unable to decompress stream.");
byte[] data = so.GetData();
foreach (ImageRendition rendition in renditions) {
for (int i = 0; i < rendition.StreamLength; i++)
data[i + rendition.StreamOffset] = 0x20; // space
}
so.SetData(data);
so.CompressFlate();
}
}
public void Purge() {
// First establish the count of images; per page (or xobject), per image.
Dictionary<int, Dictionary<int, int>> imageCountPerParent = new Dictionary<int, Dictionary<int, int>>();
foreach (ImageProperties image in _images) {
foreach (ImageRendition rendition in image.Renditions) {
int parent = rendition.StreamObject is FormXObject ? rendition.StreamID : rendition.PageID;
Dictionary<int, int> imageCount = null;
imageCountPerParent.TryGetValue(parent, out imageCount);
if (imageCount == null) {
imageCount = new Dictionary<int, int>();
imageCountPerParent[parent] = imageCount;
}
int count = 0;
if (!imageCount.TryGetValue(image.PixMap.ID, out count)) {
imageCount[image.PixMap.ID] = 0;
count = 0;
}
if (!_redactionSet.Contains(rendition))
imageCount[image.PixMap.ID] = count + 1;
}
}
// Then remove the ones that have been redacted
foreach (KeyValuePair<int, Dictionary<int, int>> pair1 in imageCountPerParent) {
HashSet<int> imagesToRemove = new HashSet<int>();
foreach (KeyValuePair<int, int> pair2 in pair1.Value) {
if (pair2.Value == 0) // no references left
imagesToRemove.Add(pair2.Key);
}
if (imagesToRemove.Count > 0) {
IndirectObject io = _doc.ObjectSoup[pair1.Key];
while (io != null) {
Atom rez = io.Resolve(Atom.GetItem(io.Atom, "Resources"));
DictAtom xobjs = io.Resolve(Atom.GetItem(rez, "XObject")) as DictAtom;
if ((xobjs != null) && (xobjs.Count > 0)) {
List<string> namesToRemove = new List<string>();
foreach (KeyValuePair<string, Atom> pair in xobjs) {
IndirectObject xobj = io.ResolveObj(pair.Value);
if (xobj == null)
continue; // shouldn't ever happen
if (imagesToRemove.Contains(xobj.ID))
namesToRemove.Add(pair.Key);
}
foreach (string key in namesToRemove)
xobjs.Remove(key);
}
io = _imagesCoverWholeDoc ? io.ResolveObj(Atom.GetItem(io.Atom, "Parent")) : null;
}
}
}
}
}
// It is possible, though uncommon, for page contents to be shared between pages. If this happens
// and we redact part from one place in the document then it will also be redacted from other places
// it appears.
// This class keeps track of shared resources and performs a copy-on-write function as necessary.
// You just pass it the stream you want to modify and the page it appears on and it passes you back
// a stream that you can modify.
// In most cases the stream that you pass in will be the same as the one you get back. However if the
// stream is used on other pages, the function will duplicate the stream, adjust any references on the
// page so that the new stream is used in place of the old, then pass back the duplicate.
class ResourceTracker {
private Doc _doc;
private Dictionary<StreamObject, Dictionary<Page, int>> _pages;
private Dictionary<StreamObject, Dictionary<Page, Tuple<DictAtom, string>>> _xobjs;
private Dictionary<int, Dictionary<int, int>> _copyOnWriteCacheID;
private bool _identity; // most of the time we don't need any copies
private ResourceTracker() { }
public ResourceTracker(Doc doc) {
_doc = doc;
_pages = new Dictionary<StreamObject, Dictionary<Page, int>>();
_xobjs = new Dictionary<StreamObject, Dictionary<Page, Tuple<DictAtom, string>>>();
_copyOnWriteCacheID = new Dictionary<int, Dictionary<int, int>>();
// assemble all resources which may be shared
foreach (Page page in doc.ObjectSoup.Catalog.Pages.GetPageArrayAll()) {
StreamObject[] layers = page.GetLayers();
for (int i = 0; i < layers.Length; i++) {
StreamObject so = layers[i];
Dictionary<Page, int> entry = null;
_pages.TryGetValue(so, out entry);
if (entry == null) {
entry = new Dictionary<Page, int>();
_pages[so] = entry;
}
Debug.Assert(!entry.ContainsKey(page));
entry[page] = i;
}
IList<DictAtom> resourceDicts = page.GetResourceDictsByType("XObject", true, true, true, true, new HashSet<int>());
foreach (DictAtom resources in resourceDicts) {
foreach (var resource in resources) {
FormXObject formxobj = page.ResolveObj(resource.Value) as FormXObject;
if (formxobj == null)
continue;
Dictionary<Page, Tuple<DictAtom, string>> entry = null;
_xobjs.TryGetValue(formxobj, out entry);
if (entry == null) {
entry = new Dictionary<Page, Tuple<DictAtom, string>>();
_xobjs[formxobj] = entry;
}
entry[page] = new Tuple<DictAtom, string>(resources, resource.Key);
}
}
}
// remove unique entries just to simplify things
List<StreamObject> unique = new List<StreamObject>();
foreach (var pair in _pages)
if (pair.Value.Count <= 1)
unique.Add(pair.Key);
foreach (var item in unique)
_pages.Remove(item);
unique.Clear();
foreach (var pair in _xobjs)
if (pair.Value.Count <= 1)
unique.Add(pair.Key);
foreach (var item in unique)
_xobjs.Remove(item);
_identity = (_pages.Count == 0) && (_xobjs.Count == 0);
}
public int CopyOnWrite(int streamID, int pageID) {
if (_identity)
return streamID;
Dictionary<int, int> entry = null;
_copyOnWriteCacheID.TryGetValue(pageID, out entry);
if (entry != null) {
int id = 0;
entry.TryGetValue(streamID, out id);
if (id > 0)
return id;
}
else {
entry = new Dictionary<int, int>();
_copyOnWriteCacheID[pageID] = entry;
}
StreamObject so = UncachedCopyOnWrite(_doc.ObjectSoup[streamID] as StreamObject, _doc.ObjectSoup[pageID] as Page);
Debug.Assert(so != null);
entry[streamID] = so.ID;
return so.ID;
}
private StreamObject UncachedCopyOnWrite(StreamObject so, Page page) {
// this function must always be called from a function which caches the results
if (_identity)
return so;
Dictionary<Page, int> entry1 = null;
_pages.TryGetValue(so, out entry1);
if (entry1 != null) {
int index = entry1[page];
StreamObject copy = (StreamObject)so.Clone();
_doc.ObjectSoup.Add(copy);
ArrayAtom array = page.Resolve(Atom.GetItem(page.Atom, "Contents")) as ArrayAtom;
if (array != null) {
Debug.Assert(array.Count > index);
array[index] = new RefAtom(copy);
}
else {
Debug.Assert(index == 0);
Atom.SetItem(page.Atom, "Contents", new RefAtom(copy));
}
entry1.Remove(page);
if (entry1.Count <= 1)
_pages.Remove(so);
return copy;
}
Dictionary<Page, Tuple<DictAtom, string>> entry2 = null;
_xobjs.TryGetValue(so, out entry2);
if (entry2 != null) {
Tuple<DictAtom, string> index = entry2[page];
Debug.Assert(so is FormXObject);
StreamObject copy = (StreamObject)so.Clone();
_doc.ObjectSoup.Add(copy);
index.Item1[index.Item2] = new RefAtom(copy);
entry2.Remove(page);
return copy;
}
return so;
}
}
}