matrixFont/source/func/u_map_render.pas
riva 8f95708c07 refactoring & formatting:
- symbol.pas:
    class TSymbol       -> TMatrixChar
    type  TSymbolField  -> TCharCanvas
    type  TPSymbolField -> TPCharCanvas
    field FSymbol       -> FCharCanvas
    prop  Symbol        -> CharCanvas
    proc  LoadSymbol    -> LoadChar

 - font.pas:
    class TFont         -> TMatrixFont
    type  TSymbolField  -> TCharCanvas
    type  TFontSet      -> TMxCharArray
    field FSymbol       -> FCharArray

 - fm_gen.pas:
    var   FontSet       -> mxFont
2024-05-31 15:10:05 +03:00

497 lines
12 KiB
ObjectPascal

{ unit u_map_render.pas
---------------------
Provides functions for rendering char map to PNG image.
(c) Riva, 2024
This file is the part of matrixFont project.
}
unit u_map_render;
{$mode ObjFPC}{$H+}
interface
uses
Classes, SysUtils, Types, Graphics, FPImgCmn, base64,
font, symbol, appAbout, u_helpers;
function AddTextChunkToPNG(const AFilename, AKey, AValue: String): Boolean;
function GetTextChunkFromPNG(const AFilename, AKey: String): String;
procedure ImplantWatermark(ABitmap: TBitmap; AData: String);
function ExtractWatermark(ABitmap: TBitmap): String;
procedure RenderMapToPNG(AFileName: String; AFont: TMatrixFont; ACols, AScale, ASpace: Integer;
AColorBG, AColor0, AColor1: TColor; ALabelFont: String);
function IsImageContainFontSet(AFileName: String; out AMetaData: String): Boolean;
function ImportFontFromPNG(AFileName, AMetaData: String; AFontOut: TMatrixFont): Boolean;
implementation
function SwapDWord(This: DWord): DWord;
begin
{$IFDEF ENDIAN_LITTLE}
Result := Swap(This);
{$ELSE}
Result := This;
{$ENDIF}
end;
function AddTextChunkToPNG(const AFilename, AKey, AValue: String): Boolean;
var
_fs: TFileStream;
_chunk: TMemoryStream;
_len: LongWord;
_data: String;
IEND: array[0..11] of Byte;
begin
Result := False;
if not (Length(AKey) in [1..79]) then Exit;
_fs := TFileStream.Create(AFilename, fmOpenReadWrite);
try
_fs.Seek(-12, soFromEnd); // go to 'IEND' chunk position
_fs.Read(IEND, SizeOf(IEND)); // backup 'IEND' chunk
_fs.Seek(-12, soFromEnd); // go to 'IEND' chunk position
_chunk := TMemoryStream.Create;
try
_data := AKey + #0 + AValue;
_len := Length(_data);
_data := 'tEXt' + _data;
_chunk.Write(SwapDWord(_len), 4);
_chunk.Write(_data[1], Length(_data));
_chunk.Write(SwapDWord(CalculateCRC(_data[1], Length(_data))), 4);
_chunk.Position := 0;
_fs.CopyFrom(_chunk, 0);
_fs.Write(IEND, SizeOf(IEND));
Result := True;
finally
_chunk.Free;
end;
finally
_fs.Free;
end;
end;
function GetTextChunkFromPNG(const AFilename, AKey: String): String;
var
_fs: TFileStream;
_buf: TBytes;
_name: array[0..3] of Char;
_tmp, _len, _crc: LongWord;
begin
Result := '';
_fs := TFileStream.Create(AFilename, fmOpenRead);
try
// skip header
_fs.Seek(8, soFromBeginning);
// search for 'tEXt' chunk
while _fs.Read(_len, 4) > 0 do
begin
_len := SwapDWord(_len);
_fs.Read(_name, 4);
if _name <> 'tEXt' then
_fs.Seek(_len + 4, soFromCurrent) // skip
else
begin
SetLength(_buf, _len);
_fs.Read(_buf[0], _len);
_fs.Read(_tmp, 4);
_crc := CalculateCRC($FFFFFFFF, _name[0], 4);
_crc := CalculateCRC(_crc, _buf[0], _len);
_crc := SwapDWord(_crc xor $FFFFFFFF);
if _crc <> _tmp then Exit;
Result := String(StringOf(_buf));
if Result.StartsWith(AKey + #0) then
Break else
Result := '';
end;
end;
finally
_fs.Free;
end;
Result := Result.Replace(AKey + #0, '');
end;
procedure ImplantWatermark(ABitmap: TBitmap; AData: String);
var
i, j, x, y: Integer;
b: Byte;
_buf: TBytes;
_crc: Cardinal;
begin
_buf := BytesOf(Chr(AData.Length div $100) + Chr(AData.Length mod $100) + AData + '_crc');
_crc := CalculateCRC(_buf[0], Length(_buf) - 4);
for i := 1 to 4 do
begin
_buf[Length(_buf) - i] := _crc mod $100;
_crc := _crc div $100;
end;
for i := 0 to High(_buf) do
begin
y := (i * 8) div ABitmap.Width;
x := (i * 8) mod ABitmap.Width;
b := _buf[i];
for j := 0 to 7 do
with ABitmap.Canvas do
begin
Pixels[x, y] := 2 * (Pixels[x, y] div 2);
Pixels[x, y] := Pixels[x, y] + (b mod 2);
b := b div 2;
x += 1;
end;
end;
end;
function ExtractWatermark(ABitmap: TBitmap): String;
var
i, x, y: Integer;
_len: Integer;
_buf: TBytes;
_crc: Cardinal = 0;
function ReadByte: Byte;
var
i: Integer;
r: Integer = 0;
begin
if x + 8 > ABitmap.Width then
begin
x := 0;
y += 1;
end;
for i := 0 to 7 do
r := r div 2 + $80 * (ABitmap.Canvas.Pixels[x + i, y] mod 2);
x += 8;
Result := Byte(r);
end;
begin
x := 0;
y := 0;
Result := '';
SetLength(_buf, 2);
_buf[0] := ReadByte;
_buf[1] := ReadByte;
_len := _buf[0] * $100 + _buf[1];
with ABitmap.Canvas do
if _len > Width * Height div 8 then Exit;
SetLength(_buf, _len + 2);
for i := 1 to _len do _buf[1 + i] := ReadByte;
for i := 1 to 4 do _crc := _crc * $100 + ReadByte;
if _crc <> CalculateCRC(_buf[0], Length(_buf)) then
Result := '' else
Result := String(StringOf(_buf)).Remove(0, 2);
end;
procedure RenderMapToPNG(AFileName: String; AFont: TMatrixFont; ACols, AScale, ASpace: Integer;
AColorBG, AColor0, AColor1: TColor; ALabelFont: String);
const
MX: Integer = 3; // top left char horz offset (counting in char places)
MY: Integer = 5; // top left char vert offset (counting in char places)
var
_export, _bmp: TBitmap;
_scaled: TPicture;
_w, _h, _r: Integer;
_meta: String;
procedure ExportInit(AMapX, AMapY: Integer);
var
_txtExt: TSize;
begin
_w := ASpace * 2 + AFont.Width;
_h := ASpace * 2 + AFont.Height;
_r := 1 + (AFont.FontLength - 1) div ACols;
_bmp := TBitmap.Create;
_export := TBitmap.Create;
_scaled := TPicture.Create;
_bmp.SetSize(AFont.Width, AFont.Height);
_export.SetSize((ACols + AMapX) * _w, (_r + AMapY) * _h);
_scaled.Bitmap.SetSize(AScale * _export.Width, AScale * _export.Height);
with _export.Canvas do
begin
Brush.Color := AColorBG;
Clear;
Clear;
end;
with _scaled.Bitmap.Canvas do
begin
Brush.Color := AColorBG;
Font.Color := AColor1;
Font.Name := ALabelFont;
_txtExt := TextExtent('0');
Font.Height := trunc(_w * AScale / 4 / _txtExt.Width * _txtExt.Height);
end;
end;
procedure DrawChar(AIndex, AX, AY: Integer);
begin
if not (AIndex in [0..AFont.FontLength - 1]) then Exit;
AFont.Item[AIndex].DrawPreview(_bmp, False, AColor0, AColor1);
_export.Canvas.Draw(_w * AX + ASpace, _h * AY + ASpace, _bmp);
end;
procedure DrawMap(AX, AY: Integer);
var
x, y: Integer;
begin
// render char map in 1:1 AScale
for x := 0 to ACols - 1 do
for y := 0 to _r - 1 do
DrawChar(y * ACols + x, x + AX, y + AY);
// output bitmap scaling
with _scaled.Bitmap.Canvas do
StretchDraw(Rect(0, 0, Width, Height), _export);
end;
procedure DrawInfo(AX, AY: Integer; S: String; CenterX: Boolean = False);
var
dx, dy: Integer;
begin
with _scaled.Bitmap.Canvas do
begin
if AX < 0 then
dx := (Width - TextWidth(s)) div 2 else
dx := _w * AScale * AX + CenterX.Select((_w * AScale - TextWidth(s)) div 2, 0);
dy := _h * AScale * AY + (_h * AScale - TextHeight(s)) div 2;
TextOut(dx, dy, S);
end;
end;
procedure DrawOffset(AIndex, AX, AY: Integer; AHorz: Boolean);
var
s: String;
begin
if not AIndex in [0..AFont.FontLength] then Exit;
if AHorz then
s := Format('+%x', [AIndex]) else
s := Format('%.2x', [AFont.FontStartItem + AIndex * ACols]);
DrawInfo(AX, AY, s, True);
end;
procedure DrawOffsets(AX, AY: Integer);
var
i: Integer;
begin
// horizontal offsets
for i := 0 to ACols - 1 do DrawOffset(i, i + AX, AY - 2, True);
// vertical offsets
for i := 0 to _r - 1 do DrawOffset(i, AX - 2, i + AY, False);
end;
function EncodeMetadata(AOffsetX, AOffsetY: Integer): String;
begin
with TStringList.Create do
begin
Add('matrixFontMeta'); // magic
Add(AFont.Name);
Add(AFont.Author);
Add(AFont.Encoding);
Add(AFont.AppChange);
Add(AFont.FontStartItem.ToString);
Add(AFont.FontLength.ToString);
Add(AFont.Width.ToString);
Add(AFont.Height.ToString);
Add(AScale.ToString);
Add(ASpace.ToString);
Add(ACols.ToString);
Add(AOffsetX.ToString);
Add(AOffsetY.ToString);
Result := CommaText;
Free;
end;
end;
begin
if not Assigned(AFont) then Exit;
try
ExportInit(MX + 1, MY + 1);
_meta := EncodeMetadata(MX, MY);
DrawMap(MX, MY);
DrawOffsets(MX, MY);
DrawInfo(-1, 0, Format('Font "%s" by ©%s', [AFont.Name, AFont.Author]));
DrawInfo(-1, 1, Format('Encoding: %s', [AFont.Encoding]));
DrawInfo(-1, MY + _r, 'Created in ' + GetAppNameVersion);
// place digital watermark on image with font metadata
ImplantWatermark(_scaled.Bitmap, _meta);
_scaled.SaveToFile(AFileName, 'png');
finally
FreeAndNil(_bmp);
FreeAndNil(_export);
FreeAndNil(_scaled);
AddTextChunkToPNG(AFileName, 'matrixFontMeta', EncodeStringBase64(_meta));
end;
end;
function IsImageContainFontSet(AFileName: String; out AMetaData: String): Boolean;
begin
Result := False;
AMetaData := '';
with TPicture.Create do
try
LoadFromFile(AFileName);
if LowerCase(ExtractFileExt(AFileName)) = '.png' then
AMetaData := DecodeStringBase64(GetTextChunkFromPNG(AFileName, 'matrixFontMeta'));
if AMetaData.IsEmpty then AMetaData := ExtractWatermark(Bitmap);
finally
Free;
end;
Result := not AMetaData.IsEmpty;
end;
function ImportFontFromPNG(AFileName, AMetaData: String; AFontOut: TMatrixFont): Boolean;
var
pic: TPicture;
pixRef: TColor;
isRefSet: Boolean = False;
ch: Integer = 0;
scale, space, cols, offsetX, offsetY: Integer;
function DecodeMetadata(S: String): Boolean;
var
i: Integer = 0;
l: TStringList;
function NextString: String;
begin
Result := l.Strings[i];
Inc(i);
end;
function NextInteger: Integer;
begin
Result := NextString.ToInteger;
end;
begin
Result := False;
l := TStringList.Create;
l.CommaText := S;
if NextString.Equals('matrixFontMeta') then
begin
with AFontOut do
begin
Result := True;
Name := NextString;
Author := NextString;
Encoding := NextString;
AppCreate := NextString;
AppCurrent := GetAppNameVersion;
FontStartItem := NextInteger;
FontLength := NextInteger;
Width := NextInteger;
Height := NextInteger;
end;
scale := NextInteger;
space := NextInteger;
cols := NextInteger;
offsetX := NextInteger;
offsetY := NextInteger;
end;
l.Free;
end;
procedure GetRefPixel(AX, AY: Integer);
begin
if not isRefSet then
begin
pixRef := pic.Bitmap.Canvas.Pixels[AX, AY];
isRefSet := True;
end;
end;
function ExtractNextChar: Boolean;
var
i, j, x, y: Integer;
begin
Result := False;
try
if not (ch in [0..AFontOut.FontLength - 1]) then Exit;
x := scale * (space + (offsetX + ch mod cols) * (2 * space + AFontOut.Width));
y := scale * (space + (offsetY + ch div cols) * (2 * space + AFontOut.Height));
GetRefPixel(x, y);
for i := 0 to AFontOut.Width - 1 do
for j := 0 to AFontOut.Height - 1 do
with pic.Bitmap.Canvas do
AFontOut.Item[ch].PixelAction(i, j,
(abs(Pixels[x + i * scale, y + j * scale] - pixRef) > 1).Select(paSet, paClear));
Result := True;
Inc(ch);
except
end;
end;
function ExtractFont: Boolean;
begin
Result := False;
while ExtractNextChar do ;
Result := True;
end;
begin
try
pic := TPicture.Create;
pic.LoadFromFile(AFileName);
Result := DecodeMetadata(AMetaData) and ExtractFont;
except
Result := False;
end;
pic.Free;
end;
end.