From 26f028c42173060a4917352ff1e83984708f096d Mon Sep 17 00:00:00 2001 From: Eliot Jones Date: Fri, 22 Apr 2022 16:32:38 -0400 Subject: [PATCH] #437 actually use code written for multi-part xref tables and fix bug --- src/UglyToad.PdfPig/Writer/TokenWriter.cs | 1061 ++++++++++----------- 1 file changed, 514 insertions(+), 547 deletions(-) diff --git a/src/UglyToad.PdfPig/Writer/TokenWriter.cs b/src/UglyToad.PdfPig/Writer/TokenWriter.cs index 0e2e2eee..88202ad6 100644 --- a/src/UglyToad.PdfPig/Writer/TokenWriter.cs +++ b/src/UglyToad.PdfPig/Writer/TokenWriter.cs @@ -1,378 +1,345 @@ -namespace UglyToad.PdfPig.Writer -{ - using System; - using System.Collections.Generic; - using System.Globalization; - using System.Text; - using System.IO; - using System.Linq; - using Core; - using Graphics.Operations; - using Tokens; - using Util; - - /// - /// Writes any type of to the corresponding PDF document format output. - /// - public class TokenWriter - { - private static readonly byte Backslash = GetByte("\\"); - - private static readonly byte ArrayStart = GetByte("["); - private static readonly byte ArrayEnd = GetByte("]"); - - private static readonly byte[] DictionaryStart = OtherEncodings.StringAsLatin1Bytes("<<"); - private static readonly byte[] DictionaryEnd = OtherEncodings.StringAsLatin1Bytes(">>"); - - private static readonly byte Comment = GetByte("%"); - - private static readonly byte[] Eof = OtherEncodings.StringAsLatin1Bytes("%%EOF"); - - private static readonly byte[] FalseBytes = OtherEncodings.StringAsLatin1Bytes("false"); - - private static readonly byte HexStart = GetByte("<"); - private static readonly byte HexEnd = GetByte(">"); - - private static readonly byte InUseEntry = GetByte("n"); - - private static readonly byte NameStart = GetByte("/"); - - private static readonly byte[] Null = OtherEncodings.StringAsLatin1Bytes("null"); - - private static readonly byte[] ObjStart = OtherEncodings.StringAsLatin1Bytes("obj"); - private static readonly byte[] ObjEnd = OtherEncodings.StringAsLatin1Bytes("endobj"); - - private static readonly byte RByte = GetByte("R"); - - private static readonly byte[] StartXref = OtherEncodings.StringAsLatin1Bytes("startxref"); - - private static readonly byte[] StreamStart = OtherEncodings.StringAsLatin1Bytes("stream"); - private static readonly byte[] StreamEnd = OtherEncodings.StringAsLatin1Bytes("endstream"); - - private static readonly byte StringStart = GetByte("("); - private static readonly byte StringEnd = GetByte(")"); - - private static readonly byte[] Trailer = OtherEncodings.StringAsLatin1Bytes("trailer"); - - private static readonly byte[] TrueBytes = OtherEncodings.StringAsLatin1Bytes("true"); - - private static readonly byte Whitespace = GetByte(" "); - - private static readonly byte[] Xref = OtherEncodings.StringAsLatin1Bytes("xref"); - - private static readonly HashSet DelimiterChars = new HashSet - { - '(', - ')', - '<', - '>', - '[', - ']', - '{', - '}', - '/', - '%' - }; - - /// - /// Writes the given input token to the output stream with the correct PDF format and encoding including whitespace and line breaks as applicable. - /// - /// The token to write to the stream. - /// The stream to write the token to. - public static void WriteToken(IToken token, Stream outputStream) +namespace UglyToad.PdfPig.Writer +{ + using System; + using System.Collections.Generic; + using System.Globalization; + using System.Text; + using System.IO; + using System.Linq; + using Core; + using Graphics.Operations; + using Tokens; + using Util; + + /// + /// Writes any type of to the corresponding PDF document format output. + /// + public class TokenWriter + { + private static readonly byte Backslash = GetByte("\\"); + + private static readonly byte ArrayStart = GetByte("["); + private static readonly byte ArrayEnd = GetByte("]"); + + private static readonly byte[] DictionaryStart = OtherEncodings.StringAsLatin1Bytes("<<"); + private static readonly byte[] DictionaryEnd = OtherEncodings.StringAsLatin1Bytes(">>"); + + private static readonly byte Comment = GetByte("%"); + + private static readonly byte[] Eof = OtherEncodings.StringAsLatin1Bytes("%%EOF"); + + private static readonly byte[] FalseBytes = OtherEncodings.StringAsLatin1Bytes("false"); + + private static readonly byte HexStart = GetByte("<"); + private static readonly byte HexEnd = GetByte(">"); + + private static readonly byte InUseEntry = GetByte("n"); + + private static readonly byte NameStart = GetByte("/"); + + private static readonly byte[] Null = OtherEncodings.StringAsLatin1Bytes("null"); + + private static readonly byte[] ObjStart = OtherEncodings.StringAsLatin1Bytes("obj"); + private static readonly byte[] ObjEnd = OtherEncodings.StringAsLatin1Bytes("endobj"); + + private static readonly byte RByte = GetByte("R"); + + private static readonly byte[] StartXref = OtherEncodings.StringAsLatin1Bytes("startxref"); + + private static readonly byte[] StreamStart = OtherEncodings.StringAsLatin1Bytes("stream"); + private static readonly byte[] StreamEnd = OtherEncodings.StringAsLatin1Bytes("endstream"); + + private static readonly byte StringStart = GetByte("("); + private static readonly byte StringEnd = GetByte(")"); + + private static readonly byte[] Trailer = OtherEncodings.StringAsLatin1Bytes("trailer"); + + private static readonly byte[] TrueBytes = OtherEncodings.StringAsLatin1Bytes("true"); + + private static readonly byte Whitespace = GetByte(" "); + + private static readonly byte[] Xref = OtherEncodings.StringAsLatin1Bytes("xref"); + + private static readonly HashSet DelimiterChars = new HashSet + { + '(', + ')', + '<', + '>', + '[', + ']', + '{', + '}', + '/', + '%' + }; + + /// + /// Writes the given input token to the output stream with the correct PDF format and encoding including whitespace and line breaks as applicable. + /// + /// The token to write to the stream. + /// The stream to write the token to. + public static void WriteToken(IToken token, Stream outputStream) { if (token == null) { throw new ArgumentNullException(nameof(token)); - } - switch (token) - { - case ArrayToken array: - WriteArray(array, outputStream); - break; - case BooleanToken boolean: - WriteBoolean(boolean, outputStream); - break; - case CommentToken comment: - WriteComment(comment, outputStream); - break; - case DictionaryToken dictionary: - WriteDictionary(dictionary, outputStream); - break; - case HexToken hex: - WriteHex(hex, outputStream); - break; - case IndirectReferenceToken reference: - WriteIndirectReference(reference, outputStream); - break; - case NameToken name: - WriteName(name, outputStream); - break; - case NullToken _: - outputStream.Write(Null, 0, Null.Length); - WriteWhitespace(outputStream); - break; - case NumericToken number: - WriteNumber(number, outputStream); - break; - case ObjectToken objectToken: - WriteObject(objectToken, outputStream); - break; - case StreamToken streamToken: - WriteStream(streamToken, outputStream); - break; - case StringToken stringToken: - WriteString(stringToken, outputStream); + } + switch (token) + { + case ArrayToken array: + WriteArray(array, outputStream); + break; + case BooleanToken boolean: + WriteBoolean(boolean, outputStream); + break; + case CommentToken comment: + WriteComment(comment, outputStream); + break; + case DictionaryToken dictionary: + WriteDictionary(dictionary, outputStream); + break; + case HexToken hex: + WriteHex(hex, outputStream); + break; + case IndirectReferenceToken reference: + WriteIndirectReference(reference, outputStream); + break; + case NameToken name: + WriteName(name, outputStream); + break; + case NullToken _: + outputStream.Write(Null, 0, Null.Length); + WriteWhitespace(outputStream); + break; + case NumericToken number: + WriteNumber(number, outputStream); + break; + case ObjectToken objectToken: + WriteObject(objectToken, outputStream); + break; + case StreamToken streamToken: + WriteStream(streamToken, outputStream); + break; + case StringToken stringToken: + WriteString(stringToken, outputStream); break; default: - throw new PdfDocumentFormatException($"Attempted to write token type of {token.GetType()} but was not known."); - } - } - - /// - /// Writes a valid single section cross-reference (xref) table plus trailer dictionary to the output for the set of object offsets. - /// - /// The byte offset from the start of the document for each object in the document. - /// The object representing the catalog dictionary which is referenced from the trailer dictionary. - /// The output stream to write to. - /// The object reference for the document information dictionary if present. - internal static void WriteCrossReferenceTable(IReadOnlyDictionary objectOffsets, - IndirectReference catalogToken, - Stream outputStream, - IndirectReference? documentInformationReference) - { - if (objectOffsets.Count == 0) - { - throw new InvalidOperationException("Could not write empty cross reference table."); - } - - WriteLineBreak(outputStream); - var position = outputStream.Position; - outputStream.Write(Xref, 0, Xref.Length); - WriteLineBreak(outputStream); - - WriteLong(0, outputStream); - WriteWhitespace(outputStream); - // 1 extra for the free entry. - WriteLong(objectOffsets.Count + 1, outputStream); - WriteWhitespace(outputStream); - WriteLineBreak(outputStream); - - WriteFirstXrefEmptyEntry(outputStream); - - foreach (var keyValuePair in objectOffsets.OrderBy(x => x.Key.ObjectNumber)) - { - /* - * nnnnnnnnnn ggggg n eol - * where: - * nnnnnnnnnn is a 10-digit byte offset - * ggggg is a 5-digit generation number - * n is a literal keyword identifying this as an in-use entry - * eol is a 2-character end-of-line sequence ('\r\n' or ' \n') - */ - var paddedOffset = OtherEncodings.StringAsLatin1Bytes(keyValuePair.Value.ToString("D10")); - outputStream.Write(paddedOffset, 0, paddedOffset.Length); - - WriteWhitespace(outputStream); - - var generation = OtherEncodings.StringAsLatin1Bytes(keyValuePair.Key.Generation.ToString("D5")); - outputStream.Write(generation, 0, generation.Length); - - WriteWhitespace(outputStream); - - outputStream.WriteByte(InUseEntry); - - WriteWhitespace(outputStream); - WriteLineBreak(outputStream); - } - - outputStream.Write(Trailer, 0, Trailer.Length); - WriteLineBreak(outputStream); - - var identifier = new ArrayToken(new IToken[] - { - new HexToken(Guid.NewGuid().ToString("N").ToCharArray()), - new HexToken(Guid.NewGuid().ToString("N").ToCharArray()) - }); - - var trailerDictionaryData = new Dictionary - { - // 1 for the free entry. - {NameToken.Size, new NumericToken(objectOffsets.Count + 1)}, - {NameToken.Root, new IndirectReferenceToken(catalogToken)}, - {NameToken.Id, identifier} - }; - - if (documentInformationReference.HasValue) - { - trailerDictionaryData[NameToken.Info] = new IndirectReferenceToken(documentInformationReference.Value); - } - - var trailerDictionary = new DictionaryToken(trailerDictionaryData); - - WriteDictionary(trailerDictionary, outputStream); - WriteLineBreak(outputStream); - - outputStream.Write(StartXref, 0, StartXref.Length); - WriteLineBreak(outputStream); - - WriteLong(position, outputStream); - WriteLineBreak(outputStream); - - // Complete! - outputStream.Write(Eof, 0, Eof.Length); - } - - private static void WriteCrossReferenceTable(IReadOnlyDictionary objectOffsets, Stream outputStream) - { - var sets = new List(); - - var orderedList = objectOffsets.OrderBy(x => x.Key.ObjectNumber).ToList(); - - long firstObjectNumber = 0; - long currentObjNum = 0; - var items = new List - { - // Zero entry - null - }; - - foreach (var item in orderedList) - { - var step = item.Key.ObjectNumber - currentObjNum; - if (step == 1) - { - currentObjNum = item.Key.ObjectNumber; - items.Add(new XrefSeries.OffsetAndGeneration(item.Value, item.Key.Generation)); - } - else - { - sets.Add(new XrefSeries(firstObjectNumber, items)); - items = new List - { - new XrefSeries.OffsetAndGeneration(item.Value, item.Key.Generation) - }; - - currentObjNum = item.Key.ObjectNumber; - firstObjectNumber = item.Key.ObjectNumber; - } - } - - foreach (var series in sets) - { - WriteLong(0, outputStream); - WriteWhitespace(outputStream); - // 1 extra for the free entry. - WriteLong(series.Offsets.Count, outputStream); - - WriteWhitespace(outputStream); - WriteLineBreak(outputStream); - - foreach (var offset in series.Offsets) - { - if (offset != null) - { - /* - * nnnnnnnnnn ggggg n eol - * where: - * nnnnnnnnnn is a 10-digit byte offset - * ggggg is a 5-digit generation number - * n is a literal keyword identifying this as an in-use entry - * eol is a 2-character end-of-line sequence ('\r\n' or ' \n') - */ - var paddedOffset = OtherEncodings.StringAsLatin1Bytes(offset.Offset.ToString("D10", CultureInfo.InvariantCulture)); - outputStream.Write(paddedOffset, 0, paddedOffset.Length); - - WriteWhitespace(outputStream); - - var generation = OtherEncodings.StringAsLatin1Bytes(offset.Generation.ToString("D5", CultureInfo.InvariantCulture)); - outputStream.Write(generation, 0, generation.Length); - - WriteWhitespace(outputStream); - - outputStream.WriteByte(InUseEntry); - - WriteWhitespace(outputStream); - WriteLineBreak(outputStream); - - } - else - { - WriteFirstXrefEmptyEntry(outputStream); - } - } - } - } - - /// - /// Writes pre-serialized token as an object token to the output stream. - /// - /// Object number of the indirect object. - /// Generation of the indirect object. - /// Pre-serialized object contents. - /// The stream to write the token to. - internal static void WriteObject(long objectNumber, int generation, byte[] data, Stream outputStream) - { - WriteLong(objectNumber, outputStream); - WriteWhitespace(outputStream); - - WriteInt(generation, outputStream); - WriteWhitespace(outputStream); - - outputStream.Write(ObjStart, 0, ObjStart.Length); - WriteLineBreak(outputStream); - - outputStream.Write(data, 0, data.Length); - - WriteLineBreak(outputStream); - outputStream.Write(ObjEnd, 0, ObjEnd.Length); - - WriteLineBreak(outputStream); - } - - private static void WriteHex(HexToken hex, Stream stream) - { - stream.WriteByte(HexStart); - stream.WriteText(hex.GetHexString()); - stream.WriteByte(HexEnd); - } - - private static void WriteArray(ArrayToken array, Stream outputStream) - { - outputStream.WriteByte(ArrayStart); - WriteWhitespace(outputStream); - - for (var i = 0; i < array.Data.Count; i++) - { - var value = array.Data[i]; - WriteToken(value, outputStream); - } - - outputStream.WriteByte(ArrayEnd); - WriteWhitespace(outputStream); - } - - private static void WriteBoolean(BooleanToken boolean, Stream outputStream) - { - var bytes = boolean.Data ? TrueBytes : FalseBytes; - outputStream.Write(bytes, 0, bytes.Length); - WriteWhitespace(outputStream); - } - - private static void WriteComment(CommentToken comment, Stream outputStream) - { - var bytes = OtherEncodings.StringAsLatin1Bytes(comment.Data); - outputStream.WriteByte(Comment); - outputStream.Write(bytes, 0, bytes.Length); - WriteLineBreak(outputStream); - } - - private static void WriteDictionary(DictionaryToken dictionary, Stream outputStream) - { - outputStream.Write(DictionaryStart, 0, DictionaryStart.Length); - - foreach (var pair in dictionary.Data) - { + throw new PdfDocumentFormatException($"Attempted to write token type of {token.GetType()} but was not known."); + } + } + + /// + /// Writes a valid single section cross-reference (xref) table plus trailer dictionary to the output for the set of object offsets. + /// + /// The byte offset from the start of the document for each object in the document. + /// The object representing the catalog dictionary which is referenced from the trailer dictionary. + /// The output stream to write to. + /// The object reference for the document information dictionary if present. + internal static void WriteCrossReferenceTable(IReadOnlyDictionary objectOffsets, + IndirectReference catalogToken, + Stream outputStream, + IndirectReference? documentInformationReference) + { + if (objectOffsets.Count == 0) + { + throw new InvalidOperationException("Could not write empty cross reference table."); + } + + WriteLineBreak(outputStream); + var position = outputStream.Position; + outputStream.Write(Xref, 0, Xref.Length); + WriteLineBreak(outputStream); + + var sets = new List(); + + var orderedList = objectOffsets.OrderBy(x => x.Key.ObjectNumber).ToList(); + + long firstObjectNumber = 0; + long currentObjNum = 0; + var items = new List + { + // Zero entry + null + }; + + foreach (var item in orderedList) + { + var step = item.Key.ObjectNumber - currentObjNum; + if (step == 1) + { + currentObjNum = item.Key.ObjectNumber; + items.Add(new XrefSeries.OffsetAndGeneration(item.Value, item.Key.Generation)); + } + else + { + sets.Add(new XrefSeries(firstObjectNumber, items)); + items = new List + { + new XrefSeries.OffsetAndGeneration(item.Value, item.Key.Generation) + }; + + currentObjNum = item.Key.ObjectNumber; + firstObjectNumber = item.Key.ObjectNumber; + } + } + + if (items.Count > 0) + { + sets.Add(new XrefSeries(firstObjectNumber, items)); + } + + foreach (var series in sets) + { + WriteLong(series.First, outputStream); + WriteWhitespace(outputStream); + + WriteLong(series.Offsets.Count, outputStream); + + WriteWhitespace(outputStream); + WriteLineBreak(outputStream); + + foreach (var offset in series.Offsets) + { + if (offset != null) + { + /* + * nnnnnnnnnn ggggg n eol + * where: + * nnnnnnnnnn is a 10-digit byte offset + * ggggg is a 5-digit generation number + * n is a literal keyword identifying this as an in-use entry + * eol is a 2-character end-of-line sequence ('\r\n' or ' \n') + */ + var paddedOffset = OtherEncodings.StringAsLatin1Bytes(offset.Offset.ToString("D10", CultureInfo.InvariantCulture)); + outputStream.Write(paddedOffset, 0, paddedOffset.Length); + + WriteWhitespace(outputStream); + + var generation = OtherEncodings.StringAsLatin1Bytes(offset.Generation.ToString("D5", CultureInfo.InvariantCulture)); + outputStream.Write(generation, 0, generation.Length); + + WriteWhitespace(outputStream); + + outputStream.WriteByte(InUseEntry); + + WriteWhitespace(outputStream); + WriteLineBreak(outputStream); + + } + else + { + WriteFirstXrefEmptyEntry(outputStream); + } + } + } + + outputStream.Write(Trailer, 0, Trailer.Length); + WriteLineBreak(outputStream); + + var identifier = new ArrayToken(new IToken[] + { + new HexToken(Guid.NewGuid().ToString("N").ToCharArray()), + new HexToken(Guid.NewGuid().ToString("N").ToCharArray()) + }); + + var trailerDictionaryData = new Dictionary + { + // 1 for the free entry. + {NameToken.Size, new NumericToken(objectOffsets.Count + 1)}, + {NameToken.Root, new IndirectReferenceToken(catalogToken)}, + {NameToken.Id, identifier} + }; + + if (documentInformationReference.HasValue) + { + trailerDictionaryData[NameToken.Info] = new IndirectReferenceToken(documentInformationReference.Value); + } + + var trailerDictionary = new DictionaryToken(trailerDictionaryData); + + WriteDictionary(trailerDictionary, outputStream); + WriteLineBreak(outputStream); + + outputStream.Write(StartXref, 0, StartXref.Length); + WriteLineBreak(outputStream); + + WriteLong(position, outputStream); + WriteLineBreak(outputStream); + + // Complete! + outputStream.Write(Eof, 0, Eof.Length); + } + + /// + /// Writes pre-serialized token as an object token to the output stream. + /// + /// Object number of the indirect object. + /// Generation of the indirect object. + /// Pre-serialized object contents. + /// The stream to write the token to. + internal static void WriteObject(long objectNumber, int generation, byte[] data, Stream outputStream) + { + WriteLong(objectNumber, outputStream); + WriteWhitespace(outputStream); + + WriteInt(generation, outputStream); + WriteWhitespace(outputStream); + + outputStream.Write(ObjStart, 0, ObjStart.Length); + WriteLineBreak(outputStream); + + outputStream.Write(data, 0, data.Length); + + WriteLineBreak(outputStream); + outputStream.Write(ObjEnd, 0, ObjEnd.Length); + + WriteLineBreak(outputStream); + } + + private static void WriteHex(HexToken hex, Stream stream) + { + stream.WriteByte(HexStart); + stream.WriteText(hex.GetHexString()); + stream.WriteByte(HexEnd); + } + + private static void WriteArray(ArrayToken array, Stream outputStream) + { + outputStream.WriteByte(ArrayStart); + WriteWhitespace(outputStream); + + for (var i = 0; i < array.Data.Count; i++) + { + var value = array.Data[i]; + WriteToken(value, outputStream); + } + + outputStream.WriteByte(ArrayEnd); + WriteWhitespace(outputStream); + } + + private static void WriteBoolean(BooleanToken boolean, Stream outputStream) + { + var bytes = boolean.Data ? TrueBytes : FalseBytes; + outputStream.Write(bytes, 0, bytes.Length); + WriteWhitespace(outputStream); + } + + private static void WriteComment(CommentToken comment, Stream outputStream) + { + var bytes = OtherEncodings.StringAsLatin1Bytes(comment.Data); + outputStream.WriteByte(Comment); + outputStream.Write(bytes, 0, bytes.Length); + WriteLineBreak(outputStream); + } + + private static void WriteDictionary(DictionaryToken dictionary, Stream outputStream) + { + outputStream.Write(DictionaryStart, 0, DictionaryStart.Length); + + foreach (var pair in dictionary.Data) + { WriteName(pair.Key, outputStream); // handle scenario where PdfPig has a null value under some circumstances @@ -383,104 +350,104 @@ else { WriteToken(pair.Value, outputStream); - } - } - - outputStream.Write(DictionaryEnd, 0, DictionaryEnd.Length); - } - - private static void WriteIndirectReference(IndirectReferenceToken reference, Stream outputStream) - { - WriteLong(reference.Data.ObjectNumber, outputStream); - WriteWhitespace(outputStream); - - WriteInt(reference.Data.Generation, outputStream); - WriteWhitespace(outputStream); - - outputStream.WriteByte(RByte); - WriteWhitespace(outputStream); - } - - private static void WriteName(NameToken name, Stream outputStream) - { - WriteName(name.Data, outputStream); - } - - private static void WriteName(string name, Stream outputStream) - { - /* - * Beginning with PDF 1.2, any character except null (character code 0) may be - * included in a name by writing its 2-digit hexadecimal code, preceded by the number sign character (#). - * This is required for delimiter and whitespace characters. - * This is recommended for characters whose codes are outside the range 33 (!) to 126 (~). - */ - - var sb = new StringBuilder(); - - foreach (var c in name) - { - if (c < 33 || c > 126 || DelimiterChars.Contains(c)) - { - var str = Hex.GetString(new[] { (byte)c }); - sb.Append('#').Append(str); - } - else - { - sb.Append(c); - } - } - - var bytes = OtherEncodings.StringAsLatin1Bytes(sb.ToString()); - - outputStream.WriteByte(NameStart); - outputStream.Write(bytes, 0, bytes.Length); - WriteWhitespace(outputStream); - } - - private static void WriteNumber(NumericToken number, Stream outputStream) - { - if (!number.HasDecimalPlaces) - { - WriteInt(number.Int, outputStream); - } - else - { - var bytes = OtherEncodings.StringAsLatin1Bytes(number.Data.ToString("G", CultureInfo.InvariantCulture)); - outputStream.Write(bytes, 0, bytes.Length); - } - - WriteWhitespace(outputStream); - } - - private static void WriteObject(ObjectToken objectToken, Stream outputStream) - { - WriteLong(objectToken.Number.ObjectNumber, outputStream); - WriteWhitespace(outputStream); - - WriteInt(objectToken.Number.Generation, outputStream); - WriteWhitespace(outputStream); - - outputStream.Write(ObjStart, 0, ObjStart.Length); - WriteLineBreak(outputStream); - - WriteToken(objectToken.Data, outputStream); - - WriteLineBreak(outputStream); - outputStream.Write(ObjEnd, 0, ObjEnd.Length); - - WriteLineBreak(outputStream); - } - - private static void WriteStream(StreamToken streamToken, Stream outputStream) - { - WriteDictionary(streamToken.StreamDictionary, outputStream); - WriteLineBreak(outputStream); - outputStream.Write(StreamStart, 0, StreamStart.Length); - WriteLineBreak(outputStream); - outputStream.Write(streamToken.Data.ToArray(), 0, streamToken.Data.Count); - WriteLineBreak(outputStream); - outputStream.Write(StreamEnd, 0, StreamEnd.Length); - } + } + } + + outputStream.Write(DictionaryEnd, 0, DictionaryEnd.Length); + } + + private static void WriteIndirectReference(IndirectReferenceToken reference, Stream outputStream) + { + WriteLong(reference.Data.ObjectNumber, outputStream); + WriteWhitespace(outputStream); + + WriteInt(reference.Data.Generation, outputStream); + WriteWhitespace(outputStream); + + outputStream.WriteByte(RByte); + WriteWhitespace(outputStream); + } + + private static void WriteName(NameToken name, Stream outputStream) + { + WriteName(name.Data, outputStream); + } + + private static void WriteName(string name, Stream outputStream) + { + /* + * Beginning with PDF 1.2, any character except null (character code 0) may be + * included in a name by writing its 2-digit hexadecimal code, preceded by the number sign character (#). + * This is required for delimiter and whitespace characters. + * This is recommended for characters whose codes are outside the range 33 (!) to 126 (~). + */ + + var sb = new StringBuilder(); + + foreach (var c in name) + { + if (c < 33 || c > 126 || DelimiterChars.Contains(c)) + { + var str = Hex.GetString(new[] { (byte)c }); + sb.Append('#').Append(str); + } + else + { + sb.Append(c); + } + } + + var bytes = OtherEncodings.StringAsLatin1Bytes(sb.ToString()); + + outputStream.WriteByte(NameStart); + outputStream.Write(bytes, 0, bytes.Length); + WriteWhitespace(outputStream); + } + + private static void WriteNumber(NumericToken number, Stream outputStream) + { + if (!number.HasDecimalPlaces) + { + WriteInt(number.Int, outputStream); + } + else + { + var bytes = OtherEncodings.StringAsLatin1Bytes(number.Data.ToString("G", CultureInfo.InvariantCulture)); + outputStream.Write(bytes, 0, bytes.Length); + } + + WriteWhitespace(outputStream); + } + + private static void WriteObject(ObjectToken objectToken, Stream outputStream) + { + WriteLong(objectToken.Number.ObjectNumber, outputStream); + WriteWhitespace(outputStream); + + WriteInt(objectToken.Number.Generation, outputStream); + WriteWhitespace(outputStream); + + outputStream.Write(ObjStart, 0, ObjStart.Length); + WriteLineBreak(outputStream); + + WriteToken(objectToken.Data, outputStream); + + WriteLineBreak(outputStream); + outputStream.Write(ObjEnd, 0, ObjEnd.Length); + + WriteLineBreak(outputStream); + } + + private static void WriteStream(StreamToken streamToken, Stream outputStream) + { + WriteDictionary(streamToken.StreamDictionary, outputStream); + WriteLineBreak(outputStream); + outputStream.Write(StreamStart, 0, StreamStart.Length); + WriteLineBreak(outputStream); + outputStream.Write(streamToken.Data.ToArray(), 0, streamToken.Data.Count); + WriteLineBreak(outputStream); + outputStream.Write(StreamEnd, 0, StreamEnd.Length); + } private static int[] EscapeNeeded = new int[] { @@ -489,12 +456,12 @@ private static int[] Escaped = new int[] { 'r', 'n', 't', 'b', 'f', '\\' - }; - private static void WriteString(StringToken stringToken, Stream outputStream) + }; + private static void WriteString(StringToken stringToken, Stream outputStream) { outputStream.WriteByte(StringStart); - if (stringToken.EncodedWith == StringToken.Encoding.Iso88591) + if (stringToken.EncodedWith == StringToken.Encoding.Iso88591) { // iso 88591 (or really PdfDocEncoding in non-contentstream circumstances shouldn't // have these chars but seems like internally this isn't obeyed (see: @@ -545,84 +512,84 @@ } outputStream.WriteByte(StringEnd); - WriteWhitespace(outputStream); - } - - private static void WriteInt(int value, Stream outputStream) - { - var bytes = OtherEncodings.StringAsLatin1Bytes(value.ToString("G", CultureInfo.InvariantCulture)); - outputStream.Write(bytes, 0, bytes.Length); - } - - private static void WriteLineBreak(Stream outputStream) - { - outputStream.WriteNewLine(); - } - - private static void WriteLong(long value, Stream outputStream) - { - var bytes = OtherEncodings.StringAsLatin1Bytes(value.ToString("G", CultureInfo.InvariantCulture)); - outputStream.Write(bytes, 0, bytes.Length); - } - - private static void WriteWhitespace(Stream outputStream) - { - outputStream.WriteByte(Whitespace); - } - - private static void WriteFirstXrefEmptyEntry(Stream outputStream) - { - /* - * The first entry in the table (object number 0) is always free and has a generation number of 65,535; - * it is the head of the linked list of free objects. - */ - - outputStream.WriteText(new string('0', 10)); - outputStream.WriteWhiteSpace(); - outputStream.WriteText("65535"); - outputStream.WriteWhiteSpace(); - outputStream.WriteText("f"); - outputStream.WriteWhiteSpace(); - outputStream.WriteNewLine(); - } - - private static byte GetByte(string value) - { - var bytes = OtherEncodings.StringAsLatin1Bytes(value); - - if (bytes.Length > 1) - { - throw new InvalidOperationException(); - } - - return bytes[0]; + WriteWhitespace(outputStream); } - private class XrefSeries - { - public long First { get; } - - public IReadOnlyList Offsets { get; } - - public XrefSeries(long first, IReadOnlyList offsets) - { - First = first; - Offsets = offsets; + private static void WriteInt(int value, Stream outputStream) + { + var bytes = OtherEncodings.StringAsLatin1Bytes(value.ToString("G", CultureInfo.InvariantCulture)); + outputStream.Write(bytes, 0, bytes.Length); + } + + private static void WriteLineBreak(Stream outputStream) + { + outputStream.WriteNewLine(); + } + + private static void WriteLong(long value, Stream outputStream) + { + var bytes = OtherEncodings.StringAsLatin1Bytes(value.ToString("G", CultureInfo.InvariantCulture)); + outputStream.Write(bytes, 0, bytes.Length); + } + + private static void WriteWhitespace(Stream outputStream) + { + outputStream.WriteByte(Whitespace); + } + + private static void WriteFirstXrefEmptyEntry(Stream outputStream) + { + /* + * The first entry in the table (object number 0) is always free and has a generation number of 65,535; + * it is the head of the linked list of free objects. + */ + + outputStream.WriteText(new string('0', 10)); + outputStream.WriteWhiteSpace(); + outputStream.WriteText("65535"); + outputStream.WriteWhiteSpace(); + outputStream.WriteText("f"); + outputStream.WriteWhiteSpace(); + outputStream.WriteNewLine(); + } + + private static byte GetByte(string value) + { + var bytes = OtherEncodings.StringAsLatin1Bytes(value); + + if (bytes.Length > 1) + { + throw new InvalidOperationException(); } - public class OffsetAndGeneration - { - public long Offset { get; } - - public long Generation { get; } - - public OffsetAndGeneration(long offset, long generation) - { - Offset = offset; - Generation = generation; - } - } - } - } -} - + return bytes[0]; + } + + private class XrefSeries + { + public long First { get; } + + public IReadOnlyList Offsets { get; } + + public XrefSeries(long first, IReadOnlyList offsets) + { + First = first; + Offsets = offsets; + } + + public class OffsetAndGeneration + { + public long Offset { get; } + + public long Generation { get; } + + public OffsetAndGeneration(long offset, long generation) + { + Offset = offset; + Generation = generation; + } + } + } + } +} +