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 } }