using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Drawing;
using System.Drawing.Imaging;
using System.IO;
using System.Linq;
using System.Text.RegularExpressions;
using System.Threading.Tasks;
using System.Xml.Linq;
namespace XPrint.Image
{
///
/// 使用potrace将位图转换为彩色SVG的工具类(透明背景版本)
///
public class ColorPotraceConverter
{
#region 外部程序路径配置
private string _pngquantPath = "pngquant";
private string _pngnqPath = "pngnq";
private string _convertPath = "magick convert";
private string _identifyPath = "magick identify";
private string _potracePath = "potrace";
private string _potraceOptions = "";
#endregion
#region 配置参数
public int MaxCommandLength { get; set; } = 1900;
public int VerboseLevel { get; set; } = 0;
public int Despeckle { get; set; } = 2;
public float SmoothCorners { get; set; } = 1.0f;
public float OptimizePaths { get; set; } = 0.2f;
public bool Stack { get; set; } = false;
public float Prescale { get; set; } = 1.0f;
public bool Background { get; set; } = false; // 不再用于设置实体背景
public string Width { get; set; } = null;
public string Height { get; set; } = null;
public string Dither { get; set; } = null;
public int? Colors { get; set; } = null;
public string RemapPalette { get; set; } = null;
public string QuantizationAlgorithm { get; set; } = "mc";
#endregion
///
/// 初始化彩色SVG转换器
///
public ColorPotraceConverter() { }
///
/// 设置外部程序路径
///
public void SetExternalProgramPaths(string potracePath, string convertPath = null,
string identifyPath = null, string pngquantPath = null, string pngnqPath = null)
{
if (!string.IsNullOrEmpty(potracePath))
_potracePath = potracePath;
if (!string.IsNullOrEmpty(convertPath))
_convertPath = convertPath;
if (!string.IsNullOrEmpty(identifyPath))
_identifyPath = identifyPath;
if (!string.IsNullOrEmpty(pngquantPath))
_pngquantPath = pngquantPath;
if (!string.IsNullOrEmpty(pngnqPath))
_pngnqPath = pngnqPath;
}
///
/// 转换多个图像文件为彩色SVG(透明背景)
///
public void ConvertFiles(List inputFiles, string outputPattern, int? coreCount = null)
{
// 解析输入输出文件对
var filePairs = GetInputOutputPairs(inputFiles, outputPattern);
if (!filePairs.Any())
{
Report("没有找到有效的输入文件");
return;
}
// 确定进程数
int processCount = coreCount ?? Environment.ProcessorCount;
processCount = Math.Max(1, processCount);
Report($"开始处理 {filePairs.Count} 个文件,使用 {processCount} 个进程");
// 创建临时目录
string tempDir = Path.Combine(Path.GetTempPath(), "ColorPotrace_" + Guid.NewGuid().ToString("N"));
Directory.CreateDirectory(tempDir);
try
{
// 并行处理文件
Parallel.ForEach(filePairs, new ParallelOptions { MaxDegreeOfParallelism = processCount },
pair => ConvertSingleFile(pair.Input, pair.Output, tempDir));
}
finally
{
// 清理临时文件
if (Directory.Exists(tempDir))
{
try
{
Directory.Delete(tempDir, true);
}
catch (Exception ex)
{
Report($"清理临时文件时出错: {ex.Message}", 1);
}
}
}
Report("所有文件处理完成");
}
///
/// 转换单个文件为彩色SVG(透明背景)
///
private void ConvertSingleFile(string inputPath, string outputPath, string tempDir)
{
try
{
Report($"处理文件: {inputPath} -> {outputPath}", 1);
// 确保输出目录存在
string outputDir = Path.GetDirectoryName(outputPath);
if (!Directory.Exists(outputDir))
Directory.CreateDirectory(outputDir);
// 生成临时文件名
string baseName = Path.GetFileNameWithoutExtension(inputPath);
string scaledFile = Path.Combine(tempDir, $"{baseName}~scaled.png");
string reducedFile = Path.Combine(tempDir, $"{baseName}~reduced.png");
// 步骤1: 重缩放图像 - 保留透明度
string filter = (Colors == 0) ? "point" : "lanczos";
RescaleImageWithTransparency(inputPath, scaledFile, Prescale, filter);
// 步骤2: 缩减颜色或重映射颜色 - 保留透明度
if (Colors.HasValue && Colors.Value > 0)
{
QuantizeImageWithTransparency(scaledFile, reducedFile, Colors.Value, QuantizationAlgorithm, Dither);
}
else if (!string.IsNullOrEmpty(RemapPalette))
{
RemapImageColorsWithTransparency(scaledFile, reducedFile, RemapPalette, Dither);
}
else
{
File.Copy(scaledFile, reducedFile, true);
}
// 步骤3: 提取颜色表(排除透明色)
List colorTable = GetColorTableExcludingTransparent(reducedFile);
if (colorTable.Count == 0)
{
Report("无法从图像中提取颜色表(可能全是透明)", 1);
return;
}
Report($"发现 {colorTable.Count} 种颜色(已排除透明色)", 1);
// 步骤4: 为每种颜色创建孤立图层并描摹
List traceFiles = new List();
for (int i = 0; i < colorTable.Count; i++)
{
string color = colorTable[i];
string isolatedFile = Path.Combine(tempDir, $"{baseName}~isolated_{i}.png");
string traceFile = Path.Combine(tempDir, $"{baseName}~trace_{i}.svg");
// 孤立颜色 - 保留透明区域
IsolateColorWithTransparency(reducedFile, isolatedFile, color, colorTable, Stack, tempDir);
// 描摹 - 确保透明区域不被填充
TraceImage(isolatedFile, traceFile, color);
traceFiles.Add(traceFile);
}
// 步骤5: 合并所有SVG图层(确保背景透明)
MergeSvgLayersWithTransparentBackground(traceFiles, outputPath);
// 清理临时文件
foreach (var file in traceFiles)
DeleteFile(file);
DeleteFile(scaledFile);
DeleteFile(reducedFile);
DeleteFile(Path.Combine(tempDir, $"{baseName}~temp.png"));
}
catch (Exception ex)
{
Report($"处理文件时出错: {ex.Message}", 1);
throw;
}
}
#region 图像处理方法(支持透明)
///
/// 重缩放图像并保留透明度
///
private void RescaleImageWithTransparency(string source, string destination, float scale, string filter)
{
if (scale == 1.0f)
{
// 不缩放,直接复制或转换为PNG并保留透明度
if (Path.GetExtension(source).Equals(".png", StringComparison.OrdinalIgnoreCase))
{
File.Copy(source, destination, true);
}
else
{
string command = $"{_convertPath} \"{source}\" -background none -alpha on \"{destination}\"";
RunCommand(command);
}
}
else
{
int scalePercent = (int)(scale * 100);
string command = $"{_convertPath} \"{source}\" -filter {filter} -resize {scalePercent}% " +
$"-background none -alpha on \"{destination}\"";
RunCommand(command);
}
}
///
/// 量化缩减图像颜色并强制保留透明度
///
/// 源图像路径
/// 目标图像路径
/// 目标颜色数量
/// 量化算法
/// 拟色算法
private void QuantizeImageWithTransparency(
string source,
string destination,
int colorCount,
string algorithm,
string dither)
{
// 验证源文件是否包含alpha通道
if (!HasAlphaChannel(source))
{
Report($"警告: 源文件 {Path.GetFileName(source)} 不包含透明度信息", 1);
}
// 无需量化,直接复制文件并确保保留透明度
if (colorCount <= 1)
{
PreserveTransparencyCopy(source, destination);
return;
}
// 根据选择的算法执行不同的量化处理
switch (algorithm.ToLower())
{
// 中切算法(增强透明度保护)
case "mc":
ProcessMedianCutAlgorithm(source, destination, colorCount, dither);
break;
// 自适应空间细分算法
case "as":
ProcessAdaptiveSubdivisionAlgorithm(source, destination, colorCount, dither);
break;
// 神经量化算法
case "nq":
ProcessNeuralQuantizationAlgorithm(source, destination, colorCount, dither);
break;
// 未知算法处理
default:
throw new ArgumentException($"未知的量化算法: {algorithm}");
}
// 验证处理后的文件是否保留了透明度
if (!HasAlphaChannel(destination))
{
Report($"警告: 处理后文件 {Path.GetFileName(destination)} 丢失了透明度信息", 1);
// 尝试恢复透明度
RestoreTransparency(source, destination);
}
}
///
/// 检查图像是否包含alpha通道
///
private bool HasAlphaChannel(string imagePath)
{
try
{
string command = $"{_identifyPath} -format \"%[channels]\" \"{imagePath}\"";
string output = RunCommand(command, true).ToLower();
return output.Contains("a") || output.Contains("alpha");
}
catch
{
// 命令执行失败时假设包含alpha通道
return true;
}
}
///
/// 复制文件并确保保留透明度
///
private void PreserveTransparencyCopy(string source, string destination)
{
// 使用convert命令而非直接复制,确保透明度保留
string command = $"{_convertPath} \"{source}\" " +
"-alpha on " +
"-background none " +
"-define png:color-type=6 " + // 明确指定带alpha通道的PNG格式
"\"{destination}\"";
RunCommand(command);
}
///
/// 尝试恢复丢失的透明度
///
private void RestoreTransparency(string originalImage, string processedImage)
{
string tempFile = Path.ChangeExtension(processedImage, ".tmp.png");
// 使用原始图像的alpha通道修复处理后的图像
string command = $"{_convertPath} \"{processedImage}\" " +
"-alpha off " +
"( \"{originalImage}\" -alpha extract ) " +
"-alpha off -compose CopyOpacity -composite " +
"-define png:color-type=6 " +
"\"{tempFile}\"";
try
{
RunCommand(command);
if (File.Exists(tempFile))
{
File.Delete(processedImage);
File.Move(tempFile, processedImage);
Report("已成功恢复透明度信息", 1);
}
}
catch (Exception ex)
{
Report($"恢复透明度失败: {ex.Message}", 1);
if (File.Exists(tempFile))
File.Delete(tempFile);
}
}
///
/// 使用中切算法处理图像量化(增强透明度保护)
///
private void ProcessMedianCutAlgorithm(string source, string destination, int colorCount, string dither)
{
// 构建拟色选项
string ditherOption = dither == "floydsteinberg" ? "" : "--nofs";
// 构建pngquant命令(明确保留alpha通道)
string command = $"{_pngquantPath} " +
$"--force " +
$"{ditherOption} " +
$"{colorCount} " +
$"--skip-if-larger " +
$"--ext .png " +
$"--quality 0-100 " + // 确保质量设置不会影响透明度
$"-- " +
$"\"{source}\"";
// 执行量化命令
RunCommand(command);
// 处理pngquant的输出文件(默认会在原文件名后添加-fs8后缀)
string tempOutput = Path.ChangeExtension(source, $"-fs8.png");
File.Copy(source, tempOutput);
// 确保目标目录存在
string destinationDir = Path.GetDirectoryName(destination);
if (!Directory.Exists(destinationDir))
{
Directory.CreateDirectory(destinationDir);
}
// 移动处理后的文件到目标位置
if (File.Exists(tempOutput))
{
// 使用convert命令转移文件以确保透明度保留
string convertCommand = $"{_convertPath} \"{tempOutput}\" " +
"-alpha on " +
"-background none " +
"-define png:color-type=6 " +
$"\"{destination}\"";
RunCommand(convertCommand);
// 删除临时文件
File.Delete(tempOutput);
}
else
{
// 量化失败时使用带透明度保护的复制
PreserveTransparencyCopy(source, destination);
}
}
///
/// 使用自适应空间细分算法处理图像量化(增强透明度保护)
///
private void ProcessAdaptiveSubdivisionAlgorithm(string source, string destination, int colorCount, string dither)
{
string ditherParam = dither ?? "None";
string command = $"{_convertPath} \"{source}\" " +
$"-dither {ditherParam} " +
$"-colors {colorCount} " +
$"-alpha on " + // 明确保留alpha通道
$"-background none " + // 背景设为透明
$"+repage " + // 移除虚拟画布信息
$"-define png:color-type=6 " + // 强制使用带alpha的PNG格式
$"\"{destination}\"";
RunCommand(command);
}
///
/// 使用神经量化算法处理图像量化(增强透明度保护)
///
private void ProcessNeuralQuantizationAlgorithm(string source, string destination, int colorCount, string dither)
{
string ditherOption = dither == "floydsteinberg" ? "-Q f " : "";
string tempOutputNq = Path.Combine(
Path.GetDirectoryName(source),
$"{Path.GetFileNameWithoutExtension(source)}~quant.png"
);
string command = $"{_pngnqPath} " +
$"-f " +
$"{ditherOption}" +
$"-d \"{Path.GetDirectoryName(source)}\" " +
$"-n {colorCount} " +
$"-e ~quant.png " +
$"\"{source}\"";
RunCommand(command);
// 移动生成的文件到目标位置,使用convert确保透明度
if (File.Exists(tempOutputNq))
{
string convertCommand = $"{_convertPath} \"{tempOutputNq}\" " +
"-alpha on " +
"-background none " +
"-define png:color-type=6 " +
"\"{destination}\"";
RunCommand(convertCommand);
File.Delete(tempOutputNq);
}
else
{
// 量化失败时使用带透明度保护的复制
PreserveTransparencyCopy(source, destination);
throw new Exception("pngnq 未能生成输出文件");
}
}
///
/// 使用调色板重映射图像颜色并保留透明度
///
private void RemapImageColorsWithTransparency(string source, string destination, string paletteImage, string dither)
{
if (!File.Exists(paletteImage))
throw new FileNotFoundException("未找到调色板图像", paletteImage);
string ditherParam = dither ?? "None";
string command = $"{_convertPath} \"{source}\" -dither {ditherParam} -remap \"{paletteImage}\" " +
$"-background none -alpha on \"{destination}\"";
RunCommand(command);
}
///
/// 从图像中提取颜色表(排除透明色)
///
private List GetColorTableExcludingTransparent(string imagePath)
{
string command = $"{_convertPath} \"{imagePath}\" -unique-colors txt:-";
string output = RunCommand(command, true, false);
// 提取十六进制颜色并排除透明色
MatchCollection matches = Regex.Matches(output, "#[0-9A-Fa-f]{6}");
var colors = matches.Cast()
.Select(m => m.Value.ToUpper())
.Distinct()
.ToList();
// 排除可能的透明色表示(白色在某些情况下可能代表透明)
// 更精确的做法是检查alpha通道,但这里简化处理
colors.Remove("#FFFFFF"); // 移除白色(如果它代表透明背景)
return colors;
}
///
/// 孤立指定颜色并保留透明区域
///
private void IsolateColorWithTransparency(string source, string destination, string targetColor,
List palette, bool stack, string tempDir)
{
int colorIndex = palette.IndexOf(targetColor);
if (colorIndex == -1)
throw new ArgumentException("目标颜色不在调色板中");
// 获取不在调色板中的颜色作为临时背景和前景
string bgTransparent = "#00000000"; // 透明色
string fgBlack = "#000000";
string bgAlmostWhite = GetNonPaletteColor(palette, false, new[] { bgTransparent, fgBlack });
string fgAlmostBlack = GetNonPaletteColor(palette, true, new[] { bgAlmostWhite, bgTransparent, fgBlack });
// 创建临时文件
string tempFile = Path.Combine(tempDir, Path.GetFileNameWithoutExtension(source) + "~temp.png");
// 构建颜色替换命令 - 保留透明区域
string commandPrefix = $"{_convertPath} \"{source}\" -alpha on";
string commandSuffix = $" -alpha on \"{tempFile}\"";
string commandMiddle = "";
for (int i = 0; i < palette.Count; i++)
{
string fillColor = (i == colorIndex || (i > colorIndex && stack)) ? fgAlmostBlack : bgAlmostWhite;
commandMiddle += $" -fill \"{fillColor}\" -opaque \"{palette[i]}\"";
// 如果命令太长,执行并重置
if (commandMiddle.Length >= MaxCommandLength || i == palette.Count - 1)
{
string fullCommand = commandPrefix + commandMiddle + commandSuffix;
RunCommand(fullCommand);
commandMiddle = "";
}
}
// 转换为黑白并保留透明
string finalCommand = $"{_convertPath} \"{tempFile}\" -fill white -opaque \"{bgAlmostWhite}\" " +
$"-fill black -opaque \"{fgAlmostBlack}\" -alpha on \"{destination}\"";
RunCommand(finalCommand);
}
///
/// 描摹图像为SVG(确保透明区域不填充)
///
private void TraceImage(string source, string destination, string color)
{
//if (!string.IsNullOrEmpty(source) && File.Exists(source))
//{
// if (!source.EndsWith(".bmp") && !source.EndsWith(".BMP"))
// {
// string oldSource = source;
// using (var bmp = Bitmap.FromFile(source))
// {
// source = Path.Combine(Path.GetDirectoryName(source), Guid.NewGuid().ToString("N") + ".bmp");
// bmp.Save(source, ImageFormat.Bmp);
// }
// File.Delete(oldSource);
// }
//}
string widthParam = !string.IsNullOrEmpty(Width) ? $"--width {Width}" : "";
string heightParam = !string.IsNullOrEmpty(Height) ? $"--height {Height}" : "";
// 添加参数确保只填充前景,背景保持透明
string command = $"{_potracePath} \"{source}\" {_potraceOptions} --svg -o \"{destination}\" " +
$"-C \"{color}\" -t {Despeckle} -a {SmoothCorners} -O {OptimizePaths} " +
$"{widthParam} {heightParam} --fillcolor \"{color}\"";
RunCommand(command);
}
///
/// 合并多个SVG图层并确保背景透明
///
private void MergeSvgLayersWithTransparentBackground(List svgFiles, string outputPath)
{
// 创建一个新的SVG文档,只声明一次默认命名空间
XNamespace svgNs = "http://www.w3.org/2000/svg";
XDocument mergedDoc = new XDocument(
new XElement(svgNs + "svg",
new XAttribute(XNamespace.Xmlns + "svg", svgNs), // 正确声明命名空间
new XAttribute("version", "1.1"),
// 不设置背景色,保持透明
new XAttribute("style", "background: transparent;")
)
);
XElement root = mergedDoc.Root;
if (root == null)
throw new InvalidOperationException("无法创建SVG根元素");
// 记录是否已经设置了尺寸属性
bool hasSizeAttributes = false;
// 合并所有SVG内容
foreach (string file in svgFiles)
{
XDocument doc = XDocument.Load(file);
XElement docRoot = doc.Root;
if (docRoot == null)
continue;
// 处理第一个SVG文件的尺寸属性(只设置一次)
if (!hasSizeAttributes)
{
// 复制宽度、高度和视图框属性(排除命名空间属性)
foreach (XAttribute attr in docRoot.Attributes())
{
if (attr.Name.LocalName == "width" ||
attr.Name.LocalName == "height" ||
attr.Name.LocalName == "viewBox")
{
root.SetAttributeValue(attr.Name, attr.Value);
}
}
hasSizeAttributes = true;
}
// 复制所有子元素,并确保它们使用正确的命名空间
foreach (XElement child in docRoot.Elements())
{
// 移除子元素可能携带的命名空间声明,避免冲突
var cleanedChild = CleanElementNamespace(child, svgNs);
// 确保没有设置全局背景
if (cleanedChild.Name.LocalName == "rect" &&
cleanedChild.Attribute("width") != null &&
cleanedChild.Attribute("height") != null &&
cleanedChild.Attribute("fill") != null)
{
// 过滤掉可能的背景矩形
continue;
}
root.Add(cleanedChild);
}
}
mergedDoc.Save(outputPath);
}
#endregion
#region 辅助方法
///
/// 获取输入输出文件对
///
private List<(string Input, string Output)> GetInputOutputPairs(List inputPatterns, string outputPattern)
{
List<(string, string)> result = new List<(string, string)>();
HashSet processedInputs = new HashSet();
foreach (string pattern in inputPatterns)
{
// 处理通配符
string directory = Path.GetDirectoryName(pattern);
string fileName = Path.GetFileName(pattern);
if (string.IsNullOrEmpty(directory))
directory = Environment.CurrentDirectory;
if (!Directory.Exists(directory))
continue;
// 获取匹配的文件
foreach (string file in Directory.EnumerateFiles(directory, fileName))
{
if (processedInputs.Contains(file))
continue;
processedInputs.Add(file);
// 生成输出文件名
string baseName = Path.GetFileNameWithoutExtension(file);
string outputFile = outputPattern.Replace("{0}", baseName);
result.Add((file, outputFile));
}
}
return result;
}
///
/// 运行外部命令
///
private string RunCommand(string command, bool captureOutput = false, bool captureError = true)
{
Report($"执行命令: {command}", 2);
ProcessStartInfo startInfo = new ProcessStartInfo
{
FileName = "cmd.exe",
Arguments = $"/c {command}",
UseShellExecute = false,
RedirectStandardOutput = captureOutput,
RedirectStandardError = captureError,
CreateNoWindow = true
};
using (Process process = Process.Start(startInfo))
{
if (process == null)
throw new InvalidOperationException("无法启动进程");
string output = captureOutput ? process.StandardOutput.ReadToEnd() : null;
string error = captureError ? process.StandardError.ReadToEnd() : null;
process.WaitForExit();
if (process.ExitCode != 0)
throw new Exception($"命令执行失败 (代码 {process.ExitCode}): {error}");
return output;
}
}
///
/// 获取不在调色板中的颜色
///
private string GetNonPaletteColor(List palette, bool startFromBlack, IEnumerable avoidColors = null)
{
HashSet allAvoid = new HashSet(palette.Select(c => c.ToUpper()));
if (avoidColors != null)
foreach (string color in avoidColors)
allAvoid.Add(color.ToUpper());
if (startFromBlack)
{
// 从黑色开始查找
for (int i = 0; i <= 0xFFFFFF; i++)
{
string color = $"#{i:X6}";
if (!allAvoid.Contains(color))
return color;
}
}
else
{
// 从白色开始查找
for (int i = 0xFFFFFF; i >= 0; i--)
{
string color = $"#{i:X6}";
if (!allAvoid.Contains(color))
return color;
}
}
throw new Exception("无法找到不在调色板中的颜色");
}
///
/// 清理元素的命名空间声明,确保使用指定的命名空间
///
private XElement CleanElementNamespace(XElement element, XNamespace targetNs)
{
// 创建一个使用目标命名空间的新元素
XElement cleaned = new XElement(targetNs + element.Name.LocalName);
// 复制属性,但排除命名空间声明
foreach (XAttribute attr in element.Attributes())
{
if (!attr.IsNamespaceDeclaration)
{
cleaned.Add(new XAttribute(attr.Name, attr.Value));
}
}
// 递归清理子元素
foreach (XElement child in element.Elements())
{
cleaned.Add(CleanElementNamespace(child, targetNs));
}
// 复制文本内容
if (!string.IsNullOrEmpty(element.Value))
{
cleaned.Value = element.Value;
}
return cleaned;
}
///
/// 删除文件(如果存在)
///
private void DeleteFile(string path)
{
if (File.Exists(path))
{
try
{
File.Delete(path);
}
catch (Exception ex)
{
Report($"无法删除临时文件 {path}: {ex.Message}", 1);
}
}
}
///
/// 报告信息
///
private void Report(string message, int level = 1)
{
if (VerboseLevel >= level)
Console.WriteLine($"[{DateTime.Now:HH:mm:ss}] {message}");
}
///
/// 获取图像宽度
///
private int GetImageWidth(string imagePath)
{
string command = $"{_identifyPath} -ping -format \"%w\" \"{imagePath}\"";
string output = RunCommand(command, true);
if (int.TryParse(output, out int width))
return width;
throw new Exception("无法获取图像宽度");
}
#endregion
}
}