XPrintServer/XPrint.Image/ColorVectorConverter.cs
2025-11-16 19:33:01 +08:00

696 lines
27 KiB
C#
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

using CrazyStudio.Core.Common.Tools;
using HPPH;
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Drawing;
using System.Drawing.Imaging;
using System.IO;
using System.Linq;
using System.Runtime.InteropServices;
using System.Text;
using System.Threading.Tasks;
using TouchSocket.Core;
//public enum QuantizationAlgorithm { MedianCut, AdaptiveSpatial, NeuQuant }
//public enum QuantizationAlgorithm { MedianCut, AdaptiveSpatial }
public enum QuantizationAlgorithm { MedianCut }
public enum DitheringType { None, FloydSteinberg, Riemersma }
public class ColorVectorConverter
{
private string _potraceExePath = "potrace.exe";
private string _pngquantPath = "pngquant.exe"; // 需安装pngquant
private string _pngnqPath = "pngnq.exe"; // 需安装pngnq
private string _convertPath = "magick convert"; // ImageMagick的convert工具
private bool _verbose = false;
private int _colorCount = 16; // 目标颜色数量
private QuantizationAlgorithm _quantAlgorithm = QuantizationAlgorithm.MedianCut;
private DitheringType _dithering = DitheringType.None;
private bool _stackTracing = true; // 堆栈描摹
private float _prescale = 1.0f; // 预缩放比例(提升细节)
private int maxHandleSize = 3072;
public ColorVectorConverter(string potraceExePath, string pngquantPath, bool verbose = false)
{
_potraceExePath = potraceExePath;
_pngquantPath = pngquantPath;
_verbose = verbose;
}
public async Task ConvertToSvg(string inputFile, string outputFile, int colorCount = 16, float prescale = 1.0f,
QuantizationAlgorithm quantAlgorithm = QuantizationAlgorithm.MedianCut,
DitheringType dithering = DitheringType.None, bool stackTracing = false)
{
_colorCount = Math.Clamp(colorCount, 2, 256);
//限制倍数到0.5-2倍
_prescale = Math.Clamp(prescale, 0.5f, 2);
_quantAlgorithm = quantAlgorithm;
_dithering = dithering;
_stackTracing = stackTracing;
try
{
Image? originalImage = null;
if (inputFile.StartsWith("https:") || inputFile.StartsWith("http:"))
{
originalImage = await HttpTool.GetImageFromUrlAsync(inputFile);
}
else
{
#pragma warning disable CA1416 // 验证平台兼容性
originalImage = Image.FromFile(inputFile);
#pragma warning restore CA1416 // 验证平台兼容性
}
using (originalImage)
{
Bitmap? scaledImage = null;
int maxSize = Math.Max(originalImage.Width, originalImage.Height);
if (maxSize * _prescale > maxHandleSize)
{
int newWidth = 0, newHeight = 0;
if (originalImage.Width > originalImage.Height)
{
newWidth = maxHandleSize;
newHeight = (int)(originalImage.Height * (newWidth / (float)originalImage.Width));
}
else
{
newHeight = maxHandleSize;
newWidth = (int)(originalImage.Width * (newHeight / (float)originalImage.Height));
}
scaledImage = ResizeAndPreserveAlpha(originalImage, newWidth, newHeight);
_prescale = (float)(Math.Max(newWidth, newHeight)) / Math.Max(originalImage.Width, originalImage.Height);
}
else
{
// 步骤1预缩放并确保包含Alpha通道关键保留透明信息
scaledImage = PrescaleAndPreserveAlpha(originalImage);
}
// 步骤2颜色量化保留透明通道
var quantizedImagePath = QuantizeImage(scaledImage!);
#pragma warning disable CA1416 // 验证平台兼容性
var quantizedImage = new Bitmap(quantizedImagePath);
#pragma warning restore CA1416 // 验证平台兼容性
// 步骤3提取量化后的颜色表排除透明色
var colorTable = ExtractColorTable(quantizedImage);
if (_verbose)
Console.WriteLine($"量化完成,保留 {colorTable.Count} 种颜色(已排除透明)");
// 步骤4基于量化颜色表生成颜色段
var colorSegments = CreateSegmentsFromColorTable(colorTable);
// 步骤5生成每个颜色段的掩码并矢量化保留透明区域
var svgLayers = new List<string>();
for (int i = 0; i < colorSegments.Count; i++)
{
var segment = colorSegments[i];
var mask = CreateSegmentMask(quantizedImage, segment.Colors, colorSegments, i);
var svgLayer = VectorizeMask(mask, segment.AverageColor);
if (!string.IsNullOrEmpty(svgLayer))
svgLayers.Add(svgLayer);
}
quantizedImage.Dispose();
if (File.Exists(quantizedImagePath)) File.Delete(quantizedImagePath);
// 步骤6合并图层并保存确保SVG背景透明
float dpiX = scaledImage.HorizontalResolution;
float dpiY = scaledImage.HorizontalResolution;
SaveCombinedSvg(svgLayers, outputFile, scaledImage.Width, scaledImage.Height, dpiX, dpiY);
scaledImage.Dispose();
//Console.WriteLine($"SVG生成完成{outputFile}");
}
}
catch (Exception ex)
{
Console.WriteLine($"转换失败:{ex.Message}");
if (_verbose)
Console.WriteLine(ex.StackTrace);
}
}
#region
/// <summary>
/// 预缩放并确保Alpha通道存在关键保留原始透明信息
/// </summary>
private Bitmap PrescaleAndPreserveAlpha(Image image)
{
int newWidth = (int)(image.Width * _prescale);
int newHeight = (int)(image.Height * _prescale);
// 强制使用带Alpha通道的格式
var scaled = new Bitmap(newWidth, newHeight, PixelFormat.Format32bppArgb);
using (var g = Graphics.FromImage(scaled))
{
// 初始化透明背景
g.Clear(Color.Transparent);
// 高质量缩放并保留透明度
g.InterpolationMode = System.Drawing.Drawing2D.InterpolationMode.HighQualityBicubic;
g.DrawImage(image, 0, 0, newWidth, newHeight);
}
return scaled;
}
/// <summary>
/// 预缩放并确保Alpha通道存在关键保留原始透明信息
/// </summary>
private Bitmap ResizeAndPreserveAlpha(Image image, int newWidth, int newHeight)
{
// 强制使用带Alpha通道的格式
var scaled = new Bitmap(newWidth, newHeight, PixelFormat.Format32bppArgb);
using (var g = Graphics.FromImage(scaled))
{
// 初始化透明背景
g.Clear(Color.Transparent);
// 高质量缩放并保留透明度
g.InterpolationMode = System.Drawing.Drawing2D.InterpolationMode.HighQualityBicubic;
g.DrawImage(image, 0, 0, newWidth, newHeight);
}
return scaled;
}
/// <summary>
/// 颜色量化(保留透明通道,不量化透明像素)
/// </summary>
private string QuantizeImage(Bitmap image)
{
string tempInput = Path.GetTempFileName() + ".png";
string tempOutput = Path.GetTempFileName() + ".png";
// 保存时确保保留Alpha通道
image.Save(tempInput, ImageFormat.Png);
try
{
switch (_quantAlgorithm)
{
case QuantizationAlgorithm.MedianCut:
// pngquant默认保留透明通道
string ditherArg = _dithering == DitheringType.FloydSteinberg ? "" : "--nofs";
RunCommand($"{_pngquantPath} --force {ditherArg} {_colorCount} --output {tempOutput} {tempInput}");
break;
//case QuantizationAlgorithm.NeuQuant:
// // pngnq需要特殊参数保留透明
// string ditherNq = _dithering == DitheringType.FloydSteinberg ? "-Q f" : "";
// RunCommand($"{_pngnqPath} -f {ditherNq} -n {_colorCount} -e .png -k transparent {tempInput}");
// string nqOutput = Path.ChangeExtension(tempInput, "~quant.png");
// if (File.Exists(nqOutput)) File.Move(nqOutput, tempOutput);
// break;
//case QuantizationAlgorithm.AdaptiveSpatial:
// // ImageMagick明确保留透明
// string ditherIm = _dithering switch
// {
// DitheringType.FloydSteinberg => "-dither FloydSteinberg",
// DitheringType.Riemersma => "-dither Riemersma",
// _ => "-dither None"
// };
// RunCommand($"{_convertPath} {tempInput} {ditherIm} -colors {_colorCount} -channel RGBA -separate -combine {tempOutput}");
// break;
}
return tempOutput;
}
finally
{
File.Delete(tempInput);
}
}
/// <summary>
/// 提取颜色表(排除透明色)
/// </summary>
private List<Color> ExtractColorTable(Bitmap image)
{
// 存储唯一颜色(排除透明)
var uniqueColors = new HashSet<int>();
Rectangle rect = new Rectangle(0, 0, image.Width, image.Height);
// 锁定像素数据以提高访问效率
BitmapData data = image.LockBits(rect, ImageLockMode.ReadOnly, PixelFormat.Format32bppArgb);
try
{
IntPtr ptr = data.Scan0;
int bytes = Math.Abs(data.Stride) * image.Height;
byte[] argbValues = new byte[bytes];
Marshal.Copy(ptr, argbValues, 0, bytes);
// 遍历所有像素
for (int i = 0; i < argbValues.Length; i += 4)
{
// 从ARGB数组中解析通道注意存储顺序是BGR而非RGB
byte blue = argbValues[i];
byte green = argbValues[i + 1];
byte red = argbValues[i + 2];
byte alpha = argbValues[i + 3];
// 跳过透明或接近透明的像素Alpha <= 10
if (alpha <= 10)
{
continue;
}
// 将颜色转换为ARGB整数并添加到哈希集自动去重
int colorArgb = Color.FromArgb(alpha, red, green, blue).ToArgb();
uniqueColors.Add(colorArgb);
}
}
finally
{
// 解锁像素数据
image.UnlockBits(data);
}
// 将哈希集中的ARGB值转换为Color对象并返回
return uniqueColors.Distinct().Select(argb => Color.FromArgb(argb)).ToList();
}
#endregion
#region
/// <summary>
/// 创建颜色段掩码(严格保留透明区域)
/// </summary>
private Bitmap CreateSegmentMask(Bitmap image, List<Color> segmentColors, List<ColorSegment> allSegments, int currentIndex)
{
int width = image.Width;
int height = image.Height;
var mask = new Bitmap(width, height, PixelFormat.Format32bppArgb);
Rectangle rect = new Rectangle(0, 0, width, height);
BitmapData srcData = image.LockBits(rect, ImageLockMode.ReadOnly, PixelFormat.Format32bppArgb);
BitmapData destData = mask.LockBits(rect, ImageLockMode.WriteOnly, PixelFormat.Format32bppArgb);
try
{
IntPtr srcPtr = srcData.Scan0;
IntPtr destPtr = destData.Scan0;
int stride = srcData.Stride;
int bytesPerPixel = 4;
// 当前颜色段的颜色集
HashSet<int> currentColors = new HashSet<int>(segmentColors.Select(c => c.ToArgb()));
// 堆栈描摹颜色集
HashSet<int> stackColors = new HashSet<int>();
if (_stackTracing && currentIndex < allSegments.Count - 1)
{
for (int i = currentIndex + 1; i < allSegments.Count; i++)
foreach (var c in allSegments[i].Colors)
stackColors.Add(c.ToArgb());
}
Parallel.For(0, height, y =>
{
unsafe
{
byte* srcRow = (byte*)srcPtr + (y * stride);
byte* destRow = (byte*)destPtr + (y * stride);
for (int x = 0; x < width; x++)
{
// 读取原始像素的Alpha值关键判断透明
byte a = srcRow[x * bytesPerPixel + 3];
byte b = srcRow[x * bytesPerPixel];
byte g = srcRow[x * bytesPerPixel + 1];
byte r = srcRow[x * bytesPerPixel + 2];
int argb = Color.FromArgb(a, r, g, b).ToArgb();
// 完全透明的像素Alpha=0在掩码中也保持透明
if (a <= 10) // 允许10的容差接近透明
{
//SetPixel(destRow, x, 0, 0, 0, 0); // 透明
SetPixel(destRow, x, 255, 255, 255, 255); // 背景(白)
}
// 非透明像素按颜色段处理
else if (currentColors.Contains(argb) || (_stackTracing && stackColors.Contains(argb)))
{
SetPixel(destRow, x, 0, 0, 0, 255); // 前景(黑)
}
else
{
SetPixel(destRow, x, 255, 255, 255, 255); // 背景(白)
}
}
}
});
}
finally
{
image.UnlockBits(srcData);
mask.UnlockBits(destData);
}
return SmoothMask(mask);
}
#endregion
#region SVG生成
/// <summary>
/// 矢量化优化Potrace参数保留细节
/// </summary>
private string VectorizeMask(Bitmap mask, Color color)
{
if (!HasValidContent(mask)) return null;
using (var stream = new MemoryStream())
{
// 临时文件存储掩码避免内存流格式问题Potrace对标准输入的BMP格式可能有严格要求
//string tempBmpPath = Path.GetTempFileName() + ".bmp";
//mask.Save(@"F:\111.bmp", ImageFormat.Bmp);
//mask.Save(@"F:\111.png", ImageFormat.Png);
mask.Save(stream, ImageFormat.Bmp);
stream.Position = 0;
float dpiX = mask.HorizontalResolution;
float dpiY = mask.VerticalResolution;
// 生成Potrace命令添加平滑参数保留区域形态
string colorHex = $"#{color.R:X2}{color.G:X2}{color.B:X2}";
var args = new List<string>
{
"--svg",
$"-C {colorHex}", // 应用颜色段的平均颜色
"-t 1", // 轻微去噪,保留细节
"-a 1.0", // 平滑转角
"-O 0.5", // 适度优化路径
"-s",
$"-r {dpiX}",
$"--width {mask.Width /_prescale / dpiX}",//单位英寸1英寸=像素数量/dpi
$"--height {mask.Height / _prescale / dpiY}",
"-"
};
mask.Dispose();
// 调用Potrace矢量化
using (var process = new Process())
{
process.StartInfo = new ProcessStartInfo
{
FileName = _potraceExePath,
Arguments = string.Join(" ", args),
RedirectStandardInput = true,
RedirectStandardOutput = true,
RedirectStandardError = true,
UseShellExecute = false,
CreateNoWindow = true,
StandardOutputEncoding = Encoding.UTF8
};
process.Start();
stream.CopyTo(process.StandardInput.BaseStream);
process.StandardInput.Close();
string svgOutput = process.StandardOutput.ReadToEnd();
string error = process.StandardError.ReadToEnd();
process.WaitForExit();
if (process.ExitCode != 0)
{
if (_verbose)
Console.WriteLine($"矢量化错误:{error}");
return string.Empty;
}
//File.WriteAllText(@$"F:\{Guid.NewGuid()}.svg", svgOutput);
return ExtractSvgPath(svgOutput, color.A);
}
}
}
private string ExtractSvgPath(string rawSvg, byte alpha)
{
if (string.IsNullOrWhiteSpace(rawSvg)) return string.Empty;
// 匹配原始<g>标签及其属性(非贪婪匹配)
var gTagRegex = new System.Text.RegularExpressions.Regex(
@"<g\s+([^>]*?)>",
System.Text.RegularExpressions.RegexOptions.IgnoreCase
);
var gTagMatch = gTagRegex.Match(rawSvg);
// 提取原始<g>标签内的所有属性
string originalGAttributes = gTagMatch.Success ? gTagMatch.Groups[1].Value : "";
// 提取所有path元素
var pathRegex = new System.Text.RegularExpressions.Regex(
@"<path\s+[^>]*?/>",
System.Text.RegularExpressions.RegexOptions.IgnoreCase
);
var matches = pathRegex.Matches(rawSvg);
if (matches.Count == 0) return string.Empty;
// 计算透明度属性(如果需要)
string opacityAttribute = alpha < 255 ? $"opacity=\"{alpha / 255.0:0.00}\"" : "";
// 合并原始属性和新透明度属性(去重处理)
var attributes = new List<string>();
if (!string.IsNullOrWhiteSpace(originalGAttributes))
{
attributes.AddRange(originalGAttributes.Split(
new[] { ' ' },
StringSplitOptions.RemoveEmptyEntries
));
}
if (!string.IsNullOrWhiteSpace(opacityAttribute))
{
// 移除可能存在的旧opacity属性
attributes.RemoveAll(a => a.StartsWith("opacity=", StringComparison.OrdinalIgnoreCase));
attributes.Add(opacityAttribute);
}
// 构建新的<g>标签
var result = new StringBuilder();
result.Append($"<g {string.Join(" ", attributes)}>");
// 添加所有path元素
foreach (var match in matches)
result.AppendLine(match.ToString());
result.Append("</g>");
return result.ToString();
}
/// <summary>
/// 保存合并的SVG明确设置透明背景
/// </summary>
private void SaveCombinedSvg(List<string> layers, string outputPath, int width, int height, float dpiX, float dpiY)
{
//width = (int)(width / _prescale);
//height = (int)(height / _prescale);
int widthPx = (int)((width / _prescale) * (72 / dpiX));// $"{width / _prescale * 72 / dpiX}pt";
int heightPx = (int)((height / _prescale) * (72 / dpiX));//$"{height / _prescale * 72 / dpiY}pt";
var svg = new StringBuilder();
svg.AppendLine("<?xml version=\"1.0\" encoding=\"UTF-8\"?>");
// 关键SVG根元素不设置背景色默认透明
svg.AppendLine($"<svg xmlns=\"http://www.w3.org/2000/svg\" " +
$"width=\"{widthPx}\" height=\"{heightPx}\" " +
$"viewBox=\"0 0 {widthPx} {heightPx}\" " +
//$"style=\"transform: scale({1 / _prescale})\" " +
$"preserveAspectRatio=\"xMidYMid meet\">");
// 添加透明背景提示(不影响实际显示,但明确意图)
svg.AppendLine($" <!-- 透明背景 -->");
// 按亮度排序图层
foreach (var layer in layers.OrderBy(l => GetLayerBrightness(l)))
svg.AppendLine(layer);
svg.AppendLine("</svg>");
Directory.CreateDirectory(Path.GetDirectoryName(outputPath));
File.WriteAllText(outputPath, svg.ToString(), new UTF8Encoding(false));
}
#endregion
#region
private string RunCommand(string command, bool returnOutput = false)
{
using (var process = new Process())
{
process.StartInfo = new ProcessStartInfo
{
FileName = "cmd.exe",
Arguments = $"/c {command}",
RedirectStandardOutput = returnOutput,
RedirectStandardError = true,
UseShellExecute = false,
CreateNoWindow = true,
StandardOutputEncoding = returnOutput ? Encoding.UTF8 : null
};
process.Start();
string error = process.StandardError.ReadToEnd();
process.WaitForExit();
if (process.ExitCode != 0)
throw new Exception($"命令执行失败:{command}\n错误{error}");
if (returnOutput)
{
string lastCommandOutput = process.StandardOutput.ReadToEnd();
return lastCommandOutput;
}
}
return string.Empty;
}
private unsafe bool IsWhite(byte* row, int x, int bytesPerPixel)
{
int idx = x * bytesPerPixel;
return row[idx] == 255 && row[idx + 1] == 255 && row[idx + 2] == 255 && row[idx + 3] == 255;
}
private int CountBlackNeighbors(IntPtr srcPtr, int stride, int x, int y, int bytesPerPixel)
{
int count = 0;
unsafe
{
for (int dy = -1; dy <= 1; dy++)
{
byte* neighborRow = (byte*)srcPtr + ((y + dy) * stride);
for (int dx = -1; dx <= 1; dx++)
{
if (dx == 0 && dy == 0) continue;
int nx = x + dx;
int idx = nx * bytesPerPixel;
if (neighborRow[idx] == 0 && neighborRow[idx + 1] == 0 &&
neighborRow[idx + 2] == 0 && neighborRow[idx + 3] == 255)
count++;
}
}
}
return count;
}
private unsafe void SetPixel(byte* row, int x, byte b, byte g, byte r, byte a)
{
int idx = x * 4;
row[idx] = b;
row[idx + 1] = g;
row[idx + 2] = r;
row[idx + 3] = a;
}
private bool HasValidContent(Bitmap mask)
{
Rectangle rect = new Rectangle(0, 0, mask.Width, mask.Height);
BitmapData data = mask.LockBits(rect, ImageLockMode.ReadOnly, PixelFormat.Format32bppArgb);
try
{
IntPtr ptr = data.Scan0;
int bytes = Math.Abs(data.Stride) * mask.Height;
byte[] argb = new byte[bytes];
Marshal.Copy(ptr, argb, 0, bytes);
for (int i = 0; i < argb.Length; i += 4)
if (argb[i] == 0 && argb[i + 1] == 0 && argb[i + 2] == 0 && argb[i + 3] == 255)
return true;
return false;
}
finally { mask.UnlockBits(data); }
}
private double GetLayerBrightness(string svgLayer)
{
var match = System.Text.RegularExpressions.Regex.Match(svgLayer, @"#([0-9A-Fa-f]{6})");
if (match.Success)
{
var color = ColorTranslator.FromHtml($"#{match.Groups[1].Value}");
return (0.299 * color.R + 0.587 * color.G + 0.114 * color.B) / 255;
}
return 0.5;
}
/// <summary>
/// 轻度平滑(仅修复微小空洞,保留边缘)
/// </summary>
private Bitmap SmoothMask(Bitmap mask)
{
int width = mask.Width;
int height = mask.Height;
var smoothed = new Bitmap(mask);
Rectangle rect = new Rectangle(0, 0, width, height);
BitmapData srcData = mask.LockBits(rect, ImageLockMode.ReadOnly, PixelFormat.Format32bppArgb);
BitmapData destData = smoothed.LockBits(rect, ImageLockMode.WriteOnly, PixelFormat.Format32bppArgb);
try
{
IntPtr srcPtr = srcData.Scan0;
IntPtr destPtr = destData.Scan0;
int stride = srcData.Stride;
int bytesPerPixel = 4;
Parallel.For(1, height - 1, y =>
{
unsafe
{
byte* srcRow = (byte*)srcPtr + (y * stride);
byte* destRow = (byte*)destPtr + (y * stride);
for (int x = 1; x < width - 1; x++)
{
// 仅修复完全孤立的1x1空洞避免模糊边缘
if (IsWhite(srcRow, x, bytesPerPixel))
{
int blackCount = CountBlackNeighbors(srcPtr, stride, x, y, bytesPerPixel);
if (blackCount == 8) // 被8个黑像素包围的白像素空洞
SetPixel(destRow, x, 0, 0, 0, 255);
}
}
}
});
}
finally
{
mask.UnlockBits(srcData);
smoothed.UnlockBits(destData);
}
return smoothed;
}
/// <summary>
/// 基于量化颜色表创建颜色段(确保每个颜色对应一个段)
/// </summary>
private List<ColorSegment> CreateSegmentsFromColorTable(List<Color> colorTable)
{
// 按亮度排序(避免图层覆盖错误)
var sortedColors = colorTable.OrderBy(c => (0.299 * c.R + 0.587 * c.G + 0.114 * c.B)).ToList();
return sortedColors.Select((color, idx) =>
new ColorSegment(idx, new List<Color> { color })).ToList();
}
#endregion
}
public class ColorSegment
{
public int Id { get; }
public List<Color> Colors { get; }
public Color AverageColor { get; }
public ColorSegment(int id, List<Color> colors)
{
Id = id;
Colors = colors;
AverageColor = colors.Count == 1 ? colors[0] : CalculateAverageColor(colors);
}
private Color CalculateAverageColor(List<Color> colors)
{
int r = 0, g = 0, b = 0, a = 0;
foreach (var c in colors)
{
r += c.R;
g += c.G;
b += c.B;
a += c.A;
}
return Color.FromArgb(a / colors.Count, r / colors.Count, g / colors.Count, b / colors.Count);
}
}