* Forgot to add subsetter font

git-svn-id: trunk@35084 -
This commit is contained in:
michael 2016-12-09 13:22:48 +00:00
parent b7083402cf
commit 4406ec7e68
3 changed files with 995 additions and 0 deletions

1
.gitattributes vendored
View File

@ -2600,6 +2600,7 @@ packages/fcl-pdf/src/fpparsettf.pp svneol=native#text/plain
packages/fcl-pdf/src/fppdf.pp svneol=native#text/plain
packages/fcl-pdf/src/fpttf.pp svneol=native#text/plain
packages/fcl-pdf/src/fpttfencodings.pp svneol=native#text/plain
packages/fcl-pdf/src/fpttfsubsetter.pp svneol=native#text/plain
packages/fcl-pdf/tests/fonts/README.txt svneol=native#text/plain
packages/fcl-pdf/tests/fpparsettf_test.pas svneol=native#text/plain
packages/fcl-pdf/tests/fppdf_test.pas svneol=native#text/plain

View File

@ -35,11 +35,15 @@ begin
T:=P.Targets.AddUnit('src/fpparsettf.pp');
With T do
Dependencies.AddUnit('fpttfencodings');
T:=P.Targets.AddUnit('src/fpttfsubsetter.pp');
With T do
Dependencies.AddUnit('fpparsettf');
T:=P.Targets.AddUnit('src/fpttf.pp');
T:=P.Targets.AddUnit('src/fppdf.pp');
With T do
begin
Dependencies.AddUnit('fpparsettf');
Dependencies.AddUnit('fpttfsubsetter');
end;
// md5.ref

View File

@ -0,0 +1,990 @@
{
This file is part of the Free Component Library (FCL)
Copyright (c) 2016 by Graeme Geldenhuys
This unit creates a new TTF subset font file, reducing the file
size in the process. This is primarily so the new font file can
be embedded in PDF documents.
See the file COPYING.FPC, included in this distribution,
for details about the copyright.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
**********************************************************************}
unit fpTTFSubsetter;
{$mode objfpc}{$H+}
{ $R+}
// enable this define for more verbose output
{.$define gdebug}
interface
uses
Classes,
SysUtils,
fpparsettf,
FPFontTextMapping;
type
ETTFSubsetter = class(Exception);
TArrayUInt32 = array of UInt32;
TFontSubsetter = class(TObject)
private
FPrefix: string;
FHasAddedCompoundReferences: boolean; // one glyph made up of multiple glyphs
FKeepTables: TStrings;
FFontInfo: TTFFileInfo;
FGlyphIDList: TTextMappingList;
FStream: TFileStream; // original TTF file
FGlyphLocations: array of UInt32;
function Int32HighestOneBit(const AValue: integer): integer;
function Int32Log2(const AValue: integer): integer;
function ToUInt32(const AHigh, ALow: UInt32): UInt32;
function ToUInt32(const ABytes: AnsiString): UInt32;
function GetRawTable(const ATableName: AnsiString): TMemoryStream;
function WriteFileHeader(AOutStream: TStream; const nTables: integer): uint32;
function WriteTableHeader(AOutStream: TStream; const ATag: AnsiString; const AOffset: UInt32; const AData: TStream): uint32;
procedure WriteTableBodies(AOutStream: TStream; const ATables: TStringList);
// AGlyphID is the original GlyphID in the original TTF file
function GetCharIDfromGlyphID(const AGlyphID: uint32): uint32;
{ Copy glyph data as-is for a specific glyphID. }
function GetRawGlyphData(const AGlyphID: UInt16): TMemoryStream;
procedure LoadLocations;
// Stream writing functions.
procedure WriteInt16(AStream: TStream; const AValue: Int16); inline;
procedure WriteUInt32(AStream: TStream; const AValue: UInt32); inline;
procedure WriteUInt16(AStream: TStream; const AValue: UInt16); inline;
function ReadInt16(AStream: TStream): Int16; inline;
function ReadUInt32(AStream: TStream): UInt32; inline;
function ReadUInt16(AStream: TStream): UInt16; inline;
procedure AddCompoundReferences;
function buildHeadTable: TStream;
function buildHheaTable: TStream;
function buildMaxpTable: TStream;
function buildFpgmTable: TStream;
function buildPrepTable: TStream;
function buildCvtTable: TStream;
function buildGlyfTable(var newOffsets: TArrayUInt32): TStream;
function buildLocaTable(var newOffsets: TArrayUInt32): TStream;
function buildCmapTable: TStream;
function buildHmtxTable: TStream;
public
constructor Create(const AFont: TTFFileInfo; const AGlyphIDList: TTextMappingList);
constructor Create(const AFont: TTFFileInfo);
destructor Destroy; override;
procedure SaveToFile(const AFileName: String);
procedure SaveToStream(const AStream: TStream);
// Add the given Unicode codepoint to the subset.
procedure Add(const ACodePoint: uint32);
// The prefix to add to the font's PostScript name.
property Prefix: string read FPrefix write FPrefix;
end;
implementation
uses
math;
resourcestring
rsErrFontInfoNotAssigned = 'FontInfo was not assigned';
rsErrFailedToReadFromStream = 'Failed to read from file stream';
rsErrCantFindFontFile = 'Can''t find the actual TTF font file.';
rsErrGlyphLocationsNotLoaded = 'Glyph Location data has not been loaded yet.';
const
PAD_BUF: array[ 1..3 ] of Byte = ( $0, $0, $0 );
{ TFontSubsetter }
{ The method simply returns the int value with a single one-bit, in the position
of the highest-order one-bit in the specified value, or zero if the specified
value is itself equal to zero. }
function TFontSubsetter.Int32HighestOneBit(const AValue: integer): integer;
var
i: integer;
begin
i := AValue;
i := i or (i shr 1);
i := i or (i shr 2);
i := i or (i shr 4);
i := i or (i shr 8);
i := i or (i shr 16);
// i := i or (i shr 32);
Result := i - (i shr 1);
end;
function TFontSubsetter.Int32Log2(const AValue: integer): integer;
begin
if AValue <= 0 then
raise Exception.Create('Illegal argument');
// Result := 31 - Integer.numberOfLeadingZeros(n);
Result := Floor(Log10(AValue) / Log10(2));
end;
function TFontSubsetter.ToUInt32(const AHigh, ALow: UInt32): UInt32;
begin
result := ((AHigh and $FFFF) shl 16) or (ALow and $FFFF);
end;
function TFontSubsetter.ToUInt32(const ABytes: AnsiString): UInt32;
var
b: array of Byte absolute ABytes;
begin
Result := (b[0] and $FF) shl 24
or (b[1] and $FF) shl 16
or (b[2] and $FF) shl 8
or (b[3] and $FF);
end;
function TFontSubsetter.GetRawTable(const ATableName: AnsiString): TMemoryStream;
var
lEntry: TTableDirectoryEntry;
begin
Result := nil;
FillMem(@lEntry, SizeOf(TTableDirectoryEntry), 0);
if not FFontInfo.GetTableDirEntry(ATableName, lEntry) then
Exit;
Result := TMemoryStream.Create;
FStream.Seek(lEntry.offset, soFromBeginning);
if Result.CopyFrom(FStream, lEntry.Length) <> lEntry.Length then
raise ETTF.Create('GetRawTable: ' + rsErrFailedToReadFromStream);
end;
{ AOutStream: the data output stream.
nTables: the number of font tables.
result: the file offset of the first TTF table to write. }
function TFontSubsetter.WriteFileHeader(AOutStream: TStream; const nTables: integer): uint32;
var
mask: integer;
searchRange: integer;
entrySelector: integer;
rangeShift: integer;
begin
WriteUInt32(AOutStream, $00010000);
WriteUInt16(AOutStream, nTables);
mask := Int32HighestOneBit(nTables);
searchRange := mask * 16;
WriteUInt16(AOutStream, searchRange);
entrySelector := Int32Log2(mask);
WriteUInt16(AOutStream, entrySelector);
rangeShift := 16 * nTables - searchRange;
WriteUInt16(AOutStream, rangeShift);
result := $00010000 + ToUInt32(nTables, searchRange) + ToUInt32(entrySelector, rangeShift);
end;
function TFontSubsetter.WriteTableHeader(AOutStream: TStream; const ATag: AnsiString; const AOffset: UInt32;
const AData: TStream): uint32;
var
checksum: UInt32;
n: integer;
lByte: Byte;
begin
AData.Position := 0;
checksum := 0;
for n := 0 to AData.Size-1 do
begin
lByte := AData.ReadByte;
checksum := checksum + (((lByte and $FF) shl 24) - n mod 4 * 8);
end;
checksum := checksum and $FFFFFFFF;
AOutStream.WriteBuffer(Pointer(ATag)^, 4); // Tag is always 4 bytes - written as-is, no NtoBE() required
WriteUInt32(AOutStream, checksum);
WriteUInt32(AOutStream, AOffset);
WriteUInt32(AOutStream, AData.Size);
{$ifdef gdebug}
writeln(Format('tag: "%s" CRC: %8.8x offset: %8.8x (%2:7d bytes) size: %8.8x (%3:7d bytes)', [ATag, checksum, AOffset, AData.Size]));
{$endif}
// account for the checksum twice, once for the header field, once for the content itself
Result := ToUInt32(ATag) + checksum + checksum + AOffset + AData.Size;
end;
procedure TFontSubsetter.WriteTableBodies(AOutStream: TStream; const ATables: TStringList);
var
i: integer;
n: uint64;
lData: TStream;
begin
for i := 0 to ATables.Count-1 do
begin
lData := TStream(ATables.Objects[i]);
if lData <> nil then
begin
lData.Position := 0;
n := lData.Size;
AOutStream.CopyFrom(lData, lData.Size);
end;
if (n mod 4) <> 0 then
begin
{$ifdef gdebug}
writeln('Padding applied at the end of ', ATables[i], ': ', 4 - (n mod 4), ' byte(s)');
{$endif}
AOutStream.WriteBuffer(PAD_BUF, 4 - (n mod 4));
end;
end;
end;
function TFontSubsetter.GetCharIDfromGlyphID(const AGlyphID: uint32): uint32;
var
i: integer;
begin
Result := 0;
for i := 0 to Length(FFontInfo.Chars)-1 do
if FFontInfo.Chars[i] = AGlyphID then
begin
Result := i;
Exit;
end;
end;
function TFontSubsetter.GetRawGlyphData(const AGlyphID: UInt16): TMemoryStream;
var
lGlyf: TTableDirectoryEntry;
lSize: UInt16;
begin
Result := nil;
if Length(FGlyphLocations) < 2 then
raise ETTF.Create(rsErrGlyphLocationsNotLoaded);
FillMem(@lGlyf, SizeOf(TTableDirectoryEntry), 0);
FFontInfo.GetTableDirEntry(TTFTableNames[ttglyf], lGlyf);
lSize := FGlyphLocations[AGlyphID+1] - FGlyphLocations[AGlyphID];
Result := TMemoryStream.Create;
if lSize > 0 then
begin
FStream.Seek(lGlyf.offset + FGlyphLocations[AGlyphID], soFromBeginning);
if Result.CopyFrom(FStream, lSize) <> lSize then
raise ETTF.Create('GetRawGlyphData: ' + rsErrFailedToReadFromStream)
else
Result.Position := 0;
end;
end;
procedure TFontSubsetter.LoadLocations;
var
lLocaEntry: TTableDirectoryEntry;
lGlyf: TTableDirectoryEntry;
ms: TMemoryStream;
numLocations: integer;
n: integer;
begin
FillMem(@lGlyf, SizeOf(TTableDirectoryEntry), 0);
FillMem(@lLocaEntry, SizeOf(TTableDirectoryEntry), 0);
FFontInfo.GetTableDirEntry(TTFTableNames[ttglyf], lGlyf);
if FFontInfo.GetTableDirEntry(TTFTableNames[ttloca], lLocaEntry) then
begin
ms := TMemoryStream.Create;
try
FStream.Seek(lLocaEntry.offset, soFromBeginning);
if ms.CopyFrom(FStream, lLocaEntry.Length) <> lLocaEntry.Length then
raise ETTF.Create('LoadLocations: ' + rsErrFailedToReadFromStream)
else
ms.Position := 0;
if FFontInfo.Head.IndexToLocFormat = 0 then
begin
// Short offsets
numLocations := lLocaEntry.Length shr 1;
{$IFDEF gDEBUG}
Writeln('Number of Glyph locations ( 16 bits offsets ): ', numLocations );
{$ENDIF}
SetLength(FGlyphLocations, numLocations);
for n := 0 to numLocations-1 do
FGlyphLocations[n] := BEtoN(ms.ReadWord) * 2;
end
else
begin
// Long offsets
numLocations := lLocaEntry.Length shr 2;
{$IFDEF gDEBUG}
Writeln('Number of Glyph locations ( 32 bits offsets ): ', numLocations );
{$ENDIF}
SetLength(FGlyphLocations, numLocations);
for n := 0 to numLocations-1 do
FGlyphLocations[n] := BEtoN(ms.ReadDWord);
end;
finally
ms.Free;
end;
end
else
begin
{$ifdef gDEBUG}
Writeln('WARNING: ''loca'' table is not found.');
{$endif}
end;
end;
procedure TFontSubsetter.WriteInt16(AStream: TStream; const AValue: Int16);
begin
AStream.WriteBuffer(NtoBE(AValue), 2);
end;
procedure TFontSubsetter.WriteUInt32(AStream: TStream; const AValue: UInt32);
begin
AStream.WriteDWord(NtoBE(AValue));
end;
procedure TFontSubsetter.WriteUInt16(AStream: TStream; const AValue: UInt16);
begin
AStream.WriteWord(NtoBE(AValue));
end;
function TFontSubsetter.ReadInt16(AStream: TStream): Int16;
begin
Result:=Int16(ReadUInt16(AStream));
end;
function TFontSubsetter.ReadUInt32(AStream: TStream): UInt32;
begin
Result:=0;
AStream.ReadBuffer(Result,SizeOf(Result));
Result:=BEtoN(Result);
end;
function TFontSubsetter.ReadUInt16(AStream: TStream): UInt16;
begin
Result:=0;
AStream.ReadBuffer(Result,SizeOf(Result));
Result:=BEtoN(Result);
end;
procedure TFontSubsetter.AddCompoundReferences;
var
GlyphIDsToAdd: TStringList;
n: integer;
gs: TMemoryStream;
buf: TGlyphHeader;
i: integer;
flags: uint16;
glyphIndex: uint16;
cid: uint16;
hasNested: boolean;
begin
if FhasAddedCompoundReferences then
Exit;
FhasAddedCompoundReferences := True;
LoadLocations;
repeat
GlyphIDsToAdd := TStringList.Create;
GlyphIDsToAdd.Duplicates := dupIgnore;
GlyphIDsToAdd.Sorted := True;
for n := 0 to FGlyphIDList.Count-1 do
begin
if not Assigned(FGlyphIDList[n].GlyphData) then
FGlyphIDList[n].GlyphData := GetRawGlyphData(FGlyphIDList[n].GlyphID);
gs := TMemoryStream(FGlyphIDList[n].GlyphData);
gs.Position := 0;
if gs.Size > 0 then
begin
FillMem(@buf, SizeOf(TGlyphHeader), 0);
gs.ReadBuffer(buf, SizeOf(Buf));
if buf.numberOfContours = -1 then
begin
FGlyphIDList[n].IsCompoundGlyph := True;
{$IFDEF gDEBUG}
writeln('char: ', IntToHex(FGlyphIDList[n].CharID, 4));
writeln(' glyph data size: ', gs.Size);
writeln(' numberOfContours: ', buf.numberOfContours);
{$ENDIF}
repeat
flags := ReadUInt16(gs);
glyphIndex := ReadUInt16(gs);
// find compound glyph ID's and add them to the GlyphIDsToAdd list
if not FGlyphIDList.Contains(glyphIndex) then
begin
{$IFDEF gDEBUG}
writeln(Format(' glyphIndex: %.4x (%0:d) ', [glyphIndex]));
{$ENDIF}
GlyphIDsToAdd.Add(IntToStr(glyphIndex));
end;
// ARG_1_AND_2_ARE_WORDS
if (flags and (1 shl 0)) <> 0 then
ReadUInt32(gs)
else
ReadUInt16(gs);
// WE_HAVE_A_TWO_BY_TWO
if (flags and (1 shl 7)) <> 0 then
begin
ReadUInt32(gs);
ReadUInt32(gs);
end
// WE_HAVE_AN_X_AND_Y_SCALE
else if (flags and (1 shl 6)) <> 0 then
begin
ReadUInt32(gs);
end
// WE_HAVE_A_SCALE
else if (flags and (1 shl 3)) <> 0 then
begin
ReadUInt16(gs);
end;
until (flags and (1 shl 5)) = 0; // MORE_COMPONENTS
end; { if buf.numberOfContours = -1 }
end; { if gs.Size > 0 }
end; { for n ... FGlyphIDList.Count-1 }
if GlyphIDsToAdd.Count > 0 then
begin
for i := 0 to GlyphIDsToAdd.Count-1 do
begin
glyphIndex := StrToInt(GlyphIDsToAdd[i]);
cid := GetCharIDfromGlyphID(glyphIndex); // lookup original charID
FGlyphIDList.Add(cid, glyphIndex);
end;
end;
hasNested := GlyphIDsToAdd.Count > 0;
FreeAndNil(GlyphIDsToAdd);
until (hasNested = false);
end;
function TFontSubsetter.buildHeadTable: TStream;
var
t: THead;
rec: THead;
i: Integer;
begin
Result := TMemoryStream.Create;
t := FFontInfo.Head;
FillMem(@rec, SizeOf(THead), 0);
rec.FileVersion.Version := NtoBE(t.FileVersion.Version);
rec.FontRevision.Version := NtoBE(t.FontRevision.Version);
rec.CheckSumAdjustment := 0;
rec.MagicNumber := NtoBE(t.MagicNumber);
rec.Flags := NtoBE(t.Flags);
rec.UnitsPerEm := NtoBE(t.UnitsPerEm);
rec.Created := NtoBE(t.Created);
rec.Modified := NtoBE(t.Modified);
For i := 0 to 3 do
rec.BBox[i] := NtoBE(t.BBox[i]);
rec.MacStyle := NtoBE(t.MacStyle);
rec.LowestRecPPEM := NtoBE(t.LowestRecPPEM);
rec.FontDirectionHint := NtoBE(t.FontDirectionHint);
// force long format of 'loca' table. ie: 'loca' table offsets are in 4-Bytes each, not Words.
rec.IndexToLocFormat := NtoBE(Int16(1)); //NtoBE(t.IndexToLocFormat);
rec.glyphDataFormat := NtoBE(t.glyphDataFormat);
Result.WriteBuffer(rec, SizeOf(THead));
end;
function TFontSubsetter.buildHheaTable: TStream;
var
t: THHead;
rec: THHead;
hmetrics: UInt16;
begin
Result := TMemoryStream.Create;
t := FFontInfo.HHead;
FillMem(@rec, SizeOf(THHead), 0);
rec.TableVersion.Version := NtoBE(t.TableVersion.Version);
rec.Ascender := NtoBE(t.Ascender);
rec.Descender := NtoBE(t.Descender);
rec.LineGap := NtoBE(t.LineGap);
rec.AdvanceWidthMax := NtoBE(t.AdvanceWidthMax);
rec.MinLeftSideBearing := NtoBE(t.MinLeftSideBearing);
rec.MinRightSideBearing := NtoBE(t.MinRightSideBearing);
rec.XMaxExtent := NtoBE(t.XMaxExtent);
rec.CaretSlopeRise := NtoBE(t.CaretSlopeRise);
rec.CaretSlopeRun := NtoBE(t.CaretSlopeRun);
rec.caretOffset := NtoBE(t.caretOffset);
rec.metricDataFormat := NtoBE(t.metricDataFormat);
// rec.numberOfHMetrics := NtoBE(t.numberOfHMetrics);
hmetrics := FGlyphIDList.Count;
if (FGlyphIDList.Items[FGlyphIDList.Count-1].GlyphID >= t.numberOfHMetrics) and (not FGlyphIDList.Contains(t.numberOfHMetrics-1)) then
inc(hmetrics);
rec.numberOfHMetrics := NtoBE(hmetrics);
Result.WriteBuffer(rec, SizeOf(THHead));
end;
function TFontSubsetter.buildMaxpTable: TStream;
var
t: TMaxP;
rec: TMaxP;
lCount: word;
begin
Result := TMemoryStream.Create;
t := FFontInfo.MaxP;
FillMem(@rec, SizeOf(TMaxP), 0);
rec.VersionNumber.Version := NtoBE(t.VersionNumber.Version);
lCount := FGlyphIDList.Count;
rec.numGlyphs := NtoBE(lCount);
rec.maxPoints := NtoBE(t.maxPoints);
rec.maxContours := NtoBE(t.maxContours);
rec.maxCompositePoints := NtoBE(t.maxCompositePoints);
rec.maxCompositeContours := NtoBE(t.maxCompositeContours);
rec.maxZones := NtoBE(t.maxZones);
rec.maxTwilightPoints := NtoBE(t.maxTwilightPoints);
rec.maxStorage := NtoBE(t.maxStorage);
rec.maxFunctionDefs := NtoBE(t.maxFunctionDefs);
rec.maxInstructionDefs := NtoBE(t.maxInstructionDefs);
rec.maxStackElements := NtoBE(t.maxStackElements);
rec.maxSizeOfInstructions := NtoBE(t.maxSizeOfInstructions);
rec.maxComponentElements := NtoBE(t.maxComponentElements);
rec.maxComponentDepth := NtoBE(t.maxComponentDepth);
Result.WriteBuffer(rec, SizeOf(TMaxP));
end;
function TFontSubsetter.buildFpgmTable: TStream;
begin
Result := GetRawTable('fpgm');
Result.Position := 0;
end;
function TFontSubsetter.buildPrepTable: TStream;
begin
Result := GetRawTable('prep');
Result.Position := 0;
end;
function TFontSubsetter.buildCvtTable: TStream;
begin
Result := GetRawTable('cvt ');
Result.Position := 0;
end;
function TFontSubsetter.buildGlyfTable(var newOffsets: TArrayUInt32): TStream;
var
n: integer;
lOffset: uint32;
lLen: uint32;
gs: TMemoryStream;
buf: TGlyphHeader;
flags: uint16;
glyphIndex: uint16;
begin
lOffset := 0;
Result := TMemoryStream.Create;
LoadLocations;
{ - Assign new glyph indexes
- Retrieve glyph data in it doesn't yet exist (retrieved from original TTF file)
- Now fix GlyphID references in Compound Glyphs to point to new GlyphIDs }
for n := 0 to FGlyphIDList.Count-1 do
begin
FGlyphIDList[n].NewGlyphID := n;
if not Assigned(FGlyphIDList[n].GlyphData) then
FGlyphIDList[n].GlyphData := GetRawGlyphData(FGlyphIDList[n].GlyphID);
if not FGlyphIDList[n].IsCompoundGlyph then
Continue;
{$IFDEF gDEBUG}
writeln(Format('found compound glyph: %.4x glyphID: %d', [FGlyphIDList[n].CharID, FGlyphIDList[n].GlyphID]));
{$ENDIF}
gs := TMemoryStream(FGlyphIDList[n].GlyphData);
gs.Position := 0;
if gs.Size > 0 then
begin
FillMem(@buf, SizeOf(TGlyphHeader), 0);
gs.ReadBuffer(buf, SizeOf(Buf));
if buf.numberOfContours = -1 then
begin
repeat
flags := ReadUInt16(gs);
lOffset := gs.Position;
glyphIndex := ReadUInt16(gs);
// now write new GlyphID in it's place.
gs.Position := lOffset;
glyphIndex := FGlyphIDList.GetNewGlyphID(GetCharIDfromGlyphID(glyphIndex));
WriteUInt16(gs, glyphIndex);
// ARG_1_AND_2_ARE_WORDS
if (flags and (1 shl 0)) <> 0 then
ReadUInt32(gs)
else
ReadUInt16(gs);
// WE_HAVE_A_TWO_BY_TWO
if (flags and (1 shl 7)) <> 0 then
begin
ReadUInt32(gs);
ReadUInt32(gs);
end
// WE_HAVE_AN_X_AND_Y_SCALE
else if (flags and (1 shl 6)) <> 0 then
begin
ReadUInt32(gs);
end
// WE_HAVE_A_SCALE
else if (flags and (1 shl 3)) <> 0 then
begin
ReadUInt16(gs);
end;
until (flags and (1 shl 5)) = 0; // MORE_COMPONENTS
end; { if buf.numberOfContours = -1 }
end; { if gs.Size > 0 }
end; { for n ... FGlyphIDList.Count-1 }
// write all glyph data to resulting data stream
lOffset := 0;
for n := 0 to FGlyphIDList.Count-1 do
begin
newOffsets[n] := lOffset;
lOffset := lOffset + FGlyphIDList[n].GlyphData.Size;
FGlyphIDList[n].GlyphData.Position := 0;
Result.CopyFrom(FGlyphIDList[n].GlyphData, FGlyphIDList[n].GlyphData.Size);
// 4-byte alignment
if (lOffset mod 4) <> 0 then
begin
lLen := 4 - (lOffset mod 4);
Result.WriteBuffer(PAD_BUF, lLen);
lOffset := lOffset + lLen;
end;
end;
newOffsets[n+1] := lOffset;
end;
// write as UInt32 as defined in head.indexToLocFormat field (long format).
function TFontSubsetter.buildLocaTable(var newOffsets: TArrayUInt32): TStream;
var
i: integer;
begin
Result := TMemoryStream.Create;
for i := 0 to Length(newOffsets)-1 do
WriteUInt32(Result, newOffsets[i]);
end;
function TFontSubsetter.buildCmapTable: TStream;
const
// platform
PLATFORM_UNICODE = 0;
PLATFORM_MACINTOSH = 1;
// value 2 is reserved; do not use
PLATFORM_WINDOWS = 3;
// Mac encodings
ENCODING_MAC_ROMAN = 0;
// Windows encodings
ENCODING_WIN_SYMBOL = 0; // Unicode, non-standard character set
ENCODING_WIN_UNICODE_BMP = 1; // Unicode BMP (UCS-2)
ENCODING_WIN_SHIFT_JIS = 2;
ENCODING_WIN_BIG5 = 3;
ENCODING_WIN_PRC = 4;
ENCODING_WIN_WANSUNG = 5;
ENCODING_WIN_JOHAB = 6;
ENCODING_WIN_UNICODE_FULL = 10; // Unicode Full (UCS-4)
// Unicode encodings
ENCODING_UNICODE_1_0 = 0;
ENCODING_UNICODE_1_1 = 1;
ENCODING_UNICODE_2_0_BMP = 3;
ENCODING_UNICODE_2_0_FULL = 4;
var
segCount: UInt16;
searchRange: UInt16;
i: integer;
startCode: Array of Integer;
endCode: Array of Integer;
idDelta: Array of Integer;
lastChar: integer;
prevChar: integer;
lastGid: integer;
itm: TTextMapping;
begin
Result := TMemoryStream.Create;
SetLength(startCode, FGlyphIDList.Count);
SetLength(endCode, FGlyphIDList.Count);
SetLength(idDelta, FGlyphIDList.Count);
// cmap header
WriteUInt16(Result, 0); // version
WriteUInt16(Result, 1); // numberSubTables
// encoding record
WriteUInt16(Result, PLATFORM_WINDOWS); // platformID
WriteUInt16(Result, ENCODING_WIN_UNICODE_BMP); // platformSpecificID
WriteUInt32(Result, 4 * 2 + 4); // offset
// build Format 4 subtable (Unicode BMP)
lastChar := 0;
prevChar := lastChar;
lastGid := FGlyphIDList[0].NewGlyphID;
segCount := 0;
for i := 0 to FGlyphIDList.Count-1 do
begin
itm := FGlyphIDList[i];
if itm.CharID > $FFFF then
raise Exception.Create('non-BMP Unicode character');
if (itm.CharID <> FGlyphIDList[prevChar].CharID+1) or ((itm.NewGlyphID - lastGid) <> (itm.CharID - FGlyphIDList[lastChar].CharID)) then
begin
if (lastGid <> 0) then
begin
{ don't emit ranges, which map to GID 0, the undef glyph is emitted at the very last segment }
startCode[segCount] := FGlyphIDList[lastChar].CharID;
endCode[segCount] := FGlyphIDList[prevChar].CharID;
idDelta[segCount] := lastGid - FGlyphIDList[lastChar].CharID;
inc(segCount);
end
else if not (FGlyphIDList[lastChar].CharID = FGlyphIDList[prevChar].CharID) then
begin
{ shorten ranges which start with GID 0 by one }
startCode[segCount] := FGlyphIDList[lastChar].CharID + 1;
endCode[segCount] := FGlyphIDList[prevChar].CharID;
idDelta[segCount] := lastGid - FGlyphIDList[lastChar].CharID;
inc(segCount);
end;
lastGid := itm.NewGlyphID;
lastChar := i;
end;
prevChar := i;
end;
// trailing segment
startCode[segCount] := FGlyphIDList[lastChar].CharID;
endCode[segCount] := FGlyphIDList[prevChar].CharID;
idDelta[segCount] := lastGid - FGlyphIDList[lastChar].CharID;
inc(segCount);
// GID 0
startCode[segCount] := $FFFF;
endCode[segCount] := $FFFF;
idDelta[segCount] := 1;
inc(segCount);
// write format 4 subtable
searchRange := trunc(2 * Power(2, Floor(Log2(segCount))));
WriteUInt16(Result, 4); // format
WriteUInt16(Result, 8 * 2 + segCount * 4*2); // length
WriteUInt16(Result, 0); // language
WriteUInt16(Result, segCount * 2); // segCountX2
WriteUInt16(Result, searchRange); // searchRange
WriteUInt16(Result, trunc(log2(searchRange / 2))); // entrySelector
WriteUInt16(Result, 2 * segCount - searchRange); // rangeShift
// write endCode
for i := 0 to segCount-1 do
WriteUInt16(Result, endCode[i]);
// reservedPad
WriteUInt16(Result, 0);
// startCode
for i := 0 to segCount-1 do
WriteUInt16(Result, startCode[i]);
// idDelta
for i := 0 to segCount-1 do
WriteUInt16(Result, idDelta[i]);
// idRangeOffset
for i := 0 to segCount-1 do
WriteUInt16(Result, 0);
end;
function TFontSubsetter.buildHmtxTable: TStream;
var
n: integer;
begin
Result := TMemoryStream.Create;
for n := 0 to FGlyphIDList.Count-1 do
begin
WriteUInt16(Result, FFontInfo.Widths[FGlyphIDList[n].GlyphID].AdvanceWidth);
WriteInt16(Result, FFontInfo.Widths[FGlyphIDList[n].GlyphID].LSB);
end;
end;
constructor TFontSubsetter.Create(const AFont: TTFFileInfo; const AGlyphIDList: TTextMappingList);
begin
FFontInfo := AFont;
if not Assigned(FFontInfo) then
raise ETTFSubsetter.Create(rsErrFontInfoNotAssigned);
FGlyphIDList := AGlyphIDList;
FKeepTables := TStringList.Create;
FHasAddedCompoundReferences := False;
FPrefix := '';
FhasAddedCompoundReferences := False;
// create a default list
FKeepTables.Add('head');
FKeepTables.Add('hhea');
FKeepTables.Add('maxp');
FKeepTables.Add('hmtx');
FKeepTables.Add('cmap');
FKeepTables.Add('fpgm');
FKeepTables.Add('prep');
FKeepTables.Add('cvt ');
FKeepTables.Add('loca');
FKeepTables.Add('glyf');
if Assigned(FGlyphIDList) then
FGlyphIDList.Sort;
if FFontInfo.Filename <> '' then
FStream := TFileStream.Create(FFontInfo.FileName, fmOpenRead or fmShareDenyNone)
else
raise ETTF.Create(rsErrCantFindFontFile);
end;
constructor TFontSubsetter.Create(const AFont: TTFFileInfo);
begin
Create(AFont, nil);
end;
destructor TFontSubsetter.Destroy;
var
i: integer;
begin
// the owner of FGlyphIDList doesn't need the GlyphData information
for i := 0 to FGlyphIDList.Count-1 do
FGlyphIDList[i].GlyphData.Free;
FStream.Free;
FKeepTables.Free;
inherited Destroy;
end;
procedure TFontSubsetter.SaveToFile(const AFileName: String);
var
fs: TFileStream;
begin
fs := TFileStream.Create(AFileName, fmCreate);
try
SaveToStream(fs);
finally
FreeAndNil(fs);
end;
end;
procedure TFontSubsetter.SaveToStream(const AStream: TStream);
var
checksum: uint64;
offset: uint64;
head: TStream;
hhea: TStream;
maxp: TStream;
hmtx: TStream;
cmap: TStream;
fpgm: TStream;
prep: TStream;
cvt: TStream;
loca: TStream;
glyf: TStream;
newLoca: TArrayUInt32;
tables: TStringList;
i: integer;
o: uint64;
p: uint64;
lPadding: byte;
begin
// resolve compound glyph references
AddCompoundReferences;
// always copy GID 0
FGlyphIDList.Add(0, 0);
FGlyphIDList.Sort;
SetLength(newLoca, FGlyphIDList.Count+1);
head := buildHeadTable(); // done
hhea := buildHheaTable(); // done
maxp := buildMaxpTable(); // done
fpgm := buildFpgmTable(); // done
prep := buildPrepTable(); // done
cvt := buildCvtTable(); // done
glyf := buildGlyfTable(newLoca); // done
loca := buildLocaTable(newLoca); // done
cmap := buildCmapTable();
hmtx := buildHmtxTable();
tables := TStringList.Create;
tables.CaseSensitive := True;
if Assigned(cmap) then
tables.AddObject('cmap', cmap);
if Assigned(glyf) then
tables.AddObject('glyf', glyf);
tables.AddObject('head', head);
tables.AddObject('hhea', hhea);
tables.AddObject('hmtx', hmtx);
if Assigned(loca) then
tables.AddObject('loca', loca);
tables.AddObject('maxp', maxp);
tables.AddObject('fpgm', fpgm);
tables.AddObject('prep', prep);
tables.AddObject('cvt ', cvt);
tables.Sort;
// calculate checksum
checksum := writeFileHeader(AStream, tables.Count);
offset := 12 + (16 * tables.Count);
lPadding := 0;
for i := 0 to tables.Count-1 do
begin
if tables.Objects[i] <> nil then
begin
checksum := checksum + WriteTableHeader(AStream, tables.Strings[i], offset, TStream(tables.Objects[i]));
p := TStream(tables.Objects[i]).Size;
// table bodies must be 4-byte aligned - calculate the padding so the tableHeader.Offset field can reflect that.
if (p mod 4) = 0 then
lPadding := 0
else
lPadding := 4 - (p mod 4);
o := p + lPadding;
offset := offset + o;
end;
end;
checksum := $B1B0AFBA - (checksum and $ffffffff);
// update head.ChecksumAdjustment field
head.Seek(8, soBeginning);
WriteUInt32(head, checksum);
// write table bodies
WriteTableBodies(AStream, tables);
for i := 0 to tables.Count-1 do
TStream(tables.Objects[i]).Free;
tables.Free;
end;
procedure TFontSubsetter.Add(const ACodePoint: uint32);
var
gid: uint32;
begin
gid := FFontInfo.Chars[ACodePoint];
if gid <> 0 then
FGlyphIDList.Add(ACodePoint, FFontInfo.Chars[ACodePoint]);
end;
end.