#8 tidy up truetype font internally. some more work on a potential document creation api

This commit is contained in:
Eliot Jones
2018-11-25 13:56:27 +00:00
parent 9f783ddef0
commit d5a50f2236
17 changed files with 278 additions and 76 deletions

View File

@@ -35,43 +35,43 @@
Assert.Equal(1, font.Version);
Assert.Equal(1, font.HeaderTable.Version);
Assert.Equal(1, font.HeaderTable.Revision);
Assert.Equal(1, font.TableRegister.HeaderTable.Version);
Assert.Equal(1, font.TableRegister.HeaderTable.Revision);
Assert.Equal(1142661421, font.HeaderTable.CheckSumAdjustment);
Assert.Equal(1594834165, font.HeaderTable.MagicNumber);
Assert.Equal(1142661421, font.TableRegister.HeaderTable.CheckSumAdjustment);
Assert.Equal(1594834165, font.TableRegister.HeaderTable.MagicNumber);
Assert.Equal(9, font.HeaderTable.Flags);
Assert.Equal(9, font.TableRegister.HeaderTable.Flags);
Assert.Equal(2048, font.HeaderTable.UnitsPerEm);
Assert.Equal(2048, font.TableRegister.HeaderTable.UnitsPerEm);
Assert.Equal(2008, font.HeaderTable.Created.Year);
Assert.Equal(10, font.HeaderTable.Created.Month);
Assert.Equal(13, font.HeaderTable.Created.Day);
Assert.Equal(12, font.HeaderTable.Created.Hour);
Assert.Equal(29, font.HeaderTable.Created.Minute);
Assert.Equal(34, font.HeaderTable.Created.Second);
Assert.Equal(2008, font.TableRegister.HeaderTable.Created.Year);
Assert.Equal(10, font.TableRegister.HeaderTable.Created.Month);
Assert.Equal(13, font.TableRegister.HeaderTable.Created.Day);
Assert.Equal(12, font.TableRegister.HeaderTable.Created.Hour);
Assert.Equal(29, font.TableRegister.HeaderTable.Created.Minute);
Assert.Equal(34, font.TableRegister.HeaderTable.Created.Second);
Assert.Equal(2011, font.HeaderTable.Modified.Year);
Assert.Equal(12, font.HeaderTable.Modified.Month);
Assert.Equal(31, font.HeaderTable.Modified.Day);
Assert.Equal(5, font.HeaderTable.Modified.Hour);
Assert.Equal(13, font.HeaderTable.Modified.Minute);
Assert.Equal(10, font.HeaderTable.Modified.Second);
Assert.Equal(2011, font.TableRegister.HeaderTable.Modified.Year);
Assert.Equal(12, font.TableRegister.HeaderTable.Modified.Month);
Assert.Equal(31, font.TableRegister.HeaderTable.Modified.Day);
Assert.Equal(5, font.TableRegister.HeaderTable.Modified.Hour);
Assert.Equal(13, font.TableRegister.HeaderTable.Modified.Minute);
Assert.Equal(10, font.TableRegister.HeaderTable.Modified.Second);
Assert.Equal(-980, font.HeaderTable.Bounds.Left);
Assert.Equal(-555, font.HeaderTable.Bounds.Bottom);
Assert.Equal(-980, font.TableRegister.HeaderTable.Bounds.Left);
Assert.Equal(-555, font.TableRegister.HeaderTable.Bounds.Bottom);
Assert.Equal(2396, font.HeaderTable.Bounds.Right);
Assert.Equal(2163, font.HeaderTable.Bounds.Top);
Assert.Equal(2396, font.TableRegister.HeaderTable.Bounds.Right);
Assert.Equal(2163, font.TableRegister.HeaderTable.Bounds.Top);
Assert.Equal(HeaderTable.HeaderMacStyle.None, font.HeaderTable.MacStyle);
Assert.Equal(9, font.HeaderTable.LowestRecommendedPpem);
Assert.Equal(HeaderTable.HeaderMacStyle.None, font.TableRegister.HeaderTable.MacStyle);
Assert.Equal(9, font.TableRegister.HeaderTable.LowestRecommendedPpem);
Assert.Equal(HeaderTable.FontDirection.StronglyLeftToRightWithNeutrals, font.HeaderTable.FontDirectionHint);
Assert.Equal(HeaderTable.FontDirection.StronglyLeftToRightWithNeutrals, font.TableRegister.HeaderTable.FontDirectionHint);
Assert.Equal(0, font.HeaderTable.IndexToLocFormat);
Assert.Equal(0, font.HeaderTable.GlyphDataFormat);
Assert.Equal(0, font.TableRegister.HeaderTable.IndexToLocFormat);
Assert.Equal(0, font.TableRegister.HeaderTable.GlyphDataFormat);
}
[Fact]
@@ -139,7 +139,7 @@
var font = parser.Parse(input);
Assert.NotNull(font.GlyphTable);
Assert.NotNull(font.TableRegister.GlyphTable);
}
[Fact]
@@ -151,7 +151,7 @@
var font = parser.Parse(input);
Assert.NotNull(font.GlyphTable);
Assert.NotNull(font.TableRegister.GlyphTable);
}
[Fact]
@@ -175,7 +175,7 @@
var height = decimal.Parse(match.Groups["height"].Value);
var points = int.Parse(match.Groups["points"].Value);
var glyph = font.GlyphTable.Glyphs[i];
var glyph = font.TableRegister.GlyphTable.Glyphs[i];
// Vendor data ignores the empty glyph bounds.
if (width == 0 && height == 0)

View File

@@ -0,0 +1,27 @@
namespace UglyToad.PdfPig.Tests.Writer
{
using System;
using System.IO;
using Content;
using PdfPig.Geometry;
using PdfPig.Writer;
using Xunit;
public class PdfDocumentBuilderTests
{
[Fact]
public void CanLoadFontAndWriteText()
{
var builder = new PdfDocumentBuilder();
var page = builder.AddPage(PageSize.A4);
var path = Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "Fonts", "TrueType");
var file = Path.Combine(path, "Andada-Regular.ttf");
var font = builder.AddTrueTypeFont(File.ReadAllBytes(file));
page.AddText("One", 12, new PdfPoint(30, 50), font);
}
}
}

View File

@@ -179,7 +179,7 @@
{
var scale = 1000m;
if (fontProgram?.HeaderTable != null)
if (fontProgram?.TableRegister.HeaderTable != null)
{
scale = fontProgram.GetFontMatrixMultiplier();
}

View File

@@ -1,33 +1,115 @@
namespace UglyToad.PdfPig.Fonts.TrueType.Parser
{
using System;
using Tables;
using Util.JetBrains.Annotations;
/// <summary>
/// Holds tables while parsing a TrueType font
/// Holds tables while contained in a TrueType font.
/// </summary>
internal class TableRegister
{
public GlyphDataTable GlyphDataTable { get; set; }
/// <summary>
/// This table contains global information about the font.
/// </summary>
[NotNull]
public HeaderTable HeaderTable { get; }
public HeaderTable HeaderTable { get; set; }
/// <summary>
/// This table contains the data that defines the appearance of the glyphs in the font.
/// </summary>
[NotNull]
public GlyphDataTable GlyphTable { get; }
public HorizontalHeaderTable HorizontalHeaderTable { get; set; }
/// <summary>
/// This table contains information needed to layout fonts whose characters are written horizontally.
/// </summary>
[NotNull]
public HorizontalHeaderTable HorizontalHeaderTable { get; }
public HorizontalMetricsTable HorizontalMetricsTable { get; set; }
/// <summary>
/// This table contains metric information for the horizontal layout each of the glyphs in the font.
/// </summary>
[NotNull]
public HorizontalMetricsTable HorizontalMetricsTable { get; }
public IndexToLocationTable IndexToLocationTable { get; set; }
/// <summary>
/// This table stores the offsets to the locations of the glyphs (relative to the glyph table).
/// </summary>
[NotNull]
public IndexToLocationTable IndexToLocationTable { get; }
public BasicMaximumProfileTable MaximumProfileTable { get; set; }
/// <summary>
/// This table establishes the memory requirements for the font.
/// </summary>
[NotNull]
public BasicMaximumProfileTable MaximumProfileTable { get; }
public PostScriptTable PostScriptTable { get; set; }
public PostScriptTable PostScriptTable { get; }
/// <summary>
/// Defines mapping of character codes to glyph index values in the font.
/// Can contain multiple sub-tables to support multiple encoding schemes.
/// Where a character code isn't found it should map to index 0.
/// </summary>
public CMapTable CMapTable { get; set; }
public CMapTable CMapTable { get; }
public KerningTable KerningTable { get; set; }
public KerningTable KerningTable { get; }
/// <summary>
/// Create a new <see cref="TableRegister"/>.
/// </summary>
/// <param name="builder">The builder with necessary tables set.</param>
public TableRegister([NotNull] Builder builder)
{
if (builder == null)
{
throw new ArgumentNullException(nameof(builder));
}
HeaderTable = builder.HeaderTable ?? throw new ArgumentException("The builder did not contain the header table");
GlyphTable = builder.GlyphDataTable ?? throw new ArgumentException("The builder did not contain the glyph data table.");
HorizontalHeaderTable = builder.HorizontalHeaderTable ?? throw new ArgumentException("The builder did not contain the horizontal header table.");
HorizontalMetricsTable = builder.HorizontalMetricsTable;
IndexToLocationTable = builder.IndexToLocationTable ?? throw new ArgumentException("The builder did not contain the index to location table.");
MaximumProfileTable = builder.MaximumProfileTable ?? throw new ArgumentException("The builder did not contain the maximum profile table.");
PostScriptTable = builder.PostScriptTable;
CMapTable = builder.CMapTable;
KerningTable = builder.KerningTable;
}
/// <summary>
/// Used to gather the necessary tables for a TrueType font.
/// </summary>
public class Builder
{
public GlyphDataTable GlyphDataTable { get; set; }
public HeaderTable HeaderTable { get; set; }
public HorizontalHeaderTable HorizontalHeaderTable { get; set; }
public HorizontalMetricsTable HorizontalMetricsTable { get; set; }
public IndexToLocationTable IndexToLocationTable { get; set; }
public BasicMaximumProfileTable MaximumProfileTable { get; set; }
public PostScriptTable PostScriptTable { get; set; }
/// <summary>
/// Defines mapping of character codes to glyph index values in the font.
/// Can contain multiple sub-tables to support multiple encoding schemes.
/// Where a character code isn't found it should map to index 0.
/// </summary>
public CMapTable CMapTable { get; set; }
public KerningTable KerningTable { get; set; }
public TableRegister Build()
{
return new TableRegister(this);
}
}
}
}

View File

@@ -57,7 +57,7 @@
{
var isPostScript = tables.ContainsKey(TrueTypeHeaderTable.Cff);
var tableRegister = new TableRegister();
var builder = new TableRegister.Builder();
if (!tables.TryGetValue(TrueTypeHeaderTable.Head, out var table))
{
@@ -65,7 +65,7 @@
}
// head
tableRegister.HeaderTable = HeaderTable.Load(data, table);
builder.HeaderTable = HeaderTable.Load(data, table);
if (!tables.TryGetValue(TrueTypeHeaderTable.Hhea, out var hHead))
{
@@ -73,7 +73,7 @@
}
// hhea
tableRegister.HorizontalHeaderTable = HorizontalHeaderTable.Load(data, hHead);
builder.HorizontalHeaderTable = HorizontalHeaderTable.Load(data, hHead);
if (!tables.TryGetValue(TrueTypeHeaderTable.Maxp, out var maxHeaderTable))
{
@@ -81,12 +81,12 @@
}
// maxp
tableRegister.MaximumProfileTable = BasicMaximumProfileTable.Load(data, maxHeaderTable);
builder.MaximumProfileTable = BasicMaximumProfileTable.Load(data, maxHeaderTable);
// post
if (tables.TryGetValue(TrueTypeHeaderTable.Post, out var postscriptHeaderTable))
{
tableRegister.PostScriptTable = PostScriptTable.Load(data, postscriptHeaderTable, tableRegister.MaximumProfileTable);
builder.PostScriptTable = PostScriptTable.Load(data, postscriptHeaderTable, builder.MaximumProfileTable);
}
if (!isPostScript)
@@ -97,8 +97,8 @@
}
// loca
tableRegister.IndexToLocationTable =
IndexToLocationTable.Load(data, indexToLocationHeaderTable, tableRegister);
builder.IndexToLocationTable =
IndexToLocationTable.Load(data, indexToLocationHeaderTable, builder);
if (!tables.TryGetValue(TrueTypeHeaderTable.Glyf, out var glyphHeaderTable))
{
@@ -106,15 +106,15 @@
}
// glyf
tableRegister.GlyphDataTable = GlyphDataTable.Load(data, glyphHeaderTable, tableRegister);
builder.GlyphDataTable = GlyphDataTable.Load(data, glyphHeaderTable, builder);
OptionallyParseTables(tables, data, tableRegister);
OptionallyParseTables(tables, data, builder);
}
return new TrueTypeFontProgram(version, tables, tableRegister);
return new TrueTypeFontProgram(version, tables, builder.Build());
}
private static void OptionallyParseTables(IReadOnlyDictionary<string, TrueTypeHeaderTable> tables, TrueTypeDataBytes data, TableRegister tableRegister)
private static void OptionallyParseTables(IReadOnlyDictionary<string, TrueTypeHeaderTable> tables, TrueTypeDataBytes data, TableRegister.Builder tableRegister)
{
// cmap
if (tables.TryGetValue(TrueTypeHeaderTable.Cmap, out var cmap))

View File

@@ -76,7 +76,7 @@
return false;
}
public static CMapTable Load(TrueTypeDataBytes data, TrueTypeHeaderTable table, TableRegister tableRegister)
public static CMapTable Load(TrueTypeDataBytes data, TrueTypeHeaderTable table, TableRegister.Builder tableRegister)
{
data.Seek(table.Offset);

View File

@@ -8,7 +8,8 @@
using Util.JetBrains.Annotations;
/// <summary>
/// Describes the glyphs in the font.
/// The 'glyf' table contains the data that defines the appearance of the glyphs in the font.
/// This includes specification of the points that describe the contours that make up a glyph outline and the instructions that grid-fit that glyph.
/// </summary>
internal class GlyphDataTable : ITable
{
@@ -25,7 +26,7 @@
Glyphs = glyphs ?? throw new ArgumentNullException(nameof(glyphs));
}
public static GlyphDataTable Load(TrueTypeDataBytes data, TrueTypeHeaderTable table, TableRegister tableRegister)
public static GlyphDataTable Load(TrueTypeDataBytes data, TrueTypeHeaderTable table, TableRegister.Builder tableRegister)
{
data.Seek(table.Offset);

View File

@@ -4,7 +4,8 @@
using Geometry;
/// <summary>
/// Gives global information about the font.
/// The 'head' table contains global information about the font.
/// It contains things like as the font version number, the creation and modification dates, revision number and basic typographic data that applies to the font as a whole.
/// </summary>
internal class HeaderTable : ITable
{

View File

@@ -2,6 +2,10 @@
{
using System;
/// <summary>
/// The 'hhea' table contains information needed to layout fonts whose characters are written horizontally, that is, either left to right or right to left.
/// This table contains information that is general to the font as a whole.
/// </summary>
internal class HorizontalHeaderTable : ITable
{
public string Tag => TrueTypeHeaderTable.Hhea;

View File

@@ -2,6 +2,9 @@
{
using Parser;
/// <summary>
/// The 'hmtx' table contains metric information for the horizontal layout each of the glyphs in the font.
/// </summary>
internal class HorizontalMetricsTable : ITable
{
private readonly int[] advancedWidths;
@@ -22,7 +25,7 @@
DirectoryTable = directoryTable;
}
public static HorizontalMetricsTable Load(TrueTypeDataBytes data, TrueTypeHeaderTable table, TableRegister tableRegister)
public static HorizontalMetricsTable Load(TrueTypeDataBytes data, TrueTypeHeaderTable table, TableRegister.Builder tableRegister)
{
var glyphCount = tableRegister.MaximumProfileTable.NumberOfGlyphs;
var metricCount = tableRegister.HorizontalHeaderTable.NumberOfHeaderMetrics;

View File

@@ -25,7 +25,7 @@
GlyphOffsets = glyphOffsets;
}
public static IndexToLocationTable Load(TrueTypeDataBytes data, TrueTypeHeaderTable table, TableRegister tableRegister)
public static IndexToLocationTable Load(TrueTypeDataBytes data, TrueTypeHeaderTable table, TableRegister.Builder tableRegister)
{
const short shortFormat = 0;
const short longFormat = 1;

View File

@@ -64,6 +64,7 @@
}
}
/// <inheritdoc />
internal class MaximumProfileTable : BasicMaximumProfileTable
{
/// <summary>
@@ -131,6 +132,9 @@
/// </summary>
public int MaximumComponentDepth { get; }
/// <summary>
/// Create a new <see cref="MaximumProfileTable"/>.
/// </summary>
public MaximumProfileTable(TrueTypeHeaderTable directoryTable, float version, int numberOfGlyphs, int maximumPoints, int maximumContours, int maximumCompositePoints, int maximumCompositeContours, int maximumZones, int maximumTwilightPoints, int maximumStorage, int maximumFunctionDefinitions, int maximumInstructionDefinitions, int maximumStackElements, int maximumSizeOfInstructions, int maximumComponentElements, int maximumComponentDepth) : base(directoryTable, version, numberOfGlyphs)
{
MaximumPoints = maximumPoints;

View File

@@ -5,7 +5,6 @@
using CidFonts;
using Geometry;
using Parser;
using Tables;
internal class TrueTypeFontProgram : ICidFontProgram
{
@@ -13,9 +12,6 @@
public IReadOnlyDictionary<string, TrueTypeHeaderTable> TableHeaders { get; }
public HeaderTable HeaderTable { get; }
public CMapTable CMapTable { get; }
public GlyphDataTable GlyphTable { get; }
public TableRegister TableRegister { get; }
public TrueTypeFontProgram(decimal version, IReadOnlyDictionary<string, TrueTypeHeaderTable> tableHeaders, TableRegister tableRegister)
@@ -23,9 +19,6 @@
Version = version;
TableHeaders = tableHeaders;
TableRegister = tableRegister ?? throw new ArgumentNullException(nameof(tableRegister));
HeaderTable = tableRegister.HeaderTable;
CMapTable = tableRegister.CMapTable;
GlyphTable = tableRegister.GlyphDataTable;
}
public bool TryGetBoundingBox(int characterIdentifier, out PdfRectangle boundingBox) => TryGetBoundingBox(characterIdentifier, null, out boundingBox);
@@ -38,7 +31,7 @@
return false;
}
var glyph = GlyphTable.Glyphs[index];
var glyph = TableRegister.GlyphTable.Glyphs[index];
if (glyph?.Bounds == null)
{
@@ -72,7 +65,7 @@
public int GetFontMatrixMultiplier()
{
return HeaderTable?.UnitsPerEm ?? 1000;
return TableRegister.HeaderTable.UnitsPerEm;
}
private bool TryGetBoundingAdvancedWidthByIndex(int index, out decimal width)
@@ -93,17 +86,12 @@
return true;
}
if (CMapTable == null)
if (TableRegister.CMapTable == null)
{
return false;
}
if (!CMapTable.TryGetGlyphIndex(characterIdentifier, out glyphIndex))
{
return false;
}
return true;
return TableRegister.CMapTable.TryGetGlyphIndex(characterIdentifier, out glyphIndex);
}
}
}

View File

@@ -1,5 +1,8 @@
namespace UglyToad.PdfPig.Fonts.TrueType
{
using System;
using Util.JetBrains.Annotations;
/// <summary>
/// A table directory entry from the TrueType font file.
/// </summary>
@@ -161,6 +164,7 @@
/// <summary>
/// The 4 byte tag identifying the table.
/// </summary>
[NotNull]
public string Tag { get; }
/// <summary>
@@ -180,7 +184,7 @@
public TrueTypeHeaderTable(string tag, long checkSum, long offset, long length)
{
Tag = tag;
Tag = tag ?? throw new ArgumentNullException(nameof(tag));
CheckSum = checkSum;
Offset = offset;
Length = length;

View File

@@ -1,5 +1,7 @@
namespace UglyToad.PdfPig.Geometry
{
using System.Diagnostics;
/// <summary>
/// A point in a PDF file.
/// </summary>
@@ -28,6 +30,7 @@
/// <summary>
/// Create a new <see cref="PdfPoint"/> at this position.
/// </summary>
[DebuggerStepThrough]
public PdfPoint(decimal x, decimal y)
{
X = x;
@@ -37,6 +40,7 @@
/// <summary>
/// Create a new <see cref="PdfPoint"/> at this position.
/// </summary>
[DebuggerStepThrough]
public PdfPoint(int x, int y)
{
X = x;
@@ -46,6 +50,7 @@
/// <summary>
/// Create a new <see cref="PdfPoint"/> at this position.
/// </summary>
[DebuggerStepThrough]
public PdfPoint(double x, double y)
{
X = (decimal)x;

View File

@@ -1,6 +1,7 @@
namespace UglyToad.PdfPig.Writer
{
using System;
using Fonts.TrueType;
using Geometry;
internal class PdfPageBuilder
@@ -16,5 +17,53 @@
this.documentBuilder = documentBuilder ?? throw new ArgumentNullException(nameof(documentBuilder));
PageNumber = number;
}
public PdfPageBuilder AddText(string text, decimal fontSize, PdfPoint position, PdfDocumentBuilder.AddedFont font)
{
if (font == null)
{
throw new ArgumentNullException(nameof(font));
}
if (text == null)
{
throw new ArgumentNullException(nameof(text));
}
if (!documentBuilder.Fonts.TryGetValue(font.Id, out var fontProgram))
{
throw new ArgumentException($"No font has been added to the PdfDocumentBuilder with Id: {font.Id}. " +
$"Use {nameof(documentBuilder.AddTrueTypeFont)} to register a font.", nameof(font));
}
if (fontSize <= 0)
{
throw new ArgumentOutOfRangeException(nameof(fontSize), "Font size must be greater than 0");
}
var width = CalculateGlyphSpaceTextWidth(text, fontProgram);
Console.WriteLine(width);
return this;
}
private static decimal CalculateGlyphSpaceTextWidth(string text, TrueTypeFontProgram font)
{
var width = 0m;
for (var i = 0; i < text.Length; i++)
{
var c = text[i];
if(!font.TryGetBoundingBox(c, out var rect))
{
throw new InvalidOperationException($"The font does not contain a character: {c}.");
}
width += rect.Width;
}
return width;
}
}
}

View File

@@ -4,15 +4,38 @@
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 PdfPageBuilder AddPage(PageSize size, bool isPortrait)
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))
{
@@ -54,5 +77,16 @@
{
}
public class AddedFont
{
public Guid Id { get; }
internal AddedFont(Guid id)
{
Id = id;
}
}
}
}