diff --git a/Assets/font.png b/Assets/font.png new file mode 100644 index 0000000..b4ca210 Binary files /dev/null and b/Assets/font.png differ diff --git a/Image2Display/Image2Display/Assets/Languages/en-US.axaml b/Image2Display/Image2Display/Assets/Languages/en-US.axaml index 7ffdeda..d1005aa 100644 --- a/Image2Display/Image2Display/Assets/Languages/en-US.axaml +++ b/Image2Display/Image2Display/Assets/Languages/en-US.axaml @@ -193,6 +193,7 @@ Copy to Clipboard Export File Total Characters + Fail to load font file, format error. Settings General diff --git a/Image2Display/Image2Display/Assets/Languages/zh-CN.axaml b/Image2Display/Image2Display/Assets/Languages/zh-CN.axaml index 35843dd..3bcd31e 100644 --- a/Image2Display/Image2Display/Assets/Languages/zh-CN.axaml +++ b/Image2Display/Image2Display/Assets/Languages/zh-CN.axaml @@ -132,7 +132,7 @@ 最低有效位(LSB) 成功 数据已导出。 - 导出失败 + 失败 导出调色板 导出当前图片所使用的所有颜色 导出数据 @@ -196,6 +196,7 @@ 复制到剪贴板 导出文件 总字符数量 + 字体文件加载失败,文件格式不正确。 设置 diff --git a/Image2Display/Image2Display/Helpers/DemoFontData.cs b/Image2Display/Image2Display/Helpers/DemoFontData.cs index cdffc6e..852fed5 100644 --- a/Image2Display/Image2Display/Helpers/DemoFontData.cs +++ b/Image2Display/Image2Display/Helpers/DemoFontData.cs @@ -8,6 +8,10 @@ namespace Image2Display.Helpers; public class DemoFontData { + public static byte[] Default + { + get => ZiSymbol; + } public static readonly byte[] ZiSymbol = [ 0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x01,0x4B,0x48,0x06,0x00,0x00,0x00,0x00,0x00, 0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x88,0xFF,0xA2,0x00, diff --git a/Image2Display/Image2Display/Helpers/FontConvert.cs b/Image2Display/Image2Display/Helpers/FontConvert.cs index b43f056..1ddec5f 100644 --- a/Image2Display/Image2Display/Helpers/FontConvert.cs +++ b/Image2Display/Image2Display/Helpers/FontConvert.cs @@ -1,7 +1,10 @@ using Image2Display.Models; +using SixLabors.ImageSharp; using SixLabors.ImageSharp.PixelFormats; +using SkiaSharp; using System; using System.Collections.Generic; +using System.Diagnostics; using System.Linq; using System.Text; using System.Threading.Tasks; @@ -18,7 +21,7 @@ public class FontConvert /// 像素宽度 /// 图片缓冲区,没给的话会新建一个 /// 图片缓冲 - public static ImageData GetImage(byte[] data, int width, int height,ImageData? image = null) + public static ImageData GetImage(byte[] data, int width, int height, ImageData? image = null) { //像素尺寸 var pixelSize = 8; @@ -65,5 +68,224 @@ public static ImageData GetImage(byte[] data, int width, int height,ImageData? i return image; } + public static byte[] GetData(SKTypeface font, int size, char c, int width, int height, int offsetx, int offsety) + { + //创建画布 + using var surface = SKSurface.Create(new SKImageInfo(width, height)); + //创建画笔 + using var paint = new SKPaint + { + Color = SKColors.White, + TextSize = size, + Typeface = font, + TextAlign = SKTextAlign.Center, + IsAntialias = true, + }; + //画布上画字 + surface.Canvas.DrawText(c.ToString(), width/2 + offsetx, height - offsety, paint); + //获取像素数据 + IntPtr data = surface.PeekPixels().GetPixels(); + var result = new byte[width * height]; + + // 获取像素数据 + using var pixmap = surface.PeekPixels(); + if (pixmap != null) + { + // 遍历像素数据 + for (int y = 0; y < pixmap.Height; y++) + { + for (int x = 0; x < pixmap.Width; x++) + { + IntPtr pixel = pixmap.GetPixels(x, y); + var buff = new byte[pixmap.BytesPerPixel]; + System.Runtime.InteropServices.Marshal.Copy(pixel, buff, 0, buff.Length); + result[x + y * width] = buff[0]; + } + } + } + + return result; + } + + /// + /// 数据二值化处理(仅用于预览) + /// + public static void ThresholdImage(byte[] data, byte threshold) + { + for (int i = 0; i < data.Length; i++) + { + data[i] = data[i] > threshold ? (byte)255 : (byte)0; + } + } + + /// + /// 图片转换为特定灰度的图片(仅用于预览) + /// + public static void GrayScaleImage(byte[] data, int bit) + { + for (int i = 0; i < data.Length; i++) + { + //这个灰度位数,有多少种颜色 + var number = 1 << bit; + //每个颜色的间隔 + var interval = 255 / (number - 1); + //把每个格子的灰度对齐到最近的颜色 + data[i] = (byte)((data[i] / interval) * interval); + } + } + + /// + /// 反转图像颜色(仅用于预览) + /// + public static void InvertImage(byte[] data) + { + for (int i = 0; i < data.Length; i++) + { + data[i] = (byte)(255 - data[i]); + } + } + + private static void IteratePixel(byte[] data, int rotate, int width, int height, Action action) + { + //主要开始1 主要结束1 次要开始2 次要结束2 是否先x后y + var (m1, m2, p1, p2, xFirst) = rotate switch + { + 0 => (0, width - 1, 0, height - 1, true), + 1 => (width - 1, 0, 0, height - 1, true), + 2 => (0, width - 1, height - 1, 0, true), + 3 => (width - 1, 0, height - 1, 0, true), + 4 => (0, height - 1, 0, width - 1, false), + 5 => (height - 1, 0, 0, width - 1, false), + 6 => (0, height - 1, width - 1, 0, false), + 7 => (height - 1, 0, width - 1, 0, false), + _ => throw new NotImplementedException(), + }; + + var mStep = m1 < m2 ? 1 : -1; + var pStep = p1 < p2 ? 1 : -1; + if (xFirst) + { + for (var y = p1; y != p2 + pStep; y += pStep) + { + for (var x = m1; x != m2 + mStep; x += mStep) + { + action(data[x + y * width]); + } + } + } + else + { + for (var x = p1; x != p2 + pStep; x += pStep) + { + for (var y = m1; y != m2 + mStep; y += mStep) + { + action(data[x + y * width]); + } + } + } + } + + /// + /// 获取处理后的数据 + /// + /// + public static List GetResultData( + byte[] raw, int width, int height, + bool isGray, int bit, byte threshold, + bool isInvert, int byteOrder, bool bitOrderMSB) + { + //先取反 + if(isInvert) + InvertImage(raw); + + var bitLength = 1; + if (isGray) + bitLength = bit; + + var result = new List(); + int bitIndex = 0; + byte lastByte = 0; + //按像素顺序遍历 + IteratePixel(raw, byteOrder, width, height, (b) => + { + if(isGray) + { + //灰度处理 + b = (byte)(b >> (8 - bitLength)); + } + else + { + //二值化处理 + b = b > threshold ? (byte)1 : (byte)0; + } + //反向数据 + if (!bitOrderMSB) + { + var temp = (byte)0; + for (int i = 0; i < bitLength; i++) + { + temp <<= 1; + temp |= (byte)(b & 1); + b >>= 1; + } + b = temp; + } + //添加到结果 + lastByte <<= bitLength; + lastByte |= b; + bitIndex += bitLength; + if (bitIndex >= 8) + { + result.Add(lastByte); + lastByte = 0; + bitIndex = 0; + } + }); + //最后一个字节 + if (bitIndex > 0) + { + lastByte <<= 8 - bitIndex; + result.Add(lastByte); + } + return result; + } + + /// + /// 将字体数据转换为C数组 + /// + /// 数据,每个字一个数组 + /// 字库宽度 + /// 字库高度 + /// 实际用的字符集 + /// + public static string ByteListToCArray(List> data, int width, int height, IList charset) + { + var sb = new StringBuilder(); + if (Utils.Settings.Language.Contains("ZH", StringComparison.CurrentCultureIgnoreCase)) + sb.Append("/*@注意:此文件由Image2Display生成 */\n"); + else + sb.Append("/*@Notice: This file is generated by Image2Display */\n"); + if (data.Count == 0 || data[0].Count == 0 || charset.Count == 0) + sb.Append("/* no data */\n"); + else + { + sb.Append($"/*@Size: {width}x{height}, " + + $"Char: {charset.Count}," + + $"Data per char: {data[0].Count} */\n"); + } + sb.Append("const uint8_t fonts[] = {\n"); + //每行一个字 + for (int i = 0; i < charset.Count; i++) + { + sb.Append($"/* {charset[i]} */\n"); + foreach (var b in data[i]) + { + sb.Append($"0x{b:X2},"); + } + sb.Append("\n\n"); + } + sb.Append("};\n"); + return sb.ToString(); + } } diff --git a/Image2Display/Image2Display/ViewModels/FontConvertViewModel.cs b/Image2Display/Image2Display/ViewModels/FontConvertViewModel.cs index 3079c86..f9bac7e 100644 --- a/Image2Display/Image2Display/ViewModels/FontConvertViewModel.cs +++ b/Image2Display/Image2Display/ViewModels/FontConvertViewModel.cs @@ -1,9 +1,11 @@ using Avalonia.Controls; using Avalonia.Media.Imaging; +using Avalonia.Platform.Storage; using CommunityToolkit.Mvvm.ComponentModel; using CommunityToolkit.Mvvm.Input; using Image2Display.Helpers; using Image2Display.Models; +using SkiaSharp; using System; using System.Collections.Generic; using System.IO; @@ -20,6 +22,8 @@ public partial class FontConvertViewModel : ViewModelBase [ObservableProperty] private bool _UseSystemFont = true; + [ObservableProperty] + private bool _EnableUseSystemFontCheckBox = true; [ObservableProperty] private List _SystemFontList = new(); @@ -55,7 +59,7 @@ public partial class FontConvertViewModel : ViewModelBase //字体配置 [ObservableProperty] - private int _FontSize = 12; + private int _FontSize = 50; [ObservableProperty] private bool _FontBold = false; [ObservableProperty] @@ -67,9 +71,9 @@ public partial class FontConvertViewModel : ViewModelBase //字模尺寸 [ObservableProperty] - private int _FontWidth = 20; + private int _FontWidth = 50; [ObservableProperty] - private int _FontHeight = 20; + private int _FontHeight = 50; [ObservableProperty] private int _FontXOffset = 0; [ObservableProperty] @@ -79,7 +83,7 @@ public partial class FontConvertViewModel : ViewModelBase [ObservableProperty] private bool _UseThreshold = true; [ObservableProperty] - private int _Threshold = 128; + private byte _Threshold = 128; //灰度位数 0~2: 2、4、8 [ObservableProperty] private int _GrayBitIndex = 0; @@ -96,6 +100,25 @@ public partial class FontConvertViewModel : ViewModelBase [ObservableProperty] private bool _RLECompress = false; + //提示文本 + [ObservableProperty] + private bool _IsShowSuccess = false; + [ObservableProperty] + private bool _IsShowFail = false; + [ObservableProperty] + private string _FailMessage = ""; + + private void ShowSuccess() + { + IsShowSuccess = true; + IsShowFail = false; + } + private void ShowFail(string message) + { + IsShowSuccess = false; + IsShowFail = true; + FailMessage = message; + } [ObservableProperty] private string _PreviewText = Utils.GetI18n("PreviewText"); @@ -109,8 +132,9 @@ public partial class FontConvertViewModel : ViewModelBase public FontConvertViewModel() { //初始化图片成格子图 - var pic = FontConvert.GetImage(DemoFontData.ASymbol, 20, 20, ImageCache); - OriginalImage = new Bitmap(pic.GetStream()); + var pic = FontConvert.GetImage(DemoFontData.Default, 20, 20, ImageCache); + using var ps = pic.GetStream(); + OriginalImage = new Bitmap(ps); //用skia接口获取系统字体列表 var fontMgr = SkiaSharp.SKFontManager.Default; @@ -132,11 +156,18 @@ public FontConvertViewModel() }; SystemFontList.Add(item); } - + if(SystemFontList.Count > 0) + ReloadSKTypeface(); + else + { + UseSystemFont = false; + EnableUseSystemFontCheckBox = false; + } + /*****************************************/ //字符集需要刷新的情况 string[] charsetChangedElements = [ - nameof(CharsetUppercaseLetters), + nameof(CharsetUppercaseLetters), nameof(CharsetLowercaseLetters), nameof(CharsetNumbers), nameof(CharsetPunctuation), @@ -146,17 +177,54 @@ public FontConvertViewModel() nameof(CharsetGB2312), nameof(CustomCharset) ]; + //字符预览需要刷新的情况 + string[] previewChangedElements = + [ + nameof(UseSystemFont), + nameof(SelectedFontIndex), + nameof(FontName), + nameof(FontSize), + nameof(FontBold), + nameof(FontItalic), + nameof(FontUnderline), + nameof(FontStrikeout), + nameof(FontWidth), + nameof(FontHeight), + nameof(FontXOffset), + nameof(FontYOffset), + nameof(UseThreshold), + nameof(Threshold), + nameof(GrayBitIndex), + nameof(Invert), + ]; + //字体需要刷新的情况 + string[] fontChangedElements = + [ + nameof(UseSystemFont), + nameof(SelectedFontIndex), + nameof(FontBold), + nameof(FontItalic), + nameof(FontUnderline), + nameof(FontStrikeout), + ]; PropertyChanged += async (sender, e) => { //某个变量被更改 var name = e.PropertyName; + //更新字体 + if (fontChangedElements.Contains(name)) + ReloadSKTypeface(); + //刷新字符集 + if (previewChangedElements.Contains(name)) + await RefreshPreview(); + //刷新预览 if (charsetChangedElements.Contains(name)) { await RefreshCharset(); + await RefreshPreview(); } - }; } @@ -168,6 +236,9 @@ private async Task RefreshCharset() await Task.Run(() => { chars.Clear(); + + if (!string.IsNullOrEmpty(CustomCharset)) + chars.AddRange(CustomCharset); if (CharsetUppercaseLetters) chars.AddRange(Charset.GetUppercaseLetters()); if (CharsetLowercaseLetters) @@ -184,47 +255,228 @@ await Task.Run(() => chars.AddRange(Charset.GetSecondLevelChinese()); if (CharsetGB2312) chars.AddRange(Charset.GetGB2312()); - if (!string.IsNullOrEmpty(CustomCharset)) - chars.AddRange(CustomCharset); //去除重复字符 chars = chars.Distinct().ToList(); //刷新字符数量 TotalCharacters = chars.Count; + //预览索引重置 + PreviewIndex = 0; + }); + } + + private int PreviewIndex = 0; + private async Task RefreshPreview() + { + await Task.Run(() => + { + //如果没有字符,显示默认字符 + if (chars.Count == 0) + { + var pic = FontConvert.GetImage(DemoFontData.Default, 20, 20, ImageCache); + using var ps = pic.GetStream(); + OriginalImage = new Bitmap(ps); + return; + } + + if (LoadedFont == null) + return; + + //获取原始图像数据 + var data = FontConvert.GetData(LoadedFont, + FontSize, chars[PreviewIndex], + FontWidth, FontHeight, + FontXOffset, FontYOffset); + + // 各种处理 + if (UseThreshold) + { + FontConvert.ThresholdImage(data, Threshold); + } + else + { + var grayBit = GrayBitIndex switch + { + 0 => 2, + 1 => 4, + 2 => 8, + _ => 2 + }; + FontConvert.GrayScaleImage(data, grayBit); + } + if(Invert) + FontConvert.InvertImage(data); + + var fp = FontConvert.GetImage(data, FontWidth, FontHeight, ImageCache); + using var fps = fp.GetStream(); + OriginalImage = new Bitmap(fps); }); } [RelayCommand] private async Task ShowPreviousCharacter() { - //TODO 显示上一个字符 + if (chars.Count == 0) + return; + PreviewIndex -= 1; + if (PreviewIndex < 0) + PreviewIndex = chars.Count - 1; + await RefreshPreview(); } [RelayCommand] private async Task ShowNextCharacter() { - //TODO 显示下一个字符 + if (chars.Count == 0) + return; + PreviewIndex += 1; + if (PreviewIndex >= chars.Count) + PreviewIndex = 0; + await RefreshPreview(); } [RelayCommand] private async Task ShowRandomCharacter() { - //TODO 随机显示一个字符 + if (chars.Count == 0) + return; + PreviewIndex = new Random().Next(0, chars.Count); + await RefreshPreview(); } + private SKTypeface? LoadedFont = null; + private string? LastFontPath = null; [RelayCommand] private async Task LoadFontFile() { - //TODO 加载字体文件 + var fileType = new FilePickerFileType("Font") + { + Patterns = ["*.ttf","*.otf"], + AppleUniformTypeIdentifiers = ["public.item"], + MimeTypes = ["font/ttf", "font/otf"] + }; + var files = await DialogHelper.ShowOpenFileDialogAsync(fileType, false); + if (files.Count == 0) + return; + var filePath = files[0].Path.LocalPath; + + //尝试加载字体文件,失败则提示 + if(!ReloadSKTypeface(filePath)) + { + ShowFail(Utils.GetI18n("LoadFontFileFail")); + return; + } + + FontName = Path.GetFileName(filePath); + LastFontPath = filePath; + await RefreshPreview(); + } + + private bool ReloadSKTypeface(string? path = null) + { + if (UseSystemFont) + { + if (SystemFontList.Count == 0) + return false; + var fontName = SystemFontList[SelectedFontIndex].Tag as string; + LoadedFont = SKTypeface.FromFamilyName(fontName, + FontBold ? SKFontStyleWeight.Bold : SKFontStyleWeight.Normal, + SKFontStyleWidth.Normal, + FontItalic ? SKFontStyleSlant.Italic : SKFontStyleSlant.Upright); + } + else + { + path ??= LastFontPath; + if (string.IsNullOrEmpty(path)) + return false; + var font = SKTypeface.FromFile(path); + if (font == null) + return false; + LoadedFont = font; + } + return true; + } + + + private async Task GetFontData() + { + if(LoadedFont == null) + { + ShowFail("no font"); + return string.Empty; + } + var result = string.Empty; + var error = string.Empty; + await Task.Run(() => + { + var data = new List>(); + foreach (var c in chars) + { + var raw = FontConvert.GetData(LoadedFont, + FontSize, c, + FontWidth, FontHeight, + FontXOffset, FontYOffset); + + var grayBit = GrayBitIndex switch + { + 0 => 2, + 1 => 4, + 2 => 8, + _ => 2 + }; + var d = FontConvert.GetResultData(raw, FontWidth, FontHeight, + !UseThreshold, grayBit, Threshold, + Invert, ByteOrderIndex, BitOrderMSB); + data.Add(d); + } + result = FontConvert.ByteListToCArray(data, FontWidth, FontHeight, chars); + }); + if (!string.IsNullOrEmpty(error)) + { + ShowFail(error); + return string.Empty; + } + return result; } [RelayCommand] private async Task CopyFontCode() { - //TODO 复制字体代码 + var data = await GetFontData(); + if(string.IsNullOrEmpty(data)) + return; + if (await Utils.CopyString(data)) + { + ShowSuccess(); + } + else + { + ShowFail("Copy failed"); + } } + public static FilePickerFileType CFiles { get; } = new("C Files") + { + Patterns = ["*.c"], + AppleUniformTypeIdentifiers = ["public.source-code"], + MimeTypes = ["text/x-csrc"] + }; [RelayCommand] private async Task SaveFontFile() { - //TODO 保存字体文件 + //保存数据 + var path = await DialogHelper.ShowSaveFileDialogAsync("font_data", CFiles); + if (path == null) + return; + var data = await GetFontData(); + if (string.IsNullOrEmpty(data)) + return; + try + { + await File.WriteAllTextAsync(path, data); + ShowSuccess(); + } + catch (Exception e) + { + ShowFail("Save failed: " + e.Message); + } } } } diff --git a/Image2Display/Image2Display/Views/FontConvertView.axaml b/Image2Display/Image2Display/Views/FontConvertView.axaml index f7d0ab2..c6a34eb 100644 --- a/Image2Display/Image2Display/Views/FontConvertView.axaml +++ b/Image2Display/Image2Display/Views/FontConvertView.axaml @@ -20,7 +20,7 @@ Margin="10" BorderBrush="{DynamicResource SurfaceStrokeColorDefaultBrush}" BorderThickness="2"> - + @@ -62,16 +62,17 @@ + IsChecked="{Binding UseSystemFont}" + IsEnabled="{Binding EnableUseSystemFontCheckBox}" /> - +