感谢您的查看和帮助!
Tif转JPG,属性里面有下面的属性,我不希望有这些数据,如何删除?
分辨率单位:2
颜色表示:sRGB
我在 tiffFrame.Save(jpgFilePath, jpgOptions);后加了一个RemoveSpecificMetadata方法用来删除分辨率单位:2、颜色表示:sRGB可以实现需求,但是文件流保存了两次,影响性能,是否可以在 tiffFrame.Save(jpgFilePath, jpgOptions);之前就可以删除分辨率单位:2、颜色表示:sRGB,就不用RemoveSpecificMetadata再删除保存一次了?
/// <summary>
/// 【高性能并行版】多页TIF转单页JPG(帧级并行处理,确保页码顺序)
/// 核心特性:
/// 1. 单个TIF文件内部多帧并行处理,显著提升处理速度
/// 2. 使用ConcurrentDictionary保证页码顺序,解决并行乱序问题
/// 3. 支持取消操作,及时响应外部中断请求
/// 4. 智能并行度控制,避免内存溢出和资源竞争
/// </summary>
/// <param name="tifFilePath">TIF文件完整路径(支持.tif/.tiff扩展名)</param>
/// <param name="outputDir">JPG输出目录(不存在时会自动创建)</param>
/// <param name="jpgQuality">JPG压缩质量(1-100,默认90为高质量)</param>
/// <param name="maxDegreeOfParallelism">最大并行度(默认4,建议设置为CPU核心数)</param>
/// <param name="cancellationToken">取消令牌,支持操作中断和资源清理</param>
/// <returns>
/// 按原始页码顺序排序的JPG文件路径列表(即使并行处理乱序,输出顺序也保证正确)
/// </returns>
/// <exception cref="FileNotFoundException">TIF文件不存在或无法访问</exception>
/// <exception cref="ArgumentOutOfRangeException">JPG质量参数超出1-100范围</exception>
/// <exception cref="OperationCanceledException">操作被用户或外部请求取消</exception>
/// <exception cref="InvalidOperationException">Aspose.Imaging库内部异常</exception>
public static async Task<List<string>> SplitMultiPageTiffToJpgAsync(
string tifFilePath,
string outputDir,
int jpgQuality = 90,
int maxDegreeOfParallelism = 4,
CancellationToken cancellationToken = default)
{
// ========== 1. 前置条件验证 ==========
// 验证文件存在性:避免后续操作因文件不存在而失败
if (!File.Exists(tifFilePath))
{
throw new FileNotFoundException("TIF文件不存在或路径错误,请检查文件路径", tifFilePath);
}
// 验证JPG质量参数:确保在有效范围内,避免编码器异常
if (jpgQuality < 1 || jpgQuality > 100)
{
throw new ArgumentOutOfRangeException(
nameof(jpgQuality),
jpgQuality,
"JPG质量参数必须在1-100范围内(1:最低质量,100:最高质量)");
}
// 创建输出目录:确保目录存在,避免保存时出现IO异常
// 注意:Directory.CreateDirectory对于已存在的目录不会报错
Directory.CreateDirectory(outputDir);
// 提取文件名前缀:用于生成有序的输出文件名
string fileNamePrefix = System.IO.Path.GetFileNameWithoutExtension(tifFilePath);
// ========== 2. 初始化线程安全结果集合 ==========
// 使用ConcurrentDictionary存储处理结果:
// Key: 原始页码索引(0-based,用于排序和保证顺序)
// Value: 生成的JPG文件完整路径
// 选择ConcurrentDictionary的原因:
// 1. 线程安全,支持多线程并发写入
// 2. 支持按键排序,解决并行处理的乱序问题
var results = new ConcurrentDictionary<int, string>();
// ========== 3. 加载TIF文件并进入处理流程 ==========
// 使用using语句确保Aspose.Imaging资源正确释放
using (Aspose.Imaging.Image baseImage = Aspose.Imaging.Image.Load(tifFilePath))
using (Aspose.Imaging.FileFormats.Tiff.TiffImage tiffImage =
(Aspose.Imaging.FileFormats.Tiff.TiffImage)baseImage)
{
// 检查取消请求:加载完成后立即检查,避免不必要的处理
cancellationToken.ThrowIfCancellationRequested();
// 检查TIF文件是否包含有效帧(页面)
if (tiffImage.Frames.Length == 0)
{
Console.WriteLine($"警告:TIF文件无有效页面 - {tifFilePath}");
return new List<string>(); // 返回空列表而不是null,遵循C#最佳实践
}
// 记录文件信息,便于调试和监控
Console.WriteLine($"开始处理:{tifFilePath},共{tiffImage.Frames.Length}页");
// ========== 4. 配置并行处理选项 ==========
var parallelOptions = new ParallelOptions
{
// 最大并行度控制策略:
// 1. 避免过度并行导致内存溢出(TIF帧可能占用大量内存)
// 2. 默认值4:平衡性能与内存消耗
// 3. 用户可通过参数调整,对于小图片可增加,大图片应减少
MaxDegreeOfParallelism = maxDegreeOfParallelism,
// 传递取消令牌:支持Parallel.For循环内部响应取消请求
CancellationToken = cancellationToken
};
try
{
// ========== 5. 执行并行处理(核心逻辑) ==========
// 使用Task.Run包装Parallel.For的原因:
// 1. Parallel.For是阻塞调用,Task.Run使其在后台线程运行
// 2. 支持真正的异步等待,避免阻塞调用线程
// 3. 便于取消操作传播到Parallel循环内部
await Task.Run(() =>
{
// Parallel.For是.NET框架的并行循环实现
// 优势:
// 1. 自动分区数据,负载均衡
// 2. 支持取消操作
// 3. 线程池优化,避免线程创建开销
Parallel.For(0, tiffImage.Frames.Length, parallelOptions, (frameIndex, state) =>
{
// 检查取消请求:每个迭代开始前检查,及时响应取消
cancellationToken.ThrowIfCancellationRequested();
try
{
// 获取当前帧对象
var tiffFrame = tiffImage.Frames[frameIndex];
// 【关键设计】计算原始页码
// frameIndex: 0-based,原始顺序索引
// originalPageNumber: 1-based,用户可见的页码
// 即使第二页先处理完成,originalPageNumber仍然是2
int originalPageNumber = frameIndex + 1;
// 生成JPG文件名(按原始页码编号)
// D4格式:页码补零到4位(如0001、0010、0100)
string jpgFileName = $"{fileNamePrefix}_{originalPageNumber:D4}.jpg";
string jpgFilePath = System.IO.Path.Combine(outputDir, jpgFileName);
// ========== 6. 配置JPG保存选项 ==========
var jpgOptions = new Aspose.Imaging.ImageOptions.JpegOptions
{
Quality = jpgQuality, // 压缩质量
ColorType = Aspose.Imaging.FileFormats.Jpeg.JpegCompressionColorMode.Rgb, // RGB颜色模式
CompressionType = Aspose.Imaging.FileFormats.Jpeg.JpegCompressionMode.Baseline, // 标准JPEG
ResolutionUnit = Aspose.Imaging.ResolutionUnit.Inch // 分辨率单位:英寸
};
// 初始化EXIF数据容器(用于存储DPI信息)
if (jpgOptions.ExifData == null)
jpgOptions.ExifData = new Aspose.Imaging.Exif.JpegExifData();
// ========== 7. 提取并处理原始DPI信息 ==========
// 读取帧级分辨率(部分TIF每页DPI可能不同)
double frameDpiX = tiffFrame.HorizontalResolution;
double frameDpiY = tiffFrame.VerticalResolution;
// 异常DPI处理:≤0的值视为无效,使用300DPI作为默认值
// 300DPI是打印和扫描的常用标准分辨率
frameDpiX = frameDpiX <= 0 ? 300 : frameDpiX;
frameDpiY = frameDpiY <= 0 ? 300 : frameDpiY;
// 计算平均DPI(水平和垂直分辨率可能不同)
int finalDpi = (int)Math.Round((frameDpiX + frameDpiY) / 2);
// ========== 8. 保存为JPG文件 ==========
// Aspose.Imaging的Save方法是同步操作
// 注意:每个帧独立保存,无共享状态,线程安全
tiffFrame.Save(jpgFilePath, jpgOptions);
// ========== 9. 清理元数据(可选但推荐) ==========
// 移除不必要的EXIF元数据,减少文件体积
// 同时设置正确的DPI值
RemoveSpecificMetadata(jpgFilePath, jpgQuality, finalDpi);
// ========== 10. 存储处理结果 ==========
// 使用frameIndex作为键,确保后续能按原始顺序排序
// TryAdd是线程安全的,不会因为并发写入而冲突
bool added = results.TryAdd(frameIndex, jpgFilePath);
if (added)
{
// 成功日志(生产环境可替换为正式日志)
Console.WriteLine($"√ 第{originalPageNumber:D3}页处理完成: {System.IO.Path.GetFileName(jpgFilePath)}");
}
else
{
// 理论上不会发生,但保留错误处理
Console.WriteLine($"! 第{originalPageNumber}页结果存储失败(键冲突)");
}
}
catch (OperationCanceledException)
{
// 取消异常:停止当前迭代并传递取消状态
// state.Stop()通知Parallel.For停止调度新任务
state.Stop();
throw; // 重新抛出,让外层catch处理
}
catch (Exception ex)
{
// 【容错设计】单帧处理失败不影响其他帧
// 记录错误但继续处理其他页面
Console.WriteLine($"× 第{frameIndex + 1}页处理失败: {ex.Message}");
// 生产环境建议记录详细异常信息:
// _logger.Error(ex, $"TIF第{frameIndex + 1}页处理异常");
// 注意:失败的帧不会添加到results字典
// 这样返回的列表只包含成功处理的页面
}
}); // Parallel.For结束
}, cancellationToken); // Task.Run结束,传递取消令牌
}
catch (OperationCanceledException)
{
// ========== 11. 取消操作的清理处理 ==========
Console.WriteLine($"操作被取消,开始清理已生成的文件...");
// 清理已生成的部分文件(避免留下不完整输出)
foreach (var filePath in results.Values)
{
try
{
if (File.Exists(filePath))
{
File.Delete(filePath);
Console.WriteLine($"已清理: {System.IO.Path.GetFileName(filePath)}");
}
}
catch (Exception cleanupEx)
{
// 清理失败只记录,不抛出异常
Console.WriteLine($"清理失败: {System.IO.Path.GetFileName(filePath)} - {cleanupEx.Message}");
}
}
// 重新抛出取消异常,让调用方知道操作被取消
throw;
}
catch (Exception ex) when (!(ex is OperationCanceledException))
{
// ========== 12. 其他异常处理 ==========
// 非取消异常,记录并重新抛出
Console.WriteLine($"TIF拆分发生未预期异常: {ex.Message}");
throw new InvalidOperationException($"TIF文件拆分失败: {tifFilePath}", ex);
}
} // using语句结束,自动释放Aspose.Imaging资源
// ========== 13. 排序并返回结果 ==========
// 【关键步骤】虽然处理顺序可能乱序,但排序后保证页码顺序
// 业务需求:OCR处理需要按原始顺序处理页面
var sortedResults = results
.OrderBy(kv => kv.Key) // 按键(原始页码索引)升序排序
.Select(kv => kv.Value) // 提取文件路径
.ToList();
// 统计输出
int successCount = sortedResults.Count;
Console.WriteLine($"处理完成: 成功{successCount}页,失败{results.Count - successCount}页");
return sortedResults;
}
/// <summary>
/// 移除图片中指定的EXIF元数据项并统一设置DPI
/// 核心作用:
/// 1. 设置图片的DPI为指定值
/// 2. 删除分辨率单位、颜色空间相关元数据
/// 3. 清理不需要的元数据以减少文件体积或满足特定需求
/// </summary>
/// <param name="filePath">图片文件路径(将直接覆盖原文件)</param>
/// <param name="quality">JPG编码质量(1-100)</param>
/// <param name="dpi">要设置的DPI值(默认为300)</param>
/// <exception cref="FileNotFoundException">当指定的文件路径不存在时抛出</exception>
/// <exception cref="IOException">文件读写过程中发生异常时抛出</exception>
public static void RemoveSpecificMetadata(string filePath, int quality, int dpi = 300)
{
// 1. 校验文件是否存在:避免对不存在的文件进行无效操作,提前抛出明确异常
if (!File.Exists(filePath))
{
throw new FileNotFoundException("待处理的图片文件不存在,请检查路径是否正确", filePath);
}
// 2. 修正质量参数范围:JpegEncoder仅支持1-100的质量值,超出则自动钳位到合法范围
// (Math.Clamp:小于1则取1,大于100则取100,范围内保持原值)
quality = Math.Clamp(quality, 1, 100);
// 3. 加载图片并自动释放资源:
// - using语句会在代码块结束后自动调用image.Dispose(),释放文件句柄和内存,比手动Dispose更安全
// - 此处使用无类型Image.Load():仅处理元数据(不涉及像素操作)时无需指定像素格式,简洁且兼容多种图片原始格式
using (var imageSharp = SixLabors.ImageSharp.Image.Load(filePath))
{
// 获取当前图片的DPI
double originalDpiX = imageSharp.Metadata.HorizontalResolution;
double originalDpiY = imageSharp.Metadata.VerticalResolution;
//// 设置水平分辨率和垂直分辨率
//imageSharp.Metadata.HorizontalResolution = dpi;
//imageSharp.Metadata.VerticalResolution = dpi;
// 4. 获取图片的EXIF配置文件:EXIF是存储图片元数据(如分辨率、拍摄信息)的标准
// 若图片无EXIF数据(exif为null),则跳过EXIF标签删除逻辑
ExifProfile? exif = imageSharp.Metadata.ExifProfile;
if (exif != null)
{
// 删除分辨率单位(TagId: 0x0128):标识分辨率的单位(如英寸/厘米)
exif.RemoveValue(ExifTag.ResolutionUnit);
// 删除颜色空间(TagId: 0xA001):标识图片的颜色编码标准(如sRGB)
exif.RemoveValue(ExifTag.ColorSpace);
}
//// 5. 删除ICC配置文件:ICC是用于颜色管理的配置文件,删除后可进一步减小文件体积
//// (若图片依赖ICC进行颜色校准,删除后可能导致不同设备上的颜色显示略有差异,根据需求选择是否保留此行)
//image.Metadata.IccProfile = null;
// 设置编码器的分辨率,确保DPI正确保留
if (originalDpiX > 0 && originalDpiY > 0)
{
imageSharp.Metadata.HorizontalResolution = originalDpiX;
imageSharp.Metadata.VerticalResolution = originalDpiY;
imageSharp.Metadata.ResolutionUnits = PixelResolutionUnit.PixelsPerInch;
}
else
{
// 如果读取不到DPI,使用300作为默认值
imageSharp.Metadata.HorizontalResolution = 300;
imageSharp.Metadata.VerticalResolution = 300;
imageSharp.Metadata.ResolutionUnits = PixelResolutionUnit.PixelsPerInch;
}
var jpegMetadata = imageSharp.Metadata.GetJpegMetadata();
quality = (jpegMetadata != null && jpegMetadata.Quality != 0)
? jpegMetadata.Quality
: 80; // 兜底质量值
// 6. 配置JPG编码器:指定输出格式为JPG,设置编码参数
var encoder = new JpegEncoder
{
// 编码质量:由外部传入(已做范围修正)
Quality = quality,
ColorType = JpegEncodingColor.YCbCrRatio420 // 色度子采样配置:优先使用原始元数据值,无则默认4:2:0
//ColorType = JpegEncodingColor.YCbCrRatio420
};
try
{
// 7. 覆盖原文件保存:Image.Load()加载后已释放原文件句柄,可安全覆盖
// 若原文件被其他进程占用(如正在预览),会抛出IOException
imageSharp.Save(filePath, encoder);
}
catch (Exception ex)
{
// 8. 异常封装:将底层异常转换为更易理解的IO异常,附加具体错误信息
throw new IOException($"覆盖图片文件失败!可能原因:文件被占用、权限不足、路径非法。文件路径:{filePath}", ex);
}
}
}