[教程]为 FMX TPathData 增加旋转及编码优化

话不多说,直接 Show code:

1. 声明

TZPathData = class helper for TPathData
private
    function GetAsBytes:TBytes;
    function SetAsBytes(const ABytes:TBytes);
    function GetAsString:String;
    function SetAsString(const S:String);
public
    procedure Rotate(const Angle:Single);
    property AsString:String read GetAsString write SetAsString;
    property AsBytes:TBytes read GetAsBytes write SetAsBytes;
end;
  • Rotate : 将当前路径旋转指定的角度
  • AsString : 用于替换 Data 属性,重新实际编码函数,以减少生成的路径字符串体积
  • AsBytes : 按二进制的方式来进一步编码压缩路径数据

2. 实现

function TZPathData.GetAsBytes: TBytes;
// 压缩下指令,直接后面跟最多63个同一类型的值
// [Op:2][Count:6][DataArray]
// DataArray 存储的是绝对坐标,单精度是32位,这块我们不再压缩,毕竟和损失精度相比,收益并不明显
var
  I, ACount: Integer;
const
  OpCodes: array [TZPathPointKind] of Byte = (OP_MOVE_TO, OP_LINE_TO, OP_CURVE_TO, OP_CLOSE);
  procedure MovePoints(AKind: TZPathPointKind; AItemPoints: Integer);
  var
    pValue: PPointF;
    c: Integer;
  begin
    Result[ACount] := OpCodes[AKind];
    pValue := @Result[ACount + 1];
    repeat
      for c := 0 to AItemPoints - 1 do
      begin
        pValue^ := FPathData[I].Point;
        Inc(pValue);
        Inc(I);
      end;
      Result[ACount] := OpCodes[AKind] or ((Result[ACount] and $3F) + 1);
    until (I = Count) or (FPathData[I].Kind <> AKind) or (I = 63);
    Inc(ACount, 1 + (Result[ACount] and $3F) * AItemPoints * SizeOf(TPointF));
  end;

begin
  I := 0;
  ACount := 0;
  SetLength(Result, (1 + Count * SizeOf(Single) * 3) * Count);
  while I < Count do
  begin
    case FPathData[I].Kind of
      TZPathPointKind.MoveTo, TZPathPointKind.LineTo:
        MovePoints(FPathData[I].Kind, 1);
      TZPathPointKind.CurveTo:
        MovePoints(FPathData[I].Kind, 3);
      TZPathPointKind.Close:
        begin
          Result[ACount] := OP_CLOSE;
          Inc(ACount);
          Inc(I);
        end;
    end;
  end;
  SetLength(Result, ACount);
end;

procedure TZPathData.SetAsBytes(const ABytes: TBytes);
var
  I, ACount: Integer;
begin
  I := 0;
  FPathData.Clear;
  while I < Length(ABytes) do
  begin
    case ABytes[I] and $C0 of
      OP_MOVE_TO:
        begin
          ACount := ABytes[I] and $3F;
          Inc(I);
          repeat
            MoveTo(PPointF(@ABytes[I])^);
            Inc(I, SizeOf(TPointF));
            Dec(ACount);
          until (ACount = 0) or (I = Length(ABytes));
        end;
      OP_LINE_TO:
        begin
          ACount := ABytes[I] and $3F;
          Inc(I);
          repeat
            LineTo(PPointF(@ABytes[I])^);
            Inc(I, SizeOf(TPointF));
            Dec(ACount);
          until (ACount = 0) or (I = Length(ABytes));
        end;
      OP_CURVE_TO:
        begin
          ACount := ABytes[I] and $3F;
          Inc(I);
          repeat
            CurveTo(PPointF(@ABytes[I])^, PPointF(@ABytes[I + SizeOf(TPointF)])^,
              PPointF(@ABytes[I + SizeOf(TPointF) * 2])^);
            Inc(I, SizeOf(TPointF) * 3);
            Dec(ACount);
          until (ACount = 0) or (I = Length(ABytes));
        end;
      OP_CLOSE:
        begin
          ClosePath;
          Inc(I);
        end;
    end;
  end;
end;

procedure TZPathData.Rotate(const Angle: Single);
begin
  ApplyMatrix(TMatrix.CreateRotation(Angle));
end;

function TZPathData.GetAsString: string;
var
  I: Integer;
  Builder: TStringBuilder;
  ALastPos, pt: TPointF;
  ALastKind: TZPathPointKind;
  AIsRelative: Boolean;
begin
  // 代码优化自 FMX 的 TPathData,通过减少重复指令和使用相对坐标来减少内容
  Builder := TStringBuilder.Create;
  try
    I := 0;
    AIsRelative := False;
    ALastKind := TZPathPointKind.Close;
    while I < Count do
    begin
      if AIsRelative then
      begin
        pt.X := FPathData[I].Point.X - ALastPos.X;
        pt.Y := FPathData[I].Point.Y - ALastPos.Y;
        case FPathData[I].Kind of
          TZPathPointKind.MoveTo:
            begin
              if ALastKind <> FPathData[I].Kind then
                Builder.Append('m');
              Builder.Append(FloatToStr(pt.X, TFormatSettings.Invariant)).Append(',')
                .Append(FloatToStr(pt.Y, TFormatSettings.Invariant)).Append(' ');
            end;
          TZPathPointKind.LineTo:
            begin
              if ALastKind <> FPathData[I].Kind then
                Builder.Append('l');
              Builder.Append(FloatToStr(pt.X, TFormatSettings.Invariant)).Append(',')
                .Append(FloatToStr(pt.Y, TFormatSettings.Invariant)).Append(' ');
            end;
          TZPathPointKind.CurveTo:
            begin
              if ALastKind <> FPathData[I].Kind then
                Builder.Append('c');
              Builder.Append(FloatToStr(pt.X, TFormatSettings.Invariant)).Append(',')
                .Append(FloatToStr(pt.Y, TFormatSettings.Invariant)).Append(' ');

              Builder.Append(FloatToStr(FPathData[I + 1].Point.X - ALastPos.X, TFormatSettings.Invariant)).Append(',')
                .Append(FloatToStr(FPathData[I + 1].Point.Y - ALastPos.Y, TFormatSettings.Invariant)).Append(' ');

              Builder.Append(FloatToStr(FPathData[I + 2].Point.X - ALastPos.X, TFormatSettings.Invariant)).Append(',')
                .Append(FloatToStr(FPathData[I + 2].Point.Y - ALastPos.Y, TFormatSettings.Invariant)).Append(' ');

              Inc(I, 2);
            end;
          TZPathPointKind.Close:
            begin
              Builder.Append('Z ');
              AIsRelative := False;
            end;
        end;
      end
      else
      begin
        AIsRelative := True;
        case FPathData[I].Kind of
          TZPathPointKind.MoveTo:
            Builder.Append('M').Append(FloatToStr(FPathData[I].Point.X, TFormatSettings.Invariant)).Append(',')
              .Append(FloatToStr(FPathData[I].Point.Y, TFormatSettings.Invariant)).Append(' ');
          TZPathPointKind.LineTo:
            Builder.Append('L').Append(FloatToStr(FPathData[I].Point.X, TFormatSettings.Invariant)).Append(',')
              .Append(FloatToStr(FPathData[I].Point.Y, TFormatSettings.Invariant)).Append(' ');
          TZPathPointKind.CurveTo:
            begin
              Builder.Append('C').Append(FloatToStr(FPathData[I].Point.X, TFormatSettings.Invariant)).Append(',')
                .Append(FloatToStr(FPathData[I].Point.Y, TFormatSettings.Invariant)).Append(' ');

              Builder.Append(FloatToStr(FPathData[I + 1].Point.X, TFormatSettings.Invariant)).Append(',')
                .Append(FloatToStr(FPathData[I + 1].Point.Y, TFormatSettings.Invariant)).Append(' ');

              Builder.Append(FloatToStr(FPathData[I + 2].Point.X, TFormatSettings.Invariant)).Append(',')
                .Append(FloatToStr(FPathData[I + 2].Point.Y, TFormatSettings.Invariant)).Append(' ');

              Inc(I, 2);
            end;
          TZPathPointKind.Close:
            begin
              Builder.Append('Z ');
              AIsRelative := False;
            end;
        end;
      end;
      ALastKind := FPathData[I].Kind;
      ALastPos := FPathData[I].Point;
      Inc(I);
    end;
    Result := Builder.ToString(True);
  finally
    Builder.Free;
  end;
end;
procedure TZPathData.SetAsString(const Value: string);
begin
  Data:=Value;
end;

3. 实际效果

原始路径图像:

  • 通过 Data 属性获取路径字符串长度: 2303 字节 (100%)
  • 使用 AsString 属性获取路径字符串长度: 1841 字节 (79.9%)
  • 通过 AsBytes 生成的二进制序列长度: 796 字节 (34.6%)

旋转 45 度后的图像:

滚动至顶部