// ===========================================================================
// ©2013-2024 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.Linq;
using System.Text;
using System.IO;
using System.Threading.Tasks;
using System.Windows.Forms;
using System.Net;
using System.Xml;
using System.Diagnostics;
using WebSupergoo.ABCpdf14;
using WebSupergoo.ABCpdf14.Objects;
using WebSupergoo.ABCpdf14.Atoms;
using WebSupergoo.ABCpdf14.Operations;
using WebSupergoo.ABCpdf14.Elements;
using System.Net.Http.Headers;
namespace GetColors {
public partial class Form1 : Form {
public Form1() {
InitializeComponent();
backgroundWorker1.WorkerReportsProgress = true;
}
internal string Root;
internal List<string> Files;
private void Form1_Load(object sender, EventArgs e) {
Root = Directory.GetParent(Directory.GetCurrentDirectory()).Parent.FullName + "\\";
Files = new List<string>();
}
private void Form1_Activated(object sender, EventArgs e) {
string item = comboBox1.SelectedItem != null ? (string)comboBox1.SelectedItem : null;
comboBox1.Items.Clear();
foreach (string path in Directory.GetFiles(Root, "*.pdf", SearchOption.TopDirectoryOnly))
comboBox1.Items.Add(path);
foreach (string path in Directory.GetFiles(Root, "*.eps", SearchOption.TopDirectoryOnly))
comboBox1.Items.Add(path);
if (comboBox1.Items.Count > 0)
comboBox1.SelectedIndex = item == null ? 0 : comboBox1.FindStringExact(item);
}
private void button1_Click(object sender, EventArgs e) {
string file = (string)comboBox1.SelectedItem;
// get colors
ColorScanner scanner = new ColorScanner();
HashSet<AbstractColor> colors = new HashSet<AbstractColor>();
TimeSpan time = scanner.Scan(file, colors);
// sort colors
List<AbstractColor> list = new List<AbstractColor>(colors);
list.Sort((i1, i2) => {
string i1s = (i1.Space == null ? "na" : i1.Space.ColorSpace.ToString()) + i1.Color.ToString();
string i2s = (i2.Space == null ? "na" : i2.Space.ColorSpace.ToString()) + i2.Color.ToString();
return i1s.CompareTo(i2s);
});
// report colors
StringBuilder sb = new StringBuilder();
sb.AppendLine("Document \"" + Path.GetFileName(file) + "\" contains " + colors.Count.ToString() + " colors.");
foreach (AbstractColor color in list)
sb.AppendLine(color.ToString());
sb.AppendLine("");
sb.AppendLine($"Processed in {(int)time.TotalMilliseconds} ms.");
textBox1.Text = sb.ToString();
}
private void button2_Click(object sender, EventArgs e) {
MakeSimpleSvg((string)comboBox1.SelectedItem, 0);
}
private void button3_Click(object sender, EventArgs e) {
List<string> files = new List<string>();
for (int i = 0; i < comboBox1.Items.Count; i++)
files.Add((string)comboBox1.Items[i]);
Files = files;
backgroundWorker1.RunWorkerAsync();
}
private void backgroundWorker1_DoWork(object sender, DoWorkEventArgs e) {
int total = Files.Count;
while (Files.Count > 0) {
string file = Files[Files.Count - 1];
Files.RemoveAt(Files.Count - 1);
int done = total - Files.Count;
string txt = MakeSimpleSvg(file, done);
backgroundWorker1.ReportProgress((done * 100) / total, txt);
}
backgroundWorker1.ReportProgress(0);
}
private void backgroundWorker1_ProgressChanged(object sender, ProgressChangedEventArgs e) {
textBox1.Text += (string)e.UserState;
progressBar1.Value = e.ProgressPercentage;
}
private string MakeSimpleSvg(string srcPath, int count) {
string txt = "";
string dstDir = Path.Combine(Root, "_svg");
Directory.CreateDirectory(dstDir);
string name = Path.GetFileNameWithoutExtension(srcPath);
try {
txt += $"Document \"{name}\"\r\n";
TimeSpan time = new TimeSpan();
SvgExporter scanner = new SvgExporter();
scanner.Warnings = new List<string>();
using (Doc doc = new Doc()) {
Stopwatch timer = new Stopwatch();
timer.Start();
doc.Read(srcPath);
timer.Stop();
txt += $"Read in {(int)timer.ElapsedMilliseconds} ms.\r\n";
int pageCount = Math.Min((int)maxPages.Value, doc.PageCount);
for (int j = 0; j < pageCount; j++) {
doc.PageNumber = j + 1;
time += scanner.Scan(doc);
string svgPath = Path.Combine(dstDir, $"{name}_p{j + 1}.svg");
File.WriteAllText(svgPath, scanner.Svg.ToString());
scanner.Svg.Clear();
}
}
txt += $"Scanned in {(int)time.TotalMilliseconds} ms.\r\n";
if (scanner.Warnings.Count > 0)
txt += $"Warnings:\r\n" + string.Join("\r\n", scanner.Warnings);
txt += "\r\n";
}
catch (Exception ex) {
string errPath = Path.Combine(Path.Combine(Root, "_svg"), $"{name}_error.txt");
File.WriteAllText(errPath, ex.ToString(), Encoding.UTF8);
}
return txt;
}
}
public class ColorScanner : ContentStreamScanner {
public int Precision { get; set; } = 255;
public ColorScanner() {
List<string> ops = new List<string>(new string[] { "q", "Q" });
ops.AddRange(ContentStreamScanner.OperatorCategories.Color);
Operators = new HashSet<string>(ops);
}
public HashSet<AbstractColor> Colors { get; } = new HashSet<AbstractColor>();
public TimeSpan Scan(string file, HashSet<AbstractColor> colors) {
Stopwatch timer = new Stopwatch();
using (Doc doc = new Doc()) {
doc.Read(file);
for (int i = 0; i < doc.PageCount; i++) {
doc.PageNumber = i + 1;
Page page = (Page)doc.ObjectSoup[doc.Page];
ContentStreamOperation op = new ContentStreamOperation(doc);
op.AddPages(i + 1);
foreach (var pair in op.ArrayAtoms) {
Colors.Clear();
timer.Start();
Process(pair.Key, pair.Value);
timer.Stop();
ResolveColorSpaces(page, pair.Key.ID, Colors, colors);
}
}
}
return timer.Elapsed;
}
public override void ProcessItem(IndirectObject owner, ArrayAtom array, string op, int pos) {
base.ProcessItem(owner, array, op, pos);
switch (op) {
case "g":
case "rg":
case "k":
case "sc":
case "scn":
Colors.Add(new AbstractColor(State.FillColorSpace, State.FillColor, Precision));
break;
case "G":
case "RG":
case "K":
case "SC":
case "SCN":
Colors.Add(new AbstractColor(State.StrokeColorSpace, State.StrokeColor, Precision));
break;
}
}
public void ResolveColorSpaces(Page page, int id, HashSet<AbstractColor> src, HashSet<AbstractColor> dst) {
IDictionary<string, Atom> spaces = page.GetResourceMap(id, "ColorSpace");
foreach (string deviceSpace in new string[] { "DeviceGray", "DeviceRGB", "DeviceCMYK" })
if (!spaces.ContainsKey(deviceSpace))
spaces[deviceSpace] = new NameAtom(deviceSpace);
foreach (AbstractColor color in src) {
if (color.Name != "Pattern") {
Atom space = spaces[color.Name];
color.Space = (ColorSpaceElement)ElementFactories.AutodetectFactory(space, page, null);
color.Name = page.Resolve(space).ToString();
}
dst.Add(color);
}
}
}
public class AbstractColor {
public AbstractColor() { Precision = 255; }
public AbstractColor(string name, double[] color, int precision) {
Name = name; Color = color; Space = null; Precision = precision;
}
public double[] Color { get; set; }
public string Name { get; set; }
public ColorSpaceElement Space { get; set; }
public int Precision { get; set; }
public override int GetHashCode() {
int hash = 17;
int count = Color != null ? Color.Length : 0;
for (int i = 0; i < count; i++)
hash = hash * 23 + ToInt(Color[i]).GetHashCode();
if (Name != null)
hash = hash * 23 + Name.GetHashCode();
return hash;
}
public override bool Equals(object obj) {
AbstractColor other = obj as AbstractColor;
if (other == null)
return false;
int n1 = Color != null ? Color.Length : 0;
int n2 = other.Color != null ? other.Color.Length : 0;
if (n1 != n2)
return false;
for (int i = 0; i < n1; i++) {
if (ToInt(Color[i]) != ToInt(other.Color[i]))
return false;
}
if (Name != other.Name)
return false;
return true;
}
public override string ToString() {
StringBuilder sb = new StringBuilder();
sb.Append("Components: ");
int n1 = Color != null ? Color.Length : 0;
string format = Precision == 0xFF ? "x2" : (Precision == 0xFFFF ? "x4" : "");
for (int i = 0; i < n1; i++) {
if (i != 0)
sb.Append(", ");
sb.Append(ToInt(Color[i]).ToString(format));
}
sb.Append(" Space: ");
sb.Append(Name);
return sb.ToString();
}
private int ToInt(double value) {
return (int)Math.Round(value * Precision);
}
}
public class SvgExporter : ContentStreamScanner {
public SvgExporter() {
List<string> ops = new List<string>();
ops.AddRange(OperatorCategories.SpecialGraphicsState);
ops.AddRange(OperatorCategories.TextObjects);
ops.AddRange(OperatorCategories.TextPositioning);
ops.AddRange(OperatorCategories.TextShowing);
ops.AddRange(OperatorCategories.TextState);
ops.AddRange(OperatorCategories.XObjects);
ops.AddRange(OperatorCategories.PathConstruction);
ops.AddRange(OperatorCategories.PathPainting);
ops.AddRange(OperatorCategories.ClippingPaths);
ops.Add("gs");
Operators = new HashSet<string>(ops);
DoFormXObjects = true;
}
private StringBuilder _path { get; set; } = new StringBuilder();
private double _x = 0, _y = 0;
public StringBuilder Svg { get; set; } = new StringBuilder();
public TimeSpan Scan(Doc doc) {
Stopwatch timer = new Stopwatch();
Page page = (Page)doc.ObjectSoup[doc.Page];
ArrayAtom contents = ArrayAtom.FromContentStream(page.GetContentData());
bool topLevel = Svg.Length == 0;
timer.Start();
if (topLevel) {
double w = page.MediaBox.Width, h = page.MediaBox.Height;
Svg.AppendLine("<?xml version=\"1.0\" encoding=\"UTF-8\" standalone=\"no\"?>");
Svg.AppendLine($"<svg width=\"{w}\" height=\"{h}\" x=\"0\" y=\"0\" version=\"1.1\" baseProfile=\"full\" xmlns=\"http://www.w3.org/2000/svg\" xmlns:xlink=\"http://www.w3.org/1999/xlink\" viewBox=\"0 -{h} {w} {h}\">");
Svg.AppendLine($"\t<rect x=\"0\" y=\"{-h}\" width=\"{w}\" height=\"{h}\" fill=\"seashell\" />");
}
Process(page, contents);
if (topLevel)
Svg.AppendLine($"</svg>");
timer.Stop();
return timer.Elapsed;
}
public override void ProcessItem(IndirectObject owner, ArrayAtom contents, string op, int pos) {
double GetDbl(int idx) {
return Atom.GetDouble(contents[pos - idx]);
}
string GetTransform() {
var s = State.CTM;
if (s == null || s.IsIdentity) return "";
var el = s.Elements;
return $"transform =\"matrix({el[0]}, {el[1]}, {el[2]}, {-el[3]}, {el[4]}, {-el[5]})\"";
}
base.ProcessItem(owner, contents, op, pos);
switch (op) {
case "m":
_x = GetDbl(2); _y = GetDbl(1);
_path.Append($"M{_x} {_y} ");
break;
case "l":
_x = GetDbl(2); _y = GetDbl(1);
_path.Append($"L{_x} {_y} ");
break;
case "c":
_path.Append($"C{GetDbl(6)} {GetDbl(5)} {GetDbl(4)} {GetDbl(3)} {GetDbl(2)} {GetDbl(1)} ");
_x = GetDbl(2); _y = GetDbl(1);
break;
case "v":
_path.Append($"C{_x} {_y} {GetDbl(4)} {GetDbl(3)} {GetDbl(2)} {GetDbl(1)} ");
_x = GetDbl(2); _y = GetDbl(1);
break;
case "y":
_path.Append($"C{GetDbl(4)} {GetDbl(3)} {GetDbl(4)} {GetDbl(3)} {GetDbl(2)} {GetDbl(1)} ");
_x = GetDbl(2); _y = GetDbl(1);
break;
case "h":
_path.Append($"Z ");
break;
case "re":
_x = GetDbl(4); _y = GetDbl(3);
double w = GetDbl(2), h = GetDbl(1);
_path.Append($"M{_x} {_y} L{_x + w} {_y} L{_x + w} {_y + h} L{_x} {_y + h} Z ");
break;
case "S":
Svg.AppendLine($"\t<path d=\"{_path}\" fill=\"none\" stroke=\"blue\" />");
_path.Clear();
break;
case "s":
Svg.AppendLine($"\t<path d=\"{_path}Z\" fill=\"none\" stroke=\"blue\" />");
_path.Clear();
break;
case "f":
case "F":
Svg.AppendLine($"\t<path d=\"{_path}Z\" fill=\"red\" stroke=\"none\" {GetTransform()}/>");
_path.Clear();
break;
case "f*":
Svg.AppendLine($"\t<path d=\"{_path}Z\" fill=\"red\" fill-rule=\"evenodd\" stroke=\"none\" {GetTransform()}/>");
_path.Clear();
break;
case "B":
Svg.AppendLine($"\t<path d=\"{_path}\" fill=\"red\" stroke=\"blue\" {GetTransform()}/>");
_path.Clear();
break;
case "B*":
Svg.AppendLine($"\t<path d=\"{_path}\" fill=\"red\" fill-rule=\"evenodd\" stroke=\"blue\" {GetTransform()}/>");
_path.Clear();
break;
case "b":
Svg.AppendLine($"\t<path d=\"{_path}Z\" fill=\"red\" stroke=\"blue\" {GetTransform()}/>");
_path.Clear();
break;
case "b*":
Svg.AppendLine($"\t<path d=\"{_path}Z\" fill=\"red\" fill-rule=\"evenodd\" stroke=\"blue\" {GetTransform()}/>");
_path.Clear();
break;
case "n":
break;
case "W":
// nonzero clipping path not supported here
_path.Clear();
break;
case "W*":
// evenodd clipping path not supported here
_path.Clear();
break;
}
}
public override void ShowText(List<int> codes, List<int> widths, List<double> advances, double advanceTotal, StringBuilder text, List<string> strings, bool vertical) {
GraphicsState state = State;
FontObject font = (FontObject)Resources.GetResource(Owners.Peek(), ResourceType.Font, state.TextFont).Object;
double[] saved = Text.TextMatrix.Elements;
string comment = VetText(string.Join("", text.ToString())).Replace("--", "\u2013\u2013"); // n-dash characters
Svg.AppendLine($"\t<g><!-- {WebUtility.HtmlEncode(comment)} -->");
for (int i = 0; i < codes.Count; i++) {
string part = strings[i];
if (part == null)
part = text[i].ToString();
double[] trm = Text.GetTextRenderingMatrix(state).Elements;
string matrix = $"transform=\"matrix({trm[0]} {trm[1]} {trm[2]} {trm[3]} {trm[4]} {-trm[5]})\"";
string family = $"font-family=\"{WebUtility.HtmlEncode(VetText(font.BaseFont))}\"";
string html = WebUtility.HtmlEncode(VetText(part));
Svg.AppendLine($"\t\t<text font-size=\"1\" {matrix} {family}>{html}</text>");
Text.Advance(advances[i], vertical);
}
Svg.AppendLine("\t</g>");
Text.TextMatrix.Elements = saved;
}
public static string VetText(string text) {
if (text == null)
return " ";
bool ok = true;
for (int i = 0; i < text.Length; i++) {
if (!XmlConvert.IsXmlChar(text[i])) {
ok = false;
break;
}
}
if (ok) return text;
StringBuilder sb = new StringBuilder(text.Length);
for (int i = 0; i < text.Length; i++) {
char c = text[i];
sb.Append(XmlConvert.IsXmlChar(c) ? c : ' ');
}
return sb.ToString();
}
}
}