mirror of
https://github.com/wmjordan/PDFPatcher.git
synced 2026-10-18 21:52:34 +08:00
788 lines
25 KiB
C#
788 lines
25 KiB
C#
using System;
|
|
using System.Collections.Generic;
|
|
using System.Diagnostics;
|
|
using System.Drawing;
|
|
using System.Drawing.Imaging;
|
|
using System.IO;
|
|
using System.Linq;
|
|
using FreeImageAPI;
|
|
using iTextSharp.text.pdf;
|
|
using PDFPatcher.Common;
|
|
using PDFPatcher.Model;
|
|
using PDFPatcher.Processor.Imaging;
|
|
|
|
namespace PDFPatcher.Processor
|
|
{
|
|
sealed class ImageExtractor
|
|
{
|
|
readonly PdfPageImageProcessor _parser;
|
|
int _totalImageCount;
|
|
int _imageCount;
|
|
int _activePage; // 在导出文件图片时,使用此属性命名文件
|
|
readonly string _fileMask;
|
|
int _pageRotation;
|
|
readonly ImageExtracterOptions _options;
|
|
readonly List<ImageInfo> _imageInfoList = [];
|
|
readonly List<ImageDisposition> _imagePosList = [];
|
|
readonly HashSet<PdfObject> _exportedImages = [];
|
|
readonly Stack<PdfObject> _visited = new Stack<PdfObject>();
|
|
|
|
internal List<ImageInfo> InfoList => _imageInfoList;
|
|
internal List<ImageDisposition> PosList => _imagePosList;
|
|
internal bool PrintImageLocation { get; set; }
|
|
|
|
public ImageExtractor(ImageExtracterOptions options) {
|
|
_fileMask = String.IsNullOrEmpty(options.FileMask) ? "0" : options.FileMask;
|
|
_options = options;
|
|
_parser = new PdfPageImageProcessor(_imagePosList, _imageInfoList);
|
|
}
|
|
|
|
internal void ExtractPageImages(PdfReader reader, int pageNum) {
|
|
if (pageNum < 1 || pageNum > reader.NumberOfPages) {
|
|
return;
|
|
}
|
|
_activePage = pageNum;
|
|
_parser.Reset();
|
|
_imageCount = 0;
|
|
_imageInfoList.Clear();
|
|
_imagePosList.Clear();
|
|
_visited.Clear();
|
|
var o = reader.GetPageNRelease(pageNum);
|
|
if (o == null) {
|
|
return;
|
|
}
|
|
|
|
// 收集页面字典引用的图片
|
|
var pp = o.Locate<PdfDictionary>(PdfName.RESOURCES, PdfName.XOBJECT);
|
|
if (pp != null) {
|
|
ExtractImageInstances(pp, false);
|
|
// 收集页面指令引用的图片
|
|
_parser.ProcessContent(reader.GetPageContent(pageNum), o.Locate<PdfDictionary>(PdfName.RESOURCES));
|
|
_imagePosList.Sort();
|
|
|
|
// 删除页面字典中没有在渲染指令中出现的图片
|
|
if (!_options.ExtractOutOfPageImages) {
|
|
_imageInfoList.RemoveAll(IsOutOfPage);
|
|
}
|
|
}
|
|
pp = o.Locate<PdfDictionary>(PdfName.RESOURCES, PdfName.PATTERN);
|
|
if (pp != null) {
|
|
ExtractImageInstances(pp, true);
|
|
}
|
|
// 收集批注中的图片
|
|
if (_options.ExtractAnnotationImages) {
|
|
var an = o.Locate<PdfArray>(PdfName.ANNOTS);
|
|
if (an != null) {
|
|
foreach (var item in an.ArrayList) {
|
|
ExtractImageInstances(PdfReader.GetPdfObjectRelease(item) as PdfDictionary, true);
|
|
}
|
|
}
|
|
}
|
|
if (_imageInfoList.Count == 0) {
|
|
return;
|
|
}
|
|
_pageRotation = PdfHelper.GetPageRotation(o);
|
|
|
|
_imageInfoList.Sort((x, y) => {
|
|
var xi = _imagePosList.Find((info) => info.Image == x);
|
|
var yi = _imagePosList.Find((info) => info.Image == y);
|
|
if (xi == null) {
|
|
return yi == null ? 0 : -1;
|
|
}
|
|
else if (yi == null) {
|
|
return -1;
|
|
}
|
|
return xi.CompareTo(yi);
|
|
});
|
|
foreach (var item in _imageInfoList) {
|
|
try {
|
|
ExtractImage(item);
|
|
}
|
|
catch (FreeImageException ex) {
|
|
if (item.ReferenceCount > 0) {
|
|
Tracker.TraceMessage(Tracker.Category.Error, "在导出第 " + pageNum + " 页图片时遇到错误:" + ex.Message);
|
|
}
|
|
}
|
|
}
|
|
if (_options.MergeImages && _imagePosList.Count > 1) {
|
|
// 合并相同宽度、相同类型的图片
|
|
MergeImages();
|
|
}
|
|
}
|
|
|
|
bool IsOutOfPage(ImageInfo image) {
|
|
if (!image.IsPageImage) {
|
|
return false;
|
|
}
|
|
var r = image.InlineImage.PdfRef;
|
|
foreach (var item in _imagePosList) {
|
|
var ir = item.Image.InlineImage?.PdfRef;
|
|
if (ir == null || (ir.Number == r.Number && ir.Generation == r.Generation)) {
|
|
return false;
|
|
}
|
|
}
|
|
return true;
|
|
}
|
|
|
|
void ExtractImageInstances(PdfDictionary source, bool recursive) {
|
|
if (source == null) {
|
|
return;
|
|
}
|
|
foreach (var item in source) {
|
|
if (PdfName.SMASK.Equals(item.Key)
|
|
|| PdfName.MASK.Equals(item.Key)
|
|
|| !_options.AllowRedundantImages && _exportedImages.Contains(item.Value)
|
|
|| PdfReader.GetPdfObject(item.Value) is not PdfDictionary obj
|
|
|| _visited.Contains(item.Value)) {
|
|
continue;
|
|
}
|
|
_visited.Push(item.Value);
|
|
if (obj is not PRStream stream) {
|
|
goto NEXT;
|
|
}
|
|
PdfName subType = stream.GetAsName(PdfName.SUBTYPE);
|
|
if (PdfName.IMAGE.Equals(subType)) {
|
|
try {
|
|
_imageInfoList.Add(new ImageInfo(item.Value as PRIndirectReference, !recursive));
|
|
}
|
|
catch (NullReferenceException) {
|
|
Debug.WriteLine(item.Value);
|
|
}
|
|
continue;
|
|
}
|
|
if (PdfName.FORM.Equals(subType)) {
|
|
var fr = stream.Locate<PdfDictionary>(PdfName.RESOURCES, PdfName.XOBJECT);
|
|
if (fr == null) {
|
|
continue;
|
|
}
|
|
foreach (var fri in fr) {
|
|
if (_exportedImages.Contains(fri.Value)) {
|
|
continue;
|
|
}
|
|
stream = PdfReader.GetPdfObject(fri.Value) as PRStream;
|
|
if (stream != null) {
|
|
subType = stream.GetAsName(PdfName.SUBTYPE);
|
|
if (PdfName.IMAGE.Equals(subType)) {
|
|
_imageInfoList.Add(new ImageInfo(fri.Value as PRIndirectReference, false));
|
|
}
|
|
else if (recursive || PdfName.FORM.Equals(subType)) {
|
|
ExtractImageInstances(stream, true);
|
|
}
|
|
}
|
|
else if (recursive) {
|
|
ExtractImageInstances(stream, true);
|
|
}
|
|
}
|
|
}
|
|
NEXT:
|
|
if (recursive) {
|
|
ExtractImageInstances(obj, true);
|
|
}
|
|
_visited.Pop();
|
|
}
|
|
}
|
|
|
|
internal void ExtractImage(ImageInfo info) {
|
|
if (!_options.AllowRedundantImages
|
|
&& !_exportedImages.Add(info.InlineImage.PdfRef)) {
|
|
return;
|
|
}
|
|
if (_totalImageCount == 0 && !Directory.Exists(_options.OutputPath)) {
|
|
Directory.CreateDirectory(_options.OutputPath);
|
|
}
|
|
var bytes = info.DecodeImage(_options);
|
|
if (bytes == null) {
|
|
return;
|
|
}
|
|
if (info.LastDecodeError != null) {
|
|
Tracker.TraceMessage(Tracker.Category.Error, info.LastDecodeError);
|
|
return;
|
|
}
|
|
var fileName = GetNewImageFileName();
|
|
if (info.ExtName == Constants.FileExtensions.Png
|
|
|| info.ExtName == Constants.FileExtensions.Tif
|
|
) {
|
|
SaveBitmap(info, bytes, fileName);
|
|
}
|
|
else {
|
|
SaveImageBytes(info, bytes, fileName);
|
|
}
|
|
if (info.Mask != null) {
|
|
using (var m = info.Mask) {
|
|
m.Palette.CreateGrayscalePalette();
|
|
m.Save($"{fileName}[mask]{(info.Mask.PixelFormat == PixelFormat.Format1bppIndexed ? Constants.FileExtensions.Tif : Constants.FileExtensions.Png)}");
|
|
}
|
|
}
|
|
_totalImageCount++;
|
|
}
|
|
|
|
void SaveImageBytes(ImageInfo info, byte[] bytes, string fileName) {
|
|
var vFlip = _options.VerticalFlipImages ^ info.VerticalFlip;
|
|
var n = fileName + info.ExtName;
|
|
if (PrintImageLocation) {
|
|
Tracker.TraceMessage(Tracker.Category.OutputFile, n);
|
|
Tracker.TraceMessage("导出图片:" + n);
|
|
}
|
|
if (info.ExtName == Constants.FileExtensions.Jp2) {
|
|
if (vFlip || _pageRotation != 0) {
|
|
try {
|
|
using (var bmp = FreeImageHelper.CreateFromBytes(bytes)) {
|
|
RotateBitmap(bmp, _pageRotation, vFlip);
|
|
info.CreatePaletteAndIccProfile(bmp);
|
|
try {
|
|
bmp.Save(n);
|
|
}
|
|
catch (FreeImageException) {
|
|
File.Delete(n);
|
|
bmp.Save(new FilePath(n).ChangeExtension(Constants.FileExtensions.Png));
|
|
}
|
|
SaveMaskedImage(info, bmp, fileName);
|
|
}
|
|
}
|
|
catch (FreeImageException ex) {
|
|
Tracker.TraceMessage(ex);
|
|
bytes.DumpBytes(n);
|
|
}
|
|
}
|
|
else {
|
|
using (FileStream f = new FileStream(n, FileMode.Create)) {
|
|
f.Write(bytes, 0, bytes.Length);
|
|
}
|
|
|
|
if (info.Mask != null) {
|
|
using (var bmp = FreeImageHelper.CreateFromBytes(bytes)) {
|
|
SaveMaskedImage(info, bmp, fileName);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
else if (PdfName.DEVICECMYK.Equals(info.ColorSpace)) {
|
|
using (var bmp = FreeImageHelper.CreateFromBytes(bytes, FREE_IMAGE_LOAD_FLAGS.JPEG_CMYK | FREE_IMAGE_LOAD_FLAGS.TIFF_CMYK)) {
|
|
RotateBitmap(bmp, _pageRotation, vFlip);
|
|
if (info.InvertCmyk && !_options.SuppressCmykInversion) {
|
|
// NOTE: CMYK 图片一般都是反色写入 PDF,导出后需要取反颜色,才能与阅读器上看到的一致
|
|
bmp.Invert();
|
|
// 不能再用 JPEG 格式保存,否则会有质量损失
|
|
bmp.Save(n = fileName + Constants.FileExtensions.Tiff, FREE_IMAGE_FORMAT.FIF_TIFF, FREE_IMAGE_SAVE_FLAGS.TIFF_CMYK | FREE_IMAGE_SAVE_FLAGS.TIFF_DEFLATE);
|
|
}
|
|
else if (_pageRotation != 0 || vFlip) {
|
|
bmp.Save(n = fileName + Constants.FileExtensions.Tiff, FREE_IMAGE_FORMAT.FIF_TIFF, FREE_IMAGE_SAVE_FLAGS.TIFF_CMYK | FREE_IMAGE_SAVE_FLAGS.TIFF_DEFLATE);
|
|
}
|
|
else {
|
|
bytes.DumpBytes(n = fileName + info.ExtName);
|
|
}
|
|
SaveMaskedImage(info, bmp, fileName);
|
|
if (PrintImageLocation) {
|
|
Tracker.TraceMessage("导出图片:" + n);
|
|
}
|
|
}
|
|
}
|
|
else {
|
|
using (FileStream f = new FileStream(n, FileMode.Create)) {
|
|
f.Write(bytes, 0, bytes.Length);
|
|
}
|
|
if (info.ExtName == Constants.FileExtensions.Jpg) {
|
|
if (info.Mask != null) {
|
|
using (var bmp = FreeImageHelper.CreateFromBytes(bytes, FREE_IMAGE_FORMAT.FIF_JPEG)) {
|
|
SaveMaskedImage(info, bmp, fileName);
|
|
}
|
|
}
|
|
if (vFlip) {
|
|
TransformJpeg(n, FREE_IMAGE_JPEG_OPERATION.FIJPEG_OP_FLIP_V);
|
|
}
|
|
if (_pageRotation != 0) {
|
|
TransformJpeg(n,
|
|
_pageRotation == 90 ? FREE_IMAGE_JPEG_OPERATION.FIJPEG_OP_ROTATE_90
|
|
: _pageRotation == 180 ? FREE_IMAGE_JPEG_OPERATION.FIJPEG_OP_ROTATE_180
|
|
: _pageRotation == 270 ? FREE_IMAGE_JPEG_OPERATION.FIJPEG_OP_ROTATE_270
|
|
: FREE_IMAGE_JPEG_OPERATION.FIJPEG_OP_NONE);
|
|
}
|
|
}
|
|
}
|
|
info.FileName = n;
|
|
}
|
|
|
|
static void SwapRedBlue(FreeImageBitmap bmp) {
|
|
var r = bmp.GetChannel(FREE_IMAGE_COLOR_CHANNEL.FICC_RED);
|
|
var b = bmp.GetChannel(FREE_IMAGE_COLOR_CHANNEL.FICC_BLUE);
|
|
bmp.SetChannel(b, FREE_IMAGE_COLOR_CHANNEL.FICC_RED);
|
|
bmp.SetChannel(r, FREE_IMAGE_COLOR_CHANNEL.FICC_BLUE);
|
|
}
|
|
|
|
static void TransformJpeg(string fileName, FREE_IMAGE_JPEG_OPERATION operation) {
|
|
var tmpName = fileName + Constants.FileExtensions.Tmp;
|
|
if (FreeImageBitmap.JPEGTransform(fileName, tmpName, operation, true)) {
|
|
File.Delete(fileName);
|
|
File.Move(tmpName, fileName);
|
|
return;
|
|
}
|
|
File.Delete(tmpName);
|
|
RotateFlipType type;
|
|
switch (operation) {
|
|
case FREE_IMAGE_JPEG_OPERATION.FIJPEG_OP_FLIP_H:
|
|
type = RotateFlipType.RotateNoneFlipX;
|
|
break;
|
|
case FREE_IMAGE_JPEG_OPERATION.FIJPEG_OP_FLIP_V:
|
|
type = RotateFlipType.RotateNoneFlipY;
|
|
break;
|
|
case FREE_IMAGE_JPEG_OPERATION.FIJPEG_OP_ROTATE_90:
|
|
type = RotateFlipType.Rotate270FlipNone;
|
|
break;
|
|
case FREE_IMAGE_JPEG_OPERATION.FIJPEG_OP_ROTATE_180:
|
|
type = RotateFlipType.Rotate180FlipNone;
|
|
break;
|
|
case FREE_IMAGE_JPEG_OPERATION.FIJPEG_OP_ROTATE_270:
|
|
type = RotateFlipType.Rotate90FlipNone;
|
|
break;
|
|
default:
|
|
Tracker.TraceMessage(Tracker.Category.Error, "无损翻转 JPG 图片失败:" + fileName);
|
|
return;
|
|
}
|
|
using (var bmp = new FreeImageBitmap(fileName)) {
|
|
bmp.RotateFlip(type);
|
|
if (bmp.UniqueColors < 256) {
|
|
bmp.ConvertColorDepth(FREE_IMAGE_COLOR_DEPTH.FICD_08_BPP);
|
|
}
|
|
bmp.Save(new FilePath(fileName).ChangeExtension(Constants.FileExtensions.Png));
|
|
}
|
|
File.Delete(fileName);
|
|
}
|
|
|
|
void SaveBitmap(ImageInfo info, byte[] bytes, string fileName) {
|
|
var vFlip = _options.VerticalFlipImages ^ info.VerticalFlip;
|
|
var ext = info.ExtName;
|
|
if (info.PixelFormat == PixelFormat.Format1bppIndexed) {
|
|
ext = !_options.MonoPng ? Constants.FileExtensions.Tif : Constants.FileExtensions.Png;
|
|
}
|
|
var n = fileName + ext;
|
|
if (PrintImageLocation) {
|
|
Tracker.TraceMessage(Tracker.Category.OutputFile, n);
|
|
Tracker.TraceMessage("导出图片:" + n);
|
|
}
|
|
if (PdfName.DEVICECMYK.Equals(info.ColorSpace)) {
|
|
// TODO: 转换字节数组的 CMYK 为 RGB 后加载到 FreeImageBitmap
|
|
//info.PixelFormat = PixelFormat.Undefined;
|
|
using (var bmp = new FreeImageBitmap(
|
|
//info.Width,
|
|
//info.Height,
|
|
//GetStride (info, bytes, vFlip),
|
|
//PixelFormat.Format32bppArgb, bytes
|
|
new MemoryStream(bytes), FREE_IMAGE_LOAD_FLAGS.JPEG_CMYK
|
|
)) {
|
|
if (info.ICCProfile != null) {
|
|
bmp.CreateICCProfile(info.ICCProfile);
|
|
}
|
|
RotateBitmap(bmp, _pageRotation, false);
|
|
if (bmp.ConvertColorDepth(FREE_IMAGE_COLOR_DEPTH.FICD_24_BPP)) {
|
|
SwapRedBlue(bmp);
|
|
n = fileName + Constants.FileExtensions.Png;
|
|
bmp.Save(n, FREE_IMAGE_FORMAT.FIF_PNG);
|
|
}
|
|
else {
|
|
bmp.Save(n,
|
|
FREE_IMAGE_FORMAT.FIF_TIFF,
|
|
FREE_IMAGE_SAVE_FLAGS.TIFF_CMYK | FREE_IMAGE_SAVE_FLAGS.TIFF_DEFLATE);
|
|
}
|
|
SaveMaskedImage(info, bmp, fileName);
|
|
}
|
|
}
|
|
else {
|
|
using (var bmp = CreateFreeImageBitmap(info, ref bytes, vFlip, true)) {
|
|
if (ext == Constants.FileExtensions.Png
|
|
&& _options.InvertBlackAndWhiteImages
|
|
&& info.PixelFormat == PixelFormat.Format1bppIndexed
|
|
&& bmp.Palette.Length == 2) {
|
|
bmp.SwapPaletteIndices(0, 1);
|
|
}
|
|
RotateBitmap(bmp, _pageRotation, false);
|
|
if (ext == Constants.FileExtensions.Tif) {
|
|
TiffHelper.Save(bmp, n);
|
|
}
|
|
else {
|
|
try {
|
|
bmp.Save(n, FREE_IMAGE_FORMAT.FIF_PNG);
|
|
}
|
|
catch (FreeImageException ex) when (ex.Message == "Unable to save bitmap") {
|
|
using (var b = bmp.ToBitmap()) {
|
|
b.Save(n, ImageFormat.Png);
|
|
}
|
|
}
|
|
catch (System.Runtime.InteropServices.SEHException) {
|
|
Tracker.TraceMessage(Tracker.Category.Error, "保存图片时出现错误,请联系程序开发者:" + n);
|
|
}
|
|
SaveMaskedImage(info, bmp, fileName);
|
|
}
|
|
}
|
|
}
|
|
info.FileName = n;
|
|
}
|
|
|
|
static void RotateBitmap(FreeImageBitmap bitmap, int rotation, bool vflip) {
|
|
if (rotation == 0 && !vflip) {
|
|
return;
|
|
}
|
|
RotateFlipType r;
|
|
switch (rotation) {
|
|
case 0: r = RotateFlipType.RotateNoneFlipY; break;
|
|
case 90: r = vflip ? RotateFlipType.Rotate270FlipY : RotateFlipType.Rotate270FlipNone; break;
|
|
case 180: r = vflip ? RotateFlipType.Rotate180FlipY : RotateFlipType.Rotate180FlipNone; break;
|
|
case 270: r = vflip ? RotateFlipType.Rotate90FlipY : RotateFlipType.Rotate90FlipNone; break;
|
|
default: return;
|
|
}
|
|
bitmap.RotateFlip(r);
|
|
}
|
|
|
|
internal static FreeImageBitmap CreateFreeImageBitmap(ImageInfo info, ref byte[] bytes, bool vFlip, bool loadPaletteAndIccp) {
|
|
if (info.ExtName != Constants.FileExtensions.Jpg && info.ExtName != Constants.FileExtensions.Jp2) {
|
|
info.ConvertDecodedBytes(ref bytes);
|
|
}
|
|
FreeImageBitmap bmp;
|
|
if (PdfName.DEVICECMYK.Equals(info.ColorSpace)) {
|
|
bmp = new FreeImageBitmap(new MemoryStream(bytes), FREE_IMAGE_LOAD_FLAGS.TIFF_CMYK | FREE_IMAGE_LOAD_FLAGS.JPEG_CMYK);
|
|
}
|
|
else if (info.ExtName == Constants.FileExtensions.Jp2 || info.ExtName == Constants.FileExtensions.Jpg) {
|
|
bmp = new FreeImageBitmap(new MemoryStream(bytes));
|
|
}
|
|
else {
|
|
bmp = new FreeImageBitmap(info.Width, info.Height, GetStride(info, vFlip), info.PixelFormat, bytes);
|
|
}
|
|
if (loadPaletteAndIccp
|
|
|| info.ColorSpace == PdfName.DEVICEGRAY && info.PaletteColorSpace == null) {
|
|
info.CreatePaletteAndIccProfile(bmp);
|
|
}
|
|
return bmp;
|
|
}
|
|
|
|
static int GetStride(ImageInfo info, bool vFlip) {
|
|
if (PdfName.COLORSPACE.Equals(info.ColorSpace)) {
|
|
return vFlip ? -(info.Width << 2) : (info.Width << 2);
|
|
}
|
|
int bitsPerPixel = Image.GetPixelFormatSize(info.PixelFormat);
|
|
int shift = 0;
|
|
if (bitsPerPixel == 8 && info.BitsPerComponent < 8) {
|
|
switch (info.BitsPerComponent) {
|
|
case 1: shift = 3; break;
|
|
case 2: shift = 2; break;
|
|
case 4: shift = 1; break;
|
|
default:
|
|
break;
|
|
}
|
|
}
|
|
var stride = ((((info.Width * bitsPerPixel) >> shift) + 7) & ~7) >> 3;
|
|
return vFlip ? -stride : stride;
|
|
}
|
|
|
|
string GetNewImageFileName() {
|
|
_imageCount++;
|
|
return String.Concat(
|
|
FileHelper.CombinePath(_options.OutputPath, _activePage.ToString(_fileMask)),
|
|
_imageCount > 1 ? "[" + _imageCount + "]" : null);
|
|
}
|
|
|
|
void MergeImages() {
|
|
var l = _imagePosList.Count;
|
|
for (int i = 0; i < l; i++) {
|
|
var imageI = _imagePosList[i];
|
|
// 由于在导出图像时仅为 PNG 和 TIF 指定 ImageInfo 的 PixelFormat,因此合并过程中仅处理这两类文件
|
|
if (imageI.Image.ReferenceCount < 1 // 图像已处理
|
|
|| imageI.Image.PixelFormat == PixelFormat.Undefined // 不属于可合并的类型
|
|
|| l - i < 2 // 是最后一张图片
|
|
) {
|
|
continue;
|
|
}
|
|
var imageParts = new ImageInfo[l - i];
|
|
var w = imageI.Image.Width;
|
|
var h = 0;
|
|
var i2 = 0;
|
|
for (int j = i; j < l; j++) {
|
|
var imageJ = _imagePosList[j];
|
|
if (imageJ.Image.ReferenceCount < 1 // 图像已处理
|
|
|| imageJ.Image.Width != w // 宽度不相符
|
|
|| Math.Abs(Math.Round(imageJ.X - imageI.X)) > 1 // 位置相差超过 1 点
|
|
) {
|
|
continue;
|
|
}
|
|
imageParts[i2] = imageJ.Image;
|
|
h += imageJ.Image.Height;
|
|
_imagePosList[j].Image.ReferenceCount--; // 避免重复处理
|
|
i2++;
|
|
}
|
|
if (i2 == 0) { // 没有符合合并条件的图片
|
|
continue;
|
|
}
|
|
if (i2 == 1) {
|
|
_imagePosList[i].Image.ReferenceCount++;
|
|
continue;
|
|
}
|
|
if (i2 < imageParts.Length) {
|
|
Array.Resize(ref imageParts, i2);
|
|
}
|
|
if (PrintImageLocation) {
|
|
Tracker.TraceMessage("合并图片:" + String.Join("、", Array.ConvertAll<ImageInfo, string>(imageParts, p => Path.GetFileName(p.FileName))));
|
|
}
|
|
var ext = Path.GetExtension(imageI.Image.FileName).ToLowerInvariant();
|
|
if (imageI.Image.PixelFormat == PixelFormat.Format1bppIndexed) {
|
|
ext = Constants.FileExtensions.Tif;
|
|
}
|
|
var f = GetNewImageFileName();
|
|
using (FreeImageBitmap bmp = new FreeImageBitmap(w, h, imageI.Image.PixelFormat)) {
|
|
var mi = new ImageMerger(bmp);
|
|
foreach (var part in imageParts) {
|
|
using (var bmp2 = FreeImageBitmap.FromFile(part.FileName)) {
|
|
mi.MergeImage(bmp2);
|
|
}
|
|
}
|
|
mi.TryReduceColor();
|
|
if (ext == Constants.FileExtensions.Jpg && _options.MergeJpgToPng) {
|
|
ext = Constants.FileExtensions.Png;
|
|
}
|
|
else if (bmp.PixelFormat == PixelFormat.Format1bppIndexed) {
|
|
if (!_options.MonoPng) {
|
|
ext = Constants.FileExtensions.Tif;
|
|
}
|
|
else {
|
|
ext = Constants.FileExtensions.Png;
|
|
}
|
|
}
|
|
f += ext;
|
|
if (PrintImageLocation) {
|
|
Tracker.TraceMessage(Tracker.Category.OutputFile, f);
|
|
Tracker.TraceMessage("保存合并后的图片:" + f);
|
|
}
|
|
if (ext == Constants.FileExtensions.Tif) {
|
|
TiffHelper.Save(bmp, f);
|
|
}
|
|
else {
|
|
bmp.Save(f);
|
|
}
|
|
var mii = new ImageInfo { FileName = f, ReferenceCount = 1, Height = h, Width = w };
|
|
_imageInfoList.Add(mii);
|
|
_imagePosList.Add(new ImageDisposition(_imagePosList[i].Ctm, mii));
|
|
}
|
|
}
|
|
DeleteIntermediateFiles();
|
|
_imageInfoList.Sort((ImageInfo x, ImageInfo y) => string.Compare(x.FileName, y.FileName, StringComparison.OrdinalIgnoreCase));
|
|
_totalImageCount -= _imageCount;
|
|
_imageCount = 0;
|
|
var newFileNames = new List<string>();
|
|
foreach (var item in _imageInfoList) {
|
|
if (item.FileName != null && item.InlineImage == null) {
|
|
string n;
|
|
do {
|
|
n = GetNewImageFileName() + Path.GetExtension(item.FileName);
|
|
} while (_imagePosList.Exists((i) => i.Image.FileName == n) || newFileNames.Contains(n));
|
|
if (PrintImageLocation) {
|
|
Tracker.TraceMessage(String.Concat("重命名合并后的文件 ", item.FileName, " 为 ", n));
|
|
Tracker.TraceMessage(Tracker.Category.OutputFile, n);
|
|
}
|
|
newFileNames.Add(n);
|
|
File.Delete(n);
|
|
File.Move(item.FileName, n);
|
|
item.FileName = n;
|
|
}
|
|
}
|
|
_totalImageCount += _imageCount;
|
|
}
|
|
|
|
void DeleteIntermediateFiles() {
|
|
foreach (var item in _imageInfoList) {
|
|
if (item.ReferenceCount < 1 && item.FileName is not null) {
|
|
File.Delete(item.FileName);
|
|
item.FileName = null;
|
|
}
|
|
}
|
|
}
|
|
|
|
static void SaveMaskedImage(ImageInfo info, FreeImageBitmap bmp, string fileName) {
|
|
if (info.Mask != null
|
|
&& info.Mask.Size == bmp.Size
|
|
&& (bmp.PixelFormat == PixelFormat.Format32bppArgb
|
|
|| bmp.ConvertColorDepth(FREE_IMAGE_COLOR_DEPTH.FICD_32_BPP))) {
|
|
var m = (FreeImageBitmap)info.Mask.Clone();
|
|
if ((m.PixelFormat == PixelFormat.Format8bppIndexed || m.ConvertColorDepth(FREE_IMAGE_COLOR_DEPTH.FICD_08_BPP | FREE_IMAGE_COLOR_DEPTH.FICD_FORCE_GREYSCALE))
|
|
&& bmp.SetChannel(m, FREE_IMAGE_COLOR_CHANNEL.FICC_ALPHA)) {
|
|
bmp.Save($"{fileName}[merged]{Constants.FileExtensions.Png}");
|
|
}
|
|
}
|
|
}
|
|
|
|
sealed class ImageMerger
|
|
{
|
|
readonly FreeImageBitmap _Image;
|
|
ColorType _ColorType;
|
|
RGBQUAD[] _Colors;
|
|
int _Height;
|
|
|
|
public ImageMerger(FreeImageBitmap image) {
|
|
_Image = image;
|
|
if (!image.HasPalette) {
|
|
_ColorType = ColorType.FullColor;
|
|
}
|
|
}
|
|
|
|
public void MergeImage(FreeImageBitmap bmp) {
|
|
if (_Height == 0) {
|
|
PasteFirstImage(bmp);
|
|
return;
|
|
}
|
|
if (_ColorType == ColorType.FullColor) {
|
|
PasteIntoFullColor(bmp);
|
|
return;
|
|
}
|
|
if (!bmp.HasPalette) {
|
|
ExpandColorSpaceAndPaste(bmp);
|
|
return;
|
|
}
|
|
var p = bmp.Palette.ToArray();
|
|
if (p.SequenceEqual(_Colors)) {
|
|
PasteAndIncrementHeight(bmp);
|
|
return;
|
|
}
|
|
switch (_ColorType) {
|
|
case ColorType.BlackAndWhite:
|
|
if (p[0].Color == Color.White
|
|
&& p[1].Color == Color.Black
|
|
&& bmp.Invert()) {
|
|
PasteAndIncrementHeight(bmp);
|
|
return;
|
|
}
|
|
break;
|
|
case ColorType.WhiteAndBlack:
|
|
if (p[0].Color == Color.Black
|
|
&& p[1].Color == Color.White
|
|
&& bmp.Invert()) {
|
|
PasteAndIncrementHeight(bmp);
|
|
return;
|
|
}
|
|
break;
|
|
}
|
|
TryExpandColorDepth(_Image);
|
|
TryExpandColorDepth(bmp);
|
|
PasteAndIncrementHeight(bmp);
|
|
_Colors = null;
|
|
_ColorType = ColorType.FullColor;
|
|
}
|
|
|
|
public bool TryReduceColor() {
|
|
if (_ColorType != ColorType.FullColor) {
|
|
return false;
|
|
}
|
|
var c = _Image.UniqueColors;
|
|
if (c > 256) {
|
|
return false;
|
|
}
|
|
if (c < 3) {
|
|
if (_Image.ConvertColorDepth(FREE_IMAGE_COLOR_DEPTH.FICD_01_BPP)) {
|
|
_ColorType = ColorType.BlackAndWhite;
|
|
return true;
|
|
}
|
|
return false;
|
|
}
|
|
if (_Image.ConvertColorDepth(c < 17 ? FREE_IMAGE_COLOR_DEPTH.FICD_04_BPP : FREE_IMAGE_COLOR_DEPTH.FICD_08_BPP)) {
|
|
_ColorType = ColorType.IndexedColor;
|
|
return true;
|
|
}
|
|
return false;
|
|
}
|
|
|
|
void PasteFirstImage(FreeImageBitmap bmp) {
|
|
if (bmp.HasPalette) {
|
|
_Colors = new RGBQUAD[bmp.ColorsUsed];
|
|
_Image.Palette.AsArray = bmp.Palette.AsArray;
|
|
bmp.Palette.CopyTo(_Colors, 0);
|
|
_ColorType = GetWhiteBlackType(_Colors);
|
|
}
|
|
_Image.Paste(bmp, 0, 0, Int32.MaxValue);
|
|
_Height = bmp.Height;
|
|
}
|
|
|
|
static ColorType GetWhiteBlackType(RGBQUAD[] colors) {
|
|
var c0 = colors[0].Color;
|
|
var c1 = colors[1].Color;
|
|
return c0 == Color.Black && c1 == Color.White ? ColorType.BlackAndWhite
|
|
: c0 == Color.White && c1 == Color.Black ? ColorType.WhiteAndBlack
|
|
: ColorType.IndexedColor;
|
|
}
|
|
|
|
void PasteIntoFullColor(FreeImageBitmap bmp) {
|
|
if (bmp.HasPalette) {
|
|
TryExpandColorDepth(bmp);
|
|
}
|
|
PasteAndIncrementHeight(bmp);
|
|
}
|
|
|
|
static void TryExpandColorDepth(FreeImageBitmap bmp) {
|
|
if (!bmp.ConvertColorDepth(FREE_IMAGE_COLOR_DEPTH.FICD_24_BPP)) {
|
|
throw new InvalidOperationException("合并图片失败:无法转换图片颜色");
|
|
}
|
|
}
|
|
|
|
void PasteAndIncrementHeight(FreeImageBitmap bmp) {
|
|
if (!_Image.Paste(bmp, 0, _Height, Int32.MaxValue)) {
|
|
throw new InvalidOperationException("合并图片失败:粘贴操作出错");
|
|
}
|
|
_Height += bmp.Height;
|
|
}
|
|
|
|
void ExpandColorSpaceAndPaste(FreeImageBitmap bmp) {
|
|
TryExpandColorDepth(_Image);
|
|
PasteIntoFullColor(bmp);
|
|
}
|
|
}
|
|
|
|
enum ColorType
|
|
{
|
|
Undefined,
|
|
BlackAndWhite,
|
|
WhiteAndBlack,
|
|
IndexedColor,
|
|
FullColor,
|
|
}
|
|
|
|
sealed class PdfPageImageProcessor : PdfContentStreamProcessor
|
|
{
|
|
readonly List<ImageDisposition> _posList;
|
|
readonly List<ImageInfo> _infoList;
|
|
|
|
public PdfPageImageProcessor(List<ImageDisposition> posList, List<ImageInfo> infoList) {
|
|
PopulateOperators();
|
|
_posList = posList;
|
|
_infoList = infoList;
|
|
}
|
|
protected override void InvokeOperator(PdfLiteral oper, List<PdfObject> operands) {
|
|
base.InvokeOperator(oper, operands);
|
|
switch (oper.ToString()) {
|
|
case "Do":
|
|
var xobjects = Resource.GetAsDict(PdfName.XOBJECT);
|
|
var r = xobjects.GetAsIndirectObject(operands[0] as PdfName);
|
|
var info = _infoList.Find(
|
|
i => i.InlineImage.PdfRef != null
|
|
&& i.InlineImage.PdfRef.Number == r.Number
|
|
&& i.InlineImage.PdfRef.Generation == r.Generation);
|
|
if (info != null) {
|
|
info.ReferenceCount++;
|
|
_posList.Add(new ImageDisposition(CurrentGraphicState.TransMatrix, info));
|
|
}
|
|
else if (PdfReader.GetPdfObjectRelease(r) is PRStream s
|
|
&& PdfName.FORM.Equals(s.GetAsName(PdfName.SUBTYPE))) {
|
|
Trace.WriteLine("Parse form:");
|
|
Trace.WriteLine(System.Text.Encoding.ASCII.GetString(PdfReader.GetStreamBytes(s)));
|
|
var res = s.GetAsDict(PdfName.RESOURCES);
|
|
new PdfPageImageProcessor(_posList, _infoList).ProcessContent(PdfReader.GetStreamBytes(s), res ?? Resource);
|
|
}
|
|
else {
|
|
Trace.WriteLine(String.Concat("Image ", r, " not found."));
|
|
}
|
|
break;
|
|
case "BI":
|
|
info = new ImageInfo(new PdfImageData(operands[0] as PdfDictionary, ((PdfImageData)operands[0]).RawBytes));
|
|
info.ReferenceCount++;
|
|
_infoList.Add(info);
|
|
_posList.Add(new ImageDisposition(CurrentGraphicState.TransMatrix, info));
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|