unit KM_ResFonts; {$I KaM_Remake.inc} interface uses SysUtils, KM_CommonTypes, KM_RenderTypes, KM_Points; const FONT_TAB_WIDTH = 30; type //* Font type TKMFont = ( fntAntiqua, fntGame, fntGrey, fntMetal, fntMini, fntOutline, fntArial, // Arial for MP chat fntMonospaced // Debug overlay font (Consolas 8. Set WordSpacing to any Char.Width) ); const FontTypeName: array [TKMFont] of string = ( // Used in: // - data\gfx\fonts\filenames // - utils\FontX Generator\fonts.xml 'antiqua', 'game', 'grey', 'metal', 'mini', 'outline', 'arial', 'consolas8' ); type TKMFontLoadLevel = (fllFull, fllMinimal); { Removed fonts that were in KaM: Adam (unused) Briefing (same typeface as Antiqua, just darker) Font01 (damaged) KMLobby (used for internet lobby in TPR) MainA (identical to MainMapGold in all game versions) MainA.old (probably never meant to be included in the release anyway) MainB (identical to Game) MainMapGold (same typeface as Metal, just with a goldish tint) Minimum (same as mini but with less characters) System (unused) Won (same typeface as Metal, just with a blueish tint) } TKMFontInfo = record public TexMode: TKMTexFormat; //Format font texture needs to be in end; TKMLetter = packed record public Width, Height: Word; YOffset: SmallInt; AtlasId: Word; //Was Unknown field, we use it for multi-atlas fonts to mark the letters location u1, v1, u2, v2: Single; //Location within texture atlas end; TKMFontAtlas = record TexID: Cardinal; GpuCurrent: Boolean; TexData: TKMCardinalArray; end; TKMFontAtlasArray = array of TKMFontAtlas; TKMFontSpec = class const DEFAULT_EXT = 'fntx'; FNTX_HEAD: AnsiString = 'FNTX'; FONTS_FOLDER = 'data' + PathDelim + 'gfx' + PathDelim + 'fonts' + PathDelim; private fFont: TKMFont; procedure LoadFontX(const aFileName: string; aLoadLevel: TKMFontLoadLevel; aAllowReuseAtlas0: Boolean; aPriorAtlas0ID: Cardinal); overload; function GetTexID(aIndex: Integer): Cardinal; function GetLineHeight: SmallInt; protected fTexSizeX, fTexSizeY: Word; //All atlases have same dimensions //Character atlases fAtlasCount: Byte; fAtlases: TKMFontAtlasArray; fCharCount: Word; fBaseHeight, fWordSpacing, fCharSpacing: SmallInt; fLineSpacing: Byte; //Not in KaM files, we use custom value that fits well fCodepage: Word; fIsUnicode: Boolean; fMaxWidth: Integer; public Used: array [0..High(Word)] of Byte; Letters: array [0..High(Word)] of TKMLetter; constructor Create(aFont: TKMFont); procedure LoadFontX(const aFileName: string; aLoadLevel: TKMFontLoadLevel = fllFull); overload; procedure GenerateTextures(aTexMode: TKMTexFormat); procedure Compact; function GetLetter(aChar: WideChar): TKMLetter; property AtlasCount: Byte read fAtlasCount; property TexID[aIndex: Integer]: Cardinal read GetTexID; property CharCount: Word read fCharCount; property CharSpacing: SmallInt read fCharSpacing; property LineSpacing: Byte read fLineSpacing; property LineHeight: SmallInt read GetLineHeight; property BaseHeight: SmallInt read fBaseHeight; property WordSpacing: SmallInt read fWordSpacing; function GetCharWidth(aChar: WideChar; aConsiderEolSymbol: Boolean = False): Integer; function WordWrap(aText: UnicodeString; aMaxPxWidth: Integer; aForced: Boolean; aIndentAfterNL: Boolean; aTabWidth: Integer = FONT_TAB_WIDTH): UnicodeString; function CharsThatFit(const aText: UnicodeString; aMaxPxWidth: Integer; aRound: Boolean = False; aConsiderEolSymbol: Boolean = False; aTabWidth: Integer = FONT_TAB_WIDTH): Integer; function GetTextSize(const aText: UnicodeString; var aLineCount: Integer; aCountMarkup: Boolean = False; aConsiderEolSymbol: Boolean = False; aTabWidth: Integer = FONT_TAB_WIDTH): TKMPoint; overload; function GetTextSize(const aText: UnicodeString; aCountMarkup: Boolean = False; aConsiderEolSymbol: Boolean = False; aTabWidth: Integer = FONT_TAB_WIDTH): TKMPoint; overload; function GetMaxPrintWidthOfStrings(aStrings: array of string): Integer; end; //Collection of fonts TKMResFonts = class private fContentSetValid: Boolean; fLoadLevel: TKMFontLoadLevel; fFontData: array [TKMFont] of TKMFontSpec; function GetFontData(aIndex: TKMFont): TKMFontSpec; public constructor Create; destructor Destroy; override; property FontData[aIndex: TKMFont]: TKMFontSpec read GetFontData; default; property LoadLevel: TKMFontLoadLevel read fLoadLevel; function HasContentLevel(aRequiredLevel: TKMFontLoadLevel): Boolean; function HasGPUAtlasLevel(aRequiredLevel: TKMFontLoadLevel): Boolean; procedure InvalidateGPUAtlasContext; procedure LoadFonts(aLoadLevel: TKMFontLoadLevel = fllFull); function EnsureGPUAtlasLevel(aRequiredLevel: TKMFontLoadLevel): Boolean; end; const PLACEHOLDER_CHAR = 0; // Box, used for characters missing from font FONT_INFO: array [TKMFont] of TKMFontInfo = ( (TexMode: tfRGB5A1), (TexMode: tfAlpha8), (TexMode: tfRGB5A1), (TexMode: tfRGB5A1), (TexMode: tfAlpha8), (TexMode: tfRGB5A1), (TexMode: tfRGBA8), (TexMode: tfRGBA8) ); implementation uses Classes, Math, {$IFDEF FPC} zstream, {$ENDIF} {$IFDEF WDC} ZLib, {$ENDIF} KM_Render, KaMResourceInput; { TKMFontSpec } constructor TKMFontSpec.Create(aFont: TKMFont); begin inherited Create; fFont := aFont; end; procedure TKMFontSpec.LoadFontX(const aFileName: string; aLoadLevel: TKMFontLoadLevel = fllFull); begin LoadFontX(aFileName, aLoadLevel, False, 0); end; procedure TKMFontSpec.LoadFontX(const aFileName: string; aLoadLevel: TKMFontLoadLevel; aAllowReuseAtlas0: Boolean; aPriorAtlas0ID: Cardinal); var inputStream: TStream; decompressionStream: TDecompressionStream; head: AnsiString; sourceUsed, resultUsed: array of Byte; sourceLetters: array of TKMLetter; stagedAtlases: TKMFontAtlasArray; sourceAtlasCount, loadCount: Byte; texWidth, texHeight: Word; stagedBaseHeight, stagedWordSpacing, stagedCharSpacing: SmallInt; stagedLineSpacing: Byte; pixelCount, byteCount, remaining: UInt64; readCount, I: Integer; extraByte: Byte; discardBuffer: array[0..65535] of Byte; u1, v1, u2, v2: Single; oldIndex: Integer; reuseAtlas0: Boolean; begin inputStream := KaMOpenResourceStream(aFileName); if inputStream = nil then raise Exception.Create('Font resource returned no stream'); try decompressionStream := TDecompressionStream.Create(inputStream); try SetLength(sourceUsed, 65536); SetLength(resultUsed, 65536); SetLength(sourceLetters, 65536); SetLength(head, 4); decompressionStream.ReadBuffer(head[1], 4); if head <> FNTX_HEAD then raise Exception.Create('Invalid FNTX header'); decompressionStream.ReadBuffer(stagedBaseHeight, SizeOf(stagedBaseHeight)); decompressionStream.ReadBuffer(stagedWordSpacing, SizeOf(stagedWordSpacing)); decompressionStream.ReadBuffer(stagedCharSpacing, SizeOf(stagedCharSpacing)); decompressionStream.ReadBuffer(stagedLineSpacing, SizeOf(stagedLineSpacing)); decompressionStream.ReadBuffer(sourceUsed[0], Length(sourceUsed)); for I := 0 to High(Word) do if sourceUsed[I] <> 0 then decompressionStream.ReadBuffer(sourceLetters[I], SizeOf(TKMLetter)); decompressionStream.ReadBuffer(sourceAtlasCount, SizeOf(sourceAtlasCount)); decompressionStream.ReadBuffer(texWidth, SizeOf(texWidth)); decompressionStream.ReadBuffer(texHeight, SizeOf(texHeight)); if sourceAtlasCount = 0 then raise Exception.Create('FNTX contains no atlases'); if (texWidth = 0) or (texHeight = 0) then raise Exception.Create('FNTX has invalid atlas dimensions'); pixelCount := UInt64(texWidth) * UInt64(texHeight); byteCount := pixelCount * SizeOf(Cardinal); if (pixelCount > UInt64(High(SizeInt))) or (byteCount > UInt64(High(LongInt))) then raise Exception.Create('FNTX atlas dimensions exceed supported size'); for I := 0 to High(Word) do if sourceUsed[I] <> 0 then begin if sourceLetters[I].AtlasId >= sourceAtlasCount then raise Exception.Create('FNTX glyph references an invalid atlas'); u1 := sourceLetters[I].u1; v1 := sourceLetters[I].v1; u2 := sourceLetters[I].u2; v2 := sourceLetters[I].v2; if IsNan(u1) or IsInfinite(u1) or IsNan(v1) or IsInfinite(v1) or IsNan(u2) or IsInfinite(u2) or IsNan(v2) or IsInfinite(v2) or (u1 < 0) or (u1 > 1) or (v1 < 0) or (v1 > 1) or (u2 < 0) or (u2 > 1) or (v2 < 0) or (v2 > 1) or (u1 > u2) or (v1 > v2) then raise Exception.Create('FNTX glyph has invalid atlas coordinates'); end; if aLoadLevel = fllMinimal then loadCount := 1 else loadCount := sourceAtlasCount; if (sourceUsed[PLACEHOLDER_CHAR] <> 0) and (sourceLetters[PLACEHOLDER_CHAR].AtlasId >= loadCount) then raise Exception.Create('FNTX fallback glyph is absent from selected atlas level'); for I := 0 to High(Word) do if (sourceUsed[I] <> 0) and ((aLoadLevel = fllFull) or (sourceLetters[I].AtlasId = 0)) then resultUsed[I] := sourceUsed[I]; SetLength(stagedAtlases, loadCount); for I := 0 to loadCount - 1 do begin stagedAtlases[I].TexID := 0; stagedAtlases[I].GpuCurrent := False; SetLength(stagedAtlases[I].TexData, SizeInt(pixelCount)); end; for I := 0 to sourceAtlasCount - 1 do if I < loadCount then decompressionStream.ReadBuffer(stagedAtlases[I].TexData[0], LongInt(byteCount)) else begin remaining := byteCount; while remaining > 0 do begin if remaining > SizeOf(discardBuffer) then readCount := SizeOf(discardBuffer) else readCount := Integer(remaining); decompressionStream.ReadBuffer(discardBuffer[0], readCount); Dec(remaining, UInt64(readCount)); end; end; if decompressionStream.Read(extraByte, 1) <> 0 then raise Exception.Create('FNTX contains trailing decoded data'); finally decompressionStream.Free; end; finally inputStream.Free; end; reuseAtlas0 := aAllowReuseAtlas0 and (aPriorAtlas0ID <> 0) and (Length(fAtlases) > 0) and (fAtlases[0].TexID = aPriorAtlas0ID) and fAtlases[0].GpuCurrent and TKMRender.TextureBackendReady; for oldIndex := 0 to High(fAtlases) do begin if (oldIndex = 0) and reuseAtlas0 then Continue; if fAtlases[oldIndex].GpuCurrent and (fAtlases[oldIndex].TexID <> 0) and TKMRender.TextureBackendReady then TKMRender.DeleteTexture(fAtlases[oldIndex].TexID); fAtlases[oldIndex].TexID := 0; fAtlases[oldIndex].GpuCurrent := False; end; if reuseAtlas0 and (Length(stagedAtlases) > 0) then begin stagedAtlases[0].TexID := aPriorAtlas0ID; stagedAtlases[0].GpuCurrent := True; end; for I := 0 to High(Word) do begin Used[I] := resultUsed[I]; Letters[I] := sourceLetters[I]; end; fBaseHeight := stagedBaseHeight; fWordSpacing := stagedWordSpacing; fCharSpacing := stagedCharSpacing; fLineSpacing := stagedLineSpacing; fCodepage := 0; fIsUnicode := True; fCharCount := 65535; fAtlasCount := loadCount; fTexSizeX := texWidth; fTexSizeY := texHeight; fAtlases := stagedAtlases; stagedAtlases := nil; end; // After font has been loaded and texture generated we can flush temp data procedure TKMFontSpec.Compact; var I: Integer; begin //Discard texture data to save mem for I := 0 to fAtlasCount - 1 do SetLength(fAtlases[I].TexData, 0); fTexSizeX := 0; fTexSizeY := 0; end; // Generate color texture from prepared data procedure TKMFontSpec.GenerateTextures(aTexMode: TKMTexFormat); var I: Integer; pixelCount: UInt64; textureID: Cardinal; begin if not TKMRender.TextureBackendReady then raise Exception.Create('Cannot upload font atlases without a ready texture backend'); for I := 0 to fAtlasCount - 1 do begin if fAtlases[I].GpuCurrent and (fAtlases[I].TexID <> 0) then Continue; if not TKMRender.TextureBackendReady then raise Exception.Create('Texture backend became unavailable during font atlas upload'); fAtlases[I].GpuCurrent := False; fAtlases[I].TexID := 0; pixelCount := UInt64(fTexSizeX) * UInt64(fTexSizeY); if (fTexSizeX = 0) or (fTexSizeY = 0) or (pixelCount = 0) or (pixelCount > UInt64(High(SizeInt))) or (pixelCount * SizeOf(Cardinal) > UInt64(High(LongInt))) or (UInt64(Length(fAtlases[I].TexData)) <> pixelCount) then raise Exception.Create('Font atlas pixels are unavailable or have invalid dimensions'); textureID := TKMRender.GenTexture(fTexSizeX, fTexSizeY, @fAtlases[I].TexData[0], aTexMode, ftNearest, ftNearest); if textureID = 0 then raise Exception.Create('Font atlas texture upload returned no texture ID'); if not TKMRender.TextureBackendReady then raise Exception.Create('Texture backend became unavailable during font atlas upload'); fAtlases[I].TexID := textureID; fAtlases[I].GpuCurrent := True; end; end; function TKMFontSpec.GetLetter(aChar: WideChar): TKMLetter; begin if Used[Ord(aChar)] <> 0 then Result := Letters[Ord(aChar)] else Result := Letters[PLACEHOLDER_CHAR]; end; function TKMFontSpec.GetTexID(aIndex: Integer): Cardinal; begin Result := fAtlases[aIndex].TexID; end; { TKMResFonts } constructor TKMResFonts.Create; var F: TKMFont; begin inherited; fContentSetValid := False; fLoadLevel := fllMinimal; for F := Low(TKMFont) to High(TKMFont) do fFontData[F] := TKMFontSpec.Create(F); end; destructor TKMResFonts.Destroy; var F: TKMFont; I: Integer; firstDeleteError: string; deleteFailed: Boolean; begin firstDeleteError := ''; deleteFailed := False; try for F := Low(TKMFont) to High(TKMFont) do if fFontData[F] <> nil then for I := 0 to High(fFontData[F].fAtlases) do if (fFontData[F].fAtlases[I].TexID <> 0) and fFontData[F].fAtlases[I].GpuCurrent then begin if TKMRender.TextureBackendReady then try TKMRender.DeleteTexture(fFontData[F].fAtlases[I].TexID); except on E: Exception do if not deleteFailed then begin firstDeleteError := E.Message; deleteFailed := True; end; end; fFontData[F].fAtlases[I].TexID := 0; fFontData[F].fAtlases[I].GpuCurrent := False; end; finally for F := Low(TKMFont) to High(TKMFont) do fFontData[F].Free; end; inherited; if deleteFailed then raise Exception.Create(firstDeleteError); end; function TKMResFonts.GetFontData(aIndex: TKMFont): TKMFontSpec; begin Result := fFontData[aIndex]; end; procedure TKMResFonts.LoadFonts(aLoadLevel: TKMFontLoadLevel = fllFull); type TReuseCandidate = record CanReuse: Boolean; PriorAtlas0ID: Cardinal; end; TNewFontAtlasID = record Font: TKMFont; AtlasIndex: Integer; TexID: Cardinal; end; var F: TKMFont; I, decodedCount, atlasCount: Integer; relativeIdentity, fntRef: string; reuseCandidate: array[TKMFont] of TReuseCandidate; beforeID: array of Cardinal; newIDs: array of TNewFontAtlasID; newID: TNewFontAtlasID; seen: Boolean; procedure RecordCreatedIDs(aFont: TKMFont); var J, K: Integer; begin for J := 0 to High(beforeID) do if (beforeID[J] = 0) and (fFontData[aFont].fAtlases[J].TexID <> 0) and fFontData[aFont].fAtlases[J].GpuCurrent then begin seen := False; for K := 0 to High(newIDs) do if (newIDs[K].Font = aFont) and (newIDs[K].AtlasIndex = J) and (newIDs[K].TexID = fFontData[aFont].fAtlases[J].TexID) then begin seen := True; Break; end; if not seen then begin newID.Font := aFont; newID.AtlasIndex := J; newID.TexID := fFontData[aFont].fAtlases[J].TexID; SetLength(newIDs, Length(newIDs) + 1); newIDs[High(newIDs)] := newID; end; end; end; procedure CleanupNewIDs; var J, fontIndex, atlasIndex: Integer; savedID: Cardinal; begin for J := High(newIDs) downto 0 do begin fontIndex := Ord(newIDs[J].Font); atlasIndex := newIDs[J].AtlasIndex; if (atlasIndex >= 0) and (atlasIndex < Length(fFontData[newIDs[J].Font].fAtlases)) and (fFontData[newIDs[J].Font].fAtlases[atlasIndex].TexID = newIDs[J].TexID) and fFontData[newIDs[J].Font].fAtlases[atlasIndex].GpuCurrent then begin savedID := newIDs[J].TexID; if TKMRender.TextureBackendReady then try TKMRender.DeleteTexture(savedID); except // Preserve the original LoadFonts failure while continuing safe cleanup. end; fFontData[newIDs[J].Font].fAtlases[atlasIndex].TexID := 0; fFontData[newIDs[J].Font].fAtlases[atlasIndex].GpuCurrent := False; end; end; end; procedure ReleaseAttemptPixels; var FontIndex, AtlasIndex: Integer; begin for FontIndex := Ord(Low(TKMFont)) to Ord(High(TKMFont)) do for AtlasIndex := 0 to High(fFontData[TKMFont(FontIndex)].fAtlases) do SetLength(fFontData[TKMFont(FontIndex)].fAtlases[AtlasIndex].TexData, 0); end; begin for F := Low(TKMFont) to High(TKMFont) do begin reuseCandidate[F].CanReuse := False; reuseCandidate[F].PriorAtlas0ID := 0; end; if (aLoadLevel = fllFull) and HasContentLevel(fllMinimal) and (fLoadLevel = fllMinimal) and TKMRender.TextureBackendReady then for F := Low(TKMFont) to High(TKMFont) do if (Length(fFontData[F].fAtlases) > 0) and (fFontData[F].fAtlases[0].TexID <> 0) and fFontData[F].fAtlases[0].GpuCurrent then begin reuseCandidate[F].CanReuse := True; reuseCandidate[F].PriorAtlas0ID := fFontData[F].fAtlases[0].TexID; end; fContentSetValid := False; fLoadLevel := fllMinimal; decodedCount := 0; try for F := Low(TKMFont) to High(TKMFont) do begin relativeIdentity := 'data/gfx/fonts/' + FontTypeName[F] + '.' + TKMFontSpec.DEFAULT_EXT; fntRef := KaMResolvePackagedResource(relativeIdentity); fFontData[F].LoadFontX(fntRef, aLoadLevel, reuseCandidate[F].CanReuse, reuseCandidate[F].PriorAtlas0ID); Inc(decodedCount); if decodedCount = Ord(High(TKMFont)) - Ord(Low(TKMFont)) + 1 then begin fLoadLevel := aLoadLevel; fContentSetValid := True; end; if TKMRender.TextureBackendReady then begin SetLength(beforeID, fFontData[F].fAtlasCount); for I := 0 to High(beforeID) do beforeID[I] := fFontData[F].fAtlases[I].TexID; try fFontData[F].GenerateTextures(FONT_INFO[F].TexMode); finally RecordCreatedIDs(F); end; if aLoadLevel = fllMinimal then atlasCount := 1 else atlasCount := fFontData[F].fAtlasCount; seen := True; for I := 0 to atlasCount - 1 do if (fFontData[F].fAtlases[I].TexID = 0) or not fFontData[F].fAtlases[I].GpuCurrent then seen := False; if seen then fFontData[F].Compact; end; end; except CleanupNewIDs; ReleaseAttemptPixels; raise; end; end; function TKMResFonts.HasContentLevel(aRequiredLevel: TKMFontLoadLevel): Boolean; begin if not fContentSetValid then Exit(False); case fLoadLevel of fllFull: Result := (aRequiredLevel = fllFull) or (aRequiredLevel = fllMinimal); fllMinimal: Result := aRequiredLevel = fllMinimal; end; end; function TKMResFonts.HasGPUAtlasLevel(aRequiredLevel: TKMFontLoadLevel): Boolean; var F: TKMFont; I, requiredAtlasCount: Integer; begin if not TKMRender.TextureBackendReady or not HasContentLevel(aRequiredLevel) then Exit(False); for F := Low(TKMFont) to High(TKMFont) do begin if (fFontData[F] = nil) or (fFontData[F].fAtlasCount < 1) or (Length(fFontData[F].fAtlases) < fFontData[F].fAtlasCount) then Exit(False); if aRequiredLevel = fllMinimal then requiredAtlasCount := 1 else requiredAtlasCount := fFontData[F].fAtlasCount; for I := 0 to requiredAtlasCount - 1 do if (fFontData[F].fAtlases[I].TexID = 0) or not fFontData[F].fAtlases[I].GpuCurrent then Exit(False); end; Result := True; end; function TKMResFonts.EnsureGPUAtlasLevel(aRequiredLevel: TKMFontLoadLevel): Boolean; begin if HasGPUAtlasLevel(aRequiredLevel) then Exit(True); if not TKMRender.TextureBackendReady then Exit(False); LoadFonts(aRequiredLevel); Result := HasGPUAtlasLevel(aRequiredLevel); end; procedure TKMResFonts.InvalidateGPUAtlasContext; var F: TKMFont; I: Integer; begin for F := Low(TKMFont) to High(TKMFont) do if fFontData[F] <> nil then for I := 0 to High(fFontData[F].fAtlases) do begin fFontData[F].fAtlases[I].GpuCurrent := False; fFontData[F].fAtlases[I].TexID := 0; end; end; function TKMFontSpec.GetLineHeight: SmallInt; begin Result := BaseHeight + LineSpacing; end; function TKMFontSpec.GetCharWidth(aChar: WideChar; aConsiderEolSymbol: Boolean = False): Integer; begin if (not aConsiderEolSymbol and (aChar = #124)) or (aChar = #9) then Result := 0 else if aChar = #32 then Result := WordSpacing else Result := GetLetter(aChar).Width + CharSpacing; // CharSpacing could be negative Result := Max(0, Result); end; function TKMFontSpec.WordWrap(aText: UnicodeString; aMaxPxWidth: Integer; aForced: Boolean; aIndentAfterNL: Boolean; aTabWidth: Integer = FONT_TAB_WIDTH): UnicodeString; const INDENT = ' '; var I, lineWrapPos: Integer; lastWrappable: Integer; lastWrappableIsSpace: Boolean; lastColorMarkup, afterWrapClMarkup: UnicodeString; dx, prevX: Integer; tmpColor: Integer; begin Assert(aMaxPxWidth > 0); if aText = '' then Exit (''); dx := 0; prevX := 0; lastWrappable := -1; lastWrappableIsSpace := False; lastColorMarkup := ''; afterWrapClMarkup := ''; I := 1; while I <= Length(aText) do begin //Chinese/Japanese characters (not punctuation) can always be wrapped before //Check this before we update dx since we are allowing wrapping before this char if ((Ord(aText[I]) >= 19968) and (Ord(aText[I]) <= 40870)) or ((Ord(aText[I]) >= $3040) and (Ord(aText[I]) <= $30ff)) then begin lastWrappable := I; afterWrapClMarkup := lastColorMarkup; prevX := dx; //dx does not include this char yet, since we are wrapping before it lastWrappableIsSpace := False; end; //Ignore color markups [$FFFFFF][] if (aText[I] = '[') and (I+1 <= Length(aText)) and (aText[I+1] = ']') then begin lastColorMarkup := ''; Inc(I); //Skip past end of color markup end else if (aText[I] = '[') and (I+8 <= Length(aText)) and (aText[I+1] = '$') and (aText[I+8] = ']') and TryStrToInt(Copy(aText, I+1, 7), tmpColor) then begin lastColorMarkup := Copy(aText, I, 9); Inc(I,8); //Skip past start of color markup end else if (aText[I] = #9) then dx := (Floor(dx / aTabWidth) + 1) * aTabWidth else Inc(dx, GetCharWidth(aText[I])); if SysUtils.CharInSet(aText[I], [#9,#32,#124]) then begin lastWrappable := I; afterWrapClMarkup := lastColorMarkup; prevX := dx; lastWrappableIsSpace := True; end; //This algorithm is not perfect, somehow line width is not within SizeX, but very rare if ((dx > aMaxPxWidth) and (lastWrappable <> -1)) or (aText[I] = #124) then begin if lastWrappableIsSpace then aText[lastWrappable] := #124 //Replace last whitespace with EOL else begin Inc(lastWrappable); Insert(#124, aText, lastWrappable); //Insert EOL after last wrappable char end; if afterWrapClMarkup <> '' then begin Insert('[]', aText, lastWrappable); Inc(I, 2); Inc(lastWrappable, 2); end; if (aText[I] <> #124) and aIndentAfterNL then begin Insert(INDENT, aText, lastWrappable+1); Inc(I, Length(INDENT)); Inc(dx, Length(INDENT) * WordSpacing); end; if afterWrapClMarkup <> '' then begin Insert(afterWrapClMarkup, aText, lastWrappable+1); Inc(I, Length(afterWrapClMarkup)); end; Dec(dx, prevX); //Subtract width since replaced whitespace lastWrappable := -1; end; //Force an EOL part way through a word if aForced and (dx > aMaxPxWidth) and (lastWrappable = -1) then begin Insert(#124, aText, I); //Insert an EOL before this character dx := 0; if lastColorMarkup <> '' then begin Insert('[]', aText, I); Inc(I, 2); end; lineWrapPos := I; if aIndentAfterNL then begin Insert(INDENT, aText, I+1); Inc(I, Length(INDENT)); Inc(dx, Length(INDENT) * WordSpacing); end; if lastColorMarkup <> '' then begin Insert(lastColorMarkup, aText, lineWrapPos+1); Inc(I, Length(lastColorMarkup)); end; lastWrappable := -1; end; Inc(I); end; Result := aText; end; function TKMFontSpec.CharsThatFit(const aText: UnicodeString; aMaxPxWidth: Integer; aRound: Boolean = False; aConsiderEolSymbol: Boolean = False; aTabWidth: Integer = FONT_TAB_WIDTH): Integer; var I, dx, prevX, lastCharW: Integer; begin dx := 0; Result := Length(aText); for I := 1 to Length(aText) do begin lastCharW := GetCharWidth(aText[I], aConsiderEolSymbol); prevX := dx; if aText[I] = #9 then dx := (Floor(dx / aTabWidth) + 1) * aTabWidth else Inc(dx, lastCharW); if (dx > aMaxPxWidth) then begin // If we want to get approximate result, then check if total width is closer to prev width or to current if aRound and (dx - aMaxPxWidth < aMaxPxWidth - prevX) then Result := I else Result := I - 1; //Previous character fits, this one does not Exit; end; end; end; function TKMFontSpec.GetTextSize(const aText: UnicodeString; aCountMarkup: Boolean = False; aConsiderEolSymbol: Boolean = False; aTabWidth: Integer = FONT_TAB_WIDTH): TKMPoint; var lineCount: Integer; begin Result := GetTextSize(aText, lineCount, aCountMarkup, aConsiderEolSymbol, aTabWidth); end; function TKMFontSpec.GetTextSize(const aText: UnicodeString; var aLineCount: Integer; aCountMarkup: Boolean = False; aConsiderEolSymbol: Boolean = False; aTabWidth: Integer = FONT_TAB_WIDTH): TKMPoint; var I: Integer; lineWidthInc, tmpColor: Integer; lineWidth: array of Integer; // Some fonts may have negative CharSpacing begin Result.X := 0; Result.Y := 0; if aText = '' then Exit; aLineCount := 1; if not aConsiderEolSymbol then for I := 1 to Length(aText) do if aText[I] = #124 then Inc(aLineCount); SetLength(lineWidth, aLineCount+2); //1..n+1 (for last line) aLineCount := 1; I := 1; while I <= Length(aText) do begin lineWidthInc := 0; if aCountMarkup then begin //Count all characters including markup if aText[I] = #9 then // Tab char lineWidthInc := (Floor(lineWidth[aLineCount] / aTabWidth) + 1) * aTabWidth - lineWidth[aLineCount] else lineWidthInc := GetCharWidth(aText[I], aConsiderEolSymbol); Inc(lineWidth[aLineCount], lineWidthInc); end else //Ignore color markups [$FFFFFF][] if (aText[I]='[') and (I+1 <= Length(aText)) and (aText[I+1]=']') then Inc(I) //Skip past this markup else if (aText[I]='[') and (I+8 <= Length(aText)) and (aText[I+1] = '$') and (aText[I+8]=']') and TryStrToInt(Copy(aText, I+1, 7), tmpColor) then Inc(I,8) //Skip past this markup else begin //Not markup so count width normally if aText[I] = #9 then // Tab char lineWidthInc := (Floor(lineWidth[aLineCount] / aTabWidth) + 1) * aTabWidth - lineWidth[aLineCount] else lineWidthInc := GetCharWidth(aText[I], aConsiderEolSymbol); Inc(lineWidth[aLineCount], lineWidthInc); end; if (not aConsiderEolSymbol and (aText[I] = #124)) or (I = Length(aText)) then begin // If EOL or aText end if aText[I] <> #9 then // for Tab reduce line width for CharSpacing and also for TAB 'jump' lineWidthInc := 0; lineWidth[aLineCount] := Math.Max(0, lineWidth[aLineCount] - CharSpacing - lineWidthInc); // Remove last interletter space and negate double EOLs Inc(aLineCount); end; Inc(I); end; Dec(aLineCount); Result.Y := LineHeight * aLineCount; for I := 1 to aLineCount do Result.X := Math.Max(Result.X, lineWidth[I]); end; // Return maximum of the width of specified strings when printed on screen with specified font. function TKMFontSpec.GetMaxPrintWidthOfStrings(aStrings: array of string): Integer; var I: Integer; begin Result := 0; for I := Low(aStrings) to High(aStrings) do Result := Max(Result, GetTextSize(aStrings[I]).X); end; end.