// =========================================================================== //  ©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.Collections.Generic; using System.ComponentModel; using System.Data; using System.Drawing; using System.Text; using System.Diagnostics; using System.Security.Cryptography; using SD = System.Drawing; using SD2 = System.Drawing.Drawing2D; using WebSupergoo.ABCpdf14; using WebSupergoo.ABCpdf14.Atoms; using WebSupergoo.ABCpdf14.Objects; using WebSupergoo.ABCpdf14.Operations; namespace WebSupergoo.PDFSurgeon {   public class StreamEditor {     private byte[] _liveData;     private static readonly HashSet<string> _pathMake = new HashSet<string>(new string[] { "m", "l", "c", "v", "y", "h", "re", "W", "W*" });     private static readonly HashSet<string> _pathPaint = new HashSet<string>(new string[] { "S", "s", "f", "F", "f*", "B", "B*", "b", "b*", "n" });     private StreamEditor() { }     public StreamEditor(ObjectExtractor obj) {       Extractor = obj;     }     public ObjectExtractor Extractor { get; private set; }     public StreamObject Stream { get { return Extractor.Object as StreamObject; } }     public StreamObject ContentStream { get; private set; }     public IndirectObject ResourceOwner { get; private set; }     public string OriginalText { get; set; }     public string LiveText { get; set; }     public string CurrentText { get { return Extractor.GetValue(Extractor.Doc); } set { Extractor.SetValue(Extractor.Doc, value); } }     public void Load() {       StreamObject so = Stream;       ContentStream = (so != null) && (!so.Compressed) && ((so is FormXObject) || (Extractor.Context.PageContents.Contains(so.ID))) ? so : null;       ResourceOwner = ContentStream as FormXObject;       if (ResourceOwner == null) {         int id = 0;         if (Extractor.Context.PageContentIdToPageId.TryGetValue(Extractor.Object.ID, out id))           ResourceOwner = Extractor.Doc.ObjectSoup[id];       }       OriginalText = LiveText = CurrentText;     }     public StringBuilder GetPartialContentStream(double lo, double hi) {       // get low and high values       if (lo > hi) {         double t = lo;         lo = hi;         hi = t;       }       // get stream data       if ((_liveData == null) && (ContentStream != null))         _liveData = ContentStream.GetData();       if (_liveData == null)         return null;       // analyse content stream       ArrayAtom array = ArrayAtom.FromContentStream(_liveData);       IList<Tuple<string, int>> items = OpAtom.Find(array);       Stack<int> saveRestoreStack = new Stack<int>();       Stack<int> markedContentStack = new Stack<int>();       List<Tuple<int, int>> pairs = new List<Tuple<int, int>>();       int beginText = -1, beginPath = -1;       for (int i = 0; i < items.Count; i++) {         var pair = items[i];         string op = ((OpAtom)array[pair.Item2]).Text;         if (op == "q")           saveRestoreStack.Push(i);         else if (op == "Q")           pairs.Add(new Tuple<int, int>(saveRestoreStack.Pop(), i));         if (op == "BMC" || op == "BDC")           markedContentStack.Push(i);         else if (op == "EMC") {           // Marked content start and end can be interwoven with other content start and end           // so we only put this sequence in as a pair if there is nothing inside it.           if (i == markedContentStack.Pop() + 1)             pairs.Add(new Tuple<int, int>(i - 1, i));         }         else if (op == "BT")           beginText = i;         else if (op == "ET") {           if (beginText >= 0)             pairs.Add(new Tuple<int, int>(beginText, i));           beginText = -1;         }         else if (_pathMake.Contains(op)) {           if (beginPath < 0)             beginPath = i;         }         else if (_pathPaint.Contains(op)) {           if (beginPath >= 0)             pairs.Add(new Tuple<int, int>(beginPath, i));           beginPath = -1;         }       }       //Debug.Assert(saveRestoreStack.Count == 0);       //Debug.Assert(markedContentStack.Count == 0);       // build set of sections which will be discarded       int loIndex = (int)(items.Count * lo);       int hiIndex = (int)(items.Count * hi);       Dictionary<int, int> discardPairs = new Dictionary<int, int>();       foreach (var pair in pairs) {         if (pair.Item2 < loIndex)           discardPairs[pair.Item1] = pair.Item2;         else if (pair.Item1 > hiIndex)           discardPairs[pair.Item1] = pair.Item2;       }       // build contents       int index = 0;       StringBuilder sb = new StringBuilder();       bool writeCommand = true;       for (int i = 0; i < items.Count; i++) {         int endIndex = 0;         if (discardPairs.TryGetValue(i, out endIndex)) {           writeCommand = false;           i = endIndex;         }         var pair = items[i];         if (writeCommand)           AppendCommand(sb, array, index, pair.Item2, false);         writeCommand = true;         index = pair.Item2 + 1;       }       return sb;     }     public string FormatContentStream() {       if (ContentStream == null)         return null;       return FormatContentStream(ContentStream);     }     private static string FormatContentStream(StreamObject stream) {       bool composite = false;       ArrayAtom array = ArrayAtom.FromContentStream(stream.GetData());       int indent = 0;       HashSet<string> indentPlus = new HashSet<string>(new string[] { "q", "BT" });       HashSet<string> indentMinus = new HashSet<string>(new string[] { "Q", "ET" });       IList<Tuple<string, int>> items = OpAtom.Find(array);       int index = 0;       StringBuilder code = new StringBuilder();       foreach (var pair in items) {         string op = ((OpAtom)array[pair.Item2]).Text;         // add indent to code         if (indentMinus.Contains(op))           indent--;         for (int i = 0; i < indent; i++)           code.Append(" ");         AppendCommand(code, array, index, pair.Item2, composite);         if (indentPlus.Contains(op))           indent++;         index = pair.Item2 + 1;       }       // write out any atoms that are left over       for (int i = index; i < array.Count; i++) {         code.Append(" ");         code.Append(array[i].ToString());       }       // update dict atom at start       DictAtom dict = (DictAtom)stream.Atom.Clone();       Atom.SetItem(dict, "Length", new NumAtom(code.Length));       StringBuilder result = new StringBuilder();       result.AppendLine(dict.ToString());       result.AppendLine("stream");       result.AppendLine(code.ToString());       result.AppendLine("endstream");       return result.ToString();     }     private static void AppendCommand(StringBuilder sb, ArrayAtom array, int startIndex, int endIndex, bool multibyte) {       for (int i = startIndex; i <= endIndex; i++) {         if (i != startIndex)           sb.Append(" ");         Atom item = array[i];         // We write arrays out individually so that we can override default cr lf behavior.         // We write strings out with checking so that we can use a hex encoding if appropriate.         // NB With ToString format overrides we can eliminate this function.         ArrayAtom itemArray = item as ArrayAtom;         StringAtom itemString = item as StringAtom;         if (itemArray != null) {           int n = itemArray.Count;           sb.Append("[");           for (int j = 0; j < n; j++) {             sb.Append(itemArray[j].ToString());             if (j != n - 1)               sb.Append(" ");           }           sb.Append("]");         }         else if (itemString != null) {           var data = itemString.Data;           int hexCount = 0;           for (int j = 0; j < data.Length; j++) {             if (data[j] < 32 || data[j] > 127)               hexCount++;           }           if (hexCount > data.Length / 3) {             sb.Append('<');             foreach (var b in data)               sb.Append(b.ToString("X2"));             sb.Append('>');           }           else {             sb.Append(item.ToString());           }         }         else {           sb.Append(item.ToString());         }       }       sb.AppendLine();     }   }   public class GraphicsScanner : ContentStreamScanner {     [DebuggerDisplay("Bounds = {Bounds} Text = \"{Text}\"")]     public class Fixture {       public Fixture(SD2.GraphicsPath path, int offset, int length, byte[] data) { PdfPath = path; Offset = offset; Length = length; Data = data; }       public SD2.GraphicsPath PdfPath { get; set; }       public SD2.GraphicsPath Path { get; set; }       public SD.Region Region { get; set; }       public double Area { get; set; }       public int Offset { get; set; }       public int Length { get; set; }       public byte[] Data { get; set; }       public int OwnerID { get; set; }       public string Text {         get {           int length = Math.Min(Data.Length - Offset, Length);           if (Offset < 0 || length <= 0)             return "";           return ASCIIEncoding.ASCII.GetString(Data, Offset, length).TrimEnd();         }       }       public string Context {         get {           return ASCIIEncoding.ASCII.GetString(Data, 0, Data.Length);         }       }       public RectangleF Bounds {         get {           return Path != null ? Path.GetBounds() : new RectangleF();         }       }     }     public class Graphics {       public List<Fixture> All { get; set; } = new List<Fixture>();       public Dictionary<int, List<Fixture>> Container { get; set; } = new Dictionary<int, List<Fixture>>();       public void Transform(float top, float dpi) {         var xform = new SD2.Matrix();         xform.Scale(1, -1, SD2.MatrixOrder.Append);         xform.Translate(0, (float)top, SD2.MatrixOrder.Append);         float scale = dpi / 72.0f;         xform.Scale(scale, scale, SD2.MatrixOrder.Append);         foreach (var part in All) {           part.Path = (SD2.GraphicsPath)part.PdfPath.Clone();           part.Path.Transform(xform);           try {             for (int i = 0; i < 10; i++) {               part.Area = 0;               part.Region = new Region(part.Path);               foreach (var rect in part.Region.GetRegionScans(new SD2.Matrix()))                 part.Area += Math.Abs(rect.Width * rect.Height);               if (part.Area > 1)                 break;               // Widen throws out of memory if path is a point               part.Path.Widen(new Pen(Color.Black, 1));             }           }           catch {           }         }       }     }     private static readonly HashSet<string> _ops;     static GraphicsScanner() {       List<string> state = new List<string>();       state.AddRange(ContentStreamScanner.OperatorCategories.PathPainting);       state.AddRange(ContentStreamScanner.OperatorCategories.PathConstruction);       state.AddRange(ContentStreamScanner.OperatorCategories.ClippingPaths);       state.AddRange(ContentStreamScanner.OperatorCategories.SpecialGraphicsState);       state.AddRange(ContentStreamScanner.OperatorCategories.TextObjects);       state.AddRange(ContentStreamScanner.OperatorCategories.TextState);       state.AddRange(ContentStreamScanner.OperatorCategories.TextPositioning);       state.AddRange(ContentStreamScanner.OperatorCategories.TextShowing);       state.AddRange(ContentStreamScanner.OperatorCategories.XObjects);       state.Add("Meta:InlineImage");       _ops = new HashSet<string>(state);     }     private IndirectObject _owner = null;     private byte[] _data = null;     private SD.PointF _point = new PointF();     private SD2.GraphicsPath _path = null;     private Graphics _results = null;     private int _start = 0, _pos = 0;     public GraphicsScanner(IndirectObject owner) : base() {       if (owner == null)         throw new NullReferenceException(nameof(owner));       _owner = owner;       Operators = _ops;       DoWidths = true;       DoShowText = true;       DoUnicode = false;     }     public Graphics GetResults() {       Process();       return _results;     }     public void Process() {       var contentIDs = new List<int>();       var contentEnds = new List<int>();       if (_owner is FormXObject) {         FormXObject xo = (FormXObject)_owner;         _data = new byte[xo.CopyDecompressedData()];         xo.CopyDecompressedData(_data);         contentIDs.Add(xo.ID);         contentEnds.Add(_data.Length);       }       else if (_owner is Page) {         Page page = (Page)_owner;         int offset = 0;         foreach (var so in page.GetLayers()) {           int length = so.CopyDecompressedData();           contentIDs.Add(so.ID);           contentEnds.Add(offset + length);           offset += length + 1;         }         _data = page.GetContentData();       }       else         throw new Exception("Object is not a content stream type.");       _point = new PointF();       _path = null;       _results = new Graphics();       _results.All = new List<Fixture>();       // get data       IList<int> offsets = null;       ArrayAtom array = ArrayAtom.FromContentStream(_data, out offsets);       offsets.Add(_data.Length);       Debug.Assert(offsets.Count == array.Count + 1);       // process       _start = _pos = 0;       Process(_owner, array);       // convert ArrayAtom based offsets to byte based offsets       foreach (var part in _results.All) {         Debug.Assert(part.Length >= 0);         if (part.Offset + part.Length < offsets.Count) {           int p1 = offsets[part.Offset];           int p2 = offsets[part.Offset + part.Length];           Debug.Assert(p2 - p1 >= 0);           part.Offset = p1;           part.Length = p2 - p1;         }         else {           part.Offset = 0;           part.Length = 0;         }       }       // identify original containers       if (true) {         List<Fixture> list = null;         int item = -1, last = -1, id = -1;         foreach (var part in _results.All) {           while (part.Offset >= last) {             item++;             last = contentEnds[item];             list = new List<Fixture>();             id = contentIDs[item];             _results.Container[id] = list;           }           part.OwnerID = id;           list.Add(part);         }       }       // add in annotations       if (_owner is Page) {         Page page = (Page)_owner;         foreach (var annot in page.GetAnnotations()) {           var path = MakeRectPath(annot.Rect);           var desc = $"Annotation ID {annot.ID}";           var data = ASCIIEncoding.ASCII.GetBytes(desc);           var fixture = new Fixture(path, 0, data.Length, data);           _results.All.Add(fixture);           var list = new List<Fixture>();           list.Add(fixture);           _results.Container[annot.ID] = list;         }       }     }     public override void ProcessItem(IndirectObject owner, ArrayAtom contents, string op, int pos) {       _pos = pos;       try {         base.ProcessItem(owner, contents, op, pos);       }       catch (Exception ex) { #if FALSE         Debug.Assert(false, "Unexpected exception. Probable corrupt stream."); #endif       }       switch (op) {         // path construction         case "m":           if (true) {             if (_path == null) {               _start = pos - 2;               _path = new SD2.GraphicsPath();             }             _path.StartFigure();             _point = GetPointF(contents, pos, 0, 1);           }           break;         case "l":           if (true) {             if (_path == null) {               _start = pos - 2;               _path = new SD2.GraphicsPath();             }             PointF point = GetPointF(contents, pos, 0, 1);             _path.AddLine(_point, point);             _point = point;           }           break;         case "c":           if (true) {             if (_path == null) {               _start = pos - 6;               _path = new SD2.GraphicsPath();             }             PointF control1 = GetPointF(contents, pos, 0, 3);             PointF control2 = GetPointF(contents, pos, 1, 3);             PointF point = GetPointF(contents, pos, 2, 3);             _path.AddBezier(_point, control1, control2, point);             _point = point;           }           break;         case "v":           if (true) {             if (_path == null) {               _start = pos - 4;               _path = new SD2.GraphicsPath();             }             PointF control = GetPointF(contents, pos, 0, 2);             PointF point = GetPointF(contents, pos, 1, 2);             _path.AddBezier(_point, _point, control, point);             _point = point;           }           break;         case "y":           if (true) {             if (_path == null) {               _start = pos - 4;               _path = new SD2.GraphicsPath();             }             PointF control = GetPointF(contents, pos, 0, 2);             PointF point = GetPointF(contents, pos, 1, 2);             _path.AddBezier(_point, control, point, point);             _point = point;           }           break;         case "h":           if (true) {             if (_path == null) {               _start = pos;               _path = new SD2.GraphicsPath();             }             _path.CloseFigure();           }           break;         case "re":           if (true) {             if (_path == null) {               _start = pos - 4;               _path = new SD2.GraphicsPath();             }             _path.StartFigure();             _point = GetPointF(contents, pos, 0, 2);             PointF size = GetPointF(contents, pos, 1, 2);             RectangleF rectF = new RectangleF(_point.X, _point.Y, size.X, size.Y);             _path.AddLine(rectF.X, rectF.Y, rectF.X + rectF.Width, rectF.Y);             _path.AddLine(rectF.X + rectF.Width, rectF.Y, rectF.X + rectF.Width, rectF.Y + rectF.Height);             _path.AddLine(rectF.X + rectF.Width, rectF.Y + rectF.Height, rectF.X, rectF.Y + rectF.Height);             _path.CloseFigure();           }           break;         // path drawing         case "S":         case "s":         case "f":         case "F":         case "f*":         case "B":         case "B*":         case "b":         case "b*":         case "n":           if (_path != null) {             _path.Transform(State.CTM.Matrix);             _results.All.Add(new Fixture(_path, _start, pos - _start + 1, _data));             _start = pos;             _path = null;             _point = new PointF();           }           else { #if FALSE             var snip = new ArrayAtom();             for (int i = pos - 20; i < pos + 20 && i >= 0; i++)               snip.Add(contents[i].Clone());             string snippet = snip.ToString();             Debug.Assert(false, "Degenerate path detected."); #endif           }           break;         // xobject         case "Do":           if (true) {             var pair = Resources.GetResource(_owner, ResourceType.XObject, GetName(contents, pos));             FormXObject formX = pair != null ? pair.Object as FormXObject : null;             XRect rect = formX != null ? formX.BBox : XRect.FromSides(0, 0, 1, 1);             RectangleF rectF = new RectangleF((float)rect.Left, (float)rect.Top, (float)rect.Width, (float)rect.Height);             var path = MakeRectPath(rect);             if (formX != null && formX.Matrix != null)               path.Transform(formX.Matrix.Matrix);             path.Transform(State.CTM.Matrix);             _results.All.Add(new Fixture(path, pos - 1, 2, _data));           }           break;         // other         case "Meta:InlineImage":           if (true) {             XRect rect = XRect.FromSides(0, 0, 1, 1);             var path = new SD2.GraphicsPath();             path.AddLine((float)rect.Left, (float)rect.Bottom, (float)rect.Right, (float)rect.Bottom);             path.AddLine((float)rect.Right, (float)rect.Bottom, (float)rect.Right, (float)rect.Top);             path.AddLine((float)rect.Right, (float)rect.Top, (float)rect.Left, (float)rect.Top);             path.CloseFigure();             path.Transform(State.CTM.Matrix);             _results.All.Add(new Fixture(path, pos, 1, _data));           }           break;       }     }     public override void ShowText(List<int> codes, List<int> widths, List<double> advances, double advanceTotal, StringBuilder text, List<string> strings, bool vertical) {       advanceTotal /= State.TextFontSize;       var rect = !vertical ? XRect.FromSides(0, 0, advanceTotal, 1) : XRect.FromSides(0, 0, 1, advanceTotal);       var trm = Text.GetTextRenderingMatrix(State); // includes font size and CTM       var corners = trm.TransformPoints(rect.GetCorners());       var points = new PointF[corners.Length];       for (int i = 0; i < corners.Length; i++)         points[i] = new PointF((float)corners[i].X, (float)corners[i].Y);       var path = new SD2.GraphicsPath();       path.AddLines(points);       path.CloseFigure();       _results.All.Add(new Fixture(path, _pos - 1, 2, _data));       base.ShowText(codes, widths, advances, advanceTotal, text, strings, vertical);     }     private static string GetName(ArrayAtom array, int pos) {       return array[pos - 1, (Enum)null];     }     private static PointF GetPointF(ArrayAtom array, int pos, int index, int count) {       pos = pos - (count * 2) + (index * 2);       return new PointF((float)array[pos, (double)0], (float)array[pos + 1, (double)0]);     }     private SD2.GraphicsPath MakeRectPath(XRect rect) {       var path = new SD2.GraphicsPath();       path.AddLine((float)rect.Left, (float)rect.Bottom, (float)rect.Right, (float)rect.Bottom);       path.AddLine((float)rect.Right, (float)rect.Bottom, (float)rect.Right, (float)rect.Top);       path.AddLine((float)rect.Right, (float)rect.Top, (float)rect.Left, (float)rect.Top);       path.CloseFigure();       return path;     }   } }