mirror of
https://github.com/UglyToad/PdfPig.git
synced 2025-10-14 19:05:01 +08:00
#21 further changes to truetype to get accurate information out for creating documents
This commit is contained in:
@@ -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.
|
||||
}
|
||||
}
|
||||
}
|
||||
|
106
src/UglyToad.PdfPig/Fonts/TrueType/Parser/CMapTableParser.cs
Normal file
106
src/UglyToad.PdfPig/Fonts/TrueType/Parser/CMapTableParser.cs
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
@@ -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);
|
||||
|
@@ -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))
|
||||
{
|
||||
|
@@ -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();
|
||||
|
@@ -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();
|
||||
|
@@ -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();
|
||||
|
@@ -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
|
||||
}
|
||||
}
|
@@ -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();
|
||||
|
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@@ -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];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@@ -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);
|
||||
}
|
||||
}
|
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
@@ -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>
|
||||
|
@@ -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; }
|
||||
}
|
||||
}
|
||||
}
|
Reference in New Issue
Block a user