#21 further changes to truetype to get accurate information out for creating documents

This commit is contained in:
Eliot Jones
2018-12-08 18:04:02 +00:00
parent 3a4b7b79d1
commit dc5d2b8fdd
16 changed files with 438 additions and 198 deletions

View File

@@ -1,6 +1,7 @@
namespace UglyToad.PdfPig.Tests.Writer
{
using System;
using System.Diagnostics;
using System.IO;
using System.Linq;
using Content;
@@ -17,6 +18,8 @@
{
var result = CreateSingleBlankPage();
WriteFile(nameof(CanWriteSinglePageHelloWorld), result);
Assert.NotEmpty(result);
var str = OtherEncodings.BytesAsLatin1String(result);
@@ -74,6 +77,8 @@
var b = builder.Build();
WriteFile(nameof(CanWriteSinglePageStandard14FontHelloWorld), b);
using (var document = PdfDocument.Open(b))
{
var page1 = document.GetPage(1);
@@ -94,12 +99,14 @@
var font = builder.AddTrueTypeFont(File.ReadAllBytes(file));
page.AddText("Hello World!", 12, new PdfPoint(30, 50), font);
var letters = page.AddText("Hello World!", 12, new PdfPoint(30, 50), font);
Assert.NotEmpty(page.Operations);
var b = builder.Build();
WriteFile(nameof(CanWriteSinglePageHelloWorld), b);
Assert.NotEmpty(b);
using (var document = PdfDocument.Open(b))
@@ -112,6 +119,38 @@
Assert.Equal("H", h.Value);
Assert.Equal("Andada-Regular", h.FontName);
for (int i = 0; i < page1.Letters.Count; i++)
{
var readerLetter = page1.Letters[i];
var writerLetter = letters[i];
Assert.Equal(readerLetter.Value, writerLetter.Value);
//Assert.Equal(readerLetter.Location, writerLetter.Location);
//Assert.Equal(readerLetter.FontSize, writerLetter.FontSize);
//Assert.Equal(readerLetter.GlyphRectangle.Width, writerLetter.GlyphRectangle.Width);
//Assert.Equal(readerLetter.GlyphRectangle.Height, writerLetter.GlyphRectangle.Height);
//Assert.Equal(readerLetter.GlyphRectangle.BottomLeft, writerLetter.GlyphRectangle.BottomLeft);
}
}
}
private static void WriteFile(string name, byte[] bytes)
{
try
{
if (!Directory.Exists("Builder"))
{
Directory.CreateDirectory("Builder");
}
var output = Path.Combine("Builder", $"{name}.pdf");
File.WriteAllBytes(output, bytes);
}
catch
{
// ignored.
}
}
}

View File

@@ -0,0 +1,106 @@
namespace UglyToad.PdfPig.Fonts.TrueType.Parser
{
using System.Collections.Generic;
using Tables;
using Tables.CMapSubTables;
internal class CMapTableParser : ITrueTypeTableParser<CMapTable>
{
public CMapTable Parse(TrueTypeHeaderTable header, TrueTypeDataBytes data, TableRegister.Builder register)
{
data.Seek(header.Offset);
var tableVersionNumber = data.ReadUnsignedShort();
var numberOfEncodingTables = data.ReadUnsignedShort();
var subTableHeaders = new SubTableHeaderEntry[numberOfEncodingTables];
for (int i = 0; i < numberOfEncodingTables; i++)
{
var platformId = (TrueTypeCMapPlatform)data.ReadUnsignedShort();
var encodingId = data.ReadUnsignedShort();
var offset = data.ReadUnsignedInt();
subTableHeaders[i] = new SubTableHeaderEntry(platformId, encodingId, offset);
}
var tables = new List<ICMapSubTable>(numberOfEncodingTables);
var numberofGlyphs = register.MaximumProfileTable.NumberOfGlyphs;
for (var i = 0; i < subTableHeaders.Length; i++)
{
var subTableHeader = subTableHeaders[i];
data.Seek(header.Offset + subTableHeader.Offset);
var format = data.ReadUnsignedShort();
/*
* There are 9 currently available formats:
* 0: Character code and glyph indices are restricted to a single byte. Rare.
* 2: Suitable for CJK characters. Contain mixed 8/16 byte encoding.
* 4: 2 byte encoding format. Used when character codes fall into (gappy) contiguous ranges.
* 6: 'Trimmed table mapping', used when character codes fall into a single contiguous range. This is dense mapping.
* 8: 16/32 bit coverage. Uses mixed length character codes.
* 10: Similar to format 6, trimmed table/array for 32 bits.
* 12: Segmented coverage, similar to format 4 but for 32 bit/4 byte.
* 13: Many to one mappings. Used by Apple for the LastResort font.
* 14: Unicode variation sequences.
*
* Many of the formats are obsolete or not really used. Modern fonts will tend to use formats 4, 6 and 12.
* For PDF we will support 0, 2 and 4 since these are in the original TrueType spec.
*/
switch (format)
{
case 0:
{
// Simple 1 to 1 mapping of character codes to glyph codes.
var item = ByteEncodingCMapTable.Load(data, subTableHeader.PlatformId, subTableHeader.EncodingId);
tables.Add(item);
break;
}
case 2:
{
// Useful for CJK characters. Use mixed 8/16 bit encoding.
var item = HighByteMappingCMapTable.Load(data, numberofGlyphs, subTableHeader.PlatformId, subTableHeader.EncodingId);
tables.Add(item);
break;
}
case 4:
{
// Microsoft's standard mapping table.
var item = Format4CMapTable.Load(data, subTableHeader.PlatformId, subTableHeader.EncodingId);
tables.Add(item);
break;
}
case 6:
{
var item = TrimmedTableMappingCMapTable.Load(data, subTableHeader.PlatformId, subTableHeader.EncodingId);
tables.Add(item);
break;
}
}
}
return new CMapTable(tableVersionNumber, header, tables);
}
private class SubTableHeaderEntry
{
public TrueTypeCMapPlatform PlatformId { get; }
public int EncodingId { get; }
public long Offset { get; }
public SubTableHeaderEntry(TrueTypeCMapPlatform platformId, int encodingId, long offset)
{
PlatformId = platformId;
EncodingId = encodingId;
Offset = offset;
}
}
}
}

View File

@@ -0,0 +1,36 @@
namespace UglyToad.PdfPig.Fonts.TrueType.Parser
{
using Tables;
internal class HorizontalMetricsTableParser : ITrueTypeTableParser<HorizontalMetricsTable>
{
public HorizontalMetricsTable Parse(TrueTypeHeaderTable header, TrueTypeDataBytes data, TableRegister.Builder register)
{
var glyphCount = register.MaximumProfileTable.NumberOfGlyphs;
var metricCount = register.HorizontalHeaderTable.NumberOfHeaderMetrics;
data.Seek(header.Offset);
// The number of entries in the left side bearing field per entry is number of glyphs - number of metrics
var additionalLeftSideBearingLength = glyphCount - metricCount;
var advancedWidths = new int[metricCount];
// For bearings over the metric count, the width is the same as the last width in advanced widths.
var leftSideBearings = new short[glyphCount];
for (var i = 0; i < metricCount; i++)
{
advancedWidths[i] = data.ReadUnsignedShort();
leftSideBearings[i] = data.ReadSignedShort();
}
for (var i = 0; i < additionalLeftSideBearingLength; i++)
{
leftSideBearings[metricCount + i] = data.ReadSignedShort();
}
return new HorizontalMetricsTable(header, advancedWidths, leftSideBearings, metricCount);
}
}
}

View File

@@ -5,11 +5,23 @@
internal static class TableParser
{
private static readonly CMapTableParser CMapTableParser = new CMapTableParser();
private static readonly HorizontalMetricsTableParser HorizontalMetricsTableParser = new HorizontalMetricsTableParser();
private static readonly NameTableParser NameTableParser = new NameTableParser();
private static readonly Os2TableParser Os2TableParser = new Os2TableParser();
public static T Parse<T>(TrueTypeHeaderTable table, TrueTypeDataBytes data, TableRegister.Builder register) where T : ITable
{
if (typeof(T) == typeof(CMapTable))
{
return (T) (object) CMapTableParser.Parse(table, data, register);
}
if (typeof(T) == typeof(HorizontalMetricsTable))
{
return (T) (object) HorizontalMetricsTableParser.Parse(table, data, register);
}
if (typeof(T) == typeof(NameTable))
{
return (T) (object) NameTableParser.Parse(table, data, register);

View File

@@ -130,23 +130,15 @@
// cmap
if (tables.TryGetValue(TrueTypeHeaderTable.Cmap, out var cmap))
{
tableRegister.CMapTable = CMapTable.Load(data, cmap, tableRegister);
tableRegister.CMapTable = TableParser.Parse<CMapTable>(cmap, data, tableRegister);
}
// hmtx
if (tables.TryGetValue(TrueTypeHeaderTable.Hmtx, out var hmtxHeaderTable))
{
tableRegister.HorizontalMetricsTable = HorizontalMetricsTable.Load(data, hmtxHeaderTable, tableRegister);
tableRegister.HorizontalMetricsTable = TableParser.Parse<HorizontalMetricsTable>(hmtxHeaderTable, data, tableRegister);
}
// name
if (tables.TryGetValue(TrueTypeHeaderTable.Name, out var nameHeaderTable))
{
// TODO: Not important
}
// os2
// kern
if (tables.TryGetValue(TrueTypeHeaderTable.Kern, out var kernHeaderTable))
{

View File

@@ -9,18 +9,22 @@
private const int GlyphMappingLength = 256;
private readonly byte[] glyphMapping;
public int PlatformId { get; }
public TrueTypeCMapPlatform PlatformId { get; }
public int EncodingId { get; }
private ByteEncodingCMapTable(int platformId, int encodingId, byte[] glyphMapping)
public int FirstCharacterCode { get; }
public int LastCharacterCode { get; }
private ByteEncodingCMapTable(TrueTypeCMapPlatform platformId, int encodingId, byte[] glyphMapping)
{
this.glyphMapping = glyphMapping;
PlatformId = platformId;
EncodingId = encodingId;
}
public static ByteEncodingCMapTable Load(TrueTypeDataBytes data, int platformId, int encodingId)
public static ByteEncodingCMapTable Load(TrueTypeDataBytes data, TrueTypeCMapPlatform platformId, int encodingId)
{
// ReSharper disable UnusedVariable
var length = data.ReadUnsignedShort();

View File

@@ -10,10 +10,14 @@ namespace UglyToad.PdfPig.Fonts.TrueType.Tables.CMapSubTables
/// </summary>
internal class Format4CMapTable : ICMapSubTable
{
public int PlatformId { get; }
public TrueTypeCMapPlatform PlatformId { get; }
public int EncodingId { get; }
public int FirstCharacterCode { get; }
public int LastCharacterCode { get; }
public int Language { get; }
public IReadOnlyList<Segment> Segments { get; }
@@ -23,13 +27,16 @@ namespace UglyToad.PdfPig.Fonts.TrueType.Tables.CMapSubTables
/// <summary>
/// Create a new <see cref="Format4CMapTable"/>.
/// </summary>
public Format4CMapTable(int platformId, int encodingId, int language, IReadOnlyList<Segment> segments, IReadOnlyList<int> glyphIds)
public Format4CMapTable(TrueTypeCMapPlatform platformId, int encodingId, int language, IReadOnlyList<Segment> segments, IReadOnlyList<int> glyphIds)
{
PlatformId = platformId;
EncodingId = encodingId;
Language = language;
Segments = segments ?? throw new ArgumentNullException(nameof(segments));
GlyphIds = glyphIds ?? throw new ArgumentNullException(nameof(glyphIds));
FirstCharacterCode = Segments[0].StartCode;
LastCharacterCode = Segments[Segments.Count - 2].EndCode;
}
public int CharacterCodeToGlyphIndex(int characterCode)
@@ -56,7 +63,7 @@ namespace UglyToad.PdfPig.Fonts.TrueType.Tables.CMapSubTables
return 0;
}
public static Format4CMapTable Load(TrueTypeDataBytes data, int platformId, int encodingId)
public static Format4CMapTable Load(TrueTypeDataBytes data, TrueTypeCMapPlatform platformId, int encodingId)
{
// Length in bytes.
var length = data.ReadUnsignedShort();

View File

@@ -12,11 +12,15 @@
{
private readonly IReadOnlyDictionary<int, int> characterCodesToGlyphIndices;
public int PlatformId { get; }
public TrueTypeCMapPlatform PlatformId { get; }
public int EncodingId { get; }
private HighByteMappingCMapTable(int platformId, int encodingId, IReadOnlyDictionary<int, int> characterCodesToGlyphIndices)
public int FirstCharacterCode { get; }
public int LastCharacterCode { get; }
private HighByteMappingCMapTable(TrueTypeCMapPlatform platformId, int encodingId, IReadOnlyDictionary<int, int> characterCodesToGlyphIndices)
{
this.characterCodesToGlyphIndices = characterCodesToGlyphIndices ?? throw new ArgumentNullException(nameof(characterCodesToGlyphIndices));
PlatformId = platformId;
@@ -33,7 +37,7 @@
return index;
}
public static HighByteMappingCMapTable Load(TrueTypeDataBytes data, int numberOfGlyphs, int platformId, int encodingId)
public static HighByteMappingCMapTable Load(TrueTypeDataBytes data, int numberOfGlyphs, TrueTypeCMapPlatform platformId, int encodingId)
{
// ReSharper disable UnusedVariable
var length = data.ReadUnsignedShort();

View File

@@ -10,19 +10,17 @@
/// <summary>
/// The platform identifier.
/// </summary>
/// <remarks>
/// 0: Unicode
/// 1: Macintosh
/// 2: Reserved
/// 3: Microsoft
/// </remarks>
int PlatformId { get; }
TrueTypeCMapPlatform PlatformId { get; }
/// <summary>
/// Platform specific encoding indentifier.
/// </summary>
int EncodingId { get; }
int FirstCharacterCode { get; }
int LastCharacterCode { get; }
/// <summary>
/// Maps from a character code to the array index of the glyph in the font data.
/// </summary>
@@ -30,4 +28,27 @@
/// <returns>The index of the glyph information for this character.</returns>
int CharacterCodeToGlyphIndex(int characterCode);
}
/// <summary>
/// The platform identifier for a CMap table.
/// </summary>
internal enum TrueTypeCMapPlatform
{
/// <summary>
/// Unicode.
/// </summary>
Unicode = 0,
/// <summary>
/// Apple Macintosh.
/// </summary>
Macintosh = 1,
/// <summary>
/// Unused.
/// </summary>
Reserved2 = 2,
/// <summary>
/// Microsoft Windows.
/// </summary>
Windows = 3
}
}

View File

@@ -9,39 +9,44 @@ namespace UglyToad.PdfPig.Fonts.TrueType.Tables.CMapSubTables
/// </summary>
internal class TrimmedTableMappingCMapTable : ICMapSubTable
{
private readonly int firstCharacterCode;
private readonly int entryCount;
private readonly int[] glyphIndices;
public int PlatformId { get; }
public TrueTypeCMapPlatform PlatformId { get; }
public int EncodingId { get; }
public int FirstCharacterCode { get; }
public int LastCharacterCode { get; }
/// <summary>
/// Create a new <see cref="TrimmedTableMappingCMapTable"/>.
/// </summary>
public TrimmedTableMappingCMapTable(int platformId, int encodingId, int firstCharacterCode, int entryCount, int[] glyphIndices)
public TrimmedTableMappingCMapTable(TrueTypeCMapPlatform platformId, int encodingId, int firstCharacterCode, int entryCount, int[] glyphIndices)
{
this.firstCharacterCode = firstCharacterCode;
FirstCharacterCode = firstCharacterCode;
this.entryCount = entryCount;
this.glyphIndices = glyphIndices ?? throw new ArgumentNullException(nameof(glyphIndices));
LastCharacterCode = firstCharacterCode + entryCount - 1;
PlatformId = platformId;
EncodingId = encodingId;
}
public int CharacterCodeToGlyphIndex(int characterCode)
{
if (characterCode < firstCharacterCode || characterCode > firstCharacterCode + entryCount)
if (characterCode < FirstCharacterCode || characterCode > FirstCharacterCode + entryCount)
{
return 0;
}
var offset = characterCode - firstCharacterCode;
var offset = characterCode - FirstCharacterCode;
return glyphIndices[offset];
}
public static TrimmedTableMappingCMapTable Load(TrueTypeDataBytes data, int platformId, int encodingId)
public static TrimmedTableMappingCMapTable Load(TrueTypeDataBytes data, TrueTypeCMapPlatform platformId, int encodingId)
{
var length = data.ReadUnsignedShort();
var language = data.ReadUnsignedShort();

View File

@@ -1,12 +1,11 @@
namespace UglyToad.PdfPig.Fonts.TrueType.Tables
{
using Parser;
using System.Collections.Generic;
using CMapSubTables;
internal class CMapTable : ITable
{
private readonly IReadOnlyList<ICMapSubTable> subTables;
public IReadOnlyList<ICMapSubTable> SubTables { get; }
public string Tag => TrueTypeHeaderTable.Cmap;
@@ -16,7 +15,7 @@
public CMapTable(int version, TrueTypeHeaderTable directoryTable, IReadOnlyList<ICMapSubTable> subTables)
{
this.subTables = subTables;
SubTables = subTables;
Version = version;
DirectoryTable = directoryTable;
}
@@ -25,14 +24,14 @@
{
glyphIndex = 0;
if (subTables.Count == 0)
if (SubTables.Count == 0)
{
return false;
}
var windowsMapping = default(ICMapSubTable);
foreach (var subTable in subTables)
foreach (var subTable in SubTables)
{
glyphIndex = subTable.CharacterCodeToGlyphIndex(characterCode);
@@ -41,7 +40,7 @@
return true;
}
if (subTable.EncodingId == 0 && subTable.PlatformId == 3)
if (subTable.EncodingId == 0 && subTable.PlatformId == TrueTypeCMapPlatform.Windows)
{
windowsMapping = subTable;
}
@@ -75,102 +74,5 @@
return false;
}
public static CMapTable Load(TrueTypeDataBytes data, TrueTypeHeaderTable table, TableRegister.Builder tableRegister)
{
data.Seek(table.Offset);
var tableVersionNumber = data.ReadUnsignedShort();
var numberOfEncodingTables = data.ReadUnsignedShort();
var subTableHeaders = new SubTableHeaderEntry[numberOfEncodingTables];
for (int i = 0; i < numberOfEncodingTables; i++)
{
var platformId = data.ReadUnsignedShort();
var encodingId = data.ReadUnsignedShort();
var offset = data.ReadUnsignedInt();
subTableHeaders[i] = new SubTableHeaderEntry(platformId, encodingId, offset);
}
var tables = new List<ICMapSubTable>(numberOfEncodingTables);
var numberofGlyphs = tableRegister.MaximumProfileTable.NumberOfGlyphs;
for (var i = 0; i < subTableHeaders.Length; i++)
{
var header = subTableHeaders[i];
data.Seek(table.Offset + header.Offset);
var format = data.ReadUnsignedShort();
/*
* There are 9 currently available formats:
* 0: Character code and glyph indices are restricted to a single byte. Rare.
* 2: Suitable for CJK characters. Contain mixed 8/16 byte encoding.
* 4: 2 byte encoding format. Used when character codes fall into (gappy) contiguous ranges.
* 6: 'Trimmed table mapping', used when character codes fall into a single contiguous range. This is dense mapping.
* 8: 16/32 bit coverage. Uses mixed length character codes.
* 10: Similar to format 6, trimmed table/array for 32 bits.
* 12: Segmented coverage, similar to format 4 but for 32 bit/4 byte.
* 13: Many to one mappings. Used by Apple for the LastResort font.
* 14: Unicode variation sequences.
*
* Many of the formats are obsolete or not really used. Modern fonts will tend to use formats 4, 6 and 12.
* For PDF we will support 0, 2 and 4 since these are in the original TrueType spec.
*/
switch (format)
{
case 0:
{
// Simple 1 to 1 mapping of character codes to glyph codes.
var item = ByteEncodingCMapTable.Load(data, header.PlatformId, header.EncodingId);
tables.Add(item);
break;
}
case 2:
{
// Useful for CJK characters. Use mixed 8/16 bit encoding.
var item = HighByteMappingCMapTable.Load(data, numberofGlyphs, header.PlatformId, header.EncodingId);
tables.Add(item);
break;
}
case 4:
{
// Microsoft's standard mapping table.
var item = Format4CMapTable.Load(data, header.PlatformId, header.EncodingId);
tables.Add(item);
break;
}
case 6:
{
var item = TrimmedTableMappingCMapTable.Load(data, header.PlatformId, header.EncodingId);
tables.Add(item);
break;
}
}
}
return new CMapTable(tableVersionNumber, table, tables);
}
private class SubTableHeaderEntry
{
public int PlatformId { get; }
public int EncodingId { get; }
public long Offset { get; }
public SubTableHeaderEntry(int platformId, int encodingId, long offset)
{
PlatformId = platformId;
EncodingId = encodingId;
Offset = offset;
}
}
}
}

View File

@@ -1,5 +1,6 @@
namespace UglyToad.PdfPig.Fonts.TrueType.Tables
{
using System.Collections.Generic;
using Parser;
/// <summary>
@@ -7,7 +8,6 @@
/// </summary>
internal class HorizontalMetricsTable : ITable
{
private readonly int[] advancedWidths;
private readonly short[] leftSideBearings;
private readonly int metricCount;
@@ -16,54 +16,27 @@
public TrueTypeHeaderTable DirectoryTable { get; }
public IReadOnlyList<int> AdvancedWidths { get; }
public HorizontalMetricsTable(TrueTypeHeaderTable directoryTable, int[] advancedWidths, short[] leftSideBearings, int metricCount)
{
this.advancedWidths = advancedWidths;
AdvancedWidths = advancedWidths;
this.leftSideBearings = leftSideBearings;
this.metricCount = metricCount;
DirectoryTable = directoryTable;
}
public static HorizontalMetricsTable Load(TrueTypeDataBytes data, TrueTypeHeaderTable table, TableRegister.Builder tableRegister)
{
var glyphCount = tableRegister.MaximumProfileTable.NumberOfGlyphs;
var metricCount = tableRegister.HorizontalHeaderTable.NumberOfHeaderMetrics;
data.Seek(table.Offset);
// The number of entries in the left side bearing field per entry is number of glyphs - number of metrics
var additionalLeftSideBearingLength = glyphCount - metricCount;
var advancedWidths = new int[metricCount];
// For bearings over the metric count, the width is the same as the last width in advanced widths.
var leftSideBearings = new short[glyphCount];
for (var i = 0; i < metricCount; i++)
{
advancedWidths[i] = data.ReadUnsignedShort();
leftSideBearings[i] = data.ReadSignedShort();
}
for (var i = 0; i < additionalLeftSideBearingLength; i++)
{
leftSideBearings[metricCount + i] = data.ReadSignedShort();
}
return new HorizontalMetricsTable(table, advancedWidths, leftSideBearings, metricCount);
}
public int GetAdvanceWidth(int index)
{
if (index < metricCount)
{
return advancedWidths[index];
return AdvancedWidths[index];
}
// monospaced fonts may not have a width for every glyph
// the last one is for subsequent glyphs
return advancedWidths[advancedWidths.Length - 1];
return AdvancedWidths[AdvancedWidths.Count - 1];
}
}
}

View File

@@ -1,6 +1,5 @@
namespace UglyToad.PdfPig.Writer
{
using System.Collections.Generic;
using System.IO;
using Geometry;
using Tokens;
@@ -9,8 +8,12 @@
{
bool HasWidths { get; }
string Name { get; }
bool TryGetBoundingBox(char character, out PdfRectangle boundingBox);
bool TryGetAdvanceWidth(char character, out decimal width);
ObjectToken WriteFont(NameToken fontKeyName, Stream outputStream, BuilderContext context);
}
}

View File

@@ -2,11 +2,10 @@
{
using System;
using System.Collections.Generic;
using Content;
using Core;
using Fonts.TrueType;
using Geometry;
using Graphics.Operations;
using Graphics.Operations.SpecialGraphicsState;
using Graphics.Operations.TextObjects;
using Graphics.Operations.TextPositioning;
using Graphics.Operations.TextShowing;
@@ -29,7 +28,7 @@
PageNumber = number;
}
public PdfPageBuilder AddText(string text, decimal fontSize, PdfPoint position, PdfDocumentBuilder.AddedFont font)
public List<Letter> AddText(string text, decimal fontSize, PdfPoint position, PdfDocumentBuilder.AddedFont font)
{
if (font == null)
{
@@ -52,20 +51,19 @@
throw new ArgumentOutOfRangeException(nameof(fontSize), "Font size must be greater than 0");
}
var width = CalculateGlyphSpaceTextWidth(text, fontProgram);
var fm = TransformationMatrix.FromValues(1 / 1000m, 0, 1 / 1000m, 0, 0, 0);
var widthRect = fm.Transform(new PdfRectangle(0, 0, width, 0));
var fm = TransformationMatrix.FromValues(1 / 1000m, 0, 0, 1 / 1000m, 0, 0);
var textMatrix = TransformationMatrix.FromValues(1, 0, 0, 1, position.X, position.Y);
var letters = DrawLetters(text, fontProgram, fm, fontSize, textMatrix);
try
{
var realWidth = widthRect.Width;
//var realWidth = widthRect.Width;
if (realWidth + position.X > PageSize.Width)
{
throw new InvalidOperationException("Text would exceed the bounds.");
}
//if (realWidth + position.X > PageSize.Width)
//{
// throw new InvalidOperationException("Text would exceed the bounds.");
//}
var beginText = BeginText.Value;
@@ -82,12 +80,20 @@
throw;
}
return this;
return letters;
}
private static decimal CalculateGlyphSpaceTextWidth(string text, IWritingFont font)
private static List<Letter> DrawLetters(string text, IWritingFont font, TransformationMatrix fontMatrix, decimal fontSize, TransformationMatrix textMatrix)
{
var horizontalScaling = 1;
var rise = 0;
var letters = new List<Letter>();
var renderingMatrix =
TransformationMatrix.FromValues(fontSize * horizontalScaling, 0, 0, fontSize, 0, rise);
var width = 0m;
for (var i = 0; i < text.Length; i++)
{
var c = text[i];
@@ -97,10 +103,30 @@
throw new InvalidOperationException($"The font does not contain a character: {c}.");
}
width += rect.Width;
if (!font.TryGetAdvanceWidth(c, out var charWidth))
{
throw new InvalidOperationException($"The font does not contain a character: {c}.");
}
var advanceRect = new PdfRectangle(0, 0, charWidth, 0);
advanceRect = textMatrix.Transform(renderingMatrix.Transform(fontMatrix.Transform(advanceRect)));
var documentSpace = textMatrix.Transform(renderingMatrix.Transform(fontMatrix.Transform(rect)));
var letter = new Letter(c.ToString(), documentSpace, advanceRect.BottomLeft, width, fontSize, font.Name, fontSize);
letters.Add(letter);
var tx = advanceRect.Width * horizontalScaling;
var ty = 0;
var translate = TransformationMatrix.GetTranslationMatrix(tx, ty);
width += tx;
textMatrix = translate.Multiply(textMatrix);
}
return width;
return letters;
}
}
}

View File

@@ -13,6 +13,8 @@
public bool HasWidths { get; } = false;
public string Name => metrics.FontName;
public Standard14WritingFont(FontMetrics metrics)
{
this.metrics = metrics;
@@ -33,6 +35,19 @@
return true;
}
public bool TryGetAdvanceWidth(char character, out decimal width)
{
width = 0;
if (!TryGetBoundingBox(character, out var bbox))
{
return false;
}
width = bbox.Width;
return true;
}
public ObjectToken WriteFont(NameToken fontKeyName, Stream outputStream, BuilderContext context)
{
var dictionary = new Dictionary<NameToken, IToken>

View File

@@ -1,5 +1,6 @@
namespace UglyToad.PdfPig.Writer
{
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
@@ -7,6 +8,7 @@
using Fonts.Exceptions;
using Fonts.TrueType;
using Fonts.TrueType.Tables;
using Fonts.TrueType.Tables.CMapSubTables;
using Geometry;
using Tokens;
@@ -15,19 +17,26 @@
private readonly TrueTypeFontProgram font;
private readonly IReadOnlyList<byte> fontFileBytes;
public bool HasWidths { get; } = true;
public string Name => font.Name;
public TrueTypeWritingFont(TrueTypeFontProgram font, IReadOnlyList<byte> fontFileBytes)
{
this.font = font;
this.fontFileBytes = fontFileBytes;
}
public bool HasWidths { get; } = true;
public bool TryGetBoundingBox(char character, out PdfRectangle boundingBox)
{
return font.TryGetBoundingBox(character, out boundingBox);
}
public bool TryGetAdvanceWidth(char character, out decimal width)
{
return font.TryGetBoundingAdvancedWidth(character, out width);
}
public ObjectToken WriteFont(NameToken fontKeyName, Stream outputStream, BuilderContext context)
{
var bytes = fontFileBytes;
@@ -40,6 +49,8 @@
var baseFont = NameToken.Create(font.TableRegister.NameTable.GetPostscriptName());
var charCodeToGlyphId = new CharacterCodeToGlyphIdMapper(font);
var postscript = font.TableRegister.PostScriptTable;
var hhead = font.TableRegister.HorizontalHeaderTable;
@@ -56,10 +67,8 @@
{ NameToken.ItalicAngle, new NumericToken(postscript.ItalicAngle) },
{ NameToken.Ascent, new NumericToken(hhead.Ascender * scaling) },
{ NameToken.Descent, new NumericToken(hhead.Descender * scaling) },
// TODO: cap, x height, stem v
{ NameToken.CapHeight, new NumericToken(90) },
{ NameToken.StemV, new NumericToken(90) },
// TODO: font file 2
{ NameToken.FontFile2, new IndirectReferenceToken(fileRef.Number) }
};
@@ -77,22 +86,22 @@
descriptorDictionary[NameToken.StemV] = new NumericToken(bbox.Width * scaling * 0.13m);
var widths = font.TableRegister.GlyphTable.Glyphs.Select(x => new NumericToken(x.Bounds.Width)).ToArray();
var metrics = charCodeToGlyphId.GetMetrics();
var widthsRef = context.WriteObject(outputStream, new ArrayToken(widths));
var widthsRef = context.WriteObject(outputStream, metrics.Widths);
var descriptor = context.WriteObject(outputStream, new DictionaryToken(descriptorDictionary));
var dictionary = new Dictionary<NameToken, IToken>
{
{ NameToken.Type, NameToken.Font },
{ NameToken.Subtype, NameToken.TrueType },
{ NameToken.BaseFont, baseFont },
{ NameToken.FontDescriptor, new IndirectReferenceToken(descriptor.Number) },
{ NameToken.FirstChar, new NumericToken(0) },
{ NameToken.LastChar, new NumericToken(font.TableRegister.GlyphTable.Glyphs.Count - 1) },
{ NameToken.FirstChar, metrics.FirstChar },
{ NameToken.LastChar, metrics.LastChar },
{ NameToken.Widths, new IndirectReferenceToken(widthsRef.Number) },
{ NameToken.Encoding, NameToken.WinAnsiEncoding }
{ NameToken.Encoding, NameToken.MacRomanEncoding }
};
var token = new DictionaryToken(dictionary);
@@ -112,5 +121,91 @@
new NumericToken(boundingBox.Top * scaling)
});
}
private class CharacterCodeToGlyphIdMapper
{
private readonly TrueTypeFontProgram font;
private readonly ICMapSubTable cmapSubTable;
public CharacterCodeToGlyphIdMapper(TrueTypeFontProgram font)
{
var microsoftUnicode = font.TableRegister.CMapTable.SubTables.FirstOrDefault(x => x.PlatformId == TrueTypeCMapPlatform.Windows && x.EncodingId == 1);
cmapSubTable = microsoftUnicode ?? font.TableRegister.CMapTable.SubTables.FirstOrDefault(x => x.PlatformId == TrueTypeCMapPlatform.Macintosh && x.EncodingId == 0);
this.font = font ?? throw new ArgumentNullException(nameof(font));
}
public FontDictionaryMetrics GetMetrics()
{
var widths = font.TableRegister.HorizontalMetricsTable.AdvancedWidths;
var lastCharacter = 0;
var fullWidths = new List<NumericToken>();
switch (cmapSubTable)
{
case Format4CMapTable format4:
{
var firstCharacter = format4.Segments[0].StartCode;
var gid = format4.CharacterCodeToGlyphIndex(firstCharacter);
// Include unmapped character codes except for .notdef
firstCharacter -= gid - 1;
var widthIndex = 0;
var lastSegment = default(Format4CMapTable.Segment?);
for (var i = 0; i < format4.Segments.Count; i++)
{
var segment = format4.Segments[i];
if (segment.StartCode + segment.IdDelta >= 0xFFF)
{
break;
}
if (lastSegment.HasValue)
{
var endGlyph = lastSegment.Value.EndCode + lastSegment.Value.IdDelta;
var startGlyph = segment.StartCode + segment.IdDelta;
var gap = startGlyph - endGlyph - 1;
for (int j = 0; j < gap; j++)
{
fullWidths.Add(new NumericToken(0));
}
}
lastCharacter = segment.EndCode;
for (int j = 0; j < (segment.EndCode - segment.StartCode); j++)
{
var width = widths[widthIndex];
fullWidths.Add(new NumericToken(width));
widthIndex++;
}
lastSegment = segment;
}
return new FontDictionaryMetrics
{
Widths = new ArrayToken(fullWidths),
FirstChar = new NumericToken(firstCharacter),
LastChar = new NumericToken(lastCharacter)
};
}
case ByteEncodingCMapTable bytes:
default:
throw new NotSupportedException($"No dictionary mapping for format yet: {cmapSubTable.GetType().Name}.");
}
}
}
private class FontDictionaryMetrics
{
public ArrayToken Widths { get; set; }
public NumericToken FirstChar { get; set; }
public NumericToken LastChar { get; set; }
}
}
}