mirror of
https://github.com/UglyToad/PdfPig.git
synced 2025-09-22 20:13:58 +08:00
#21 create first actual pdf document based on minimal example. writer for tokens. bump language version
This commit is contained in:
@@ -4,7 +4,8 @@
|
||||
<TargetFramework>netcoreapp2.0</TargetFramework>
|
||||
|
||||
<IsPackable>false</IsPackable>
|
||||
<DebugType>full</DebugType>
|
||||
<DebugType>full</DebugType>
|
||||
<LangVersion>latest</LangVersion>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
|
@@ -4,11 +4,61 @@
|
||||
using System.IO;
|
||||
using Content;
|
||||
using PdfPig.Geometry;
|
||||
using PdfPig.Util;
|
||||
using PdfPig.Writer;
|
||||
using Xunit;
|
||||
|
||||
public class PdfDocumentBuilderTests
|
||||
{
|
||||
[Fact]
|
||||
public void CanWriteSingleBlankPage()
|
||||
{
|
||||
var result = CreateSingleBlankPage();
|
||||
|
||||
Assert.NotEmpty(result);
|
||||
|
||||
var str = OtherEncodings.BytesAsLatin1String(result);
|
||||
Assert.StartsWith("%PDF", str);
|
||||
Assert.EndsWith("%%EOF", str);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void CanReadSingleBlankPage()
|
||||
{
|
||||
var result = CreateSingleBlankPage();
|
||||
|
||||
using (var document = PdfDocument.Open(result, new ParsingOptions { UseLenientParsing = false }))
|
||||
{
|
||||
Assert.Equal(1, document.NumberOfPages);
|
||||
|
||||
var page = document.GetPage(1);
|
||||
|
||||
Assert.Equal(PageSize.A4, page.Size);
|
||||
|
||||
Assert.Empty(page.Letters);
|
||||
|
||||
Assert.NotNull(document.Structure.Catalog);
|
||||
|
||||
foreach (var offset in document.Structure.CrossReferenceTable.ObjectOffsets)
|
||||
{
|
||||
var obj = document.Structure.GetObject(offset.Key);
|
||||
|
||||
Assert.NotNull(obj);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static byte[] CreateSingleBlankPage()
|
||||
{
|
||||
var builder = new PdfDocumentBuilder();
|
||||
|
||||
builder.AddPage(PageSize.A4);
|
||||
|
||||
var result = builder.Build();
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void CanWriteSinglePageHelloWorld()
|
||||
{
|
||||
|
@@ -19,6 +19,7 @@
|
||||
<Product>PdfPig</Product>
|
||||
<PublishRepositoryUrl>true</PublishRepositoryUrl>
|
||||
<EmbedUntrackedSources>true</EmbedUntrackedSources>
|
||||
<LangVersion>latest</LangVersion>
|
||||
<AllowedOutputExtensionsInPackageBuildOutputFolder>$(AllowedOutputExtensionsInPackageBuildOutputFolder);.pdb</AllowedOutputExtensionsInPackageBuildOutputFolder>
|
||||
</PropertyGroup>
|
||||
|
||||
|
@@ -14,7 +14,7 @@
|
||||
|
||||
internal class PdfDocumentBuilder
|
||||
{
|
||||
private static readonly byte Break = (byte) '\n';
|
||||
private static readonly byte Break = (byte)'\n';
|
||||
private static readonly TrueTypeFontParser Parser = new TrueTypeFontParser();
|
||||
|
||||
private readonly Dictionary<int, PdfPageBuilder> pages = new Dictionary<int, PdfPageBuilder>();
|
||||
@@ -97,11 +97,12 @@
|
||||
// Body
|
||||
foreach (var font in fonts)
|
||||
{
|
||||
var widths = new ArrayToken(new [] { new NumericToken(0), new NumericToken(255) });
|
||||
var widths = new ArrayToken(new[] { new NumericToken(0), new NumericToken(255) });
|
||||
var widthsObj = WriteObject(widths, memory, objectLocations, ref number);
|
||||
|
||||
var descriptorRef = new IndirectReference(number++, 0);
|
||||
|
||||
// TODO
|
||||
// var descriptorRef = new IndirectReference(number++, 0);
|
||||
|
||||
var dictionary = new DictionaryToken(new Dictionary<IToken, IToken>
|
||||
{
|
||||
{ NameToken.Type, NameToken.Font },
|
||||
@@ -110,29 +111,36 @@
|
||||
{ NameToken.LastChar, new NumericToken(255) },
|
||||
{ NameToken.Encoding, NameToken.WinAnsiEncoding },
|
||||
{ NameToken.Widths, widthsObj },
|
||||
{ NameToken.FontDesc, new IndirectReferenceToken(descriptorRef) }
|
||||
//{ NameToken.FontDesc, new IndirectReferenceToken(descriptorRef) }
|
||||
});
|
||||
|
||||
var fontObj = WriteObject(dictionary, memory, objectLocations, ref number);
|
||||
fontsWritten.Add(font.Key, fontObj);
|
||||
}
|
||||
|
||||
var resources = new Dictionary<IToken, IToken>
|
||||
{
|
||||
{ NameToken.ProcSet, new ArrayToken(new []{ NameToken.Create("PDF"), NameToken.Create("Text") }) }
|
||||
};
|
||||
|
||||
var fontsDictionary = new DictionaryToken(fontsWritten.Select(x => ((IToken)fonts[x.Key].FontKey.Name, (IToken)new IndirectReferenceToken(x.Value.Number)))
|
||||
.ToDictionary(x => x.Item1, x => x.Item2));
|
||||
if (fontsWritten.Count > 0)
|
||||
{
|
||||
var fontsDictionary = new DictionaryToken(fontsWritten.Select(x => ((IToken)fonts[x.Key].FontKey.Name, (IToken)new IndirectReferenceToken(x.Value.Number)))
|
||||
.ToDictionary(x => x.Item1, x => x.Item2));
|
||||
|
||||
var fontsDictionaryRef = WriteObject(fontsDictionary, memory, objectLocations, ref number);
|
||||
var fontsDictionaryRef = WriteObject(fontsDictionary, memory, objectLocations, ref number);
|
||||
|
||||
resources.Add(NameToken.Font, new IndirectReferenceToken(fontsDictionaryRef.Number));
|
||||
}
|
||||
|
||||
var page = new DictionaryToken(new Dictionary<IToken, IToken>
|
||||
{
|
||||
{ NameToken.Type, NameToken.Page },
|
||||
{
|
||||
NameToken.Resources,
|
||||
new DictionaryToken(new Dictionary<IToken, IToken>
|
||||
{
|
||||
{ NameToken.ProcSet, new ArrayToken(new []{ NameToken.Create("PDF"), NameToken.Create("Text") }) },
|
||||
{ NameToken.Font, new IndirectReferenceToken(fontsDictionaryRef.Number) }
|
||||
})
|
||||
}
|
||||
new DictionaryToken(resources)
|
||||
},
|
||||
{ NameToken.MediaBox, RectangleToArray(pages[1].PageSize) }
|
||||
});
|
||||
|
||||
var pageRef = WriteObject(page, memory, objectLocations, ref number);
|
||||
@@ -152,20 +160,32 @@
|
||||
{ NameToken.Pages, new IndirectReferenceToken(pagesRef.Number) }
|
||||
});
|
||||
|
||||
WriteObject(catalog, memory, objectLocations, ref number);
|
||||
var catalogRef = WriteObject(catalog, memory, objectLocations, ref number);
|
||||
|
||||
TokenWriter.WriteCrossReferenceTable(objectLocations, catalogRef, memory);
|
||||
|
||||
return memory.ToArray();
|
||||
}
|
||||
}
|
||||
|
||||
private static ObjectToken WriteObject(IToken content, MemoryStream stream, Dictionary<IndirectReference, long> objectOffsets, ref int number)
|
||||
private static ArrayToken RectangleToArray(PdfRectangle rectangle)
|
||||
{
|
||||
return new ArrayToken(new[]
|
||||
{
|
||||
new NumericToken(rectangle.BottomLeft.X),
|
||||
new NumericToken(rectangle.BottomLeft.Y),
|
||||
new NumericToken(rectangle.TopRight.X),
|
||||
new NumericToken(rectangle.TopRight.Y)
|
||||
});
|
||||
}
|
||||
|
||||
private static ObjectToken WriteObject(IToken content, Stream stream, Dictionary<IndirectReference, long> objectOffsets, ref int number)
|
||||
{
|
||||
var reference = new IndirectReference(number++, 0);
|
||||
var obj = new ObjectToken(stream.Position, reference, content);
|
||||
objectOffsets.Add(reference, obj.Position);
|
||||
// TODO: write
|
||||
stream.Write(new byte[50], 0, 50);
|
||||
stream.WriteByte(Break);
|
||||
TokenWriter.WriteToken(obj, stream);
|
||||
return obj;
|
||||
}
|
||||
|
||||
@@ -178,7 +198,7 @@
|
||||
stream.WriteByte(Break);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
internal class FontStored
|
||||
{
|
||||
public AddedFont FontKey { get; }
|
||||
|
316
src/UglyToad.PdfPig/Writer/TokenWriter.cs
Normal file
316
src/UglyToad.PdfPig/Writer/TokenWriter.cs
Normal file
@@ -0,0 +1,316 @@
|
||||
namespace UglyToad.PdfPig.Writer
|
||||
{
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using Tokens;
|
||||
using Util;
|
||||
|
||||
internal 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 EndOfLine = OtherEncodings.StringAsLatin1Bytes("\n")[0];
|
||||
|
||||
private static readonly byte[] Eof = OtherEncodings.StringAsLatin1Bytes("%%EOF");
|
||||
|
||||
private static readonly byte[] FalseBytes = OtherEncodings.StringAsLatin1Bytes("false");
|
||||
|
||||
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 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");
|
||||
|
||||
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 _:
|
||||
throw new NotImplementedException();
|
||||
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 _:
|
||||
throw new NotImplementedException();
|
||||
case StringToken stringToken:
|
||||
WriteString(stringToken, outputStream);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
public static void WriteCrossReferenceTable(IReadOnlyDictionary<IndirectReference, long> objectOffsets,
|
||||
ObjectToken catalogToken,
|
||||
Stream outputStream)
|
||||
{
|
||||
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(min, outputStream);
|
||||
WriteWhitespace(outputStream);
|
||||
WriteLong(max, outputStream);
|
||||
WriteWhitespace(outputStream);
|
||||
WriteLineBreak(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 trailerDictionary = new DictionaryToken(new Dictionary<IToken, IToken>
|
||||
{
|
||||
{NameToken.Size, new NumericToken(objectOffsets.Count) },
|
||||
{NameToken.Root, new IndirectReferenceToken(catalogToken.Number) }
|
||||
});
|
||||
|
||||
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 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"));
|
||||
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 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"));
|
||||
outputStream.Write(bytes, 0, bytes.Length);
|
||||
}
|
||||
|
||||
private static void WriteLineBreak(Stream outputStream)
|
||||
{
|
||||
outputStream.WriteByte(EndOfLine);
|
||||
}
|
||||
|
||||
private static void WriteLong(long value, Stream outputStream)
|
||||
{
|
||||
var bytes = OtherEncodings.StringAsLatin1Bytes(value.ToString("G"));
|
||||
outputStream.Write(bytes, 0, bytes.Length);
|
||||
}
|
||||
|
||||
private static void WriteWhitespace(Stream outputStream)
|
||||
{
|
||||
outputStream.WriteByte(Whitespace);
|
||||
}
|
||||
|
||||
private static byte GetByte(string value)
|
||||
{
|
||||
var bytes = OtherEncodings.StringAsLatin1Bytes(value);
|
||||
|
||||
if (bytes.Length > 1)
|
||||
{
|
||||
throw new InvalidOperationException();
|
||||
}
|
||||
|
||||
return bytes[0];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
Reference in New Issue
Block a user