PdfPig/src/UglyToad.PdfPig/Writer/TokenWriter.cs
Eliot Jones c635b919d2 make numbers culture invariant in document builder
decimal numbers were dependent on the current thread culture for the output file. this meant values like '70.679' were output as '70,679' for cultures using a comma rather than period separator for the floating point (i.e. the whole world). this resulted in the file displaying incorrectly.
2020-02-20 13:06:12 +00:00

393 lines
15 KiB
C#

namespace UglyToad.PdfPig.Writer
{
using System;
using System.Collections.Generic;
using System.Globalization;
using System.IO;
using System.Linq;
using Core;
using Graphics.Operations;
using Tokens;
/// <summary>
/// Writes any type of <see cref="IToken"/> to the corresponding PDF document format output.
/// </summary>
public class TokenWriter
{
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");
/// <summary>
/// Writes the given input token to the output stream with the correct PDF format and encoding including whitespace and line breaks as applicable.
/// </summary>
/// <param name="token">The token to write to the stream.</param>
/// <param name="outputStream">The stream to write the token to.</param>
public static void WriteToken(IToken token, Stream 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;
}
}
/// <summary>
/// Writes a valid single section cross-reference (xref) table plus trailer dictionary to the output for the set of object offsets.
/// </summary>
/// <param name="objectOffsets">The byte offset from the start of the document for each object in the document.</param>
/// <param name="catalogToken">The object representing the catalog dictionary which is referenced from the trailer dictionary.</param>
/// <param name="outputStream">The output stream to write to.</param>
/// <param name="documentInformationReference">The object reference for the document information dictionary if present.</param>
internal static void WriteCrossReferenceTable(IReadOnlyDictionary<IndirectReference, long> objectOffsets,
ObjectToken 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 min = objectOffsets.Min(x => x.Key.ObjectNumber);
var max = objectOffsets.Max(x => x.Key.ObjectNumber);
if (max - min != objectOffsets.Count - 1)
{
throw new NotSupportedException("Object numbers must form a contiguous range");
}
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<NameToken, IToken>
{
// 1 for the free entry.
{NameToken.Size, new NumericToken(objectOffsets.Count + 1)},
{NameToken.Root, new IndirectReferenceToken(catalogToken.Number)},
{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 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);
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)
{
var bytes = OtherEncodings.StringAsLatin1Bytes(name);
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 void WriteString(StringToken stringToken, Stream outputStream)
{
outputStream.WriteByte(StringStart);
var bytes = OtherEncodings.StringAsLatin1Bytes(stringToken.Data);
outputStream.Write(bytes, 0, bytes.Length);
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];
}
}
}