{* UltraStar Deluxe - Karaoke Game
*
* UltraStar Deluxe is the legal property of its developers, whose names
* are too numerous to list here. Please refer to the COPYRIGHT
* file distributed with this source distribution.
*
* This program is free software; you can redistribute it and/or
* modify it under the terms of the GNU General Public License
* as published by the Free Software Foundation; either version 2
* of the License, or (at your option) any later version.
*
* 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. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program; see the file COPYING. If not, write to
* the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor,
* Boston, MA 02110-1301, USA.
*
* $URL$
* $Id$
*}
unit UFont;
{$IFDEF FPC}
{$MODE Delphi}
{$ENDIF}
{$I switches.inc}
interface
{$IFNDEF FREETYPE_DEMO}
// Flip direction of y-axis.
// Default is a cartesian coordinate system with y-axis in upper direction
// but with USDX the y-axis is in lower direction.
{$DEFINE FLIP_YAXIS}
{$DEFINE BITMAP_FONT}
{$ENDIF}
// Enables the Freetype font cache
{$DEFINE ENABLE_FT_FACE_CACHE}
uses
FreeType,
gl,
glext,
glu,
sdl,
Math,
Classes,
SysUtils,
UUnicodeUtils,
{$IFDEF BITMAP_FONT}
UTexture,
{$ENDIF}
UPath;
type
PGLubyteArray = ^TGLubyteArray;
TGLubyteArray = array[0 .. (MaxInt div SizeOf(GLubyte))-1] of GLubyte;
TGLubyteDynArray = array of GLubyte;
TUCS4StringArray = array of UCS4String;
TGLColor = packed record
case byte of
0: ( vals: array[0..3] of GLfloat; );
1: ( r, g, b, a: GLfloat; );
end;
TBoundsDbl = record
Left, Right: double;
Bottom, Top: double;
end;
TPositionDbl = record
X, Y: double;
end;
TTextureSize = record
Width, Height: integer;
end;
TBitmapCoords = record
Left, Top: double;
Width, Height: integer;
end;
EFontError = class(Exception);
{**
* Abstract base class representing a glyph.
*}
TGlyph = class
protected
function GetAdvance(): TPositionDbl; virtual; abstract;
function GetBounds(): TBoundsDbl; virtual; abstract;
public
procedure Render(UseDisplayLists: boolean); virtual; abstract;
procedure RenderReflection(); virtual; abstract;
{** Distance to next glyph (in pixels) *}
property Advance: TPositionDbl read GetAdvance;
{** Glyph bounding box (in pixels) *}
property Bounds: TBoundsDbl read GetBounds;
end;
{**
* Font styles used by TFont.Style
*}
TFontStyle = set of (Italic, Underline, Reflect);
{**
* Base font class.
*}
TFont = class
private
{** Non-virtual reset-method used in Create() and Reset() }
procedure ResetIntern();
protected
fFilename: IPath;
fStyle: TFontStyle;
fUseKerning: boolean;
fLineSpacing: single; // must be inited by subclass
fReflectionSpacing: single; // must be inited by subclass to -2*Descender
fGlyphSpacing: single;
fReflectionPass: boolean;
{**
* Splits lines in Text seperated by newline (char-code #13).
* @param Text UCS-4 encoded string
* @param Lines splitted UCS4String lines
*}
procedure SplitLines(const Text: UCS4String; var Lines: TUCS4StringArray);
{**
* Print an array of UCS4Strings. Each array-item is a line of text.
* Lines of text are seperated by the line-spacing.
* This is the base function for all text drawing.
*}
procedure Print(const Text: TUCS4StringArray); overload; virtual;
{**
* Draws an underline.
*}
procedure DrawUnderline(const Text: UCS4String); virtual;
{**
* Renders (one) line of text.
*}
procedure Render(const Text: UCS4String); virtual; abstract;
{**
* Returns the bounds of text-lines contained in Text.
* @param(Advance if true the right bound is set to the advance instead
* of the minimal right bound.)
*}
function BBox(const Text: TUCS4StringArray; Advance: boolean): TBoundsDbl; overload; virtual; abstract;
{**
* Resets all user settings to default values.
* Override methods should always call the inherited version.
*}
procedure Reset(); virtual;
function GetHeight(): single; virtual; abstract;
function GetAscender(): single; virtual; abstract;
function GetDescender(): single; virtual; abstract;
procedure SetLineSpacing(Spacing: single); virtual;
function GetLineSpacing(): single; virtual;
procedure SetGlyphSpacing(Spacing: single); virtual;
function GetGlyphSpacing(): single; virtual;
procedure SetReflectionSpacing(Spacing: single); virtual;
function GetReflectionSpacing(): single; virtual;
procedure SetStyle(Style: TFontStyle); virtual;
function GetStyle(): TFontStyle; virtual;
function GetUnderlinePosition(): single; virtual; abstract;
function GetUnderlineThickness(): single; virtual; abstract;
procedure SetUseKerning(Enable: boolean); virtual;
function GetUseKerning(): boolean; virtual;
procedure SetReflectionPass(Enable: boolean); virtual;
{** Returns true if the current render-pass is used to draw the reflection }
property ReflectionPass: boolean read fReflectionPass write SetReflectionPass;
public
constructor Create(const Filename: IPath);
destructor Destroy(); override;
{**
* Prints a text.
*}
procedure Print(const Text: UCS4String); overload;
{** UTF-16 version of @link(Print) }
procedure Print(const Text: WideString); overload;
{** UTF-8 version of @link(Print) }
procedure Print(const Text: UTF8String); overload;
{**
* Calculates the bounding box (width and height) around Text.
* Works with Italic and Underline styles but reflections created
* with the Reflect style are not considered.
* Note that the width might differ due to kerning with appended text,
* e.g. Width('VA') <= Width('V') + Width('A').
* @param Advance if set to true, Result.Right is set to the advance of
* the given text rather than the min. right border. The advance width is
* bigger than the text's width as it additionally contains the advance
* and glyph-spacing of the last character.
*}
function BBox(const Text: UCS4String; Advance: boolean = true): TBoundsDbl; overload;
{** UTF-16 version of @link(BBox) }
function BBox(const Text: WideString; Advance: boolean = true): TBoundsDbl; overload;
{** UTF-8 version of @link(BBox) }
function BBox(const Text: UTF8String; Advance: boolean = true): TBoundsDbl; overload;
{**
* Adds a new font that is used if the default font misses a glyph
* @raises EFontError if the fallback could not be initialized
*}
procedure AddFallback(const Filename: IPath); virtual; abstract;
{** Font height }
property Height: single read GetHeight;
{** Vertical distance from baseline to top of glyph }
property Ascender: single read GetAscender;
{** Vertical distance from baseline to bottom of glyph }
property Descender: single read GetDescender;
{** Vertical distance between two baselines }
property LineSpacing: single read GetLineSpacing write SetLineSpacing;
{** Space between end and start of next glyph added to the advance width }
property GlyphSpacing: single read GetGlyphSpacing write SetGlyphSpacing;
{** Distance between normal baseline and baseline of the reflection }
property ReflectionSpacing: single read GetReflectionSpacing write SetReflectionSpacing;
{** Font style (italic/underline/...) }
property Style: TFontStyle read GetStyle write SetStyle;
{** If set to true (default) kerning will be used if available }
property UseKerning: boolean read GetUseKerning write SetUseKerning;
{** Filename }
property Filename: IPath read fFilename;
end;
const
//** Max. number of mipmap levels that a TScalableFont can contain
cMaxMipmapLevel = 5;
type
{**
* Wrapper around TFont to allow font size changes.
* The font is scaled to the requested size by a modelview matrix
* transformation (glScale) and not by rescaling the internal bitmap
* representation. This way changing the size is really fast but the result
* may lack quality on large or small scale factors.
*}
TScalableFont = class(TFont)
private
procedure ResetIntern();
protected
fScale: single; //**< current height to base-font height ratio
fStretch: single; //**< stretch factor for width (Width * fStretch)
fBaseFont: TFont; //**< shortcut for fMipmapFonts[0]
fUseMipmaps: boolean; //**< true if mipmap fonts are generated
/// Mipmap fonts (size[level+1] = size[level]/2)
fMipmapFonts: array[0..cMaxMipmapLevel] of TFont;
procedure Render(const Text: UCS4String); override;
procedure Print(const Text: TUCS4StringArray); override;
function BBox(const Text: TUCS4StringArray; Advance: boolean): TBoundsDbl; override;
{**
* Callback called for creation of each mipmap font.
* Must be defined by the subclass.
* Mipmaps created by this method are managed and freed by TScalableFont.
*}
function CreateMipmap(Level: integer; Scale: single): TFont; virtual; abstract;
{**
* Returns the mipmap level considering the current scale and projection
* matrix.
*}
function GetMipmapLevel(): integer;
{**
* Returns the scale applied to the given mipmap font.
* fScale * fBaseFont.Height / fMipmapFont[Level].Height
*}
function GetMipmapScale(Level: integer): single;
{**
* Chooses the mipmap that looks nicest with current scale and projection
* matrix.
*}
function ChooseMipmapFont(): TFont;
procedure SetHeight(Height: single); virtual;
function GetHeight(): single; override;
procedure SetStretch(Stretch: single); virtual;
function GetStretch(): single; virtual;
function GetAscender(): single; override;
function GetDescender(): single; override;
procedure SetLineSpacing(Spacing: single); override;
function GetLineSpacing(): single; override;
procedure SetGlyphSpacing(Spacing: single); override;
function GetGlyphSpacing(): single; override;
procedure SetReflectionSpacing(Spacing: single); override;
function GetReflectionSpacing(): single; override;
procedure SetStyle(Style: TFontStyle); override;
function GetStyle(): TFontStyle; override;
function GetUnderlinePosition(): single; override;
function GetUnderlineThickness(): single; override;
procedure SetUseKerning(Enable: boolean); override;
public
{**
* Creates a wrapper to make the base-font Font scalable.
* If UseMipmaps is set to true smaller fonts are created so that a
* resized (Height property changed) font looks nicer.
* The font passed is managed and freed by TScalableFont.
*}
constructor Create(Font: TFont; UseMipmaps: boolean); overload;
{**
* Frees memory. The fonts passed on Create() and mipmap creation
* are freed too.
*}
destructor Destroy(); override;
{** @seealso TFont.Reset }
procedure Reset(); override;
{** Font height }
property Height: single read GetHeight write SetHeight;
{** Factor for font stretching (NewWidth = Width*Stretch), 1.0 by default }
property Stretch: single read GetStretch write SetStretch;
end;
{**
* Table for storage of max. 256 glyphs.
* Used for the second cache level. Indexed by the LSB of the UCS4Char
* char-code.
*}
PGlyphTable = ^TGlyphTable;
TGlyphTable = array[0..255] of TGlyph;
{**
* Cache for glyphs of a single font.
* The cached glyphs are stored inside a hash-list.
* Hashing is performed in two steps:
* 1. the least significant byte (LSB) of the UCS4Char character code
* is removed (shr 8) and the result (we call it BaseCode here) looked up in
* the hash-list.
* 2. Each entry of the hash-list contains a table with max. 256 entries.
* The LSB of the char-code of a glyph is the table-offset of that glyph.
*}
TGlyphCache = class
private
fHash: TList;
{**
* Finds a glyph-table storing cached glyphs with base-code BaseCode
* (= upper char-code bytes) in the hash-list and returns the table and
* its index.
* @param(InsertPos the position of the tyble in the list if it was found,
* otherwise the position the table should be inserted)
*}
function FindGlyphTable(BaseCode: cardinal; out InsertPos: integer): PGlyphTable;
public
constructor Create();
destructor Destroy(); override;
{**
* Add glyph Glyph with char-code ch to the cache.
* @returns @true on success, @false otherwise
*}
function AddGlyph(ch: UCS4Char; const Glyph: TGlyph): boolean;
{**
* Removes the glyph with char-code ch from the cache.
*}
procedure DeleteGlyph(ch: UCS4Char);
{**
* Removes the glyph with char-code ch from the cache.
*}
function GetGlyph(ch: UCS4Char): TGlyph;
{**
* Checks if a glyph with char-code ch is cached.
*}
function HasGlyph(ch: UCS4Char): boolean;
{**
* Remove and free all cached glyphs. If KeepBaseSet is set to
* true, cached characters in the range 0..255 will not be flushed.
*}
procedure FlushCache(KeepBaseSet: boolean);
end;
{**
* Entry of a glyph-cache's (TGlyphCache) hash.
* Stores a BaseCode (upper-bytes of a glyph's char-code) and a table
* with all glyphs cached at the moment with that BaseCode.
*}
TGlyphCacheHashEntry = class
private
fBaseCode: cardinal;
public
GlyphTable: TGlyphTable;
constructor Create(BaseCode: cardinal);
{** Base-code (upper-bytes) of the glyphs stored in this entry's table }
property BaseCode: cardinal read fBaseCode;
end;
TCachedFont = class(TFont)
protected
fCache: TGlyphCache;
{**
* Retrieves a cached glyph with char-code ch from cache.
* If the glyph is not already cached, it is loaded with LoadGlyph().
*}
function GetGlyph(ch: UCS4Char): TGlyph;
{**
* Callback to create (load) a glyph with char-code ch.
* Implemented by subclasses.
*}
function LoadGlyph(ch: UCS4Char): TGlyph; virtual; abstract;
public
constructor Create(const Filename: IPath);
destructor Destroy(); override;
{**
* Remove and free all cached glyphs. If KeepBaseSet is set to
* true, the base glyphs are not be flushed.
* @seealso TGlyphCache.FlushCache
*}
procedure FlushCache(KeepBaseSet: boolean);
end;
TFTFont = class;
{**
* Freetype font face class.
*}
TFTFontFace = class
private
fFilename: IPath; //**< filename of the font-file
fFace: FT_Face; //**< Holds the height of the font
fFontUnitScale: TPositionDbl; //**< FT font-units to pixel ratio
fSize: integer;
public
{**
* @raises EFontError if the glyph could not be initialized
*}
constructor Create(const Filename: IPath; Size: integer);
destructor Destroy(); override;
property Filename: IPath read fFilename;
property Data: FT_Face read fFace;
property FontUnitScale: TPositionDbl read fFontUnitScale;
property Size: integer read fSize;
end;
{**
* Loading font faces with freetype is a slow process.
* Especially loading a font (e.g. fallback fonts) more than once is a waste
* of time. Just cache already loaded faces here.
*}
TFTFontFaceCache = class
private
fFaces: array of TFTFontFace;
fFacesRefCnt: array of integer;
public
{**
* @raises EFontError if the font could not be initialized
*}
function LoadFace(const Filename: IPath; Size: integer): TFTFontFace;
procedure UnloadFace(Face: TFTFontFace);
end;
{**
* Freetype glyph.
* Each glyph stores a texture with the glyph's image.
*}
TFTGlyph = class(TGlyph)
private
fCharCode: UCS4Char; //**< Char code
fFace: TFTFontFace; //**< Freetype face used for this glyph
fCharIndex: FT_UInt; //**< Freetype specific char-index (<> char-code)
fDisplayList: GLuint; //**< Display-list ID
fTexture: GLuint; //**< Texture ID
fBitmapCoords: TBitmapCoords; //**< Left/Top offset and Width/Height of the bitmap (in pixels)
fTexOffset: TPositionDbl; //**< Right and bottom texture offset for removal of power-of-2 padding
fTexSize: TTextureSize; //**< Texture size in pixels
fFont: TFTFont; //**< Font associated with this glyph
fAdvance: TPositionDbl; //**< Advance width of this glyph
fBounds: TBoundsDbl; //**< Glyph bounds
fOutset: single; //**< Extrusion outset
{**
* Extrudes the outline of a glyph's bitmap stored in TexBuffer with size
* fTexSize by Outset pixels.
* This is useful to create bold or outlined fonts.
* TexBuffer must be 2*Ceil(Outset) pixels higher and wider than the
* original glyph bitmap, otherwise the glyph borders cannot be extruded
* correctly.
* The bitmap must be 2* pixels wider and higher than the
* original glyph's bitmap with the latter centered in it.
*}
procedure StrokeBorder(var Glyph: FT_Glyph);
{**
* Creates an OpenGL texture (and display list) for the glyph.
* The glyph's and bitmap's metrics are set correspondingly.
* @param LoadFlags flags passed to FT_Load_Glyph()
* @raises EFontError if the glyph could not be initialized
*}
procedure CreateTexture(LoadFlags: FT_Int32);
protected
function GetAdvance(): TPositionDbl; override;
function GetBounds(): TBoundsDbl; override;
public
{**
* Creates a glyph with char-code ch from font Font.
* @param LoadFlags flags passed to FT_Load_Glyph()
*}
constructor Create(Font: TFTFont; ch: UCS4Char; Outset: single;
LoadFlags: FT_Int32);
destructor Destroy(); override;
{** Renders the glyph (normal render pass) }
procedure Render(UseDisplayLists: boolean); override;
{** Renders the glyph's reflection }
procedure RenderReflection(); override;
{** Freetype specific char-index (<> char-code) }
property CharIndex: FT_UInt read fCharIndex;
{** Freetype face used for this glyph }
property Face: TFTFontFace read fFace;
end;
TFontPart = ( fpNone, fpInner, fpOutline );
TFTFontFaceArray = array of TFTFontFace;
{**
* Freetype font class.
*}
TFTFont = class(TCachedFont)
private
procedure ResetIntern();
class function GetFaceCache(): TFTFontFaceCache;
protected
fFace: TFTFontFace; //**< Default font face
fSize: integer; //**< Font base size (in pixels)
fOutset: single; //**< size of outset extrusion (in pixels)
fPreCache: boolean; //**< pre-load base glyphs
fLoadFlags: FT_Int32; //**< FT glpyh load-flags
fUseDisplayLists: boolean; //**< true: use display-lists, false: direct drawing
fPart: TFontPart; //**< indicates the part of an outline font
fFallbackFaces: TFTFontFaceArray; //**< available fallback faces, ordered by priority
{** @seealso TCachedFont.LoadGlyph }
function LoadGlyph(ch: UCS4Char): TGlyph; override;
procedure Render(const Text: UCS4String); override;
function BBox(const Text: TUCS4StringArray; Advance: boolean): TBoundsDbl; override;
function GetHeight(): single; override;
function GetAscender(): single; override;
function GetDescender(): single; override;
function GetUnderlinePosition(): single; override;
function GetUnderlineThickness(): single; override;
public
{**
* Creates a font of size Size (in pixels) from the file Filename.
* If Outset (in pixels) is set to a value > 0 the glyphs will be extruded
* at their borders. Use it for e.g. a bold effect.
* @param LoadFlags flags passed to FT_Load_Glyph()
* @raises EFontError if the font-file could not be loaded
*}
constructor Create(const Filename: IPath;
Size: integer; Outset: single = 0.0;
PreCache: boolean = true;
LoadFlags: FT_Int32 = FT_LOAD_DEFAULT);
{**
* Frees all resources associated with the font.
*}
destructor Destroy(); override;
{** @seealso TFont.Reset }
procedure Reset(); override;
procedure AddFallback(const Filename: IPath); override;
{** Size of the base font }
property Size: integer read fSize;
{** Outset size }
property Outset: single read fOutset;
{** The part (inner/outline/none) this font represents in a composite font }
property Part: TFontPart read fPart write fPart;
{** Freetype face of this font }
property DefaultFace: TFTFontFace read fFace;
{** Available freetype fallback faces, ordered by priority }
property FallbackFaces: TFTFontFaceArray read fFallbackFaces;
end;
TFTScalableFont = class(TScalableFont)
protected
function GetOutset(): single; virtual;
function CreateMipmap(Level: integer; Scale: single): TFont; override;
public
{**
* Creates a scalable font of size Size (in pixels) from the file Filename.
* OutsetAmount is the ratio of the glyph extrusion.
* The extrusion in pixels is Size*OutsetAmount
* (0.0 -> no extrusion, 0.1 -> 10%).
*
* The memory size (in bytes) consumed by a scalable font
* - with UseMipmaps=false:
* mem = size^2 * #cached_glyphs
* - with UseMipmaps=true (all mipmap levels):
* mem = size^2 * #cached_glyphs * Sum[i=1..cMaxMipmapLevel](1/i^2)
* - with UseMipmaps=true (5 <= cMaxMipmapLevel <= 10):
* mem ~= size^2 * #cached_glyphs * 1.5
*
* Examples (for 128 cached glyphs):
* - Size: 64 pixels: 768 KB (mipmapped) or 512 KB (non-mipmapped).
* - Size 128 pixels: 3 MB (mipmapped) or 2 MB (non-mipmapped)
*
* Note: once a glyph is cached there will
*}
constructor Create(const Filename: IPath;
Size: integer; OutsetAmount: single = 0.0;
UseMipmaps: boolean = true;
PreCache: boolean = true);
procedure AddFallback(const Filename: IPath); override;
{** @seealso TGlyphCache.FlushCache }
procedure FlushCache(KeepBaseSet: boolean);
{** Outset size (in pixels) of the scaled font }
property Outset: single read GetOutset;
end;
{**
* Represents a freetype font with an additional outline around its glyphs.
* The outline size is passed on creation and cannot be changed later.
*}
TFTOutlineFont = class(TFont)
private
fSize: integer;
fOutset: single;
fInnerFont, fOutlineFont: TFTFont;
fOutlineColor: TGLColor;
fPreCache: boolean;
procedure ResetIntern();
protected
procedure DrawUnderline(const Text: UCS4String); override;
procedure Render(const Text: UCS4String); override;
function BBox(const Text: TUCS4StringArray; Advance: boolean): TBoundsDbl; override;
function GetHeight(): single; override;
function GetAscender(): single; override;
function GetDescender(): single; override;
procedure SetLineSpacing(Spacing: single); override;
procedure SetGlyphSpacing(Spacing: single); override;
procedure SetReflectionSpacing(Spacing: single); override;
procedure SetStyle(Style: TFontStyle); override;
function GetStyle(): TFontStyle; override;
function GetUnderlinePosition(): single; override;
function GetUnderlineThickness(): single; override;
procedure SetUseKerning(Enable: boolean); override;
procedure SetReflectionPass(Enable: boolean); override;
public
constructor Create(const Filename: IPath;
Size: integer; Outset: single;
PreCache: boolean = true;
LoadFlags: FT_Int32 = FT_LOAD_DEFAULT);
destructor Destroy; override;
{**
* Sets the color of the outline.
* If the alpha component is < 0, OpenGL's current alpha value will be
* used.
*}
procedure SetOutlineColor(r, g, b: GLfloat; a: GLfloat = -1.0);
{** @seealso TGlyphCache.FlushCache }
procedure FlushCache(KeepBaseSet: boolean);
procedure AddFallback(const Filename: IPath); override;
{** @seealso TFont.Reset }
procedure Reset(); override;
{** Size of the base font }
property Size: integer read fSize;
{** Outset size }
property Outset: single read fOutset;
end;
{**
* Wrapper around TOutlineFont to allow font resizing.
* @seealso TScalableFont
*}
TFTScalableOutlineFont = class(TScalableFont)
protected
function GetOutset(): single; virtual;
function CreateMipmap(Level: integer; Scale: single): TFont; override;
public
constructor Create(const Filename: IPath;
Size: integer; OutsetAmount: single;
UseMipmaps: boolean = true;
PreCache: boolean = true);
{** @seealso TFTOutlineFont.SetOutlineColor }
procedure SetOutlineColor(r, g, b: GLfloat; a: GLfloat = -1.0);
{** @seealso TGlyphCache.FlushCache }
procedure FlushCache(KeepBaseSet: boolean);
procedure AddFallback(const Filename: IPath); override;
{** Outset size }
property Outset: single read GetOutset;
end;
{$IFDEF BITMAP_FONT}
{**
* A bitmapped font loads it's glyphs from a bitmap and stores them in a
* texture. Unicode characters are not supported (but could be by supporting
* multiple textures each storing a subset of unicode glyphs).
* For backward compatibility only.
*}
TBitmapFont = class(TFont)
private
fTex: TTexture;
fTexSize: integer;
fBaseline: integer;
fAscender: integer;
fDescender: integer;
fWidths: array[0..255] of byte; //**< half widths
fOutline: integer;
fTempColor: TGLColor; //**< colours for the reflection
procedure ResetIntern();
procedure RenderChar(ch: UCS4Char; var AdvanceX: real);
{**
* Load font widths from an info file.
* @param InfoFile the name of the info (.dat) file
* @raises EFontError if the file is corrupted
*}
procedure LoadFontInfo(const InfoFile: IPath);
protected
procedure Render(const Text: UCS4String); override;
function BBox(const Text: TUCS4StringArray; Advance: boolean): TBoundsDbl; override;
function GetHeight(): single; override;
function GetAscender(): single; override;
function GetDescender(): single; override;
function GetUnderlinePosition(): single; override;
function GetUnderlineThickness(): single; override;
public
{**
* Creates a bitmapped font from image Filename and font width info
* loaded from the corresponding file with ending .dat.
* @param(Baseline y-coord of the baseline given in cartesian coords
* (y-axis up) and from the lower edge of the glyphs bounding box)
* @param(Ascender pixels from baseline to top of highest glyph)
*}
constructor Create(const Filename: IPath; Outline: integer;
Baseline, Ascender, Descender: integer);
destructor Destroy(); override;
{**
* Corrects font widths provided by the info file.
* NewWidth := Width * WidthMult + WidthAdd
*}
procedure CorrectWidths(WidthMult: real; WidthAdd: integer);
{** @seealso TFont.Reset }
procedure Reset(); override;
procedure AddFallback(const Filename: IPath); override;
end;
{$ENDIF BITMAP_FONT}
TFreeType = class
public
{**
* Returns a pointer to the freetype library singleton.
* If non exists, freetype will be initialized.
* @raises EFontError if initialization failed
*}
class function GetLibrary(): FT_Library;
class procedure FreeLibrary();
end;
implementation
uses Types;
const
//** shear factor used for the italic effect (bigger value -> more bending)
cShearFactor = 0.25;
cShearMatrix: array[0..15] of GLfloat = (
1, 0, 0, 0,
cShearFactor, 1, 0, 0,
0, 0, 1, 0,
0, 0, 0, 1
);
cShearMatrixInv: array[0..15] of GLfloat = (
1, 0, 0, 0,
-cShearFactor, 1, 0, 0,
0, 0, 1, 0,
0, 0, 0, 1
);
var
LibraryInst: FT_Library;
function NewGLColor(r, g, b, a: GLfloat): TGLColor;
begin
Result.r := r;
Result.g := g;
Result.b := b;
Result.a := a;
end;
{**
* Returns the first power of 2 >= Value.
*}
function NextPowerOf2(Value: integer): integer; {$IFDEF HasInline}inline;{$ENDIF}
begin
Result := 1;
while (Result < Value) do
Result := Result shl 1;
end;
{*
* TFont
*}
constructor TFont.Create(const Filename: IPath);
begin
inherited Create();
fFilename := Filename;
ResetIntern();
end;
destructor TFont.Destroy();
begin
inherited;
end;
procedure TFont.ResetIntern();
begin
fStyle := [];
fUseKerning := true;
fGlyphSpacing := 0.0;
fReflectionPass := false;
// must be set by subclasses
fLineSpacing := 0.0;
fReflectionSpacing := 0.0;
end;
procedure TFont.Reset();
begin
ResetIntern();
end;
procedure TFont.SplitLines(const Text: UCS4String; var Lines: TUCS4StringArray);
var
CharIndex: integer;
LineStart: integer;
LineLength: integer;
EOT: boolean; // End-Of-Text
begin
// split lines on newline
SetLength(Lines, 0);
EOT := false;
LineStart := 0;
for CharIndex := 0 to High(Text) do
begin
// check for end of text (UCS4Strings are zero-terminated)
if (CharIndex = High(Text)) then
EOT := true;
// check for newline (carriage return (#13)) or end of text
if (Text[CharIndex] = 13) or EOT then
begin
LineLength := CharIndex - LineStart;
// check if last character was a newline
if (EOT and (LineLength = 0)) then
Break;
// copy line (even if LineLength is 0)
SetLength(Lines, Length(Lines)+1);
Lines[High(Lines)] := UCS4Copy(Text, LineStart, LineLength);
LineStart := CharIndex+1;
end;
end;
end;
function TFont.BBox(const Text: UCS4String; Advance: boolean): TBoundsDbl;
var
LineArray: TUCS4StringArray;
begin
SplitLines(Text, LineArray);
Result := BBox(LineArray, Advance);
SetLength(LineArray, 0);
end;
function TFont.BBox(const Text: UTF8String; Advance: boolean): TBoundsDbl;
begin
Result := BBox(UTF8Decode(Text), Advance);
end;
function TFont.BBox(const Text: WideString; Advance: boolean): TBoundsDbl;
begin
Result := BBox(WideStringToUCS4String(Text), Advance);
end;
procedure TFont.Print(const Text: TUCS4StringArray);
var
LineIndex: integer;
begin
// recursively call this function to draw reflected text
if ((Reflect in Style) and not ReflectionPass) then
begin
ReflectionPass := true;
Print(Text);
ReflectionPass := false;
end;
// store current color, enable-flags, matrix-mode
glPushAttrib(GL_CURRENT_BIT or GL_ENABLE_BIT or GL_TRANSFORM_BIT);
// set OpenGL state
glMatrixMode(GL_MODELVIEW);
glDisable(GL_DEPTH_TEST);
glEnable(GL_BLEND);
glEnable(GL_TEXTURE_2D);
glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA);
{
// TODO: just draw texels with alpha > 0 to avoid setting z-buffer for them?
glAlphaFunc(GL_GREATER, 0);
glEnable(GL_ALPHA_TEST);
//TODO: Do we need depth-testing?
if (ReflectionPass) then
begin
glDepthMask(0);
glEnable(GL_DEPTH_TEST);
end;
}
{$IFDEF FLIP_YAXIS}
glPushMatrix();
glScalef(1, -1, 1);
{$ENDIF}
// display text
for LineIndex := 0 to High(Text) do
begin
glPushMatrix();
// move to baseline
glTranslatef(0, -LineSpacing*LineIndex, 0);
if ((Underline in Style) and not ReflectionPass) then
begin
glDisable(GL_TEXTURE_2D);
DrawUnderline(Text[LineIndex]);
glEnable(GL_TEXTURE_2D);
end;
// draw reflection
if (ReflectionPass) then
begin
// set reflection spacing
glTranslatef(0, -ReflectionSpacing, 0);
// flip y-axis
glScalef(1, -1, 1);
end;
// shear for italic effect
if (Italic in Style) then
glMultMatrixf(@cShearMatrix);
// render text line
Render(Text[LineIndex]);
glPopMatrix();
end;
// restore settings
{$IFDEF FLIP_YAXIS}
glPopMatrix();
{$ENDIF}
glPopAttrib();
end;
procedure TFont.Print(const Text: UCS4String);
var
LineArray: TUCS4StringArray;
begin
SplitLines(Text, LineArray);
Print(LineArray);
SetLength(LineArray, 0);
end;
procedure TFont.Print(const Text: UTF8String);
begin
Print(UTF8Decode(Text));
end;
procedure TFont.Print(const Text: WideString);
begin
Print(WideStringToUCS4String(Text));
end;
procedure TFont.DrawUnderline(const Text: UCS4String);
var
UnderlineY1, UnderlineY2: single;
Bounds: TBoundsDbl;
begin
UnderlineY1 := GetUnderlinePosition();
UnderlineY2 := UnderlineY1 + GetUnderlineThickness();
Bounds := BBox(Text, false);
glRectf(Bounds.Left, UnderlineY1, Bounds.Right, UnderlineY2);
end;
procedure TFont.SetStyle(Style: TFontStyle);
begin
fStyle := Style;
end;
function TFont.GetStyle(): TFontStyle;
begin
Result := fStyle;
end;
procedure TFont.SetLineSpacing(Spacing: single);
begin
fLineSpacing := Spacing;
end;
function TFont.GetLineSpacing(): single;
begin
Result := fLineSpacing;
end;
procedure TFont.SetGlyphSpacing(Spacing: single);
begin
fGlyphSpacing := Spacing;
end;
function TFont.GetGlyphSpacing(): single;
begin
Result := fGlyphSpacing;
end;
procedure TFont.SetReflectionSpacing(Spacing: single);
begin
fReflectionSpacing := Spacing;
end;
function TFont.GetReflectionSpacing(): single;
begin
Result := fReflectionSpacing;
end;
procedure TFont.SetUseKerning(Enable: boolean);
begin
fUseKerning := Enable;
end;
function TFont.GetUseKerning(): boolean;
begin
Result := fUseKerning;
end;
procedure TFont.SetReflectionPass(Enable: boolean);
begin
fReflectionPass := Enable;
end;
{*
* TScalableFont
*}
constructor TScalableFont.Create(Font: TFont; UseMipmaps: boolean);
var
MipmapLevel: integer;
begin
inherited Create(Font.Filename);
fBaseFont := Font;
fMipmapFonts[0] := Font;
fUseMipmaps := UseMipmaps;
ResetIntern();
// create mipmap fonts if requested
if (UseMipmaps) then
begin
for MipmapLevel := 1 to cMaxMipmapLevel do
begin
fMipmapFonts[MipmapLevel] := CreateMipmap(MipmapLevel, 1/(1 shl MipmapLevel));
// stop if no smaller mipmap font is returned
if (fMipmapFonts[MipmapLevel] = nil) then
Break;
end;
end;
end;
destructor TScalableFont.Destroy();
var
Level: integer;
begin
for Level := 0 to High(fMipmapFonts) do
fMipmapFonts[Level].Free;
inherited;
end;
procedure TScalableFont.ResetIntern();
begin
fScale := 1.0;
fStretch := 1.0;
end;
procedure TScalableFont.Reset();
var
Level: integer;
begin
inherited;
ResetIntern();
for Level := 0 to High(fMipmapFonts) do
if (fMipmapFonts[Level] <> nil) then
fMipmapFonts[Level].Reset();
end;
{**
* Returns the mipmap level to use with regard to the current projection
* and modelview matrix, font scale and stretch.
*
* Note:
* - for Freetype fonts, hinting and grid-fitting must be disabled, otherwise
* the glyph widths/heights ratios and advance widths of the mipmap fonts
* do not match as they are adjusted sligthly (e.g. an 'a' at size 12px has
* width 12px, but at size 6px width 8px).
* - returned mipmap-level is used for all glyphs of the current text to print.
* This is faster, much easier to handle, since we just need to create
* multiple sized fonts and select the one we need for the mipmap-level and
* it avoids that neighbored glyphs use different mipmap-level which might
* look odd because one glyph might look blurry and the other sharp.
*
* Motivation:
* We do not use OpenGL for mipmapping as the results are very bad. At least
* with automatic mipmap generation (gluBuildMipmaps) the fonts look rather
* blurry.
* Defining our own mipmaps by creating multiple textures with
* for different mimap levels is a pain, as the font size passed to freetype
* is not the size of the bitmaps created and it does not guarantee that a
* glyph bitmap of a font with font-size s/2 is half the size of the font with
* font-size s. If the bitmap size is just a single pixel bigger than the half
* we might need a texture of the next power-of-2 and the texture would not be
* half of the size of the next bigger mipmap. In addition we use a fixed one
* pixel sized border to smooth the texture (see cTexSmoothBorder) and maybe
* an outset that is added to the font, so creating a glyph mipmap that is
* exactly half the size of the next bigger one is a very difficult task.
*
* Solution:
* Use mipmap textures that are not exactly half the size of the next mipmap
* level. OpenGL does not support this (at least not without extensions).
* The trickiest task is to determine the mipmap to use by calculating the
* amount of minification that is performed in this function.
*}
function TScalableFont.GetMipmapLevel(): integer;
var
ModelMatrix, ProjMatrix: T16dArray;
WinCoords: array[0..2, 0..2] of GLdouble;
ViewPortArray: TViewPortArray;
Dist, Dist2, DistSum: double;
WidthScale, HeightScale: double;
const
// width/height of square used for determining the scale
cTestSize = 10.0;
// an offset to the mipmap-level to adjust the change-over of two consecutive
// mipmap levels. If for example the bias is 0.1 and unbiased level is 1.9
// the result level will be 2. A bias of 0.5 is equal to rounding.
// With bias=0.1 we prefer larger mipmaps over smaller ones.
cBias = 0.2;
begin
// 1. retrieve current transformation matrices for gluProject
glGetDoublev(GL_MODELVIEW_MATRIX, @ModelMatrix);
glGetDoublev(GL_PROJECTION_MATRIX, @ProjMatrix);
glGetIntegerv(GL_VIEWPORT, @ViewPortArray);
// 2. project three of the corner points of a square with size cTestSize
// to window coordinates (the square is just a dummy for a glyph)
// project point (x1, y1) to window corrdinates
gluProject(0, 0, 0,
ModelMatrix, ProjMatrix, ViewPortArray,
@WinCoords[0][0], @WinCoords[0][1], @WinCoords[0][2]);
// project point (x2, y1) to window corrdinates
gluProject(cTestSize, 0, 0,
ModelMatrix, ProjMatrix, ViewPortArray,
@WinCoords[1][0], @WinCoords[1][1], @WinCoords[1][2]);
// project point (x1, y2) to window corrdinates
gluProject(0, cTestSize, 0,
ModelMatrix, ProjMatrix, ViewPortArray,
@WinCoords[2][0], @WinCoords[2][1], @WinCoords[2][2]);
// 3. Lets see how much the width and height of the square changed.
// Calculate the width and height as displayed on the screen in window
// coordinates and calculate the ratio to the original coordinates in
// modelview space so the ratio gives us the scale (minification here).
// projected width ||(x1, y1) - (x2, y1)||
Dist := (WinCoords[0][0] - WinCoords[1][0]);
Dist2 := (WinCoords[0][1] - WinCoords[1][1]);
WidthScale := 1;
DistSum := Dist*Dist + Dist2*Dist2;
if (DistSum > 0) then
begin
WidthScale := cTestSize / Sqrt(DistSum);
end;
// projected height ||(x1, y1) - (x1, y2)||
Dist := (WinCoords[0][0] - WinCoords[2][0]);
Dist2 := (WinCoords[0][1] - WinCoords[2][1]);
HeightScale := 1;
DistSum := Dist*Dist + Dist2*Dist2;
if (DistSum > 0) then
begin
HeightScale := cTestSize / Sqrt(DistSum);
end;
//writeln(Format('Scale %f, %f', [WidthScale, HeightScale]));
// 4. Now that we have got the scale, take the bigger minification scale
// and get it to a logarithmic scale as each mipmap is 1/2 the size of its
// predecessor (Mipmap_size[i] = Mipmap_size[i-1]/2).
// The result is our mipmap-level = the index of the mipmap to use.
// Level > 0: Minification; < 0: Magnification
Result := Trunc(Log2(Max(WidthScale, HeightScale)) + cBias);
// clamp to valid range
if (Result < 0) then
Result := 0;
if (Result > High(fMipmapFonts)) then
Result := High(fMipmapFonts);
end;
function TScalableFont.GetMipmapScale(Level: integer): single;
begin
if (fMipmapFonts[Level] = nil) then
begin
Result := -1;
Exit;
end;
Result := fScale * fMipmapFonts[0].Height / fMipmapFonts[Level].Height;
end;
{**
* Returns the correct mipmap font for the current scale and projection
* matrix. The modelview scale is adjusted to the mipmap level, so
* Result.Print() will display the font in the correct size.
*}
function TScalableFont.ChooseMipmapFont(): TFont;
var
DesiredLevel: integer;
Level: integer;
MipmapScale: single;
begin
Result := nil;
DesiredLevel := GetMipmapLevel();
// get the smallest mipmap available for the desired level
// as not all levels must be assigned to a font.
for Level := DesiredLevel downto 0 do
begin
if (fMipmapFonts[Level] <> nil) then
begin
Result := fMipmapFonts[Level];
Break;
end;
end;
// since the mipmap font (if level > 0) is smaller than the base-font
// we have to scale to get its size right.
MipmapScale := fMipmapFonts[0].Height/Result.Height;
glScalef(MipmapScale, MipmapScale, 0);
end;
procedure TScalableFont.Print(const Text: TUCS4StringArray);
begin
glPushMatrix();
// set scale and stretching
glScalef(fScale * fStretch, fScale, 0);
// print text
if (fUseMipmaps) then
ChooseMipmapFont().Print(Text)
else
fBaseFont.Print(Text);
glPopMatrix();
end;
procedure TScalableFont.Render(const Text: UCS4String);
begin
Assert(false, 'Unused TScalableFont.Render() was called');
end;
function TScalableFont.BBox(const Text: TUCS4StringArray; Advance: boolean): TBoundsDbl;
begin
Result := fBaseFont.BBox(Text, Advance);
Result.Left := Result.Left * fScale * fStretch;
Result.Right := Result.Right * fScale * fStretch;
Result.Top := Result.Top * fScale;
Result.Bottom := Result.Bottom * fScale;
end;
procedure TScalableFont.SetHeight(Height: single);
begin
fScale := Height / fBaseFont.GetHeight();
end;
function TScalableFont.GetHeight(): single;
begin
Result := fBaseFont.GetHeight() * fScale;
end;
procedure TScalableFont.SetStretch(Stretch: single);
begin
fStretch := Stretch;
end;
function TScalableFont.GetStretch(): single;
begin
Result := fStretch;
end;
function TScalableFont.GetAscender(): single;
begin
Result := fBaseFont.GetAscender() * fScale;
end;
function TScalableFont.GetDescender(): single;
begin
Result := fBaseFont.GetDescender() * fScale;
end;
procedure TScalableFont.SetLineSpacing(Spacing: single);
var
Level: integer;
begin
for Level := 0 to High(fMipmapFonts) do
if (fMipmapFonts[Level] <> nil) then
fMipmapFonts[Level].SetLineSpacing(Spacing / GetMipmapScale(Level));
end;
function TScalableFont.GetLineSpacing(): single;
begin
Result := fBaseFont.GetLineSpacing() * fScale;
end;
procedure TScalableFont.SetGlyphSpacing(Spacing: single);
var
Level: integer;
begin
for Level := 0 to High(fMipmapFonts) do
if (fMipmapFonts[Level] <> nil) then
fMipmapFonts[Level].SetGlyphSpacing(Spacing / GetMipmapScale(Level));
end;
function TScalableFont.GetGlyphSpacing(): single;
begin
Result := fBaseFont.GetGlyphSpacing() * fScale;
end;
procedure TScalableFont.SetReflectionSpacing(Spacing: single);
var
Level: integer;
begin
for Level := 0 to High(fMipmapFonts) do
if ((fMipmapFonts[Level] <> nil) AND (GetMipmapScale(Level) > 0)) then
fMipmapFonts[Level].SetReflectionSpacing(Spacing / GetMipmapScale(Level));
end;
function TScalableFont.GetReflectionSpacing(): single;
begin
Result := fBaseFont.GetLineSpacing() * fScale;
end;
procedure TScalableFont.SetStyle(Style: TFontStyle);
var
Level: integer;
begin
for Level := 0 to High(fMipmapFonts) do
if (fMipmapFonts[Level] <> nil) then
fMipmapFonts[Level].SetStyle(Style);
end;
function TScalableFont.GetStyle(): TFontStyle;
begin
Result := fBaseFont.GetStyle();
end;
function TScalableFont.GetUnderlinePosition(): single;
begin
Result := fBaseFont.GetUnderlinePosition();
end;
function TScalableFont.GetUnderlineThickness(): single;
begin
Result := fBaseFont.GetUnderlinePosition();
end;
procedure TScalableFont.SetUseKerning(Enable: boolean);
var
Level: integer;
begin
for Level := 0 to High(fMipmapFonts) do
if (fMipmapFonts[Level] <> nil) then
fMipmapFonts[Level].SetUseKerning(Enable);
end;
{*
* TCachedFont
*}
constructor TCachedFont.Create(const Filename: IPath);
begin
inherited Create(Filename);
fCache := TGlyphCache.Create();
end;
destructor TCachedFont.Destroy();
begin
fCache.Free;
inherited;
end;
function TCachedFont.GetGlyph(ch: UCS4Char): TGlyph;
begin
Result := fCache.GetGlyph(ch);
if (Result = nil) then
begin
Result := LoadGlyph(ch);
if (not fCache.AddGlyph(ch, Result)) then
Result.Free;
end;
end;
procedure TCachedFont.FlushCache(KeepBaseSet: boolean);
begin
fCache.FlushCache(KeepBaseSet);
end;
{*
* TFTFontFaceCache
*}
{*
* TFTFontFace
*}
constructor TFTFontFace.Create(const Filename: IPath; Size: integer);
begin
inherited Create();
fFilename := Filename;
fSize := Size;
// load font information
if (FT_New_Face(TFreeType.GetLibrary(), PChar(Filename.ToNative), 0, fFace) <> 0) then
raise EFontError.Create('FT_New_Face: Could not load font ''' + Filename.ToNative + '''');
// support scalable fonts only
if (not FT_IS_SCALABLE(fFace)) then
raise EFontError.Create('Font is not scalable');
if (FT_Set_Pixel_Sizes(fFace, 0, Size) <> 0) then
raise EFontError.Create('FT_Set_Pixel_Sizes failes');
// get scale factor for font-unit to pixel-size transformation
fFontUnitScale.X := fFace.size.metrics.x_ppem / fFace.units_per_EM;
fFontUnitScale.Y := fFace.size.metrics.y_ppem / fFace.units_per_EM;
end;
destructor TFTFontFace.Destroy();
begin
// free face data
FT_Done_Face(fFace);
inherited;
end;
{*
* TFTFontFaceCache
*}
function TFTFontFaceCache.LoadFace(const Filename: IPath; Size: integer): TFTFontFace;
var
I: Integer;
Face: TFTFontFace;
begin
{$IFDEF ENABLE_FT_FACE_CACHE}
for I := 0 to High(fFaces) do
begin
Face := fFaces[I];
// check if we have this file in our cache
if ((Face.Filename.Equals(Filename)) and (Face.Size = Size)) then
begin
// true -> return cached face and increment ref-count
Inc(fFacesRefCnt[I]);
Result := Face;
Exit;
end;
end;
{$ENDIF}
// face not in cache -> load it
Face := TFTFontFace.Create(Filename, Size);
// add face to cache
SetLength(fFaces, Length(fFaces)+1);
SetLength(fFacesRefCnt, Length(fFaces)+1);
fFaces[High(fFaces)] := Face;
fFacesRefCnt[High(fFaces)] := 1;
Result := Face;
end;
procedure TFTFontFaceCache.UnloadFace(Face: TFTFontFace);
var
I: Integer;
begin
for I := 0 to High(fFaces) do
begin
// search face in cache
if (fFaces[I] = Face) then
begin
// decrement ref-count and free face if ref-count is 0
Dec(fFacesRefCnt[I]);
if (fFacesRefCnt[I] <= 0) then
fFaces[I].Free;
Exit;
end;
end;
end;
{*
* TFTFont
*}
constructor TFTFont.Create(
const Filename: IPath;
Size: integer; Outset: single;
PreCache: boolean;
LoadFlags: FT_Int32);
var
ch: UCS4Char;
begin
inherited Create(Filename);
fSize := Size;
fOutset := Outset;
fPreCache := PreCache;
fLoadFlags := LoadFlags;
fUseDisplayLists := true;
fPart := fpNone;
fFace := GetFaceCache.LoadFace(Filename, Size);
ResetIntern();
// pre-cache some commonly used glyphs (' ' - '~')
if (PreCache) then
begin
for ch := 32 to 126 do
fCache.AddGlyph(ch, TFTGlyph.Create(Self, ch, Outset, LoadFlags));
end;
end;
destructor TFTFont.Destroy();
var
I: integer;
begin
// free faces
GetFaceCache.UnloadFace(fFace);
for I := 0 to High(fFallbackFaces) do
GetFaceCache.UnloadFace(fFallbackFaces[I]);
inherited;
end;
var
FontFaceCache: TFTFontFaceCache = nil;
class function TFTFont.GetFaceCache(): TFTFontFaceCache;
begin
if (FontFaceCache = nil) then
FontFaceCache := TFTFontFaceCache.Create;
Result := FontFaceCache;
end;
procedure TFTFont.ResetIntern();
begin
// Note: outset and non outset fonts use same spacing
fLineSpacing := fFace.Data.height * fFace.FontUnitScale.Y;
fReflectionSpacing := -2*fFace.Data.descender * fFace.FontUnitScale.Y;
end;
procedure TFTFont.Reset();
begin
inherited;
ResetIntern();
end;
procedure TFTFont.AddFallback(const Filename: IPath);
var
FontFace: TFTFontFace;
begin
FontFace := GetFaceCache.LoadFace(Filename, Size);
SetLength(fFallbackFaces, Length(fFallbackFaces) + 1);
fFallbackFaces[High(fFallbackFaces)] := FontFace;
end;
function TFTFont.LoadGlyph(ch: UCS4Char): TGlyph;
begin
Result := TFTGlyph.Create(Self, ch, Outset, fLoadFlags);
end;
function TFTFont.BBox(const Text: TUCS4StringArray; Advance: boolean): TBoundsDbl;
var
Glyph, PrevGlyph: TFTGlyph;
TextLine: UCS4String;
LineYOffset: single;
LineIndex, CharIndex: integer;
LineBounds: TBoundsDbl;
KernDelta: FT_Vector;
UnderlinePos: double;
begin
// Reset global bounds
Result.Left := Infinity;
Result.Right := 0;
Result.Bottom := Infinity;
Result.Top := 0;
// reset last glyph
PrevGlyph := nil;
// display text
for LineIndex := 0 to High(Text) do
begin
// get next text line
TextLine := Text[LineIndex];
LineYOffset := -LineSpacing * LineIndex;
// reset line bounds
LineBounds.Left := Infinity;
LineBounds.Right := 0;
LineBounds.Bottom := Infinity;
LineBounds.Top := 0;
// for each glyph image, compute its bounding box
for CharIndex := 0 to LengthUCS4(TextLine)-1 do
begin
Glyph := TFTGlyph(GetGlyph(TextLine[CharIndex]));
if (Glyph <> nil) then
begin
// get kerning
if (fUseKerning and FT_HAS_KERNING(fFace.Data) and (PrevGlyph <> nil)) then
begin
FT_Get_Kerning(fFace.Data, PrevGlyph.CharIndex, Glyph.CharIndex,
FT_KERNING_UNSCALED, KernDelta);
LineBounds.Right := LineBounds.Right + KernDelta.x * fFace.FontUnitScale.X;
end;
// update left bound (must be done before right bound is updated)
if (LineBounds.Right + Glyph.Bounds.Left < LineBounds.Left) then
LineBounds.Left := LineBounds.Right + Glyph.Bounds.Left;
// update right bound
if (CharIndex < LengthUCS4(TextLine)-1) or // not the last character
(TextLine[CharIndex] = Ord(' ')) or // on space char (Bounds.Right = 0)
Advance then // or in advance mode
begin
// add advance and glyph spacing
LineBounds.Right := LineBounds.Right + Glyph.Advance.x + GlyphSpacing
end
else
begin
// add glyph's right bound
LineBounds.Right := LineBounds.Right + Glyph.Bounds.Right;
end;
// update bottom and top bounds
if (Glyph.Bounds.Bottom < LineBounds.Bottom) then
LineBounds.Bottom := Glyph.Bounds.Bottom;
if (Glyph.Bounds.Top > LineBounds.Top) then
LineBounds.Top := Glyph.Bounds.Top;
end;
PrevGlyph := Glyph;
end;
// handle italic font style
if (Italic in Style) then
begin
LineBounds.Left := LineBounds.Left + LineBounds.Bottom * cShearFactor;
LineBounds.Right := LineBounds.Right + LineBounds.Top * cShearFactor;
end;
// handle underlined font style
if (Underline in Style) then
begin
UnderlinePos := GetUnderlinePosition();
if (UnderlinePos < LineBounds.Bottom) then
LineBounds.Bottom := UnderlinePos;
end;
// add line offset
LineBounds.Bottom := LineBounds.Bottom + LineYOffset;
LineBounds.Top := LineBounds.Top + LineYOffset;
// adjust global bounds
if (Result.Left > LineBounds.Left) then
Result.Left := LineBounds.Left;
if (Result.Right < LineBounds.Right) then
Result.Right := LineBounds.Right;
if (Result.Bottom > LineBounds.Bottom) then
Result.Bottom := LineBounds.Bottom;
if (Result.Top < LineBounds.Top) then
Result.Top := LineBounds.Top;
end;
// if left or bottom bound was not set, set them to 0
if (IsInfinite(Result.Left)) then
Result.Left := 0.0;
if (IsInfinite(Result.Bottom)) then
Result.Bottom := 0.0;
end;
procedure TFTFont.Render(const Text: UCS4String);
var
CharIndex: integer;
Glyph, PrevGlyph: TFTGlyph;
KernDelta: FT_Vector;
begin
// reset last glyph
PrevGlyph := nil;
// draw current line
for CharIndex := 0 to LengthUCS4(Text)-1 do
begin
Glyph := TFTGlyph(GetGlyph(Text[CharIndex]));
if (Assigned(Glyph)) then
begin
// get kerning
if (fUseKerning and FT_HAS_KERNING(fFace.Data) and (PrevGlyph <> nil)) then
begin
FT_Get_Kerning(fFace.Data, PrevGlyph.CharIndex, Glyph.CharIndex,
FT_KERNING_UNSCALED, KernDelta);
glTranslatef(KernDelta.x * fFace.FontUnitScale.X, 0, 0);
end;
if (ReflectionPass) then
Glyph.RenderReflection()
else
Glyph.Render(fUseDisplayLists);
glTranslatef(Glyph.Advance.x + fGlyphSpacing, 0, 0);
end;
PrevGlyph := Glyph;
end;
end;
function TFTFont.GetHeight(): single;
begin
Result := Ascender - Descender;
end;
function TFTFont.GetAscender(): single;
begin
Result := fFace.Data.ascender * fFace.FontUnitScale.Y + Outset*2;
end;
function TFTFont.GetDescender(): single;
begin
// Note: outset is not part of the descender as the baseline is lifted
Result := fFace.Data.descender * fFace.FontUnitScale.Y;
end;
function TFTFont.GetUnderlinePosition(): single;
begin
Result := fFace.Data.underline_position * fFace.FontUnitScale.Y - Outset;
end;
function TFTFont.GetUnderlineThickness(): single;
begin
Result := fFace.Data.underline_thickness * fFace.FontUnitScale.Y + Outset*2;
end;
{*
* TFTScalableFont
*}
constructor TFTScalableFont.Create(const Filename: IPath;
Size: integer; OutsetAmount: single;
UseMipmaps: boolean;
PreCache: boolean);
var
LoadFlags: FT_Int32;
begin
LoadFlags := FT_LOAD_DEFAULT;
// Disable hinting and grid-fitting to preserve font outlines at each font
// size, otherwise the font widths/heights do not match resulting in ugly
// text size changes during zooming.
// A drawback is a reduced quality with smaller font sizes but it is not that
// bad with gray-scaled rendering (at least it looks better than OpenGL's
// linear downscaling on minification).
if (UseMipmaps) then
LoadFlags := LoadFlags or FT_LOAD_NO_HINTING;
inherited Create(
TFTFont.Create(Filename, Size, Size * OutsetAmount, PreCache, LoadFlags),
UseMipmaps);
end;
function TFTScalableFont.CreateMipmap(Level: integer; Scale: single): TFont;
var
ScaledSize: integer;
BaseFont: TFTFont;
begin
Result := nil;
BaseFont := TFTFont(fBaseFont);
ScaledSize := Round(BaseFont.Size * Scale);
// do not create mipmap fonts < 8 pixels
if (ScaledSize < 8) then
Exit;
Result := TFTFont.Create(BaseFont.Filename,
ScaledSize, BaseFont.Outset * Scale,
BaseFont.fPreCache,
FT_LOAD_DEFAULT or FT_LOAD_NO_HINTING);
end;
function TFTScalableFont.GetOutset(): single;
begin
Result := TFTFont(fBaseFont).Outset * fScale;
end;
procedure TFTScalableFont.AddFallback(const Filename: IPath);
var
Level: integer;
begin
for Level := 0 to High(fMipmapFonts) do
if (fMipmapFonts[Level] <> nil) then
TFTFont(fMipmapFonts[Level]).AddFallback(Filename);
end;
procedure TFTScalableFont.FlushCache(KeepBaseSet: boolean);
var
Level: integer;
begin
for Level := 0 to High(fMipmapFonts) do
if (fMipmapFonts[Level] <> nil) then
TFTFont(fMipmapFonts[Level]).FlushCache(KeepBaseSet);
end;
{*
* TOutlineFont
*}
constructor TFTOutlineFont.Create(
const Filename: IPath;
Size: integer; Outset: single;
PreCache: boolean;
LoadFlags: FT_Int32);
begin
inherited Create(Filename);
fSize := Size;
fOutset := Outset;
fPreCache := PreCache;
fInnerFont := TFTFont.Create(Filename, Size, 0.0, PreCache, LoadFlags);
fInnerFont.Part := fpInner;
fOutlineFont := TFTFont.Create(Filename, Size, Outset, PreCache, LoadFlags);
fOutlineFont.Part := fpOutline;
ResetIntern();
end;
destructor TFTOutlineFont.Destroy;
begin
fOutlineFont.Free;
fInnerFont.Free;
inherited;
end;
procedure TFTOutlineFont.ResetIntern();
begin
// TODO: maybe swap fInnerFont/fOutlineFont.GlyphSpacing to use the spacing
// of the outline font?
//fInnerFont.GlyphSpacing := fOutset*2;
fOutlineFont.GlyphSpacing := -fOutset*2;
fLineSpacing := fOutlineFont.LineSpacing;
fReflectionSpacing := fOutlineFont.ReflectionSpacing;
fOutlineColor := NewGLColor(0, 0, 0, -1);
end;
procedure TFTOutlineFont.Reset();
begin
inherited;
fInnerFont.Reset();
fOutlineFont.Reset();
ResetIntern();
end;
procedure TFTOutlineFont.DrawUnderline(const Text: UCS4String);
var
CurrentColor: TGLColor;
OutlineColor: TGLColor;
begin
// save current color
glGetFloatv(GL_CURRENT_COLOR, @CurrentColor.vals);
// if the outline's alpha component is < 0 use the current alpha
OutlineColor := fOutlineColor;
if (OutlineColor.a < 0) then
OutlineColor.a := CurrentColor.a;
// draw underline outline (in outline color)
glColor4fv(@OutlineColor.vals);
fOutlineFont.DrawUnderline(Text);
glColor4fv(@CurrentColor.vals);
// draw underline inner part (in current color)
glPushMatrix();
glTranslatef(fOutset, 0, 0);
fInnerFont.DrawUnderline(Text);
glPopMatrix();
end;
procedure TFTOutlineFont.Render(const Text: UCS4String);
var
CurrentColor: TGLColor;
OutlineColor: TGLColor;
begin
// save current color
glGetFloatv(GL_CURRENT_COLOR, @CurrentColor.vals);
// if the outline's alpha component is < 0 use the current alpha
OutlineColor := fOutlineColor;
if (OutlineColor.a < 0) then
OutlineColor.a := CurrentColor.a;
{ setup and render outline font }
glColor4fv(@OutlineColor.vals);
glPushMatrix();
fOutlineFont.Render(Text);
glPopMatrix();
glColor4fv(@CurrentColor.vals);
{ setup and render inner font }
glPushMatrix();
glTranslatef(fOutset, fOutset, 0);
fInnerFont.Render(Text);
glPopMatrix();
end;
procedure TFTOutlineFont.SetOutlineColor(r, g, b: GLfloat; a: GLfloat);
begin
fOutlineColor := NewGLColor(r, g, b, a);
end;
procedure TFTOutlineFont.FlushCache(KeepBaseSet: boolean);
begin
fOutlineFont.FlushCache(KeepBaseSet);
fInnerFont.FlushCache(KeepBaseSet);
end;
procedure TFTOutlineFont.AddFallback(const Filename: IPath);
begin
fOutlineFont.AddFallback(Filename);
fInnerFont.AddFallback(Filename);
end;
function TFTOutlineFont.BBox(const Text: TUCS4StringArray; Advance: boolean): TBoundsDbl;
begin
Result := fOutlineFont.BBox(Text, Advance);
end;
function TFTOutlineFont.GetHeight(): single;
begin
Result := fOutlineFont.Height;
end;
function TFTOutlineFont.GetAscender(): single;
begin
Result := fOutlineFont.Ascender;
end;
function TFTOutlineFont.GetDescender(): single;
begin
Result := fOutlineFont.Descender;
end;
procedure TFTOutlineFont.SetLineSpacing(Spacing: single);
begin
inherited SetLineSpacing(Spacing);
fInnerFont.LineSpacing := Spacing;
fOutlineFont.LineSpacing := Spacing;
end;
procedure TFTOutlineFont.SetGlyphSpacing(Spacing: single);
begin
inherited SetGlyphSpacing(Spacing);
fInnerFont.GlyphSpacing := Spacing;
fOutlineFont.GlyphSpacing := Spacing - Outset*2;
end;
procedure TFTOutlineFont.SetReflectionSpacing(Spacing: single);
begin
inherited SetReflectionSpacing(Spacing);
fInnerFont.ReflectionSpacing := Spacing;
fOutlineFont.ReflectionSpacing := Spacing;
end;
procedure TFTOutlineFont.SetStyle(Style: TFontStyle);
begin
inherited SetStyle(Style);
fInnerFont.Style := Style;
fOutlineFont.Style := Style;
end;
function TFTOutlineFont.GetStyle(): TFontStyle;
begin
Result := inherited GetStyle();
end;
function TFTOutlineFont.GetUnderlinePosition(): single;
begin
Result := fOutlineFont.GetUnderlinePosition();
end;
function TFTOutlineFont.GetUnderlineThickness(): single;
begin
Result := fOutlineFont.GetUnderlinePosition();
end;
procedure TFTOutlineFont.SetUseKerning(Enable: boolean);
begin
inherited SetUseKerning(Enable);
fInnerFont.fUseKerning := Enable;
fOutlineFont.fUseKerning := Enable;
end;
procedure TFTOutlineFont.SetReflectionPass(Enable: boolean);
begin
inherited SetReflectionPass(Enable);
fInnerFont.fReflectionPass := Enable;
fOutlineFont.fReflectionPass := Enable;
end;
{**
* TScalableOutlineFont
*}
constructor TFTScalableOutlineFont.Create(
const Filename: IPath;
Size: integer; OutsetAmount: single;
UseMipmaps: boolean;
PreCache: boolean);
var
LoadFlags: FT_Int32;
begin
LoadFlags := FT_LOAD_DEFAULT;
// Disable hinting and grid-fitting (see TFTScalableFont.Create)
if (UseMipmaps) then
LoadFlags := LoadFlags or FT_LOAD_NO_HINTING;
inherited Create(
TFTOutlineFont.Create(Filename, Size, Size*OutsetAmount, PreCache, LoadFlags),
UseMipmaps);
end;
function TFTScalableOutlineFont.CreateMipmap(Level: integer; Scale: single): TFont;
var
ScaledSize: integer;
BaseFont: TFTOutlineFont;
begin
Result := nil;
BaseFont := TFTOutlineFont(fBaseFont);
ScaledSize := Round(BaseFont.Size*Scale);
// do not create mipmap fonts < 8 pixels
if (ScaledSize < 8) then
Exit;
Result := TFTOutlineFont.Create(BaseFont.fFilename,
ScaledSize, BaseFont.fOutset*Scale,
BaseFont.fPreCache,
FT_LOAD_DEFAULT or FT_LOAD_NO_HINTING);
end;
function TFTScalableOutlineFont.GetOutset(): single;
begin
Result := TFTOutlineFont(fBaseFont).Outset * fScale;
end;
procedure TFTScalableOutlineFont.SetOutlineColor(r, g, b: GLfloat; a: GLfloat);
var
Level: integer;
begin
for Level := 0 to High(fMipmapFonts) do
if (fMipmapFonts[Level] <> nil) then
TFTOutlineFont(fMipmapFonts[Level]).SetOutlineColor(r, g, b, a);
end;
procedure TFTScalableOutlineFont.FlushCache(KeepBaseSet: boolean);
var
Level: integer;
begin
for Level := 0 to High(fMipmapFonts) do
if (fMipmapFonts[Level] <> nil) then
TFTOutlineFont(fMipmapFonts[Level]).FlushCache(KeepBaseSet);
end;
procedure TFTScalableOutlineFont.AddFallback(const Filename: IPath);
var
Level: integer;
begin
for Level := 0 to High(fMipmapFonts) do
if (fMipmapFonts[Level] <> nil) then
TFTOutlineFont(fMipmapFonts[Level]).AddFallback(Filename);
end;
{*
* TFTGlyph
*}
const
{**
* Size of the transparent border surrounding the glyph image in the texture.
* The border is necessary because OpenGL does not smooth texels at the
* border of a texture with the GL_CLAMP or GL_CLAMP_TO_EDGE styles.
* Without the border, magnified glyph textures look very ugly at their edges.
* It looks edgy, as if some pixels are missing especially on the left edge
* (just set cTexSmoothBorder to 0 to see what is meant by this).
* With the border even the glyphs edges are blended to the border (transparent)
* color and everything looks nice.
*
* Note:
* OpenGL already supports texture border by setting the border parameter
* of glTexImage*D() to 1 and using a texture size of 2^m+2b and setting the
* border pixels to the border color. In some forums it is discouraged to use
* the border parameter as only a few of the more modern graphics cards support
* this feature. On an ATI Radeon 9700 card, the slowed down to 0.5 fps and
* the glyph's background got black. So instead of using this feature we
* handle it on our own. The only drawback is that textures might get bigger
* because the border might require a higher power of 2 size instead of just
* two additional pixels.
*}
cTexSmoothBorder = 1;
procedure TFTGlyph.StrokeBorder(var Glyph: FT_Glyph);
var
Outline: PFT_Outline;
OuterStroker, InnerStroker: FT_Stroker;
OuterNumPoints, InnerNumPoints, GlyphNumPoints: FT_UInt;
OuterNumContours, InnerNumContours, GlyphNumContours: FT_UInt;
OuterBorder, InnerBorder: FT_StrokerBorder;
OutlineFlags: FT_Int;
UseStencil: boolean;
begin
// It is possible to extrude the borders of a glyph with FT_Glyph_Stroke
// but it will extrude the border to the outside and the inside of a glyph
// although we just want to extrude to the outside.
// FT_Glyph_StrokeBorder extrudes to the outside but also fills the interior
// (this is what we need for bold fonts).
// In both cases the inner font and outline font (border) will overlap.
// Normally this does not matter but it does if alpha blending is active.
// In this case if e.g. the inner color is set to white, the outline to red
// and alpha to 0.5 the inner part will not be white it will be pink.
InnerStroker := nil;
OuterStroker := nil;
// If we are to create the interior of an outlined font (fInner = true)
// we have to create two borders:
// - one extruded to the outside by fOutset pixels and
// - one extruded to the inside by almost 0 zero pixels.
// The second one is used as a stencil for the first one, clearing the
// interiour of the glyph.
// The stencil is not needed to create bold fonts.
UseStencil := (fFont.Part = fpInner);
// we cannot extrude bitmaps, only vector based glyphs.
// Check for FT_GLYPH_FORMAT_OUTLINE otherwise a cast to FT_OutlineGlyph is
// invalid and FT_Stroker_ParseOutline() will crash
if (Glyph.format <> FT_GLYPH_FORMAT_OUTLINE) then
Exit;
Outline := @FT_OutlineGlyph(Glyph).outline;
OuterBorder := FT_Outline_GetOutsideBorder(Outline);
if (OuterBorder = FT_STROKER_BORDER_LEFT) then
InnerBorder := FT_STROKER_BORDER_RIGHT
else
InnerBorder := FT_STROKER_BORDER_LEFT;
{ extrude outer border }
if (FT_Stroker_New(Glyph.library_, OuterStroker) <> 0) then
raise EFontError.Create('FT_Stroker_New failed!');
FT_Stroker_Set(
OuterStroker,
Round(fOutset * 64),
FT_STROKER_LINECAP_ROUND,
FT_STROKER_LINEJOIN_BEVEL,
0);
// similar to FT_Glyph_StrokeBorder(inner = FT_FALSE) but it is possible to
// use FT_Stroker_ExportBorder() afterwards to combine inner and outer borders
if (FT_Stroker_ParseOutline(OuterStroker, Outline, FT_FALSE) <> 0) then
raise EFontError.Create('FT_Stroker_ParseOutline failed!');
FT_Stroker_GetBorderCounts(OuterStroker, OuterBorder, OuterNumPoints, OuterNumContours);
{ extrude inner border (= stencil) }
if (UseStencil) then
begin
if (FT_Stroker_New(Glyph.library_, InnerStroker) <> 0) then
raise EFontError.Create('FT_Stroker_New failed!');
FT_Stroker_Set(
InnerStroker,
63, // extrude at most one pixel to avoid a black border
FT_STROKER_LINECAP_ROUND,
FT_STROKER_LINEJOIN_BEVEL,
0);
if (FT_Stroker_ParseOutline(InnerStroker, Outline, FT_FALSE) <> 0) then
raise EFontError.Create('FT_Stroker_ParseOutline failed!');
FT_Stroker_GetBorderCounts(InnerStroker, InnerBorder, InnerNumPoints, InnerNumContours);
end else begin
InnerNumPoints := 0;
InnerNumContours := 0;
end;
{ combine borders (subtract: OuterBorder - InnerBorder) }
GlyphNumPoints := InnerNumPoints + OuterNumPoints;
GlyphNumContours := InnerNumContours + OuterNumContours;
// save flags before deletion (TODO: set them on the resulting outline)
OutlineFlags := Outline.flags;
// resize glyph outline to hold inner and outer border
FT_Outline_Done(Glyph.Library_, Outline);
if (FT_Outline_New(Glyph.Library_, GlyphNumPoints, GlyphNumContours, Outline) <> 0) then
raise EFontError.Create('FT_Outline_New failed!');
Outline.n_points := 0;
Outline.n_contours := 0;
// add points to outline. The inner-border is used as a stencil.
FT_Stroker_ExportBorder(OuterStroker, OuterBorder, Outline);
if (UseStencil) then
FT_Stroker_ExportBorder(InnerStroker, InnerBorder, Outline);
if (FT_Outline_Check(outline) <> 0) then
raise EFontError.Create('FT_Stroker_ExportBorder failed!');
if (InnerStroker <> nil) then
FT_Stroker_Done(InnerStroker);
if (OuterStroker <> nil) then
FT_Stroker_Done(OuterStroker);
end;
procedure TFTGlyph.CreateTexture(LoadFlags: FT_Int32);
var
X, Y: integer;
Glyph: FT_Glyph;
BitmapGlyph: FT_BitmapGlyph;
Bitmap: PFT_Bitmap;
BitmapLine: PByteArray;
BitmapBuffer: PByteArray;
TexBuffer: TGLubyteDynArray;
TexLine: PGLubyteArray;
CBox: FT_BBox;
begin
// we need vector data for outlined glyphs so do not load bitmaps.
// This is necessary for mixed fonts that contain bitmap versions of smaller
// glyphs, for example in CJK fonts.
if (fOutset > 0) then
LoadFlags := LoadFlags or FT_LOAD_NO_BITMAP;
// load the Glyph for our character
if (FT_Load_Glyph(fFace.Data, fCharIndex, LoadFlags) <> 0) then
raise EFontError.Create('FT_Load_Glyph failed');
// move the face's glyph into a Glyph object
if (FT_Get_Glyph(fFace.Data^.glyph, Glyph) <> 0) then
raise EFontError.Create('FT_Get_Glyph failed');
if (fOutset > 0) then
StrokeBorder(Glyph);
// store scaled advance width/height in glyph-object
fAdvance.X := fFace.Data^.glyph^.advance.x / 64 + fOutset*2;
fAdvance.Y := fFace.Data^.glyph^.advance.y / 64 + fOutset*2;
// get the contour's bounding box (in 1/64th pixels, not font-units)
FT_Glyph_Get_CBox(Glyph, FT_GLYPH_BBOX_UNSCALED, CBox);
// convert 1/64th values to double values
fBounds.Left := CBox.xMin / 64;
fBounds.Right := CBox.xMax / 64 + fOutset*2;
fBounds.Bottom := CBox.yMin / 64;
fBounds.Top := CBox.yMax / 64 + fOutset*2;
// convert the glyph to a bitmap (and destroy original glyph image).
// Request 8 bit gray level pixel mode.
FT_Glyph_To_Bitmap(Glyph, FT_RENDER_MODE_NORMAL, nil, 1);
BitmapGlyph := FT_BitmapGlyph(Glyph);
// get bitmap offsets
fBitmapCoords.Left := BitmapGlyph^.left - cTexSmoothBorder;
// Note: add 1*fOutset for lifting the baseline so outset fonts to not intersect
// with the baseline; Ceil(fOutset) for the outset pixels added to the bitmap.
fBitmapCoords.Top := BitmapGlyph^.top + fOutset+Ceil(fOutset) + cTexSmoothBorder;
// make accessing the bitmap easier
Bitmap := @BitmapGlyph^.bitmap;
// get bitmap dimensions
fBitmapCoords.Width := Bitmap.width + (Ceil(fOutset) + cTexSmoothBorder)*2;
fBitmapCoords.Height := Bitmap.rows + (Ceil(fOutset) + cTexSmoothBorder)*2;
// get power-of-2 bitmap widths
fTexSize.Width :=
NextPowerOf2(Bitmap.width + (Ceil(fOutset) + cTexSmoothBorder)*2);
fTexSize.Height :=
NextPowerOf2(Bitmap.rows + (Ceil(fOutset) + cTexSmoothBorder)*2);
// texture-widths ignoring empty (power-of-2) padding space
fTexOffset.X := fBitmapCoords.Width / fTexSize.Width;
fTexOffset.Y := fBitmapCoords.Height / fTexSize.Height;
// allocate memory for texture data
SetLength(TexBuffer, fTexSize.Width * fTexSize.Height);
FillChar(TexBuffer[0], Length(TexBuffer), 0);
// Freetype stores the bitmap with either upper (pitch is > 0) or lower
// (pitch < 0) glyphs line first. Set the buffer to the upper line.
// See http://freetype.sourceforge.net/freetype2/docs/glyphs/glyphs-7.html
if (Bitmap.pitch > 0) then
BitmapBuffer := @Bitmap.buffer[0]
else
BitmapBuffer := @Bitmap.buffer[(Bitmap.rows-1) * Abs(Bitmap.pitch)];
// copy data to texture bitmap (upper line first).
for Y := 0 to Bitmap.rows-1 do
begin
// set pointer to first pixel in line that holds bitmap data.
// Each line starts with a cTexSmoothBorder pixel and multiple outset pixels
// that are added by Extrude() later.
TexLine := @TexBuffer[(Y + cTexSmoothBorder + Ceil(fOutset)) * fTexSize.Width +
cTexSmoothBorder + Ceil(fOutset)];
// get next lower line offset, use pitch instead of width as it tells
// us the storage direction of the lines. In addition a line might be padded.
BitmapLine := @BitmapBuffer[Y * Bitmap.pitch];
// check for pixel mode and copy pixels
// Should be 8 bit gray, but even with FT_RENDER_MODE_NORMAL, freetype
// sometimes (e.g. 16px sized japanese fonts) fallbacks to 1 bit pixels.
case (Bitmap.pixel_mode) of
FT_PIXEL_MODE_GRAY: begin // 8 bit gray
for X := 0 to Bitmap.width-1 do
TexLine[X] := BitmapLine[X];
end;
FT_PIXEL_MODE_MONO: begin // 1 bit mono
for X := 0 to Bitmap.width-1 do
TexLine[X] := High(GLubyte) * ((BitmapLine[X div 8] shr (7-(X mod 8))) and $1);
end;
else begin
// unhandled pixel format
end;
end;
end;
// allocate resources for textures and display lists
glGenTextures(1, @fTexture);
// setup texture parameters
glBindTexture(GL_TEXTURE_2D, fTexture);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR);
glPixelStorei(GL_UNPACK_ALIGNMENT, 1);
// create alpha-map (GL_ALPHA component only).
// TexCoord (0,0) corresponds to the top left pixel of the glyph,
// (1,1) to the bottom right pixel. So the glyph is flipped as OpenGL uses
// a cartesian (y-axis up) coordinate system for textures.
// See the cTexSmoothBorder comment for info on texture borders.
glTexImage2D(GL_TEXTURE_2D, 0, GL_ALPHA, fTexSize.Width, fTexSize.Height,
0, GL_ALPHA, GL_UNSIGNED_BYTE, @TexBuffer[0]);
// free expanded data
SetLength(TexBuffer, 0);
// create the display list
fDisplayList := glGenLists(1);
// render to display-list
glNewList(fDisplayList, GL_COMPILE);
Render(false);
glEndList();
// free glyph data (bitmap, etc.)
FT_Done_Glyph(Glyph);
end;
constructor TFTGlyph.Create(Font: TFTFont; ch: UCS4Char; Outset: single;
LoadFlags: FT_Int32);
var
I: integer;
begin
inherited Create();
fFont := Font;
fOutset := Outset;
fCharCode := ch;
// Note: the default face is also used if no face (neither default nor fallback)
// contains a glyph for the given char.
fFace := Font.DefaultFace;
// search the Freetype char-index (use default UNICODE charmap) in the default face
fCharIndex := FT_Get_Char_Index(fFace.Data, FT_ULONG(ch));
if (fCharIndex = 0) then
begin
// glyph not in default font, search in fallback font faces
for I := 0 to High(Font.FallbackFaces) do
begin
fCharIndex := FT_Get_Char_Index(Font.FallbackFaces[I].Data, FT_ULONG(ch));
if (fCharIndex <> 0) then
begin
fFace := Font.FallbackFaces[I];
Break;
end;
end;
end;
CreateTexture(LoadFlags);
end;
destructor TFTGlyph.Destroy;
begin
if (fDisplayList <> 0) then
glDeleteLists(fDisplayList, 1);
if (fTexture <> 0) then
glDeleteTextures(1, @fTexture);
inherited;
end;
procedure TFTGlyph.Render(UseDisplayLists: boolean);
begin
// use display-lists if enabled and exit
if (UseDisplayLists) then
begin
glCallList(fDisplayList);
Exit;
end;
glBindTexture(GL_TEXTURE_2D, fTexture);
glPushMatrix();
// move to top left glyph position
glTranslatef(fBitmapCoords.Left, fBitmapCoords.Top, 0);
// draw glyph texture
glBegin(GL_QUADS);
// top right
glTexCoord2f(fTexOffset.X, 0);
glVertex2f(fBitmapCoords.Width, 0);
// top left
glTexCoord2f(0, 0);
glVertex2f(0, 0);
// bottom left
glTexCoord2f(0, fTexOffset.Y);
glVertex2f(0, -fBitmapCoords.Height);
// bottom right
glTexCoord2f(fTexOffset.X, fTexOffset.Y);
glVertex2f(fBitmapCoords.Width, -fBitmapCoords.Height);
glEnd();
glPopMatrix();
end;
procedure TFTGlyph.RenderReflection();
var
Color: TGLColor;
TexUpperPos: single;
TexLowerPos: single;
UpperPos: single;
const
CutOff = 0.6;
begin
glPushMatrix();
glBindTexture(GL_TEXTURE_2D, fTexture);
glGetFloatv(GL_CURRENT_COLOR, @Color.vals);
// add extra space to the left of the glyph
glTranslatef(fBitmapCoords.Left, 0, 0);
// The upper position of the glyph, if CutOff is 1.0, it is fFont.Ascender.
// If CutOff is set to 0.5 only half of the glyph height is displayed.
UpperPos := fFont.Descender + fFont.Height * CutOff;
// the glyph texture's height is just the height of the glyph but not the font
// height. Setting a color for the upper and lower bounds of the glyph results
// in different color gradients. So we have to set the color values for the
// descender and ascender (as we have a cutoff, for the upper-pos here) as
// these positions are font but not glyph specific.
// To get the texture positions we have to enhance the texture at the top and
// bottom by the amount from the top to ascender (rather upper-pos here) and
// from the bottom (Height-Top) to descender. Then we have to convert those
// heights to texture coordinates by dividing by the bitmap Height and
// removing the power-of-2 padding space by multiplying with fTexOffset.Y
// (as fBitmapCoords.Height corresponds to fTexOffset.Y and not 1.0).
TexUpperPos := -(UpperPos - fBitmapCoords.Top) / fBitmapCoords.Height * fTexOffset.Y;
TexLowerPos := (-(fFont.Descender + fBitmapCoords.Height - fBitmapCoords.Top) /
fBitmapCoords.Height + 1) * fTexOffset.Y;
// draw glyph texture
glBegin(GL_QUADS);
// top right
glColor4f(Color.r, Color.g, Color.b, 0);
glTexCoord2f(fTexOffset.X, TexUpperPos);
glVertex2f(fBitmapCoords.Width, UpperPos);
// top left
glTexCoord2f(0, TexUpperPos);
glVertex2f(0, UpperPos);
// bottom left
glColor4f(Color.r, Color.g, Color.b, Color.a-0.3);
glTexCoord2f(0, TexLowerPos);
glVertex2f(0, fFont.Descender);
// bottom right
glTexCoord2f(fTexOffset.X, TexLowerPos);
glVertex2f(fBitmapCoords.Width, fFont.Descender);
glEnd();
glPopMatrix();
// restore old color
// Note: glPopAttrib(GL_CURRENT_BIT)/glPopAttrib() is much slower then
// glGetFloatv(GL_CURRENT_COLOR, ...)/glColor4fv(...)
glColor4fv(@Color.vals);
end;
function TFTGlyph.GetAdvance(): TPositionDbl;
begin
Result := fAdvance;
end;
function TFTGlyph.GetBounds(): TBoundsDbl;
begin
Result := fBounds;
end;
{*
* TGlyphCache
*}
constructor TGlyphCache.Create();
begin
inherited;
fHash := TList.Create();
end;
destructor TGlyphCache.Destroy();
begin
// free cached glyphs
FlushCache(false);
// destroy TList
fHash.Free;
inherited;
end;
function TGlyphCache.FindGlyphTable(BaseCode: cardinal; out InsertPos: integer): PGlyphTable;
var
I: integer;
Entry: TGlyphCacheHashEntry;
begin
Result := nil;
for I := 0 to fHash.Count-1 do
begin
Entry := TGlyphCacheHashEntry(fHash[I]);
if (Entry.BaseCode > BaseCode) then
begin
InsertPos := I;
Exit;
end;
if (Entry.BaseCode = BaseCode) then
begin
InsertPos := I;
Result := @Entry.GlyphTable;
Exit;
end;
end;
InsertPos := fHash.Count;
end;
function TGlyphCache.AddGlyph(ch: UCS4Char; const Glyph: TGlyph): boolean;
var
BaseCode: cardinal;
GlyphCode: integer;
InsertPos: integer;
GlyphTable: PGlyphTable;
Entry: TGlyphCacheHashEntry;
begin
Result := false;
BaseCode := Ord(ch) shr 8;
GlyphTable := FindGlyphTable(BaseCode, InsertPos);
if (GlyphTable = nil) then
begin
Entry := TGlyphCacheHashEntry.Create(BaseCode);
GlyphTable := @Entry.GlyphTable;
fHash.Insert(InsertPos, Entry);
end;
// get glyph table offset
GlyphCode := Ord(ch) and $FF;
// insert glyph into table if not present
if (GlyphTable[GlyphCode] = nil) then
begin
GlyphTable[GlyphCode] := Glyph;
Result := true;
end;
end;
procedure TGlyphCache.DeleteGlyph(ch: UCS4Char);
var
Table: PGlyphTable;
TableIndex, GlyphIndex: integer;
TableEmpty: boolean;
begin
// find table
Table := FindGlyphTable(Ord(ch) shr 8, TableIndex);
if (Table = nil) then
Exit;
// find glyph
GlyphIndex := Ord(ch) and $FF;
if (Table[GlyphIndex] <> nil) then
begin
// destroy glyph
FreeAndNil(Table[GlyphIndex]);
// check if table is empty
TableEmpty := true;
for GlyphIndex := 0 to High(Table^) do
begin
if (Table[GlyphIndex] <> nil) then
begin
TableEmpty := false;
Break;
end;
end;
// free empty table
if (TableEmpty) then
begin
fHash.Delete(TableIndex);
end;
end;
end;
function TGlyphCache.GetGlyph(ch: UCS4Char): TGlyph;
var
InsertPos: integer;
Table: PGlyphTable;
begin
Table := FindGlyphTable(Ord(ch) shr 8, InsertPos);
if (Table = nil) then
Result := nil
else
Result := Table[Ord(ch) and $FF];
end;
function TGlyphCache.HasGlyph(ch: UCS4Char): boolean;
begin
Result := (GetGlyph(ch) <> nil);
end;
procedure TGlyphCache.FlushCache(KeepBaseSet: boolean);
var
EntryIndex, TableIndex: integer;
Entry: TGlyphCacheHashEntry;
begin
// destroy cached glyphs
for EntryIndex := 0 to fHash.Count-1 do
begin
Entry := TGlyphCacheHashEntry(fHash[EntryIndex]);
// the base set (0-255) has BaseCode 0 as the upper bytes are 0.
if KeepBaseSet and (Entry.fBaseCode = 0) then
Continue;
for TableIndex := 0 to High(Entry.GlyphTable) do
begin
if (Entry.GlyphTable[TableIndex] <> nil) then
FreeAndNil(Entry.GlyphTable[TableIndex]);
end;
FreeAndNil(Entry);
end;
end;
{*
* TGlyphCacheEntry
*}
constructor TGlyphCacheHashEntry.Create(BaseCode: cardinal);
begin
inherited Create();
fBaseCode := BaseCode;
end;
{*
* TFreeType
*}
class function TFreeType.GetLibrary(): FT_Library;
begin
if (LibraryInst = nil) then
begin
// initialize freetype
if (FT_Init_FreeType(LibraryInst) <> 0) then
raise EFontError.Create('FT_Init_FreeType failed');
end;
Result := LibraryInst;
end;
class procedure TFreeType.FreeLibrary();
begin
if (LibraryInst <> nil) then
FT_Done_FreeType(LibraryInst);
LibraryInst := nil;
end;
{$IFDEF BITMAP_FONT}
{*
* TBitmapFont
*}
constructor TBitmapFont.Create(const Filename: IPath; Outline: integer;
Baseline, Ascender, Descender: integer);
begin
inherited Create(Filename);
fTex := Texture.LoadTexture(Filename, TEXTURE_TYPE_TRANSPARENT, 0);
fTexSize := 1024;
fOutline := Outline;
fBaseline := Baseline;
fAscender := Ascender;
fDescender := Descender;
LoadFontInfo(Filename.SetExtension('.dat'));
ResetIntern();
end;
destructor TBitmapFont.Destroy();
begin
glDeleteTextures(1, @fTex.TexNum);
inherited;
end;
procedure TBitmapFont.ResetIntern();
begin
fLineSpacing := Height;
end;
procedure TBitmapFont.Reset();
begin
inherited;
ResetIntern();
end;
procedure TBitmapFont.AddFallback(const Filename: IPath);
begin
// no support for fallbacks
end;
procedure TBitmapFont.CorrectWidths(WidthMult: real; WidthAdd: integer);
var
Count: integer;
begin
for Count := 0 to 255 do
fWidths[Count] := Round(fWidths[Count] * WidthMult) + WidthAdd;
end;
procedure TBitmapFont.LoadFontInfo(const InfoFile: IPath);
var
Stream: TStream;
begin
FillChar(fWidths[0], Length(fWidths), 0);
Stream := nil;
try
Stream := TBinaryFileStream.Create(InfoFile, fmOpenRead);
Stream.Read(fWidths, 256);
except
raise EFontError.Create('Could not read font info file ''' + InfoFile.ToNative + '''');
end;
Stream.Free;
end;
function TBitmapFont.BBox(const Text: TUCS4StringArray; Advance: boolean): TBoundsDbl;
var
LineIndex, CharIndex: integer;
CharCode: cardinal;
Line: UCS4String;
LineWidth: double;
begin
Result.Left := 0;
Result.Right := 0;
Result.Top := Height;
Result.Bottom := 0;
for LineIndex := 0 to High(Text) do
begin
Line := Text[LineIndex];
LineWidth := 0;
for CharIndex := 0 to LengthUCS4(Line)-1 do
begin
CharCode := Ord(Line[CharIndex]);
if (CharCode < Length(fWidths)) then
LineWidth := LineWidth + fWidths[CharCode];
end;
if (LineWidth > Result.Right) then
Result.Right := LineWidth;
end;
end;
procedure TBitmapFont.RenderChar(ch: UCS4Char; var AdvanceX: real);
var
TexX, TexY: real;
TexR, TexB: real;
GlyphWidth: real;
PL, PT: real;
PR, PB: real;
CharCode: cardinal;
begin
CharCode := Ord(ch);
if (CharCode > High(fWidths)) then
CharCode := 0;
GlyphWidth := fWidths[CharCode];
// set texture positions
TexX := (CharCode mod 16) * 1/16 + 1/32 - (GlyphWidth/2 - fOutline)/fTexSize;
TexY := (CharCode div 16) * 1/16 + {2 texels} 2/fTexSize;
TexR := (CharCode mod 16) * 1/16 + 1/32 + (GlyphWidth/2 + fOutline)/fTexSize;
TexB := (1 + CharCode div 16) * 1/16 - {2 texels} 2/fTexSize;
// set vector positions
PL := AdvanceX - fOutline;
PR := PL + GlyphWidth + fOutline*2;
PB := -fBaseline;
PT := PB + fTexSize div 16;
(*
if (Font.Blend) then
begin
glEnable(GL_BLEND);
glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA);
end;
*)
glEnable(GL_TEXTURE_2D);
glBindTexture(GL_TEXTURE_2D, fTex.TexNum);
if (not ReflectionPass) then
begin
glBegin(GL_QUADS);
glTexCoord2f(TexX, TexY); glVertex2f(PL, PT);
glTexCoord2f(TexX, TexB); glVertex2f(PL, PB);
glTexCoord2f(TexR, TexB); glVertex2f(PR, PB);
glTexCoord2f(TexR, TexY); glVertex2f(PR, PT);
glEnd;
end
else
begin
glDepthRange(0, 10);
glDepthFunc(GL_LEQUAL);
glEnable(GL_DEPTH_TEST);
glBegin(GL_QUADS);
glTexCoord2f(TexX, TexY); glVertex2f(PL, PT);
glTexCoord2f(TexX, TexB); glVertex2f(PL, PB);
glTexCoord2f(TexR, TexB); glVertex2f(PR, PB);
glTexCoord2f(TexR, TexY); glVertex2f(PR, PT);
glEnd;
glBegin(GL_QUADS);
glTexCoord2f(TexX, TexY); glVertex2f(PL, PT);
glTexCoord2f(TexX, TexB); glVertex2f(PL, PB);
glTexCoord2f(TexR, TexB); glVertex2f(PR, PB);
glTexCoord2f(TexR, TexY); glVertex2f(PR, PT);
(*
glColor4f(fTempColor.r, fTempColor.g, fTempColor.b, 0.7);
glTexCoord2f(TexX, TexB); glVertex3f(PL, PB, 0);
glTexCoord2f(TexR, TexB); glVertex3f(PR, PB, 0);
glColor4f(fTempColor.r, fTempColor.g, fTempColor.b, 0);
glTexCoord2f(TexR, (TexY + TexB)/2); glVertex3f(PR, (PT + PB)/2, 0);
glTexCoord2f(TexX, (TexY + TexB)/2); glVertex3f(PL, (PT + PB)/2, 0);
*)
glEnd;
//write the colour back
glColor4fv(@fTempColor);
glDisable(GL_DEPTH_TEST);
end; // reflection
glDisable(GL_TEXTURE_2D);
(*
if (Font.Blend) then
glDisable(GL_BLEND);
*)
AdvanceX := AdvanceX + GlyphWidth;
end;
procedure TBitmapFont.Render(const Text: UCS4String);
var
CharIndex: integer;
AdvanceX: real;
begin
// if there is no text do nothing
if (Text = nil) or (Text[0] = 0) then
Exit;
//Save the current color and alpha (for reflection)
glGetFloatv(GL_CURRENT_COLOR, @fTempColor);
AdvanceX := 0;
for CharIndex := 0 to LengthUCS4(Text)-1 do
begin
RenderChar(Text[CharIndex], AdvanceX);
end;
end;
function TBitmapFont.GetHeight(): single;
begin
Result := fAscender - fDescender;
end;
function TBitmapFont.GetAscender(): single;
begin
Result := fAscender;
end;
function TBitmapFont.GetDescender(): single;
begin
Result := fDescender;
end;
function TBitmapFont.GetUnderlinePosition(): single;
begin
Result := -2.0;
end;
function TBitmapFont.GetUnderlineThickness(): single;
begin
Result := 1.0;
end;
{$ENDIF BITMAP_FONT}
initialization
finalization
TFreeType.FreeLibrary();
end.