{******************************************} { } { FastReport v4.0 } { Graphic routines } { } { Copyright (c) 1998-2011 } { by Alexander Tzyganenko, } { Fast Reports Inc. } { } {******************************************} unit FMX.frxGraphicUtils; interface {$I fmx.inc} {$I frx.inc} uses System.SysUtils, System.Classes, System.UITypes, System.UIConsts, System.StrUtils, System.WideStrings, System.Types, System.Variants, System.Generics.Collections, System.Math, FMX.Types, FMX.Forms, FMX.frxFMX, FMX.frxClass {$IFDEF DELPHI19} , FMX.Graphics {$ENDIF} {$IFDEF DELPHI20} , System.Math.Vectors {$ENDIF} {$IFDEF DELPHI28} , FMX.BaseTypeAliases {$ENDIF}; type TBaseLine = (Normal, Subscript, Superscript); TStyleDescriptor = record FontStyle: TFontStyles; Color: TAlphaColor; BaseLine: TBaseLine; end; TParagraph = class; TLine = class; TWord = class; TRun = class; TSubStyle = (ssNormal, ssSubscript, ssSuperscript); TfrxHTMLTag = class(TObject) public Position: Integer; Size: Integer; AddY: Integer; Style: TFontStyles; Color: TAlphaColor; Default: Boolean; Small: Boolean; DontWRAP: Boolean; SubType: TSubStyle; procedure Assign(Tag: TfrxHTMLTag); end; TfrxHTMLTags = class(TObject) private FItems: TList; procedure Add(Tag: TfrxHTMLTag); function GetItems(Index: Integer): TfrxHTMLTag; public constructor Create; destructor Destroy; override; procedure Clear; function Count: Integer; property Items[Index: Integer]: TfrxHTMLTag read GetItems; default; end; TfrxHTMLTagsList = class(TObject) private FAllowTags: Boolean; FAddY: Integer; FColor: Integer; FDefColor: LongInt; FDefSize: Integer; FDefStyle: TFontStyles; FItems: TList; FPosition: Integer; FSize: Integer; FStyle: TFontStyles; FDontWRAP: Boolean; FSubStyle: TSubStyle; procedure NewLine; function Add: TfrxHTMLTag; function GetItems(Index: Integer): TfrxHTMLTags; function GetPrevTag: TfrxHTMLTag; protected procedure Wrap(TagsCount: Integer; AddBreak: Boolean); public constructor Create; destructor Destroy; override; procedure Clear; procedure SetDefaults(DefColor: TColor; DefSize: Integer; DefStyle: TFontStyles); procedure ExpandHTMLTags(var s: WideString); function Count: Integer; property AllowTags: Boolean read FAllowTags write FAllowTags; property Items[Index: Integer]: TfrxHTMLTags read GetItems; default; property Position: Integer read FPosition write FPosition; end; TAdvancedTextRenderer = class private FParagraphs: TList; FText: String; FCanvas: TCanvas; FFont: TfrxFont; FDisplayRect: TRectF; FHorzAlign: TfrxHAlign; FVertAlign: TfrxVAlign; FLineHeight: Single; FFontLineHeight: Single; FAngle: Integer; FForceJustify: Boolean; FWysiwyg: Boolean; FHtmlTags: Boolean; FWordWrap: Boolean; FRightToLeft: Boolean; FPDFMode: Boolean; FSpaceWidth: Single; FSymbolWidth: Single; FScaleFactor: Single; FFastRender: TfrxFastCanvasLayer; procedure SplitToParagraphs(const text: String); procedure AdjustParagraphLines; public constructor Create(aCanvas: TCanvas; const aText: String; aFont: TfrxFont; rect: TRectF; aHorzAlign: TfrxHAlign; aVertAlign: TfrxVAlign; aLineHeight: Single; aAngle: Integer; aWordWrap: Boolean; aForceJustify: Boolean; aHtmlTags: Boolean; aPdfMode: Boolean; aScaLeFactor: Single); destructor Destroy; override; function CalcWidth: Single; function MeasureTextWidth(const aText: String): Single; function CalcHeight: Single; overload; function CalcHeight(var charsFit: Integer; var style: TStyleDescriptor): Single; overload; function GetTabPosition(pos: Single): Single; procedure Draw; property Paragraphs: TList read FParagraphs; property Canvas: TCanvas read FCanvas; property Font: TfrxFont read FFont; property DisplayRect: TRectF read FDisplayRect; property HorzAlign: TfrxHAlign read FHorzAlign; property VertAlign: TfrxVAlign read FVertAlign; property LineHeight: Single read FLineHeight; property FontLineHeight: Single read FFontLineHeight; property Angle: Integer read FAngle; property ForceJustify: Boolean read FForceJustify; property Wysiwyg: Boolean read FWysiwyg; property HtmlTags: Boolean read FHtmlTags; property WordWrap: Boolean read FWordWrap; property RightToLeft: Boolean read FRightToLeft; property PDFMode: Boolean read FPDFMode; property SpaceWidth: Single read FSpaceWidth; property SymbolWidth: Single read FSymbolWidth; end; TParagraph = class private FLines: TList; FRenderer: TAdvancedTextRenderer; FText: String; FOriginalCharIndex: Integer; public constructor Create(const text: String; renderer: TAdvancedTextRenderer; originalCharIndex: Integer); destructor Destroy; override; function IsLast: Boolean; function IsEmpty: Boolean; function WrapLines(style: TStyleDescriptor): TStyleDescriptor; procedure AlignLines(forceJustify: Boolean); procedure Draw; property Lines: TList read FLines; property Renderer: TAdvancedTextRenderer read FRenderer; end; TLine = class private FWords: TList; FParagraph: TParagraph; FTop: Single; FWidth: Single; FOriginalCharIndex: Integer; FUnderlines: TList; FStrikeouts: TList; function GetStyle: TStyleDescriptor; function GetRenderer: TAdvancedTextRenderer; function GetLeft: Single; procedure PrepareUnderlines(list: TList; style: TFontStyle); function GetText: String; public constructor Create(paragraph: TParagraph; originalCharIndex: Integer); destructor Destroy; override; function IsLast: Boolean; procedure AlignWords(align: TfrxHAlign); procedure MakeUnderlines; procedure Draw; property Words: TList read FWords; property Left: Single read GetLeft; property Top: Single read FTop write FTop; property Width: Single read FWidth; property Text: String read GetText; property OriginalCharIndex: Integer read FOriginalCharIndex; property Renderer: TAdvancedTextRenderer read GetRenderer; property Style: TStyleDescriptor read GetStyle; property Underlines: TList read FUnderlines; property Strikeouts: TList read FStrikeouts; end; TWord = class private FRuns: TList; FLeft: Single; FWidth: Single; FLine: TLine; function GetWidth: Single; function GetTop: Single; function GetRenderer: TAdvancedTextRenderer; function GetText: String; public constructor Create(aLine: TLine); destructor Destroy; override; procedure AdjustRuns; procedure SetLine(aLine: TLine); procedure Draw; property Left: Single read FLeft write FLeft; property Width: Single read GetWidth; property Top: Single read GetTop; property Text: String read GetText; property Renderer: TAdvancedTextRenderer read GetRenderer; property Runs: TList read FRuns; end; TTab = class(TWord); TRun = class private FText: String; FStyle: TStyleDescriptor; FWord: TWord; FLeft: Single; FWidth: Single; function GetRenderer: TAdvancedTextRenderer; function GetTop: Single; function GetFont(disableUnderlinesStrikeouts: Boolean): TfrxFont; overload; public constructor Create(const aText: String; aStyle: TStyleDescriptor; aWord: TWord); function GetFont: TfrxFont; overload; procedure Draw; property Text: String read FText; property Style: TStyleDescriptor read FStyle; property Renderer: TAdvancedTextRenderer read GetRenderer; property Left: Single read FLeft write FLeft; property Top: Single read GetTop; property Width: Single read FWidth; end; implementation { TStyleDescriptor routines } function StyleDescriptor(FontStyle: TFontStyles; Color: TAlphaColor; BaseLine: TBaseLine): TStyleDescriptor; begin Result.FontStyle := FontStyle; Result.Color := Color; Result.BaseLine := BaseLine; end; function NullStyle: TStyleDescriptor; begin Result.FontStyle := []; Result.Color := 0; Result.BaseLine := TBaseLine.Normal; end; function StyleDescriptorToString(style: TStyleDescriptor): String; begin Result := ''; if fsBold in style.FontStyle then Result := Result + ''; if fsItalic in style.FontStyle then Result := Result + ''; if fsUnderline in style.FontStyle then Result := Result + ''; if fsStrikeout in style.FontStyle then Result := Result + ''; if style.BaseLine = TBaseLine.Subscript then Result := Result + ''; if style.BaseLine = TBaseLine.Superscript then Result := Result + ''; Result := Result + ''; end; function AdjY(Value, Thickness: Single): Single; begin Result := Round(Value); if (Trunc(Thickness) mod 2) = 1 then Result := Result + 0.5; end; { TfrxHTMLTag } procedure TfrxHTMLTag.Assign(Tag: TfrxHTMLTag); begin Position := Tag.Position; Size := Tag.Size; AddY := Tag.AddY; Style := Tag.Style; Color := Tag.Color; Default := Tag.Default; Small := Tag.Small; Self.SubType := Tag.SubType; end; { TfrxHTMLTags } constructor TfrxHTMLTags.Create; begin FItems := TList.Create; end; destructor TfrxHTMLTags.Destroy; begin Clear; FItems.Free; inherited; end; procedure TfrxHTMLTags.Clear; var i: Integer; begin for i := 0 to FItems.Count - 1 do TfrxHTMLTag(FItems[i]).Free; FItems.Clear; end; function TfrxHTMLTags.GetItems(Index: Integer): TfrxHTMLTag; begin Result := TfrxHTMLTag(FItems[Index]); end; function TfrxHTMLTags.Count: Integer; begin Result := FItems.Count; end; procedure TfrxHTMLTags.Add(Tag: TfrxHTMLTag); begin FItems.Add(Tag); end; { TfrxHTMLTagsList } constructor TfrxHTMLTagsList.Create; begin FItems := TList.Create; FAllowTags := True; end; destructor TfrxHTMLTagsList.Destroy; begin Clear; FItems.Free; inherited; end; procedure TfrxHTMLTagsList.Clear; var i: Integer; begin for i := 0 to FItems.Count - 1 do TfrxHTMLTags(FItems[i]).Free; FItems.Clear; end; procedure TfrxHTMLTagsList.NewLine; begin if Count <> 0 then FItems.Add(TfrxHTMLTags.Create); end; procedure TfrxHTMLTagsList.Wrap(TagsCount: Integer; AddBreak: Boolean); var i: Integer; Line, OldLine: TfrxHTMLTags; NewTag: TfrxHTMLTag; begin OldLine := Items[Count - 1]; if OldLine.Count <= TagsCount then Exit; NewLine; Line := Items[Count - 1]; for i := TagsCount to OldLine.Count - 1 do Line.Add(OldLine[i]); OldLine.FItems.Count := TagsCount; if AddBreak then begin NewTag := TfrxHTMLTag.Create; OldLine.FItems.Add(NewTag); NewTag.Assign(TfrxHTMLTag(OldLine.FItems[TagsCount - 1])) end else if Line[0].Default then Line[0].Assign(OldLine[TagsCount - 1]); end; function TfrxHTMLTagsList.Count: Integer; begin Result := FItems.Count; end; function TfrxHTMLTagsList.GetItems(Index: Integer): TfrxHTMLTags; begin Result := TfrxHTMLTags(FItems[Index]); end; function TfrxHTMLTagsList.Add: TfrxHTMLTag; var i: Integer; begin Result := TfrxHTMLTag.Create; i := Count - 1; if i = -1 then begin FItems.Add(TfrxHTMLTags.Create); i := 0; end; Items[i].Add(Result); end; function TfrxHTMLTagsList.GetPrevTag: TfrxHTMLTag; var Tags: TfrxHTMLTags; begin Result := nil; Tags := Items[Count - 1]; if Tags.Count > 1 then Result := Tags[Tags.Count - 2] else if Count > 1 then begin Tags := Items[Count - 2]; Result := Tags[Tags.Count - 1]; end; end; procedure TfrxHTMLTagsList.SetDefaults(DefColor: TColor; DefSize: Integer; DefStyle: TFontStyles); begin FDefColor := DefColor; FDefSize := DefSize; FDefStyle := DefStyle; FAddY := 0; FColor := FDefColor; FSize := FDefSize; FStyle := FDefStyle; FDontWRAP := False; FPosition := 1; Self.FSubStyle := ssNormal; Clear; end; procedure TfrxHTMLTagsList.ExpandHTMLTags(var s: WideString); var i, j, j1: Integer; b: Boolean; cl: WideString; procedure AddTag; var Tag, PrevTag: TfrxHTMLTag; begin Tag := Add; Tag.Position := FPosition; // this will help us to get position in the original text Tag.Size := FSize; Tag.Style := FStyle; Tag.Color := FColor; Tag.AddY := FAddY; Tag.DontWRAP := FDontWRAP; Tag.SubType := Self.FSubStyle; // when "Default" changes, we need to set Font.Style, Size and Color if FAllowTags then begin PrevTag := GetPrevTag; if PrevTag <> nil then Tag.Default := (FStyle = PrevTag.Style) and (Cardinal(FColor) = PrevTag.Color) and (FSize = PrevTag.Size) and (FAddY = PrevTag.AddY) and (FDontWRAP = PrevTag.DontWRAP) else Tag.Default := (FStyle = FDefStyle) and (FColor = FDefColor) and (FSize = FDefSize); end else Tag.Default := True; Tag.Small := FSize <> FDefSize; end; begin i := 1; if Length(s) = 0 then Exit; while i <= Length(s) do begin b := True; if FAllowTags then if s[i] = '<' then begin // , , tags if (i + 2 <= Length(s)) and (s[i + 2] = '>') then begin case s[i + 1] of 'b','B': FStyle := FStyle + [fsBold]; 'i','I': FStyle := FStyle + [fsItalic]; 'u','U': FStyle := FStyle + [fsUnderline]; else b := False; end; if b then begin System.Delete(s, i, 3); Inc(FPosition, 3); continue; end; end // , tags else if (i + 4 <= Length(s)) and (s[i + 4] = '>') then begin if Pos('SUB>', AnsiUpperCase(s)) = i + 1 then begin FSize := Round(FDefSize / 1.5); FAddY := 1; b := True; Self.FSubStyle := ssSubscript; end else if Pos('SUP>', AnsiUpperCase(s)) = i + 1 then begin FSize := Round(FDefSize / 1.5); FAddY := 0; b := True; Self.FSubStyle := ssSuperscript; end; if b then begin System.Delete(s, i, 5); Inc(FPosition, 5); continue; end; end // , tags else if (i + 5 <= Length(s)) and (s[i + 5] = '>') then begin if (Pos('/SUB>', AnsiUpperCase(s)) = i + 1) or (Pos('/SUP>', AnsiUpperCase(s)) = i + 1) then begin FSize := FDefSize; FAddY := 0; b := True; Self.FSubStyle := ssNormal; end; if b then begin System.Delete(s, i, 6); Inc(FPosition, 6); continue; end; end else if (i + 7 <= Length(s)) and ((s[i + 1] = 'n') or (s[i + 1] = 'N')) then begin if Pos('NOWRAP>', AnsiUpperCase(s)) = i + 1 then begin FDontWRAP := True; System.Delete(s, i, 8); Inc(FPosition, 8); continue; end; end // tag else if (i + 1 <= Length(s)) and ((s[i + 1] = 's') or (s[i + 1] = 'S')) then begin if Pos('STRIKE>', AnsiUpperCase(s)) = i + 1 then begin FStyle := FStyle + [fsStrikeOut]; System.Delete(s, i, 8); Inc(FPosition, 8); continue; end; end // , , , , , , tags else if (i + 1 <= Length(s)) and (s[i + 1] = '/') then begin if (i + 3 <= Length(s)) and (s[i + 3] = '>') then begin case s[i + 2] of 'b','B': FStyle := FStyle - [fsBold]; 'i','I': FStyle := FStyle - [fsItalic]; 'u','U': FStyle := FStyle - [fsUnderline]; else b := False; end; if b then begin System.Delete(s, i, 4); Inc(FPosition, 4); continue; end; end else if (Pos('STRIKE>', AnsiUpperCase(s)) = i + 2) then begin FStyle := FStyle - [fsStrikeOut]; System.Delete(s, i, 9); Inc(FPosition, 9); continue; end else if (Pos('NOWRAP>', AnsiUpperCase(s)) = i + 2) then begin FDontWRAP := False; System.Delete(s, i, 9); Inc(FPosition, 9); continue; end else if Pos('FONT>', AnsiUpperCase(s)) = i + 2 then begin FColor := FDefColor; System.Delete(s, i, 7); Inc(FPosition, 7); continue; end else if (Pos('SUB>', AnsiUpperCase(s)) = i + 2) or (Pos('SUP>', AnsiUpperCase(s)) = i + 2) then begin FSize := FDefSize; FAddY := 0; System.Delete(s, i, 6); Inc(FPosition, 6); continue; end end // tag else if Pos('FONT COLOR', AnsiUpperCase(s)) = i + 1 then begin j := i + 11; while (j <= Length(s)) and (s[j] <> '=') do Inc(j); Inc(j); while (j <= Length(s)) and (s[j] = ' ') do Inc(j); j1 := j; while (j <= Length(s)) and (s[j] <> '>') do Inc(j); cl := Copy(s, j1, j - j1); if cl <> '' then begin if (Length(cl) > 3) and (cl[1] = '"') and (cl[2] = '#') and (cl[Length(cl)] = '"') then begin cl := '$' + Copy(cl, 3, Length(cl) - 3); FColor := StrToInt(cl); FColor := (FColor and $000000FF) div 65536 + (FColor and $00FF0000) * 65536 + (FColor and $0000FF00); System.Delete(s, i, j - i + 1); Inc(FPosition, j - i + 1); continue; end else if IdentToAlphaColor('cl' + cl, FColor) then begin System.Delete(s, i, j - i + 1); Inc(FPosition, j - i + 1); continue; end; end; end end; AddTag; Inc(i); Inc(FPosition); end; if Length(s) = 0 then begin AddTag; s := ' '; end; end; { TAdvancedTextRenderer } function TAdvancedTextRenderer.MeasureTextWidth(const aText: String): Single; var r: TRectF; begin r := RectF(0, 0, 10000, 10000); if FFastRender <> nil then FFastRender.MeasureText(r, aText, False, [], TTextAlign.taLeading, TTextAlign.taLeading) else Canvas.MeasureText(r, aText, False, [], TTextAlign.taLeading, TTextAlign.taLeading); Result := r.Width; end; procedure TAdvancedTextRenderer.SplitToParagraphs(const text: String); var style: TStyleDescriptor; lines: TStringDynArray; line: String; i, lineLength, originalCharIndex: Integer; paragraph: TParagraph; begin style := StyleDescriptor(Font.Style, Font.Color, TBaseLine.Normal); lines := SplitString(text, #10); originalCharIndex := 0; for i := 0 to Length(lines) - 1 do begin line := lines[i]; lineLength := Length(line); if (lineLength > 0) and (line[lineLength] = #13) then Delete(line, lineLength, 1); paragraph := TParagraph.Create(line, self, originalCharIndex); FParagraphs.Add(paragraph); style := paragraph.WrapLines(style); Inc(originalCharIndex, lineLength + 1); end; // skip empty paragraphs at the end for i := FParagraphs.Count - 1 downto 0 do begin if FParagraphs[i].IsEmpty then begin FParagraphs[i].Free; FParagraphs.Delete(i); end else break; end; end; procedure TAdvancedTextRenderer.AdjustParagraphLines; var i: Integer; height, offsetY: Single; paragraph: TParagraph; line: TLine; begin // calculate text height height := 0; for paragraph in Paragraphs do height := height + paragraph.Lines.Count * LineHeight; // calculate Y offset offsetY := DisplayRect.Top; if VertAlign = vaCenter then offsetY := offsetY + (DisplayRect.Height - height) / 2 else if VertAlign = vaBottom then offsetY := offsetY + (DisplayRect.Height - height) - 1; for i := 0 to Paragraphs.Count - 1 do begin paragraph := Paragraphs[i]; paragraph.AlignLines((i = Paragraphs.Count - 1) and ForceJustify); // adjust line tops for line in paragraph.Lines do begin line.Top := offsetY; line.MakeUnderlines; offsetY := offsetY + LineHeight; end; end; end; procedure TAdvancedTextRenderer.Draw; var state: TCanvasSaveState; m, OldM: TMatrix; paragraph: TParagraph; begin // set clipping // avoid bug under win platform, after state restore matrix has unexpected values OldM := Canvas.Matrix; State := Canvas.SaveState; try { increase bounds for 2 pixels } { increase Top for 1 * Scale (preview or print) pixels for subscripts } Canvas.IntersectClipRect(RectF(DisplayRect.Left, DisplayRect.Top - 1 * FScaleFactor, DisplayRect.Right + 2, DisplayRect.Bottom)); if Angle <> 0 then begin m := CreateRotationMatrix(-DegToRad(Angle)); // offset m.m31 := OldM.m31 + DisplayRect.Left + DisplayRect.Width / 2; m.m32 := OldM.m32 + DisplayRect.Top + DisplayRect.Height / 2; Canvas.SetMatrix(m); end; for paragraph in Paragraphs do paragraph.Draw; finally // restore clipping Canvas.RestoreState(state); Canvas.SetMatrix(OldM); end; end; function TAdvancedTextRenderer.CalcHeight: Single; var charsFit: Integer; style: TStyleDescriptor; begin Result := CalcHeight(charsFit, style); end; function TAdvancedTextRenderer.CalcHeight(var charsFit: Integer; var style: TStyleDescriptor): Single; var i, j: Integer; height, displayHeight: Single; line: TLine; begin charsFit := 0; style := NullStyle; height := 0; displayHeight := DisplayRect.Height; if LineHeight > displayHeight then begin Result := 0; Exit; end; for i := 0 to Paragraphs.Count - 1 do begin for j := 0 to Paragraphs[i].Lines.Count - 1 do begin line := Paragraphs[i].Lines[j]; height := height + LineHeight; if (charsFit = 0) and (height > displayHeight) then begin charsFit := line.OriginalCharIndex; if HtmlTags then style := line.Style; height := height - LineHeight; break; end; end; if (charsFit <> 0) then break; end; if charsFit = 0 then charsFit := Length(FText); Result := height ; end; function TAdvancedTextRenderer.CalcWidth: Single; var paragraph: TParagraph; line: TLine; width: Single; begin width := 0; for paragraph in Paragraphs do for line in paragraph.Lines do begin if width < line.Width then width := line.Width; end; Result := width + FSpaceWidth; end; function TAdvancedTextRenderer.GetTabPosition(pos: Single): Single; var tabOffset, tabSize: Single; tabPosition: Integer; begin tabOffset := 0; tabSize := 64; tabPosition := Trunc((pos - tabOffset) / tabSize); if pos < tabOffset then Result := tabOffset else Result := (tabPosition + 1) * tabSize + tabOffset; end; constructor TAdvancedTextRenderer.Create(aCanvas: TCanvas; const aText: String; aFont: TfrxFont; rect: TRectF; aHorzAlign: TfrxHAlign; aVertAlign: TfrxVAlign; aLineHeight: Single; aAngle: Integer; aWordWrap: Boolean; aForceJustify: Boolean; aHtmlTags: Boolean; aPdfMode: Boolean; aScaLeFactor: Single); begin if aCanvas is TfrxFastCanvasLayer then begin FFastRender := TfrxFastCanvasLayer(aCanvas); FCanvas := FFastRender.Canvas; end else FCanvas := aCanvas; FScaleFactor := aScaLeFactor; if SameValue(FScaleFactor, 0, 0.09) then FScaleFactor := 0.1; FParagraphs := TList.Create; FText := aText; FFont := aFont; FDisplayRect := rect; FHorzAlign := aHorzAlign; FVertAlign := aVertAlign; FLineHeight := aLineHeight; FFontLineHeight := aFont.GetHeight(FCanvas) + 2; if FLineHeight = 0 then FLineHeight := FFontLineHeight else FFontLineHeight := FLineHeight; FAngle := aAngle; FWordWrap := aWordWrap; FForceJustify := aForceJustify; FHtmlTags := aHtmlTags; FPDFMode := aPdfMode; aFont.AssignToCanvas(FCanvas); {$IFDEF DELPHI25} {$IFDEF MSWINDOWS} // workaround for Tokyo - DoRenderLayout now has MeasureString call and calculates one space symbol incorrect FSpaceWidth := Round(MeasureTextWidth(' ') / 5 ); {$ELSE} FSpaceWidth := MeasureTextWidth(' '); {$ENDIF} {$ELSE} FSpaceWidth := MeasureTextWidth(' '); {$ENDIF} if Canvas.ClassName = 'TCanvasGdiPlus' then FSpaceWidth := FSpaceWidth - FScaleFactor; FSymbolWidth := MeasureTextWidth('W'); if Angle <> 0 then begin // shift displayrect FDisplayRect.Left := -rect.Width / 2; FDisplayRect.Top := -rect.Height / 2; FDisplayRect.Right := FDisplayRect.Left + rect.Width; FDisplayRect.Bottom := FDisplayRect.Top + rect.Height; // rotate displayrect if angle is 90 or 270 if ((Angle >= 90) and (Angle < 180)) or ((Angle >= 270) and (Angle < 360)) then FDisplayRect := RectF(DisplayRect.Top, DisplayRect.Left, DisplayRect.Bottom, DisplayRect.Right); end; SplitToParagraphs(aText); AdjustParagraphLines; // restore original values FDisplayRect := rect; end; destructor TAdvancedTextRenderer.Destroy; var paragraph: TParagraph; begin for paragraph in Paragraphs do paragraph.Free; Paragraphs.Free; inherited; end; { TParagraph } function TParagraph.IsLast: Boolean; begin Result := FRenderer.Paragraphs[FRenderer.Paragraphs.Count - 1] = self; end; function TParagraph.IsEmpty: Boolean; begin Result := FText = ''; end; function TParagraph.WrapLines(style: TStyleDescriptor): TStyleDescriptor; var line: TLine; aWord: TWord; text, currentWord, tag, colorName: String; width, WLineWidth: Single; skipSpace, match, isLastWord: Boolean; i, originalCharIndex, istart, iend: Integer; newStyle: TStyleDescriptor; begin line := TLine.Create(self, FOriginalCharIndex); FLines.Add(line); aWord := TWord.Create(line); line.Words.Add(aWord); WLineWidth := 0; text := FText; currentWord := ''; width := 0; skipSpace := True; originalCharIndex := FOriginalCharIndex; i := 1; while i <= Length(text) do begin if Renderer.HtmlTags and (text[i] = '<') then begin // probably html tag newStyle := style; tag := ''; match := False; // , , if i + 3-1 <= Length(text) then begin match := True; tag := LowerCase(Copy(text, i, 3)); if tag = '' then newStyle.FontStyle := newStyle.FontStyle + [fsBold] else if tag = '' then newStyle.FontStyle := newStyle.FontStyle + [fsItalic] else if tag = '' then newStyle.FontStyle := newStyle.FontStyle + [fsUnderline] else match := False; if match then Inc(i, 3); end; // , , if not match and (i + 4-1 <= Length(text)) and (text[i + 1] = '/') then begin match := True; tag := LowerCase(Copy(text, i, 4)); if tag = '' then newStyle.FontStyle := newStyle.FontStyle - [fsBold] else if tag = '' then newStyle.FontStyle := newStyle.FontStyle - [fsItalic] else if tag = '' then newStyle.FontStyle := newStyle.FontStyle - [fsUnderline] else match := False; if match then Inc(i, 4); end; // , if not match and (i + 5-1 <= Length(text)) then begin match := True; tag := LowerCase(Copy(text, i, 5)); if tag = '' then newStyle.BaseLine := TBaseLine.Subscript else if tag = '' then newStyle.BaseLine := TBaseLine.Superscript else match := False; if match then Inc(i, 5); end; // , if not match and (i + 6-1 <= Length(text)) and (text[i + 1] = '/') then begin match := True; tag := LowerCase(Copy(text, i, 6)); if (tag = '') or (tag = '') then newStyle.BaseLine := TBaseLine.Normal else match := False; if match then Inc(i, 6); end; // if not match and (i + 8-1 <= Length(text)) then begin tag := LowerCase(Copy(text, i, 8)); if tag = '' then begin newStyle.FontStyle := newStyle.FontStyle + [fsStrikeout]; match := True; Inc(i, 8); end; end; // if not match and (i + 9-1 <= Length(text)) then begin tag := LowerCase(Copy(text, i, 9)); if tag = '' then begin newStyle.FontStyle := newStyle.FontStyle - [fsStrikeout]; match := True; Inc(i, 9); end; end; // '=') do Inc(istart); Inc(istart); while (istart <= Length(text)) and (text[istart] = ' ') do Inc(istart); iend := istart; while (iend <= Length(text)) and (text[iend] <> '>') do Inc(iend); if iend <= Length(text) then begin colorName := Copy(text, istart, iend - istart); if (colorName[1] = '"') and (colorName[Length(colorName)] = '"') then colorName := Copy(colorName, 2, Length(colorName) - 2); newStyle.Color := StringToAlphaColor(colorName); i := iend + 1; match := True; end; end; end; // if not match and (i + 7-1 <= Length(text)) then begin tag := LowerCase(Copy(text, i, 7)); if tag = '' then begin newStyle.Color := Renderer.Font.Color; match := True; Inc(i, 7); end; end; if match then begin // finish the word if currentWord <> '' then aWord.Runs.Add(TRun.Create(currentWord, style, aWord)); currentWord := ''; style := newStyle; if i >= Length(text) then begin // check width width := width + aWord.Width + Renderer.SpaceWidth; if width > Trunc(Renderer.DisplayRect.Width - 0.5) then begin // line is too long, make a new line if line.Words.Count > 1 then begin // if line has several words, delete the last word from the current line line.Words.Delete(line.Words.Count - 1); // make new line line := TLine.Create(self, originalCharIndex); // and add word to it line.Words.Add(aWord); aWord.SetLine(line); FLines.Add(line); end; end; end; continue; end; end; if (text[i] = ' ') or (text[i] = #9) or (i = Length(text)) or (WLineWidth > 0) then begin // finish the last word isLastWord := i = Length(text); if isLastWord then begin currentWord := currentWord + text[i]; skipSpace := False; end; if text[i] = #9 then skipSpace := False; // space if skipSpace then currentWord := currentWord + text[i] else begin // finish the word if currentWord <> '' then aWord.Runs.Add(TRun.Create(currentWord, style, aWord)); // check width width := width + aWord.Width + Renderer.SpaceWidth; // we need addition space witdth to avoid exeeds of bounds with different scale if (width > Trunc(Renderer.DisplayRect.Width - 0.5)) or (WLineWidth > 0) then begin WLineWidth := 0; // line is too long, make a new line width := 0; if line.Words.Count > 1 then begin // if line has several words, delete the last word from the current line line.Words.Delete(line.Words.Count - 1); // make new line line := TLine.Create(self, originalCharIndex); // and add word to it line.Words.Add(aWord); aWord.SetLine(line); width := width + aWord.Width + Renderer.SpaceWidth; end else line := TLine.Create(self, FOriginalCharIndex + i + 1); FLines.Add(line); end; // TAB symbol if text[i] = #9 then begin if currentWord = '' then line.Words.Delete(line.Words.Count - 1); aWord := TTab.Create(line); line.Words.Add(aWord); // adjust width width := Renderer.GetTabPosition(width); end; if not isLastWord then begin aWord := TWord.Create(line); line.Words.Add(aWord); currentWord := ''; originalCharIndex := FOriginalCharIndex + i + 1; skipSpace := True; WLineWidth := 0; end; end; end else begin // symbol currentWord := currentWord + text[i]; skipSpace := False; // word wrap - do something better than call measure text every time // TODO // use aproximate width first if FRenderer.WordWrap and (Length(currentWord) * renderer.SymbolWidth >= Trunc(renderer.DisplayRect.Width - 0.5)) then begin while (Renderer.MeasureTextWidth(currentWord) > Trunc(renderer.DisplayRect.Width - 0.5)) and (Length(currentWord) > 1) do begin Dec(i); Delete(currentWord, Length(currentWord), 1); WLineWidth := 1; // used as flag end; if (WLineWidth = 1) or (Length(currentWord) = 1) then begin Dec(i, 1); WLineWidth := Renderer.MeasureTextWidth(currentWord); end; end; end; Inc(i); end; Result := style; end; procedure TParagraph.AlignLines(forceJustify: Boolean); var i: Integer; align: TfrxHAlign; begin for i := 0 to Lines.Count - 1 do begin align := Renderer.HorzAlign; if (align = TfrxHAlign.haBlock) and (i = Lines.Count - 1) and not forceJustify then align := TfrxHAlign.haLeft; Lines[i].AlignWords(align); end; end; procedure TParagraph.Draw; var line: TLine; begin for line in Lines do line.Draw; end; constructor TParagraph.Create(const text: String; renderer: TAdvancedTextRenderer; originalCharIndex: Integer); begin FLines := TList.Create; FText := text; FRenderer := renderer; FOriginalCharIndex := originalCharIndex; end; destructor TParagraph.Destroy; var line: TLine; begin for line in Lines do line.Free; Lines.Free; inherited; end; { TLine } function TLine.GetLeft: Single; begin if Words.Count > 0 then Result := Words[0].Left else Result := 0; end; function TLine.GetRenderer: TAdvancedTextRenderer; begin Result := FParagraph.Renderer; end; function TLine.GetStyle: TStyleDescriptor; begin Result := NullStyle; if Words.Count > 0 then if Words[0].Runs.Count > 0 then Result := Words[0].Runs[0].Style; end; function TLine.GetText: String; var word: TWord; begin Result := ''; for word in Words do begin if Result <> '' then Result := Result + ' '; Result := Result + word.Text; end; end; function TLine.IsLast: Boolean; begin Result := FParagraph.Lines[FParagraph.Lines.Count - 1] = self; end; procedure TLine.PrepareUnderlines(list: TList; style: TFontStyle); var left, right: Single; styleOn: Boolean; aWord: TWord; run: TRun; fnt: TfrxFont; begin list.Clear; if Words.Count = 0 then Exit; left := 0; right := 0; styleOn := False; for aWord in Words do begin for run in aWord.Runs do begin fnt := run.GetFont; if style in fnt.Style then begin if not styleOn then begin styleOn := True; left := run.Left; end; right := run.Left + run.Width; end; if not (style in fnt.Style) and styleOn then begin styleOn := False; list.Add(RectF(left, Top, right, Top)); end; fnt.Free; end; end; // close the style if styleOn then list.Add(RectF(left, Top, right, Top)); end; procedure TLine.AlignWords(align: TfrxHAlign); var left, rectWidth, delta, curDelta: Single; i: Integer; aWord: TWord; begin FWidth := 0; left := 0; i := 0; while i < Words.Count do begin aWord := Words[i]; aWord.Left := left; if aWord is TTab then begin left := Renderer.GetTabPosition(left); // remove tab Words.Delete(i); aWord.Free; Dec(i); end else left := left + aWord.Width + Renderer.SpaceWidth; Inc(i); end; FWidth := left; rectWidth := Renderer.DisplayRect.Width; if align = TfrxHAlign.haBlock then begin if Words.Count > 1 then begin delta := (rectWidth - FWidth) / (Words.Count - 1); curDelta := delta; for i := 1 to Words.Count - 1 do begin FWords[i].Left := FWords[i].Left + curDelta; curDelta := curDelta + delta; end; end; end else begin delta := 0; if align = TfrxHAlign.haCenter then delta := (rectWidth - FWidth) / 2 else if align = TfrxHAlign.haRight then delta := rectWidth - (FWidth - Renderer.SpaceWidth); for aWord in Words do aWord.Left := aWord.Left + delta; end; // adjust X offset for aWord in Words do begin if Renderer.RightToLeft then aWord.Left := Renderer.DisplayRect.Right - aWord.Left else aWord.Left := aWord.Left + Renderer.DisplayRect.Left; aWord.AdjustRuns; if Renderer.RightToLeft and Renderer.PDFMode then aWord.Left := aWord.Left - aWord.Width; end; end; procedure TLine.MakeUnderlines; begin PrepareUnderlines(FUnderlines, TFontStyle.fsUnderline); PrepareUnderlines(FStrikeouts, TFontStyle.fsStrikeout); end; procedure TLine.Draw; var aWord: TWord; h, w, th: Single; rect: TRectF; begin for aWord in Words do aWord.Draw; if (Underlines.Count > 0) or (Strikeouts.Count > 0) then begin h := Renderer.FontLineHeight; w := h * 0.1; // to match char X offset // invert offset in case of rtl if Renderer.RightToLeft then w := -w; th := Round(Renderer.Font.Size * 0.1); {$IFDEF Delphi25} Renderer.Canvas.Stroke.Thickness := th; {$ELSE} Renderer.Canvas.StrokeThickness := th; {$ENDIF} Renderer.Canvas.Stroke.Color := Renderer.Font.Color; // emulate underline & strikeout for rect in Underlines do Renderer.Canvas.DrawLine( PointF(rect.Left + w, AdjY(rect.Top + h - w, th)), PointF(rect.Right + w, AdjY(rect.Top + h - w, th)), 1); h := h / 2; for rect in Strikeouts do Renderer.Canvas.DrawLine( PointF(rect.Left + w, AdjY(rect.Top + h, th)), PointF(rect.Right + w, AdjY(rect.Top + h, th)), 1); end; end; constructor TLine.Create(paragraph: TParagraph; originalCharIndex: Integer); begin FWords := TList.Create; FParagraph := paragraph; FOriginalCharIndex := originalCharIndex; FUnderlines := TList.Create; FStrikeouts := TList.Create; end; destructor TLine.Destroy; var aWord: TWord; begin for aWord in Words do aWord.Free; Words.Free; Underlines.Free; Strikeouts.Free; inherited; end; { TWord } function TWord.GetText: String; var run: TRun; begin Result := ''; for run in Runs do begin Result := Result + run.Text; end; end; function TWord.GetTop: Single; begin Result := FLine.Top; end; function TWord.GetRenderer: TAdvancedTextRenderer; begin Result := FLine.Renderer; end; function TWord.GetWidth: Single; var run: TRun; begin if FWidth = -1 then begin FWidth := 0; for run in Runs do FWidth := FWidth + run.Width; end; Result := FWidth; end; procedure TWord.AdjustRuns; var aLeft: Single; run: TRun; begin aLeft := Left; for run in Runs do begin run.Left := aLeft; if Renderer.RightToLeft then begin aLeft := aLeft - run.Width; if Renderer.PDFMode then run.Left := run.Left - run.Width; end else aLeft := aLeft + run.Width; end; end; procedure TWord.SetLine(aLine: TLine); begin FLine := aLine; end; procedure TWord.Draw; var run: TRun; begin for run in Runs do run.Draw; end; constructor TWord.Create(aLine: TLine); begin FRuns := TList.Create; FLine := aLine; FWidth := -1; end; destructor TWord.Destroy; var run: TRun; begin for run in Runs do run.Free; FRuns.Free; inherited; end; { TRun } function TRun.GetRenderer: TAdvancedTextRenderer; begin Result := FWord.Renderer; end; function TRun.GetTop: Single; var baseLine: Single; begin baseLine := 0; if Style.BaseLine = TBaseLine.Subscript then baseLine := baseLine + Renderer.FontLineHeight * 0.45 else if Style.BaseLine = TBaseLine.Superscript then baseLine := baseLine - Renderer.FontLineHeight * 0.15; Result := FWord.Top + baseLine; end; function TRun.GetFont(disableUnderlinesStrikeouts: Boolean): TfrxFont; var fontSize: Single; fontStyle: TFontStyles; begin fontSize := Renderer.Font.Size; if Style.BaseLine <> TBaseLine.Normal then fontSize := fontSize * 0.6; fontStyle := Style.FontStyle; if disableUnderlinesStrikeouts then fontStyle := fontStyle - [fsUnderline, fsStrikeout]; Result := TfrxFont.Create; Result.Name := Renderer.Font.Name; Result.Color := Style.Color; Result.Size := fontSize; Result.Style := fontStyle; end; function TRun.GetFont: TfrxFont; begin Result := GetFont(false); end; procedure TRun.Draw; var font: TfrxFont; begin font := GetFont(True); font.AssignToCanvas(Renderer.Canvas); if Renderer.FFastRender <> nil then Renderer.FFastRender.FillText(RectF(Left, Top, Left + 10000, Top + 10000), FText, False, 1, [], TTextAlign.taLeading, TTextAlign.taLeading) else Renderer.Canvas.FillText(RectF(Left, Top, Left + 10000, Top + 10000), FText, False, 1, [], TTextAlign.taLeading, TTextAlign.taLeading); font.Free; end; constructor TRun.Create(const aText: String; aStyle: TStyleDescriptor; aWord: TWord); var font: TfrxFont; begin FText := aText; FStyle := aStyle; FWord := aWord; font := GetFont; font.AssignToCanvas(Renderer.Canvas); FWidth := Renderer.MeasureTextWidth(aText); font.Free; end; end.