mirror of
https://github.com/UglyToad/PdfPig.git
synced 2025-09-23 04:36:44 +08:00
#21 quick draft of minimal writing logic requirements
This commit is contained in:
@@ -10,7 +10,7 @@
|
|||||||
public class PdfDocumentBuilderTests
|
public class PdfDocumentBuilderTests
|
||||||
{
|
{
|
||||||
[Fact]
|
[Fact]
|
||||||
public void CanLoadFontAndWriteText()
|
public void CanWriteSinglePageHelloWorld()
|
||||||
{
|
{
|
||||||
var builder = new PdfDocumentBuilder();
|
var builder = new PdfDocumentBuilder();
|
||||||
|
|
||||||
@@ -21,7 +21,13 @@
|
|||||||
|
|
||||||
var font = builder.AddTrueTypeFont(File.ReadAllBytes(file));
|
var font = builder.AddTrueTypeFont(File.ReadAllBytes(file));
|
||||||
|
|
||||||
page.AddText("One", 12, new PdfPoint(30, 50), font);
|
page.AddText("Hello World!", 12, new PdfPoint(30, 50), font);
|
||||||
|
|
||||||
|
Assert.NotEmpty(page.Operations);
|
||||||
|
|
||||||
|
var b = builder.Build();
|
||||||
|
|
||||||
|
Assert.NotEmpty(b);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
209
src/UglyToad.PdfPig/Writer/PdfDocumentBuilder.cs
Normal file
209
src/UglyToad.PdfPig/Writer/PdfDocumentBuilder.cs
Normal file
@@ -0,0 +1,209 @@
|
|||||||
|
namespace UglyToad.PdfPig.Writer
|
||||||
|
{
|
||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.IO;
|
||||||
|
using System.Linq;
|
||||||
|
using Content;
|
||||||
|
using Fonts.TrueType;
|
||||||
|
using Fonts.TrueType.Parser;
|
||||||
|
using Geometry;
|
||||||
|
using IO;
|
||||||
|
using Tokens;
|
||||||
|
using Util;
|
||||||
|
|
||||||
|
internal class PdfDocumentBuilder
|
||||||
|
{
|
||||||
|
private static readonly byte Break = (byte) '\n';
|
||||||
|
private static readonly TrueTypeFontParser Parser = new TrueTypeFontParser();
|
||||||
|
|
||||||
|
private readonly Dictionary<int, PdfPageBuilder> pages = new Dictionary<int, PdfPageBuilder>();
|
||||||
|
private readonly Dictionary<Guid, FontStored> fonts = new Dictionary<Guid, FontStored>();
|
||||||
|
|
||||||
|
public IReadOnlyDictionary<int, PdfPageBuilder> Pages => pages;
|
||||||
|
public IReadOnlyDictionary<Guid, TrueTypeFontProgram> Fonts => fonts.ToDictionary(x => x.Key, x => x.Value.FontProgram);
|
||||||
|
|
||||||
|
public AddedFont AddTrueTypeFont(IReadOnlyList<byte> fontFileBytes)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var font = Parser.Parse(new TrueTypeDataBytes(new ByteArrayInputBytes(fontFileBytes)));
|
||||||
|
var id = Guid.NewGuid();
|
||||||
|
var i = fonts.Count;
|
||||||
|
var added = new AddedFont(id, NameToken.Create($"F{i}"));
|
||||||
|
fonts[id] = new FontStored(added, font);
|
||||||
|
|
||||||
|
return added;
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
throw new InvalidOperationException("Writing only supports TrueType fonts, please provide a valid TrueType font.", ex);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public PdfPageBuilder AddPage(PageSize size, bool isPortrait = true)
|
||||||
|
{
|
||||||
|
if (!size.TryGetPdfRectangle(out var rectangle))
|
||||||
|
{
|
||||||
|
throw new ArgumentException($"No rectangle found for Page Size {size}.");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!isPortrait)
|
||||||
|
{
|
||||||
|
rectangle = new PdfRectangle(0, 0, rectangle.Height, rectangle.Width);
|
||||||
|
}
|
||||||
|
|
||||||
|
PdfPageBuilder builder = null;
|
||||||
|
for (var i = 0; i < pages.Count; i++)
|
||||||
|
{
|
||||||
|
if (!pages.ContainsKey(i + 1))
|
||||||
|
{
|
||||||
|
builder = new PdfPageBuilder(i + 1, this);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (builder == null)
|
||||||
|
{
|
||||||
|
builder = new PdfPageBuilder(pages.Count + 1, this);
|
||||||
|
}
|
||||||
|
|
||||||
|
builder.PageSize = rectangle;
|
||||||
|
pages[builder.PageNumber] = builder;
|
||||||
|
|
||||||
|
return builder;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Generate(Stream stream)
|
||||||
|
{
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Generate(string fileName)
|
||||||
|
{
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
public byte[] Build()
|
||||||
|
{
|
||||||
|
var objectLocations = new Dictionary<IndirectReference, long>();
|
||||||
|
var fontsWritten = new Dictionary<Guid, ObjectToken>();
|
||||||
|
var number = 1;
|
||||||
|
using (var memory = new MemoryStream())
|
||||||
|
{
|
||||||
|
// Header
|
||||||
|
WriteString("%PDF-1.7", memory);
|
||||||
|
|
||||||
|
// Body
|
||||||
|
foreach (var font in fonts)
|
||||||
|
{
|
||||||
|
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);
|
||||||
|
|
||||||
|
var dictionary = new DictionaryToken(new Dictionary<IToken, IToken>
|
||||||
|
{
|
||||||
|
{ NameToken.Type, NameToken.Font },
|
||||||
|
{ NameToken.Subtype, NameToken.TrueType },
|
||||||
|
{ NameToken.FirstChar, new NumericToken(0) },
|
||||||
|
{ NameToken.LastChar, new NumericToken(255) },
|
||||||
|
{ NameToken.Encoding, NameToken.WinAnsiEncoding },
|
||||||
|
{ NameToken.Widths, widthsObj },
|
||||||
|
{ NameToken.FontDesc, new IndirectReferenceToken(descriptorRef) }
|
||||||
|
});
|
||||||
|
|
||||||
|
var fontObj = WriteObject(dictionary, memory, objectLocations, ref number);
|
||||||
|
fontsWritten.Add(font.Key, fontObj);
|
||||||
|
}
|
||||||
|
|
||||||
|
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 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) }
|
||||||
|
})
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
var pageRef = WriteObject(page, memory, objectLocations, ref number);
|
||||||
|
|
||||||
|
var pagesDictionary = new DictionaryToken(new Dictionary<IToken, IToken>
|
||||||
|
{
|
||||||
|
{ NameToken.Type, NameToken.Pages },
|
||||||
|
{ NameToken.Kids, new ArrayToken(new [] { new IndirectReferenceToken(pageRef.Number) }) },
|
||||||
|
{ NameToken.Count, new NumericToken(1) }
|
||||||
|
});
|
||||||
|
|
||||||
|
var pagesRef = WriteObject(pagesDictionary, memory, objectLocations, ref number);
|
||||||
|
|
||||||
|
var catalog = new DictionaryToken(new Dictionary<IToken, IToken>
|
||||||
|
{
|
||||||
|
{ NameToken.Type, NameToken.Catalog },
|
||||||
|
{ NameToken.Pages, new IndirectReferenceToken(pagesRef.Number) }
|
||||||
|
});
|
||||||
|
|
||||||
|
WriteObject(catalog, memory, objectLocations, ref number);
|
||||||
|
|
||||||
|
return memory.ToArray();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static ObjectToken WriteObject(IToken content, MemoryStream 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);
|
||||||
|
return obj;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void WriteString(string text, MemoryStream stream, bool appendBreak = true)
|
||||||
|
{
|
||||||
|
var bytes = OtherEncodings.StringAsLatin1Bytes(text);
|
||||||
|
stream.Write(bytes, 0, bytes.Length);
|
||||||
|
if (appendBreak)
|
||||||
|
{
|
||||||
|
stream.WriteByte(Break);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
internal class FontStored
|
||||||
|
{
|
||||||
|
public AddedFont FontKey { get; }
|
||||||
|
|
||||||
|
public TrueTypeFontProgram FontProgram { get; }
|
||||||
|
|
||||||
|
public FontStored(AddedFont fontKey, TrueTypeFontProgram fontProgram)
|
||||||
|
{
|
||||||
|
FontKey = fontKey;
|
||||||
|
FontProgram = fontProgram;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public class AddedFont
|
||||||
|
{
|
||||||
|
public Guid Id { get; }
|
||||||
|
|
||||||
|
public NameToken Name { get; }
|
||||||
|
|
||||||
|
internal AddedFont(Guid id, NameToken name)
|
||||||
|
{
|
||||||
|
Id = id;
|
||||||
|
Name = name ?? throw new ArgumentNullException(nameof(name));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@@ -1,14 +1,24 @@
|
|||||||
namespace UglyToad.PdfPig.Writer
|
namespace UglyToad.PdfPig.Writer
|
||||||
{
|
{
|
||||||
using System;
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using Core;
|
||||||
using Fonts.TrueType;
|
using Fonts.TrueType;
|
||||||
using Geometry;
|
using Geometry;
|
||||||
|
using Graphics.Operations;
|
||||||
|
using Graphics.Operations.SpecialGraphicsState;
|
||||||
|
using Graphics.Operations.TextObjects;
|
||||||
|
using Graphics.Operations.TextShowing;
|
||||||
|
using Graphics.Operations.TextState;
|
||||||
|
|
||||||
internal class PdfPageBuilder
|
internal class PdfPageBuilder
|
||||||
{
|
{
|
||||||
private readonly PdfDocumentBuilder documentBuilder;
|
private readonly PdfDocumentBuilder documentBuilder;
|
||||||
|
private readonly List<IGraphicsStateOperation> operations = new List<IGraphicsStateOperation>();
|
||||||
|
private BeginText lastBeginText;
|
||||||
|
|
||||||
public int PageNumber { get; }
|
public int PageNumber { get; }
|
||||||
|
public IReadOnlyList<IGraphicsStateOperation> Operations => operations;
|
||||||
|
|
||||||
public PdfRectangle PageSize { get; set; }
|
public PdfRectangle PageSize { get; set; }
|
||||||
|
|
||||||
@@ -43,7 +53,39 @@
|
|||||||
|
|
||||||
var width = CalculateGlyphSpaceTextWidth(text, fontProgram);
|
var width = CalculateGlyphSpaceTextWidth(text, fontProgram);
|
||||||
|
|
||||||
Console.WriteLine(width);
|
var fm = TransformationMatrix.FromValues(1 / 1000m, 0, 1 / 1000m, 0, 0, 0);
|
||||||
|
|
||||||
|
var widthRect = fm.Transform(new PdfRectangle(0, 0, width, 0));
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var ctm = TransformationMatrix.FromValues(position.X, 0, position.Y, 0, 0, 0);
|
||||||
|
|
||||||
|
var realWidth = ctm.Transform(widthRect).Width;
|
||||||
|
|
||||||
|
if (realWidth + position.X > PageSize.Width)
|
||||||
|
{
|
||||||
|
throw new InvalidOperationException("Text would exceed the bounds.");
|
||||||
|
}
|
||||||
|
|
||||||
|
operations.Add(new ModifyCurrentTransformationMatrix(new[]
|
||||||
|
{
|
||||||
|
position.X, 0, position.Y, 0, 0, 0
|
||||||
|
}));
|
||||||
|
|
||||||
|
var beginText = BeginText.Value;
|
||||||
|
|
||||||
|
operations.Add(beginText);
|
||||||
|
operations.Add(new SetFontAndSize(font.Name, fontSize));
|
||||||
|
operations.Add(new ShowText(text));
|
||||||
|
operations.Add(EndText.Value);
|
||||||
|
|
||||||
|
beginText = null;
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
throw;
|
||||||
|
}
|
||||||
|
|
||||||
return this;
|
return this;
|
||||||
}
|
}
|
||||||
|
@@ -1,92 +0,0 @@
|
|||||||
namespace UglyToad.PdfPig.Writer
|
|
||||||
{
|
|
||||||
using System;
|
|
||||||
using System.Collections.Generic;
|
|
||||||
using System.Runtime.InteropServices.ComTypes;
|
|
||||||
using Content;
|
|
||||||
using Fonts.TrueType;
|
|
||||||
using Fonts.TrueType.Parser;
|
|
||||||
using Geometry;
|
|
||||||
using IO;
|
|
||||||
|
|
||||||
internal class PdfDocumentBuilder
|
|
||||||
{
|
|
||||||
private static readonly TrueTypeFontParser Parser = new TrueTypeFontParser();
|
|
||||||
|
|
||||||
private readonly Dictionary<int, PdfPageBuilder> pages = new Dictionary<int, PdfPageBuilder>();
|
|
||||||
private readonly Dictionary<Guid, TrueTypeFontProgram> fonts = new Dictionary<Guid, TrueTypeFontProgram>();
|
|
||||||
|
|
||||||
public IReadOnlyDictionary<int, PdfPageBuilder> Pages => pages;
|
|
||||||
public IReadOnlyDictionary<Guid, TrueTypeFontProgram> Fonts => fonts;
|
|
||||||
|
|
||||||
public AddedFont AddTrueTypeFont(IReadOnlyList<byte> fontFileBytes)
|
|
||||||
{
|
|
||||||
try
|
|
||||||
{
|
|
||||||
var font = Parser.Parse(new TrueTypeDataBytes(new ByteArrayInputBytes(fontFileBytes)));
|
|
||||||
var id = Guid.NewGuid();
|
|
||||||
fonts[id] = font;
|
|
||||||
|
|
||||||
return new AddedFont(id);
|
|
||||||
}
|
|
||||||
catch (Exception ex)
|
|
||||||
{
|
|
||||||
throw new InvalidOperationException("Writing only supports TrueType fonts, please provide a valid TrueType font.", ex);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public PdfPageBuilder AddPage(PageSize size, bool isPortrait = true)
|
|
||||||
{
|
|
||||||
if (!size.TryGetPdfRectangle(out var rectangle))
|
|
||||||
{
|
|
||||||
throw new ArgumentException($"No rectangle found for Page Size {size}.");
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!isPortrait)
|
|
||||||
{
|
|
||||||
rectangle = new PdfRectangle(0, 0, rectangle.Height, rectangle.Width);
|
|
||||||
}
|
|
||||||
|
|
||||||
PdfPageBuilder builder = null;
|
|
||||||
for (var i = 0; i < pages.Count; i++)
|
|
||||||
{
|
|
||||||
if (!pages.ContainsKey(i + 1))
|
|
||||||
{
|
|
||||||
builder = new PdfPageBuilder(i + 1, this);
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (builder == null)
|
|
||||||
{
|
|
||||||
builder = new PdfPageBuilder(pages.Count + 1, this);
|
|
||||||
}
|
|
||||||
|
|
||||||
builder.PageSize = rectangle;
|
|
||||||
pages[builder.PageNumber] = builder;
|
|
||||||
|
|
||||||
return builder;
|
|
||||||
}
|
|
||||||
|
|
||||||
public void Generate(IStream stream)
|
|
||||||
{
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
public void Generate(string fileName)
|
|
||||||
{
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
public class AddedFont
|
|
||||||
{
|
|
||||||
public Guid Id { get; }
|
|
||||||
|
|
||||||
internal AddedFont(Guid id)
|
|
||||||
{
|
|
||||||
Id = id;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
Reference in New Issue
Block a user