using System; using System.Collections.Generic; using iTextSharp.text.error_messages; using iTextSharp.text.pdf; using iTextSharp.text.pdf.parser; using PDFPatcher.Model; namespace PDFPatcher.Processor { internal class PdfContentStreamProcessor { // Fields readonly Stack gsStack = new Stack(); readonly Stack markedContentStack = new Stack(); readonly IDictionary operators = new Dictionary(); readonly Dictionary _XObjectDoHandlers = new Dictionary(); readonly Dictionary _FontCache = new Dictionary(); readonly Dictionary _FontNameCache = new Dictionary(); //IRenderListener renderListener; ResourceDictionary _Resources; Matrix _TextLineMatrix; Matrix _TextMatrix; public readonly static IContentOperator NopOperator = new IgnoreOperatorContentOperator(); internal PdfContentStreamProcessor() { Reset(); } // Methods internal Model.GraphicsState CurrentGraphicState => gsStack.Peek(); internal Matrix TextMatrix => _TextMatrix; internal IContentOperator DefaultOperator { get; set; } protected IDictionary Fonts => _FontNameCache; protected ResourceDictionary Resource => _Resources; protected void ApplyTextAdjust(float tj) { float adjustBy = -tj / 1000f * CurrentGraphicState.FontSize * CurrentGraphicState.HorizontalScaling; _TextMatrix = new Matrix(adjustBy, 0f).Multiply(_TextMatrix); } protected void AdjustTextMatrixX(float x) { _TextMatrix = new Matrix(x, 0f).Multiply(_TextMatrix); } protected void BeginMarkedContent(PdfName tag, PdfDictionary dict) { markedContentStack.Push(new MarkedContentInfo(tag, dict)); } private void BeginText() { //this.renderListener.BeginTextBlock (); } private FontInfo GetFont(PRIndirectReference fontRef) { if (_FontCache.TryGetValue(fontRef.Number, out var r)) { r.Access++; return r.Info; } var f = new FontInfo(fontRef); _FontCache.Add(fontRef.Number, new FontInfoCache(f)); _FontNameCache.Add(fontRef.Number, f.FontName); if (_FontCache.Count > 53) { var l = new List>(_FontCache); l.Sort((x, y) => x.Value.Access - y.Value.Access); for (int i = 0; i < 10; i++) { int k = l[i].Key; _FontCache.Remove(k); _FontNameCache.Remove(k); } } return f; } protected virtual void DisplayPdfString(PdfString str) { //TextRenderInfo renderInfo = new TextRenderInfo (this.Decode (str), this.CurrentGraphicState, this.textMatrix, this.markedContentStack); //this.renderListener.RenderText (renderInfo); //this.textMatrix = new Matrix (renderInfo.GetUnscaledWidth (), 0f).Multiply (this.textMatrix); } protected virtual void DisplayXObject(PdfName xobjectName) { //IXObjectDoHandler handler; //PdfDictionary xobjects = this.resources.GetAsDict (PdfName.XOBJECT); //PdfObject xobject = xobjects.GetDirectObject (xobjectName); //PdfStream xobjectStream = (PdfStream)xobject; //PdfName subType = xobjectStream.GetAsName (PdfName.SUBTYPE); //if (!xobject.IsStream ()) { // throw new InvalidOperationException (MessageLocalization.GetComposedMessage ("XObject.1.is.not.a.stream", xobjectName)); //} //this.xobjectDoHandlers.TryGetValue (subType, out handler); //if (handler == null) { // handler = this.xobjectDoHandlers[PdfName.DEFAULT]; //} //handler.HandleXObject (this, xobjectStream, xobjects.GetAsIndirectObject (xobjectName)); } private void EndMarkedContent() { markedContentStack.Pop(); } private void EndText() { //this.renderListener.EndTextBlock (); } protected virtual void InvokeOperator(PdfLiteral oper, List operands) { IContentOperator op; if (operators.TryGetValue(oper.ToString(), out op)) { op.Invoke(this, oper, operands); } else { DefaultOperator?.Invoke(this, oper, operands); } } protected void PopulateOperators() { RegisterContentOperator("q", new PushGraphicsState()); RegisterContentOperator("Q", new PopGraphicsState()); RegisterContentOperator("cm", new ModifyCurrentTransformationMatrix()); RegisterContentOperator("gs", new ProcessGraphicsStateResource()); var tcOperator = new SetTextCharacterSpacing(); RegisterContentOperator("Tc", tcOperator); var twOperator = new SetTextWordSpacing(); RegisterContentOperator("Tw", twOperator); RegisterContentOperator("Tz", new SetTextHorizontalScaling()); var tlOperator = new SetTextLeading(); RegisterContentOperator("TL", tlOperator); RegisterContentOperator("Tf", new SetTextFont()); RegisterContentOperator("Tr", new SetTextRenderMode()); RegisterContentOperator("Ts", new SetTextRise()); RegisterContentOperator("BT", new BeginTextC()); RegisterContentOperator("ET", new EndTextC()); RegisterContentOperator("BMC", new BeginMarkedContentC()); RegisterContentOperator("BDC", new BeginMarkedContentDictionary()); RegisterContentOperator("EMC", new EndMarkedContentC()); var tdOperator = new TextMoveStartNextLine(); RegisterContentOperator("Td", tdOperator); RegisterContentOperator("TD", new TextMoveStartNextLineWithLeading(tdOperator, tlOperator)); RegisterContentOperator("Tm", new TextSetTextMatrix()); var tstarOperator = new TextMoveNextLine(tdOperator); RegisterContentOperator("T*", tstarOperator); var tjOperator = new ShowText(); RegisterContentOperator("Tj", new ShowText()); RegisterContentOperator("TJ", new ShowTextArray()); var tickOperator = new MoveNextLineAndShowText(tstarOperator, tjOperator); RegisterContentOperator("'", tickOperator); RegisterContentOperator("\"", new MoveNextLineAndShowTextWithSpacing(twOperator, tcOperator, tickOperator)); RegisterContentOperator("Do", new Do()); } protected void PopulateXObjectDoHandlers() { RegisterXObjectDoHandler(PdfName.DEFAULT, new IgnoreXObjectDoHandler()); RegisterXObjectDoHandler(PdfName.FORM, new FormXObjectDoHandler()); RegisterXObjectDoHandler(PdfName.IMAGE, new ImageXObjectDoHandler()); } public void ProcessContent(byte[] contentBytes, PdfDictionary resources) { _Resources.Push(resources); var tokenizer = new PRTokeniser(new RandomAccessFileOrArray(contentBytes)); var ps = new PdfContentParser(tokenizer); var operands = new List(); while (ps.Parse(operands).Count > 0) { var oper = (PdfLiteral)operands[operands.Count - 1]; if ("BI".Equals(oper.ToString())) { var img = InlineImageUtils.ParseInlineImage(ps, resources.GetAsDict(PdfName.COLORSPACE)); InvokeOperator(oper, new List { img, oper }); // this.renderListener.RenderImage (renderInfo); } else { InvokeOperator(oper, operands); } } tokenizer.Close(); _Resources.Pop(); } internal IContentOperator RegisterContentOperator(string operatorString, IContentOperator oper) { if (oper == null) { operators.Remove(operatorString); return null; } return operators[operatorString] = oper; } internal IXObjectDoHandler RegisterXObjectDoHandler(PdfName xobjectSubType, IXObjectDoHandler handler) { IXObjectDoHandler old; _XObjectDoHandlers.TryGetValue(xobjectSubType, out old); _XObjectDoHandlers[xobjectSubType] = handler; return old; } internal virtual void Reset() { gsStack.Clear(); gsStack.Push(new Model.GraphicsState()); _TextMatrix = null; _TextLineMatrix = null; _Resources = new ResourceDictionary(); } #region Nested Types internal interface IContentOperator { void Invoke(PdfContentStreamProcessor processor, PdfLiteral oper, List operands); } internal interface IXObjectDoHandler { void HandleXObject(PdfContentStreamProcessor processor, PdfStream stream, PdfIndirectReference refi); } protected sealed class BeginMarkedContentC : IContentOperator { // Methods public void Invoke(PdfContentStreamProcessor processor, PdfLiteral oper, List operands) { processor.BeginMarkedContent((PdfName)operands[0], new PdfDictionary()); } } protected sealed class BeginMarkedContentDictionary : IContentOperator { // Methods private PdfDictionary GetPropertiesDictionary(PdfObject operand1, PdfContentStreamProcessor.ResourceDictionary resources) { if (operand1.IsDictionary()) { return (PdfDictionary)operand1; } var dictionaryName = (PdfName)operand1; return resources.GetAsDict(dictionaryName); } public void Invoke(PdfContentStreamProcessor processor, PdfLiteral oper, List operands) { var properties = operands[1]; processor.BeginMarkedContent((PdfName)operands[0], GetPropertiesDictionary(properties, processor._Resources)); } } protected sealed class BeginTextC : IContentOperator { // Methods public void Invoke(PdfContentStreamProcessor processor, PdfLiteral oper, List operands) { processor._TextMatrix = new Matrix(); processor._TextLineMatrix = processor._TextMatrix; processor.BeginText(); } } protected sealed class Do : IContentOperator { // Methods public void Invoke(PdfContentStreamProcessor processor, PdfLiteral oper, List operands) { var xobjectName = (PdfName)operands[0]; processor.DisplayXObject(xobjectName); } } protected sealed class EndMarkedContentC : IContentOperator { // Methods public void Invoke(PdfContentStreamProcessor processor, PdfLiteral oper, List operands) { processor.EndMarkedContent(); } } protected sealed class EndTextC : IContentOperator { // Methods public void Invoke(PdfContentStreamProcessor processor, PdfLiteral oper, List operands) { processor._TextMatrix = null; processor._TextLineMatrix = null; processor.EndText(); } } protected sealed class FormXObjectDoHandler : IXObjectDoHandler { // Methods public void HandleXObject(PdfContentStreamProcessor processor, PdfStream stream, PdfIndirectReference refi) { var resources = stream.GetAsDict(PdfName.RESOURCES); var contentBytes = ContentByteUtils.GetContentBytesFromContentObject(stream); var matrix = stream.GetAsArray(PdfName.MATRIX); new PdfContentStreamProcessor.PushGraphicsState().Invoke(processor, null, null); if (matrix != null) { float a = matrix.GetAsNumber(0).FloatValue; float b = matrix.GetAsNumber(1).FloatValue; float c = matrix.GetAsNumber(2).FloatValue; float d = matrix.GetAsNumber(3).FloatValue; float e = matrix.GetAsNumber(4).FloatValue; float f = matrix.GetAsNumber(5).FloatValue; processor.CurrentGraphicState.TransMatrix = new Matrix(a, b, c, d, e, f).Multiply(processor.CurrentGraphicState.TransMatrix); } processor.ProcessContent(contentBytes, resources); new PdfContentStreamProcessor.PopGraphicsState().Invoke(processor, null, null); } } protected sealed class IgnoreOperatorContentOperator : IContentOperator { // Methods public void Invoke(PdfContentStreamProcessor processor, PdfLiteral oper, List operands) { } } protected sealed class IgnoreXObjectDoHandler : IXObjectDoHandler { // Methods public void HandleXObject(PdfContentStreamProcessor processor, PdfStream xobjectStream, PdfIndirectReference refi) { } } protected sealed class ImageXObjectDoHandler : IXObjectDoHandler { // Methods public void HandleXObject(PdfContentStreamProcessor processor, PdfStream xobjectStream, PdfIndirectReference refi) { //ImageRenderInfo renderInfo = ImageRenderInfo.CreateForXObject (processor.CurrentGraphicState.TransMatrix, refi); //processor.renderListener.RenderImage (renderInfo); } } protected sealed class ModifyCurrentTransformationMatrix : IContentOperator { // Methods public void Invoke(PdfContentStreamProcessor processor, PdfLiteral oper, List operands) { float a = ((PdfNumber)operands[0]).FloatValue; float b = ((PdfNumber)operands[1]).FloatValue; float c = ((PdfNumber)operands[2]).FloatValue; float d = ((PdfNumber)operands[3]).FloatValue; float e = ((PdfNumber)operands[4]).FloatValue; float f = ((PdfNumber)operands[5]).FloatValue; var matrix = new Matrix(a, b, c, d, e, f); var gs = processor.gsStack.Peek(); gs.TransMatrix = matrix.Multiply(gs.TransMatrix); } } protected sealed class MoveNextLineAndShowText : IContentOperator { // Fields private readonly IContentOperator showText; private readonly IContentOperator textMoveNextLine; // Methods public MoveNextLineAndShowText(IContentOperator textMoveNextLine, IContentOperator showText) { this.textMoveNextLine = textMoveNextLine; this.showText = showText; } public void Invoke(PdfContentStreamProcessor processor, PdfLiteral oper, List operands) { textMoveNextLine.Invoke(processor, null, new List(0)); showText.Invoke(processor, null, operands); } } protected sealed class MoveNextLineAndShowTextWithSpacing : IContentOperator { // Fields private readonly IContentOperator moveNextLineAndShowText; private readonly IContentOperator setTextCharacterSpacing; private readonly IContentOperator setTextWordSpacing; // Methods public MoveNextLineAndShowTextWithSpacing(IContentOperator setTextWordSpacing, IContentOperator setTextCharacterSpacing, IContentOperator moveNextLineAndShowText) { this.setTextWordSpacing = setTextWordSpacing; this.setTextCharacterSpacing = setTextCharacterSpacing; this.moveNextLineAndShowText = moveNextLineAndShowText; } public void Invoke(PdfContentStreamProcessor processor, PdfLiteral oper, List operands) { var aw = (PdfNumber)operands[0]; var ac = (PdfNumber)operands[1]; var str = (PdfString)operands[2]; var twOperands = new List(1); twOperands.Insert(0, aw); setTextWordSpacing.Invoke(processor, null, twOperands); var tcOperands = new List(1); tcOperands.Insert(0, ac); setTextCharacterSpacing.Invoke(processor, null, tcOperands); var tickOperands = new List(1); tickOperands.Insert(0, str); moveNextLineAndShowText.Invoke(processor, null, tickOperands); } } protected sealed class PopGraphicsState : IContentOperator { // Methods public void Invoke(PdfContentStreamProcessor processor, PdfLiteral oper, List operands) { try { processor.gsStack.Pop(); } catch (InvalidOperationException) { Tracker.DebugMessage("绘图状态堆栈为空。"); } } } protected sealed class ProcessGraphicsStateResource : IContentOperator { // Methods public void Invoke(PdfContentStreamProcessor processor, PdfLiteral oper, List operands) { var dictionaryName = (PdfName)operands[0]; var extGState = processor._Resources.GetAsDict(PdfName.EXTGSTATE); if (extGState == null) { throw new ArgumentException(MessageLocalization.GetComposedMessage("resources.do.not.contain.extgstate.entry.unable.to.process.oper.1", oper)); } var gsDic = extGState.GetAsDict(dictionaryName); if (gsDic == null) { throw new ArgumentException(MessageLocalization.GetComposedMessage("1.is.an.unknown.graphics.state.dictionary", dictionaryName)); } var fontParameter = gsDic.GetAsArray(PdfName.FONT); if (fontParameter != null) { var font = processor.GetFont((PRIndirectReference)fontParameter[0]); float size = fontParameter.GetAsNumber(1).FloatValue; processor.CurrentGraphicState.Font = font; processor.CurrentGraphicState.FontSize = size; } } } protected sealed class PushGraphicsState : IContentOperator { // Methods public void Invoke(PdfContentStreamProcessor processor, PdfLiteral oper, List operands) { processor.gsStack.Push(processor.gsStack.Peek().Copy()); } } protected sealed class ResourceDictionary : PdfDictionary { // Fields private readonly IList resourcesStack = new List(); // Methods public override PdfObject GetDirectObject(PdfName key) { for (int i = resourcesStack.Count - 1; i >= 0; i--) { var subResource = resourcesStack[i]; if (subResource != null) { var obj = subResource.GetDirectObject(key); if (obj != null) { return obj; } } } return base.GetDirectObject(key); } public void Pop() { resourcesStack.RemoveAt(resourcesStack.Count - 1); } public void Push(PdfDictionary resources) { resourcesStack.Add(resources); } } protected sealed class SetTextCharacterSpacing : IContentOperator { // Methods public void Invoke(PdfContentStreamProcessor processor, PdfLiteral oper, List operands) { var charSpace = (PdfNumber)operands[0]; processor.CurrentGraphicState.CharacterSpacing = charSpace.FloatValue; } } protected sealed class SetTextFont : IContentOperator { // Methods public void Invoke(PdfContentStreamProcessor processor, PdfLiteral oper, List operands) { var fontResourceName = (PdfName)operands[0]; float size = ((PdfNumber)operands[1]).FloatValue; var f = processor._Resources.GetAsDict(PdfName.FONT).Get(fontResourceName); var g = processor.CurrentGraphicState; if (f is PRIndirectReference fref) { var font = processor.GetFont(fref); g.FontID = fref.Number; g.Font = font; } else { Tracker.DebugMessage("字体(" + fontResourceName + ")不为引用。"); var fd = f as PdfDictionary; g.FontID = 0; g.Font = new FontInfo(fd, 0); } g.FontSize = size; } } protected sealed class SetTextHorizontalScaling : IContentOperator { // Methods public void Invoke(PdfContentStreamProcessor processor, PdfLiteral oper, List operands) { var scale = (PdfNumber)operands[0]; processor.CurrentGraphicState.HorizontalScaling = scale.FloatValue / 100f; } } protected sealed class SetTextLeading : IContentOperator { // Methods public void Invoke(PdfContentStreamProcessor processor, PdfLiteral oper, List operands) { var leading = (PdfNumber)operands[0]; processor.CurrentGraphicState.Leading = leading.FloatValue; } } protected sealed class SetTextRenderMode : IContentOperator { // Methods public void Invoke(PdfContentStreamProcessor processor, PdfLiteral oper, List operands) { var render = (PdfNumber)operands[0]; processor.CurrentGraphicState.RenderMode = render.IntValue; } } protected sealed class SetTextRise : IContentOperator { // Methods public void Invoke(PdfContentStreamProcessor processor, PdfLiteral oper, List operands) { var rise = (PdfNumber)operands[0]; processor.CurrentGraphicState.Rise = rise.FloatValue; } } protected sealed class SetTextWordSpacing : IContentOperator { // Methods public void Invoke(PdfContentStreamProcessor processor, PdfLiteral oper, List operands) { var wordSpace = (PdfNumber)operands[0]; processor.CurrentGraphicState.WordSpacing = wordSpace.FloatValue; } } protected sealed class ShowText : IContentOperator { // Methods public void Invoke(PdfContentStreamProcessor processor, PdfLiteral oper, List operands) { var str = (PdfString)operands[0]; processor.DisplayPdfString(str); } } protected sealed class AccumulatedShowTextArray : IContentOperator { public void Invoke(PdfContentStreamProcessor processor, PdfLiteral oper, List operands) { var array = (PdfArray)operands[0]; float adj = 0; using (var ms = new System.IO.MemoryStream(array.Length)) { foreach (PdfObject item in array.ArrayList) { if (item.Type == PdfObject.STRING) { ms.Write((item as PdfString).GetBytes(), 0, item.Length); } else { adj += ((PdfNumber)item).FloatValue; } } processor.DisplayPdfString(new PdfString(ms.ToArray())); } processor.ApplyTextAdjust(adj); } } protected sealed class ShowTextArray : IContentOperator { // Methods public void Invoke(PdfContentStreamProcessor processor, PdfLiteral oper, List operands) { var array = (PdfArray)operands[0]; //using (var ms = new System.IO.MemoryStream (array.Length)) { // foreach (PdfObject item in array.ArrayList) { // if (item.Type == PdfObject.STRING) { // ms.Write ((item as PdfString).GetBytes (), 0, item.Length); // } // else { // processor.ApplyTextAdjust (((PdfNumber)item).FloatValue); // } // } // processor.DisplayPdfString (new PdfString (ms.ToArray ())); //} float tj = 0f; foreach (PdfObject entryObj in array.ArrayList) { if (entryObj is PdfString) { processor.DisplayPdfString((PdfString)entryObj); tj = 0f; } else { tj = ((PdfNumber)entryObj).FloatValue; processor.ApplyTextAdjust(tj); } } } } protected sealed class TextMoveNextLine : IContentOperator { // Fields private readonly TextMoveStartNextLine moveStartNextLine; // Methods public TextMoveNextLine(TextMoveStartNextLine moveStartNextLine) { this.moveStartNextLine = moveStartNextLine; } public void Invoke(PdfContentStreamProcessor processor, PdfLiteral oper, List operands) { var tdoperands = new List(2); tdoperands.Insert(0, new PdfNumber(0)); tdoperands.Insert(1, new PdfNumber(-processor.CurrentGraphicState.Leading)); moveStartNextLine.Invoke(processor, null, tdoperands); } } protected sealed class TextMoveStartNextLine : IContentOperator { // Methods public void Invoke(PdfContentStreamProcessor processor, PdfLiteral oper, List operands) { float tx = ((PdfNumber)operands[0]).FloatValue; float ty = ((PdfNumber)operands[1]).FloatValue; processor._TextMatrix = new Matrix(tx, ty).Multiply(processor._TextLineMatrix); processor._TextLineMatrix = processor._TextMatrix; } } protected sealed class TextMoveStartNextLineWithLeading : IContentOperator { // Fields private readonly TextMoveStartNextLine moveStartNextLine; private readonly SetTextLeading setTextLeading; // Methods public TextMoveStartNextLineWithLeading(TextMoveStartNextLine moveStartNextLine, SetTextLeading setTextLeading) { this.moveStartNextLine = moveStartNextLine; this.setTextLeading = setTextLeading; } public void Invoke(PdfContentStreamProcessor processor, PdfLiteral oper, List operands) { float ty = ((PdfNumber)operands[1]).FloatValue; var tlOperands = new List(1); tlOperands.Insert(0, new PdfNumber(-ty)); setTextLeading.Invoke(processor, null, tlOperands); moveStartNextLine.Invoke(processor, null, operands); } } protected sealed class TextSetTextMatrix : IContentOperator { // Methods public void Invoke(PdfContentStreamProcessor processor, PdfLiteral oper, List operands) { float a = ((PdfNumber)operands[0]).FloatValue; float b = ((PdfNumber)operands[1]).FloatValue; float c = ((PdfNumber)operands[2]).FloatValue; float d = ((PdfNumber)operands[3]).FloatValue; float e = ((PdfNumber)operands[4]).FloatValue; float f = ((PdfNumber)operands[5]).FloatValue; processor._TextLineMatrix = new Matrix(a, b, c, d, e, f); processor._TextMatrix = processor._TextLineMatrix; } } #endregion internal static class InlineImageUtils { /** * Simple class in case users need to differentiate an exception from processing * inline images vs other exceptions * @since 5.0.4 */ public sealed class InlineImageParseException : System.IO.IOException { public InlineImageParseException(String message) : base(message) { } } /** * Map between key abbreviations allowed in dictionary of inline images and their * equivalent image dictionary keys */ private static readonly IDictionary inlineImageEntryAbbreviationMap; /** * Map between value abbreviations allowed in dictionary of inline images for COLORSPACE */ private static readonly IDictionary inlineImageColorSpaceAbbreviationMap; /** * Map between value abbreviations allowed in dictionary of inline images for FILTER */ private static readonly IDictionary inlineImageFilterAbbreviationMap; static InlineImageUtils() { // static initializer inlineImageEntryAbbreviationMap = new Dictionary { // allowed entries - just pass these through [PdfName.BITSPERCOMPONENT] = PdfName.BITSPERCOMPONENT, [PdfName.COLORSPACE] = PdfName.COLORSPACE, [PdfName.DECODE] = PdfName.DECODE, [PdfName.DECODEPARMS] = PdfName.DECODEPARMS, [PdfName.FILTER] = PdfName.FILTER, [PdfName.HEIGHT] = PdfName.HEIGHT, [PdfName.IMAGEMASK] = PdfName.IMAGEMASK, [PdfName.INTENT] = PdfName.INTENT, [PdfName.INTERPOLATE] = PdfName.INTERPOLATE, [PdfName.WIDTH] = PdfName.WIDTH, // abbreviations - transform these to corresponding correct values [new PdfName("BPC")] = PdfName.BITSPERCOMPONENT, [new PdfName("CS")] = PdfName.COLORSPACE, [new PdfName("D")] = PdfName.DECODE, [new PdfName("DP")] = PdfName.DECODEPARMS, [new PdfName("F")] = PdfName.FILTER, [new PdfName("H")] = PdfName.HEIGHT, [new PdfName("IM")] = PdfName.IMAGEMASK, [new PdfName("I")] = PdfName.INTERPOLATE, [new PdfName("W")] = PdfName.WIDTH }; inlineImageColorSpaceAbbreviationMap = new Dictionary { [new PdfName("G")] = PdfName.DEVICEGRAY, [new PdfName("RGB")] = PdfName.DEVICERGB, [new PdfName("CMYK")] = PdfName.DEVICECMYK, [new PdfName("I")] = PdfName.INDEXED }; inlineImageFilterAbbreviationMap = new Dictionary { [new PdfName("AHx")] = PdfName.ASCIIHEXDECODE, [new PdfName("A85")] = PdfName.ASCII85DECODE, [new PdfName("LZW")] = PdfName.LZWDECODE, [new PdfName("Fl")] = PdfName.FLATEDECODE, [new PdfName("RL")] = PdfName.RUNLENGTHDECODE, [new PdfName("CCF")] = PdfName.CCITTFAXDECODE, [new PdfName("DCT")] = PdfName.DCTDECODE }; } /** * Parses an inline image from the provided content parser. The parser must be positioned immediately following the BI operator in the content stream. * The parser will be left with current position immediately following the EI operator that terminates the inline image * @param ps the content parser to use for reading the image. * @return the parsed image * @throws IOException if anything goes wring with the parsing * @throws InlineImageParseException if parsing of the inline image failed due to issues specific to inline image processing */ public static PdfImageData ParseInlineImage(PdfContentParser ps, PdfDictionary colorSpaceDic) { var d = ParseInlineImageDictionary(ps); return new PdfImageData(d, ParseInlineImageSamples(d, colorSpaceDic, ps)); } /** * Parses the next inline image dictionary from the parser. The parser must be positioned immediately following the EI operator. * The parser will be left with position immediately following the whitespace character that follows the ID operator that ends the inline image dictionary. * @param ps the parser to extract the embedded image information from * @return the dictionary for the inline image, with any abbreviations converted to regular image dictionary keys and values * @throws IOException if the parse fails */ private static PdfDictionary ParseInlineImageDictionary(PdfContentParser ps) { // by the time we get to here, we have already parsed the BI operator var dictionary = new PdfDictionary(); for (PdfObject key = ps.ReadPRObject(); key != null && !"ID".Equals(key.ToString()); key = ps.ReadPRObject()) { var value = ps.ReadPRObject(); PdfName resolvedKey; inlineImageEntryAbbreviationMap.TryGetValue((PdfName)key, out resolvedKey); if (resolvedKey == null) resolvedKey = (PdfName)key; dictionary.Put(resolvedKey, GetAlternateValue(resolvedKey, value)); } int ch = ps.GetTokeniser().Read(); if (!PRTokeniser.IsWhitespace(ch)) throw new System.IO.IOException("Unexpected character " + ch + " found after ID in inline image"); return dictionary; } /** * Transforms value abbreviations into their corresponding real value * @param key the key that the value is for * @param value the value that might be an abbreviation * @return if value is an allowed abbreviation for the key, the expanded value for that abbreviation. Otherwise, value is returned without modification */ private static PdfObject GetAlternateValue(PdfName key, PdfObject value) { if (key == PdfName.FILTER) { if (value is PdfName pdfName && inlineImageFilterAbbreviationMap.TryGetValue(pdfName, out PdfName altValue) && altValue != null) { return altValue; } else if (value is PdfArray array) { var altArray = new PdfArray(); int count = array.Size; for (int i = 0; i < count; i++) { altArray.Add(GetAlternateValue(key, array[i])); } return altArray; } } else if (key == PdfName.COLORSPACE) { if (value is PdfName pdfName && inlineImageColorSpaceAbbreviationMap.TryGetValue(pdfName, out PdfName altValue) && altValue != null) { return altValue; } } return value; } /** * @param colorSpaceName the name of the color space. If null, a bi-tonal (black and white) color space is assumed. * @return the components per pixel for the specified color space */ private static int GetComponentsPerPixel(PdfName colorSpaceName, PdfDictionary colorSpaceDic) { if (colorSpaceName == null) return 1; if (colorSpaceName.Equals(PdfName.DEVICEGRAY)) return 1; if (colorSpaceName.Equals(PdfName.DEVICERGB)) return 3; if (colorSpaceName.Equals(PdfName.DEVICECMYK)) return 4; if (colorSpaceDic != null) { var colorSpace = colorSpaceDic.GetAsArray(colorSpaceName); if (colorSpace != null && PdfName.INDEXED.Equals(colorSpace.GetAsName(0))) { return 1; } } throw new ArgumentException("Unexpected color space " + colorSpaceName); } /** * Computes the number of unfiltered bytes that each row of the image will contain. * If the number of bytes results in a partial terminating byte, this number is rounded up * per the PDF specification * @param imageDictionary the dictionary of the inline image * @return the number of bytes per row of the image */ private static int ComputeBytesPerRow(PdfDictionary imageDictionary, PdfDictionary colorSpaceDic) { var wObj = imageDictionary.GetAsNumber(PdfName.WIDTH); var bpcObj = imageDictionary.GetAsNumber(PdfName.BITSPERCOMPONENT); int cpp = GetComponentsPerPixel(imageDictionary.GetAsName(PdfName.COLORSPACE), colorSpaceDic); int w = wObj.IntValue; int bpc = bpcObj?.IntValue ?? 1; return (w * bpc * cpp + 7) / 8; } /** * Parses the samples of the image from the underlying content parser, ignoring all filters. * The parser must be positioned immediately after the ID operator that ends the inline image's dictionary. * The parser will be left positioned immediately following the EI operator. * This is primarily useful if no filters have been applied. * @param imageDictionary the dictionary of the inline image * @param ps the content parser * @return the samples of the image * @throws IOException if anything bad happens during parsing */ private static byte[] ParseUnfilteredSamples(PdfDictionary imageDictionary, PdfDictionary colorSpaceDic, PdfContentParser ps) { // special case: when no filter is specified, we just read the number of bits // per component, multiplied by the width and height. if (imageDictionary.Contains(PdfName.FILTER)) throw new ArgumentException("Dictionary contains filters"); var h = imageDictionary.GetAsNumber(PdfName.HEIGHT); int bytesToRead = ComputeBytesPerRow(imageDictionary, colorSpaceDic) * h.IntValue; var bytes = new byte[bytesToRead]; var tokeniser = ps.GetTokeniser(); int shouldBeWhiteSpace = tokeniser.Read(); // skip next character (which better be a whitespace character - I suppose we could check for this) // from the PDF spec: Unless the image uses ASCIIHexDecode or ASCII85Decode as one of its filters, the ID operator shall be followed by a single white-space character, and the next character shall be interpreted as the first byte of image data. // unfortunately, we've seen some PDFs where there is no space following the ID, so we have to capture this case and handle it int startIndex = 0; if (!PRTokeniser.IsWhitespace(shouldBeWhiteSpace) || shouldBeWhiteSpace == 0) { bytes[0] = (byte)shouldBeWhiteSpace; startIndex++; } for (int i = startIndex; i < bytesToRead; i++) { int ch = tokeniser.Read(); if (ch == -1) throw new InlineImageParseException("End of content stream reached before end of image data"); bytes[i] = (byte)ch; } var ei = ps.ReadPRObject(); if (!ei.ToString().Equals("EI")) throw new InlineImageParseException("EI not found after end of image data"); return bytes; } /** * Parses the samples of the image from the underlying content parser, accounting for filters * The parser must be positioned immediately after the ID operator that ends the inline image's dictionary. * The parser will be left positioned immediately following the EI operator. * Note:This implementation does not actually apply the filters at this time * @param imageDictionary the dictionary of the inline image * @param ps the content parser * @return the samples of the image * @throws IOException if anything bad happens during parsing */ private static byte[] ParseInlineImageSamples(PdfDictionary imageDictionary, PdfDictionary colorSpaceDic, PdfContentParser ps) { // by the time we get to here, we have already parsed the ID operator if (!imageDictionary.Contains(PdfName.FILTER)) { return ParseUnfilteredSamples(imageDictionary, colorSpaceDic, ps); } // read all content until we reach an EI operator surrounded by whitespace. // The following algorithm has two potential issues: what if the image stream // contains EI ? // Plus, there are some streams that don't have the before the EI operator // it sounds like we would have to actually decode the content stream, which // I'd rather avoid right now. var baos = new System.IO.MemoryStream(); var accumulated = new System.IO.MemoryStream(); int ch; int found = 0; var tokeniser = ps.GetTokeniser(); byte[] ff = null; while ((ch = tokeniser.Read()) != -1) { if (found == 0 && PRTokeniser.IsWhitespace(ch)) { found++; accumulated.WriteByte((byte)ch); } else if (found == 1 && ch == 'E') { found++; accumulated.WriteByte((byte)ch); } else if (found == 1 && PRTokeniser.IsWhitespace(ch)) { // this clause is needed if we have a white space character that is part of the image data // followed by a whitespace character that precedes the EI operator. In this case, we need // to flush the first whitespace, then treat the current whitespace as the first potential // character for the end of stream check. Note that we don't increment 'found' here. baos.Write(ff = accumulated.ToArray(), 0, ff.Length); accumulated.SetLength(0); accumulated.WriteByte((byte)ch); } else if (found == 2 && ch == 'I') { found++; accumulated.WriteByte((byte)ch); } else if (found == 3 && PRTokeniser.IsWhitespace(ch)) { return baos.ToArray(); } else { baos.Write(ff = accumulated.ToArray(), 0, ff.Length); accumulated.SetLength(0); baos.WriteByte((byte)ch); found = 0; } } throw new InlineImageParseException("Could not find image data or EI"); } } sealed class FontInfoCache { public readonly FontInfo Info; public int Access; public FontInfoCache(FontInfo info) { Info = info; } } } }