// ===========================================================================
// ©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;
using System.Collections.Generic;
using System.Diagnostics;
using System.ComponentModel;
using System.IO;
using System.Text;
using WebSupergoo.ABCpdf14;
using WebSupergoo.ABCpdf14.Objects;
using WebSupergoo.ABCpdf14.Atoms;
using WebSupergoo.ABCpdf14.Operations;
using WebSupergoo.ABCpdf14.Elements;
namespace ValidatePDF {
public class ValidationUtilities {
/// <summary>
/// Validate a folder of PDF files.
/// </summary>
/// <param name="folder">The folder to process.</param>
/// <param name="checkObjects">Whether to check the document objects.</param>
/// <param name="reportOrphans">Whether to report orphaned Elements which were not fully validated.</param>
/// <param name="reportCoverage">Whether to report the types of PDF classes which were covered, and those missed.</param>
/// <param name="checkContentStreams">Whether to check the content streams.</param>
/// <param name="checkCompression">Whether to check stream compression. The checkSyntax option does this as well so typically you do not need both.</param>
/// <param name="checkSyntax">Whether to check syntax using the PDF/A validator.</param>
public static void Validate(string folder, bool checkObjects, bool reportOrphans, bool reportCoverage, bool checkContentStreams, bool checkCompression, bool checkSyntax) {
HashSet<string> coverage = new HashSet<string>();
foreach (string file in Directory.GetFiles(folder, "*.pdf"))
Validate(file, file.Substring(0, file.Length - 4) + ".txt", checkObjects, checkContentStreams, checkCompression, checkSyntax, coverage, reportOrphans);
if (reportCoverage) {
HashSet<string> missed = ValidationLog.AllTypes;
foreach (string cls in coverage)
missed.Remove(cls);
StringBuilder sb = new StringBuilder();
sb.AppendLine("Covered:");
foreach (string cls in coverage)
sb.AppendLine(cls);
sb.AppendLine("");
sb.AppendLine("Missed:");
foreach (string cls in missed)
sb.AppendLine(cls);
File.WriteAllText(Path.Combine(folder, "coverage.txt"), sb.ToString());
}
}
/// <summary>
/// Validate a PDF file, reporting to a log file.
/// </summary>
/// <param name="pdfFile">The PDF to be validated</param>
/// <param name="logFile">The log file to which results should be written</param>
/// <param name="coverage">A HashSet to be populated with the types of PDF classes which are seen during validation</param>
/// <param name="reportOrphans">Whether to report orphaned Elements which were not fully validated</param>
public static void Validate(string pdfFile, string logFile, bool checkObjects, bool checkContentStreams, bool checkCompression, bool checkSyntax, HashSet<string> coverage, bool reportOrphans) {
List<string> logs = new List<string>();
using (Doc theDoc = new Doc()) {
try {
theDoc.Read(pdfFile);
}
catch (Exception ex) {
logs.Add("Unable to read file. " + ex.Message);
checkObjects = false;
checkContentStreams = false;
}
if (checkObjects)
logs.Add(ValidateDocument(theDoc, coverage, reportOrphans));
if (checkContentStreams)
logs.Add(ValidateContents(theDoc));
if (checkCompression)
logs.Add(ValidateCompression(theDoc));
}
if (checkSyntax)
logs.Add(ValidateSyntax(pdfFile));
File.WriteAllText(logFile, string.Join("\r\n\r\n", logs));
}
/// <summary>
/// Validate the objects in a PDF file, returning the results.
/// </summary>
/// <param name="doc">The Doc to be validated</param>
/// <param name="coverage">A HashSet to be populated with the types of PDF classes which are seen during validation</param>
/// <param name="reportOrphans">Whether to report orphaned Elements which were not fully validated</param>
/// <returns>The results of validation</returns>
public static string ValidateDocument(Doc doc, HashSet<string> coverage, bool reportOrphans) {
ValidationLog validation = new ValidationLog();
validation.Coverage = coverage;
validation.Stack = new Stack<ValidationStackNode>();
CatalogElement cat = new CatalogElement(doc.ObjectSoup.Catalog);
cat.Validate(validation);
StreamObject trail = doc.ObjectSoup.Trailer;
bool isCrossRef = Atom.GetItem(trail.Atom, "W") != null;
CrossReferenceStreamElement crossRef = isCrossRef ? new CrossReferenceStreamElement(trail) : null;
FileTrailerElement fileTrailer = !isCrossRef ? new FileTrailerElement(trail) : null;
Element trailer = isCrossRef ? (Element)crossRef : (Element)fileTrailer;
trailer.Validate(validation);
if (doc.Encryption.Type != 0) {
validation.Start(trailer);
EncryptionElement encryption = crossRef != null ? crossRef.EntryEncrypt : fileTrailer.EntryEncrypt;
validation.ReportEntryVersion("Encryption.V", encryption != null ? encryption.GetEntryVersion("V") : GetEncryptionVEntryVersion(doc.Encryption.Type));
}
StringBuilder sb = validation.Log;
HashSet<int> done = validation.Done;
List<IndirectObject> missed = new List<IndirectObject>();
foreach (IndirectObject io in doc.ObjectSoup) {
if ((io != null) && (io.ID != 0) && (!done.Contains(io.ID))) {
try {
Element e = ElementFactories.AutodetectFactory(new RefAtom(io), io, null);
if ((e is ObjectStreamElement) || (e is CrossReferenceStreamElement) || (e is LinearizationParameterElement)) {
e.Validate(validation);
done = validation.Done;
}
}
catch (Exception ex) {
validation.ReportException(io, ex);
}
}
}
if (reportOrphans) {
foreach (IndirectObject io in doc.ObjectSoup) {
if ((io != null) && (io.ID != 0) && (!done.Contains(io.ID)))
missed.Add(io);
}
if (missed.Count > 0) {
HashSet<int> refOnlyObjects = new HashSet<int>();
Dictionary<int, HashSet<int>> references = new Dictionary<int, HashSet<int>>();
foreach (IndirectObject io in doc.ObjectSoup) {
if (io != null) {
bool hasContent = false;
FindAllReferences(io.ID, io.Atom, references, ref hasContent);
if (!hasContent)
refOnlyObjects.Add(io.ID);
}
}
foreach (IndirectObject io in missed) {
HashSet<int> set = null;
references.TryGetValue(io.ID, out set);
if (set == null)
continue; // object not referenced by anything - ignore
if (refOnlyObjects.Contains(io.ID))
continue; // only reference atoms - simply a stop en route to other items
sb.AppendLine("Missed ID: " + io.ID.ToString() + ": " + IndirectObjectDescription(io));
foreach (var id in set)
sb.AppendLine(" Referenced by ID: " + id.ToString() + ": " + IndirectObjectDescription(doc.ObjectSoup[id]));
}
}
}
string header = "PDF Version: " + GetVersion(validation.Version) + "\r\n";
if (validation.Version.HasFlag(PDFVersion.AdobeUndocumented))
header += "PDF Uses Undocumented Extensions\r\n";
if (validation.Version.HasFlag(PDFVersion.PDF20Deprecated)) {
header += "PDF Uses PDF 2.0 Deprecated Features\r\n";
foreach (string feature in validation.Deprecated)
header += " - " + feature + "\r\n";
}
header += "\r\n";
return header + sb.ToString();
}
/// <summary>
/// Validate the content streams in a PDF file, returning the results.
/// </summary>
/// <param name="doc">The Doc to be validated</param>
/// <returns>The results of validation</returns>
public static string ValidateContents(Doc doc)
{
StringBuilder sb = new StringBuilder();
List<IndirectObject> items = new List<IndirectObject>();
// gather items
HashSet<int> skip = new HashSet<int>();
try {
foreach (Page page in doc.ObjectSoup.Catalog.Pages.GetPageArrayAll()) {
items.Add(page);
foreach (Atom atom in page.GetResourcesByType("XObject", true, true, true, true, skip)) {
IndirectObject rez = page.ResolveObj(atom);
if (rez != null)
skip.Add(rez.ID);
if (rez is FormXObject)
items.Add(rez);
}
}
}
catch (Exception ex) {
return "Unable to validate document contents because document is badly formed.\r\n" + ex.Message;
}
// scan for errors
int pageCount = 0;
Page lastPage = null;
foreach (IndirectObject item in items) {
// get content stream
byte[] data = null;
Page page = item as Page;
FormXObject form = item as FormXObject;
if (page != null) {
lastPage = page;
pageCount++;
try {
doc.Page = page.ID;
doc.Rendering.DotsPerInch = 18; // small value for speed
doc.Rendering.GetData("dummy.jpg");
string log = VetLog(doc.Rendering.Log);
if (!string.IsNullOrWhiteSpace(log))
sb.Append(log);
}
catch (Exception ex) {
sb.AppendLine($"Unable to validate rendering of Page ID {page.ID} because of error {ex.Message}");
continue;
}
try {
data = page.GetContentData();
}
catch {
sb.AppendLine($"Unable to validate Page ID {page.ID} as content stream would not decompress");
continue;
}
}
else {
if (!form.Decompress()) {
sb.AppendLine($"Unable to validate Form XObject {form.ID} as it would not decompress");
continue;
}
data = form.GetData();
}
Debug.Assert(data != null);
// find errors in content stream
ArrayAtom array = ArrayAtom.FromContentStream(data);
Dictionary<int, string> errors = FindContentStreamErrors(lastPage, item, array);
if (errors.Count == 0)
continue;
// report errors in content stream
ArrayAtom selection = new ArrayAtom();
foreach (var pair in errors) {
int start = Math.Max(0, pair.Key - 10);
int end = Math.Min(pair.Key + 10, array.Count - 1);
if (selection.Count != 0)
selection.Add(new OpAtom("...\r\n\r\n"));
for (int i = start; i <= end; i++) {
selection.Add(array[i].Clone());
if (i == pair.Key)
selection.Add(new OpAtom(pair.Value));
}
}
sb.AppendLine("Content Stream Errors Detected:");
if (page != null)
sb.AppendLine($"Page ID {page.ID} page number {pageCount}");
else
sb.AppendLine($"Form XObject ID {form.ID} first encountered on page {pageCount}");
sb.AppendLine(ASCIIEncoding.ASCII.GetString(selection.GetData()));
}
return sb.ToString();
}
/// <summary>
/// Validate the stream compression in a PDF file, returning the results.
/// </summary>
/// <param name="doc">The Doc to be validated</param>
/// <returns>The results of validation</returns>
public static string ValidateCompression(Doc doc) {
StringBuilder sb = new StringBuilder();
List<StreamObject> items = new List<StreamObject>();
// gather items
try {
foreach (IndirectObject io in doc.ObjectSoup) {
if (io is StreamObject)
items.Add((StreamObject)io);
}
}
catch (Exception ex) {
return "Unable to validate stream compression because document is badly formed.\r\n" + ex.Message;
}
// scan for errors
foreach (StreamObject so in items) {
try {
so.CopyDecompressedData();
so.ClearCachedDecompressed();
}
catch (Exception ex) {
sb.AppendLine($"Error in StreamObject ID {so.ID}: {ex.Message}");
}
}
return sb.ToString();
}
private static Dictionary<int, string> FindContentStreamErrors(Page lastPage, IndirectObject item, ArrayAtom array)
{
// Validation of content streams is a bit open ended.
// This illustrates some basic sanity checks and can easily be extended.
IDictionary<string, Atom> fonts = null;
IDictionary<string, Atom> xobjects = null;
IDictionary<string, Atom> colorspaces = null;
OperatorState operatorState = new OperatorState();
int graphicsStateDepth = 1;
Dictionary<int, string> errors = new Dictionary<int, string>();
foreach (var pair in OpAtom.Find(array)) {
if (!operatorState.IsOK(pair.Item1))
AddError(errors, pair.Item2, "Operator \'" + pair.Item1 + "\' not valid inside " + operatorState.ObjectState.ToString() + " graphics object.");
Atom[] args = OpAtom.GetParameters(array, pair.Item2);
if (pair.Item1 == "q") {
graphicsStateDepth++;
if (graphicsStateDepth > 26)
AddError(errors, pair.Item2, "Graphics state nesting too deep.");
}
else if (pair.Item1 == "Q") {
if (graphicsStateDepth <= 1)
AddError(errors, pair.Item2, "No graphics states left to pop.");
else
graphicsStateDepth--;
}
if (args == null)
AddError(errors, pair.Item2, "Bad Parameters.");
else if (pair.Item1 == "Tf") {
if (fonts == null)
fonts = lastPage.GetResourceMap(item.ID, "Font");
string name = ((NameAtom)args[0]).Text;
if (!fonts.ContainsKey(name))
AddError(errors, pair.Item2, "Named Font does not exist.");
}
else if (pair.Item1 == "TJ") {
foreach (Atom arg in ((ArrayAtom)args[0])) {
if ((arg is NumAtom == false) && (arg is StringAtom == false)) {
AddError(errors, pair.Item2, "Bad String Arguments.");
break;
}
}
}
else if (pair.Item1 == "Do") {
if (xobjects == null)
xobjects = lastPage.GetResourceMap(item.ID, "XObject");
string name = ((NameAtom)args[0]).Text;
if (!xobjects.ContainsKey(name))
AddError(errors, pair.Item2, "Named XObject does not exist.");
}
else if ((pair.Item1 == "CS") || (pair.Item1 == "cs")) {
string name = ((NameAtom)args[0]).Text;
switch (name) {
case "DeviceGray":
break;
case "DeviceRGB":
break;
case "DeviceCMYK":
break;
case "Pattern":
break;
default:
if (colorspaces == null)
colorspaces = lastPage.GetResourceMap(item.ID, "ColorSpace");
if (!colorspaces.ContainsKey(name))
AddError(errors, pair.Item2, "Named XObject does not exist.");
break;
}
}
}
return errors;
}
/// <summary>
/// Validate the low level syntax of a PDF file, returning the results.
/// </summary>
/// <param name="file">The PDF to be validated</param>
/// <returns>The results of validation</returns>
public static string ValidateSyntax(string file) {
StringBuilder sb = new StringBuilder();
using (PdfValidationOperation op = new PdfValidationOperation()) {
op.Conformance = PdfConformance.Pdf;
using (Doc doc = op.Read(file, null)) {
}
if (op.Errors != null && op.Errors.Count > 0) {
sb.AppendLine("PDF Syntax Errors:");
for (int i = 0; i < op.Errors.Count; ++i)
sb.AppendLine(op.Errors[i]);
}
if (op.Warnings != null && op.Warnings.Count > 0) {
sb.AppendLine("PDF Syntax Warnings:");
for (int i = 0; i < op.Warnings.Count; ++i)
sb.AppendLine(op.Warnings[i]);
}
}
return sb.ToString();
}
private static void AddError(Dictionary<int, string> errors, int item, string error) {
string str = null;
errors.TryGetValue(item, out str);
if (str == null)
errors[item] = "%% " + error + "\r\n";
else {
if (str.Contains(error))
return;
errors[item] = str.Trim() + " " + error + "\r\n";
}
}
private static string IndirectObjectDescription(IndirectObject io) {
const int length = 300;
string desc = io.ToString().Replace("\r\n", "\n");
if (desc.Length > length) desc = desc.Substring(0, length - 3) + "...";
return desc;
}
private static void FindAllReferences(int id, Atom atom, Dictionary<int, HashSet<int>> references, ref bool hasContent) {
if (atom is RefAtom) {
int refid = ((RefAtom)atom).ID;
HashSet<int> set = null;
references.TryGetValue(refid, out set);
if (set == null) {
set = new HashSet<int>();
references[refid] = set;
}
set.Add(id);
}
else if (atom is ArrayAtom) {
foreach (var entry in (ArrayAtom)atom)
FindAllReferences(id, entry, references, ref hasContent);
}
else if (atom is DictAtom) {
foreach (var entry in (DictAtom)atom)
FindAllReferences(id, entry.Value, references, ref hasContent);
}
else {
hasContent = true;
}
}
private static string GetVersion(PDFVersion version) {
if (version.HasFlag(PDFVersion.PDF20))
return "200";
if (version.HasFlag(PDFVersion.PDF17Extension5))
return "175";
if (version.HasFlag(PDFVersion.PDF17Extension3))
return "173";
if (version.HasFlag(PDFVersion.PDF17))
return "170";
if (version.HasFlag(PDFVersion.PDF16))
return "160";
if (version.HasFlag(PDFVersion.PDF15))
return "150";
if (version.HasFlag(PDFVersion.PDF14))
return "140";
if (version.HasFlag(PDFVersion.PDF13))
return "130";
if (version.HasFlag(PDFVersion.PDF12))
return "120";
if (version.HasFlag(PDFVersion.PDF11))
return "110";
return "100";
}
private static PDFVersion GetEncryptionVEntryVersion(int? v) {
switch (v.HasValue ? v.Value : 0) {
case 0:
return PDFVersion.PDF10 | PDFVersion.PDF20Deprecated | PDFVersion.PDF17Obsolescent;
case 1:
return PDFVersion.PDF10 | PDFVersion.PDF20Deprecated;
case 2:
return PDFVersion.PDF14 | PDFVersion.PDF20Deprecated;
case 3:
return PDFVersion.PDF14 | PDFVersion.PDF20Deprecated | PDFVersion.PDF17Obsolescent;
case 4:
return PDFVersion.PDF15 | PDFVersion.PDF20Deprecated;
case 5:
return PDFVersion.PDF17Extension3 | PDFVersion.PDF20;
}
return PDFVersion.None;
}
private static string VetLog(string log) {
StringBuilder sb = new StringBuilder();
foreach (string line in log.Split(new char[] { '\r' })) {
string trimmed = line.TrimEnd();
if (line.Contains("Substituting"))
continue; // not interested in font substitution
if (line.Contains("No Content Read"))
continue; // if it's blank that's fine by us
sb.AppendLine(trimmed);
}
return sb.ToString();
}
}
/// <summary>
/// A simple class to demonstrate how a PDF file may be validated.
/// The Report overrides in this class are called during validation to allow errors or warnings to be flagged.
/// It is simply a matter of loggging appropriate information each time one of these is called.
/// </summary>
public class ValidationLog : ValidationBase {
public PDFVersion Version { get; private set; }
public StringBuilder Log { get; private set; }
public HashSet<string> Deprecated { get; private set; }
public bool ReportStack { get; set; }
public ValidationLog() : base() {
Version = PDFVersion.PDF10;
Log = new StringBuilder();
Deprecated = new HashSet<string>();
}
public override void ReportEntryVersion(string entry, PDFVersion version) {
Version = Version | version;
if (version.HasFlag(PDFVersion.PDF20Deprecated))
Deprecated.Add($"Deprecated Feature: \"{Target(entry)}\".");
}
public override void ReportUnknownEntryName(string entry) {
Element element = Stack.Peek().Element;
string name = element.GetType().Name;
if ((name == "FileTrailerElement") || (name == "CrossReferenceStreamElement")) {
if (entry == "ABCpdf") return; // ABCpdf adds this
if (entry == "Source") return; // and this
}
else if (name == "ResourceElement") {
if (entry == "Encoding") return; // not in spec but used in documents
}
else if (name == "StandardLayoutAttributesElement") {
if (entry == "ColumnSpan") return; // not in spec but used in documents
}
else if (name == "RichMediaAnnotationElement") {
if (entry == "AA") return; // not in spec but used in documents and these features work when displayed in Acrobat
if (entry == "BS") return; // spec says it has same entries as an Annotation but seems to share more in common with Widget
}
Log.AppendLine($"Unknown Entry: \"{Target(entry)}\".");
if (ReportStack)
Log.AppendLine($" Stack: {GetStackTrace()}");
}
public override void ReportIncorrectEntryValue(string value) {
Log.AppendLine($"Wrong Value: \"{Target()}\" = \"{value}\".");
if (ReportStack)
Log.AppendLine($" Stack: {GetStackTrace()}");
}
public override void ReportMissingRequiredEntry() {
Log.AppendLine($"Missing Required Entry: \"{Target()}\".");
if (ReportStack)
Log.AppendLine($" Stack: {GetStackTrace()}");
}
public override void ReportNotIndirectObject() {
Log.AppendLine($"Entry Not Indirect: \"{Target()}\".");
if (ReportStack)
Log.AppendLine($" Stack: {GetStackTrace()}");
}
public override void ReportIncorrectItemType(string expectedType, string actualType, Atom atom) {
base.ReportIncorrectItemType(expectedType, actualType, atom);
Element element = Stack.Peek().Element;
string entry = Stack.Peek().Entry;
string key = Stack.Peek().Index >= 0 ? Stack.Peek().Index.ToString() : null;
if ((actualType == null) && (atom != null)) {
Element e = ElementFactories.AutodetectFactory(atom, element.Host, null);
if (e != null)
actualType = e.GetType().Name;
}
if (key == null)
key = "";
Log.Append($"Wrong Type: \"{Target(key)}\". ");
if (expectedType != null)
Log.Append("Expected \"" + expectedType + "\". ");
if (actualType != null)
Log.Append("Found \"" + actualType + "\". ");
if (ReportStack)
Log.AppendLine($"Stack: {GetStackTrace()}");
}
public override void ReportIncorrectNumberOfEntries(int expected, int found) {
Log.AppendLine($"Wrong Number of Entries: \"{Target()}\". Expected {expected} found {found}.");
if (ReportStack)
Log.AppendLine($" Stack: {GetStackTrace()}");
}
public override void ReportRecursiveStructure() {
Log.AppendLine($"Document contains recursive Atom structure: \"{Target()}\".");
if (ReportStack)
Log.AppendLine($" Stack: {GetStackTrace()}");
}
public override void ReportException(Element element, Exception ex) {
Log.AppendLine($"Exception processing {element.GetType().Name} caused validation of this object to be aborted. Message \"{GetMessage(ex)}\".");
if (ReportStack)
Log.AppendLine($" Stack: {GetStackTrace()}");
}
public void ReportException(IndirectObject io, Exception ex) {
Log.AppendLine($"Exception processing IndirectObject {io.ID} caused validation of this object to be aborted. Message \"{GetMessage(ex)}\".");
}
private static string GetMessage(Exception e) {
StringBuilder msg = new StringBuilder();
for (int i = 0; i < 20; i++) {
if (e == null) break;
if (e.Message.Length > 0) {
if (i != 0)
msg.Append(" ");
msg.Append(e.Message.Replace("\r\n", ""));
}
e = e.InnerException;
}
return msg.ToString();
}
private string Target() {
return Target(null);
}
private string Target(string entry) {
var item = Stack.Peek();
string type = item.Element.GetType().Name;
if (entry == null)
entry = item.Entry;
int id = 0;
ValidationStackNode[] stack = Stack.ToArray();
for (int i = 0;i < stack.Length; i++) {
var io = stack[i].Element.Object;
if (io != null) {
id = io.ID;
break;
}
}
return $"{type}({id}).{entry}";
}
}
/// <summary>
/// A simple class to hold content stream state and determine if the operators that are provided
/// are compatible with the current state. In most situations incorrect operators can just be ignored
/// but sometimes it is ambigious as to the best action to take.
/// This information is derived from the graphics objects state chart in the PDF Specification.
/// </summary>
class OperatorState {
static HashSet<string> sGeneralGraphicsState = new HashSet<string>(ContentStreamScanner.OperatorCategories.GeneralGraphicsState);
static HashSet<string> sSpecialGraphicsState = new HashSet<string>(ContentStreamScanner.OperatorCategories.SpecialGraphicsState);
static HashSet<string> sPathConstruction = new HashSet<string>(ContentStreamScanner.OperatorCategories.PathConstruction);
static HashSet<string> sPathPainting = new HashSet<string>(ContentStreamScanner.OperatorCategories.PathPainting);
static HashSet<string> sClippingPaths = new HashSet<string>(ContentStreamScanner.OperatorCategories.ClippingPaths);
static HashSet<string> sTextState = new HashSet<string>(ContentStreamScanner.OperatorCategories.TextState);
static HashSet<string> sTextPositioning = new HashSet<string>(ContentStreamScanner.OperatorCategories.TextPositioning);
static HashSet<string> sTextShowing = new HashSet<string>(ContentStreamScanner.OperatorCategories.TextShowing);
static HashSet<string> sColor = new HashSet<string>(ContentStreamScanner.OperatorCategories.Color);
static HashSet<string> sMarkedContent = new HashSet<string>(ContentStreamScanner.OperatorCategories.MarkedContent);
public enum State { Page, Path, Text, ClippingPath, Shading, InlineImage, External }
private static Dictionary<State, HashSet<string>> sValidOps = null;
public State ObjectState { get; set; }
public bool IgnoreErrorsInCompatibilitySections { get; set; }
private int CompatibilitySectionDepth { get; set; }
public OperatorState() {
if (sValidOps == null) {
Dictionary<State, HashSet<string>> valid = new Dictionary<State, HashSet<string>>();
valid[State.Page] = new HashSet<string>();
valid[State.Page].UnionWith(sGeneralGraphicsState);
valid[State.Page].UnionWith(sSpecialGraphicsState);
valid[State.Page].UnionWith(sColor);
valid[State.Page].UnionWith(sTextState);
valid[State.Page].UnionWith(sMarkedContent);
valid[State.Path] = new HashSet<string>();
valid[State.Path].UnionWith(sPathConstruction);
valid[State.Text] = new HashSet<string>();
valid[State.Text].UnionWith(sGeneralGraphicsState);
valid[State.Text].UnionWith(sColor);
valid[State.Text].UnionWith(sTextState);
valid[State.Text].UnionWith(sTextShowing);
valid[State.Text].UnionWith(sTextPositioning);
valid[State.Text].UnionWith(sMarkedContent);
valid[State.ClippingPath] = new HashSet<string>();
valid[State.Shading] = new HashSet<string>();
valid[State.InlineImage] = new HashSet<string>();
valid[State.InlineImage].Add("ID");
valid[State.External] = new HashSet<string>();
sValidOps = valid;
}
IgnoreErrorsInCompatibilitySections = false;
ObjectState = State.Page;
CompatibilitySectionDepth = 0;
}
public bool IsOK(string op) {
if (op == "BX") {
CompatibilitySectionDepth++;
return true;
}
if (op == "EX") {
CompatibilitySectionDepth--;
return true;
}
if ((CompatibilitySectionDepth > 0) && (IgnoreErrorsInCompatibilitySections))
return true;
switch (ObjectState) {
case State.Page:
if ((op == "m") || (op == "re")) {
ObjectState = State.Path;
return true;
}
if (op == "BT") {
ObjectState = State.Text;
return true;
}
if (op == "sh") {
return true;
}
if (op == "Do") {
return true;
}
if (op == "BI") {
ObjectState = State.InlineImage;
return true;
}
break;
case State.Path:
if (sPathPainting.Contains(op)) {
ObjectState = State.Page;
return true;
}
if (sClippingPaths.Contains(op)) {
ObjectState = State.ClippingPath;
return true;
}
break;
case State.Text:
if (op == "ET") {
ObjectState = State.Page;
return true;
}
break;
case State.ClippingPath:
if (sPathPainting.Contains(op)) {
ObjectState = State.Page;
return true;
}
break;
case State.Shading:
Debug.Assert(false, "Should never hit this state because there is an immediate return from shading objects.");
break;
case State.InlineImage:
if (op == "EI") {
ObjectState = State.Page;
return true;
}
break;
case State.External:
Debug.Assert(false, "Should never hit this state because there is an immediate return from external objects.");
break;
}
return sValidOps[ObjectState].Contains(op);
}
}
}