// Quicker C# 模块 - 扫描全能王工具 v2
// 运行线程: 后台线程(STA)
//
// ====== DLL 部署说明 ======
// 1. 从 NuGet 下载以下两个包(版本必须一致,推荐 4.7.0):
// - OpenCvSharp4 → 取出 OpenCvSharp.dll
// - OpenCvSharp4.runtime.win → 取出 OpenCvSharpExtern.dll
// (在 nupkg 解压后的 runtimes/win-x64/native/ 目录下)
// - itextsharp 5.5.x → 取出 itextsharp.dll
//
// 2. 把这三个文件都放到同一个目录,例如:
// D:\UserFiles\Documents\Quicker\_packages\OpenCvSharp\4.7.0\
//
// 3. Quicker "引用DLL库"栏填写(每行一个完整路径):
// D:\...\OpenCvSharp.dll
// D:\...\itextsharp.dll
// (OpenCvSharpExtern.dll 不填,代码会自动从同目录加载)
//
// 4. 不需要引用 OpenCvSharp.Extensions.dll
//css_reference OpenCvSharp.dll;
//css_reference itextsharp.dll;
using System;
using System.Collections.Generic;
using System.Drawing;
using System.Drawing.Imaging;
using System.IO;
using System.Linq;
using System.Runtime.InteropServices;
using System.Windows.Forms;
using OpenCvSharp;
using iTextSharp.text;
using iTextSharp.text.pdf;
///
/// 文档扫描处理核心 - 全自动流水线
/// 透视矫正 → 倾斜校正 → 图像增强,一步到位
///
public static class DocumentScanner
{
///
/// 主处理入口:对单张图像执行完整扫描增强流水线
///
public static Mat ProcessImage(Mat src)
{
Mat result = src.Clone();
// 第1步:文档边缘检测 + 透视矫正
result = DetectAndCorrectPerspective(result);
// 第2步:倾斜校正
result = Deskew(result);
// 第3步:图像增强(Magic Color效果)
result = EnhanceDocument(result);
return result;
}
// ==========================================
// 第1步:文档边缘检测 + 透视矫正 + 自动裁边
// ==========================================
private static Mat DetectAndCorrectPerspective(Mat src)
{
int origW = src.Width;
int origH = src.Height;
// 缩小图像加速边缘检测
double scale = 1.0;
Mat resized = src.Clone();
if (Math.Max(origW, origH) > 1000)
{
scale = 1000.0 / Math.Max(origW, origH);
resized = new Mat();
Cv2.Resize(src, resized, new OpenCvSharp.Size(0, 0), scale, scale);
}
// 灰度 + 模糊
Mat gray = new Mat();
Cv2.CvtColor(resized, gray, ColorConversionCodes.BGR2GRAY);
Cv2.GaussianBlur(gray, gray, new OpenCvSharp.Size(5, 5), 0);
// 多策略边缘检测:先用宽松阈值,再用严格阈值
OpenCvSharp.Point[] docContour = null;
double imgArea = resized.Width * resized.Height;
int[][] cannyParams = new int[][] {
new int[] { 30, 120 }, // 宽松
new int[] { 50, 200 }, // 中等
new int[] { 75, 250 } // 严格
};
for (int p = 0; p < cannyParams.Length && docContour == null; p++)
{
Mat edged = new Mat();
Cv2.Canny(gray, edged, cannyParams[p][0], cannyParams[p][1]);
// 膨胀 + 闭运算,使边缘更连续
Mat k1 = Cv2.GetStructuringElement(MorphShapes.Rect, new OpenCvSharp.Size(5, 5));
Cv2.Dilate(edged, edged, k1);
Mat k2 = Cv2.GetStructuringElement(MorphShapes.Rect, new OpenCvSharp.Size(7, 7));
Cv2.MorphologyEx(edged, edged, MorphTypes.Close, k2);
OpenCvSharp.Point[][] contours;
HierarchyIndex[] hierarchy;
Cv2.FindContours(edged, out contours, out hierarchy,
RetrievalModes.List, ContourApproximationModes.ApproxSimple);
// 按面积降序
Array.Sort(contours, delegate(OpenCvSharp.Point[] a, OpenCvSharp.Point[] b) {
return Cv2.ContourArea(b).CompareTo(Cv2.ContourArea(a));
});
for (int i = 0; i < Math.Min(contours.Length, 15); i++)
{
double area = Cv2.ContourArea(contours[i]);
if (area < imgArea * 0.05) break; // 降低到5%
double peri = Cv2.ArcLength(contours[i], true);
OpenCvSharp.Point[] approx = Cv2.ApproxPolyDP(contours[i], 0.02 * peri, true);
if (approx.Length == 4 && Cv2.IsContourConvex(approx))
{
docContour = approx;
break;
}
}
edged.Dispose();
k1.Dispose();
k2.Dispose();
}
// 回退策略:如果没找到四边形,尝试用最大轮廓的最小外接矩形
if (docContour == null)
{
Mat edged2 = new Mat();
Cv2.Canny(gray, edged2, 30, 100);
Mat kd = Cv2.GetStructuringElement(MorphShapes.Rect, new OpenCvSharp.Size(9, 9));
Cv2.Dilate(edged2, edged2, kd);
Cv2.MorphologyEx(edged2, edged2, MorphTypes.Close, kd);
OpenCvSharp.Point[][] contours2;
HierarchyIndex[] hierarchy2;
Cv2.FindContours(edged2, out contours2, out hierarchy2,
RetrievalModes.External, ContourApproximationModes.ApproxSimple);
double maxArea = 0;
int maxIdx = -1;
for (int i = 0; i < contours2.Length; i++)
{
double a = Cv2.ContourArea(contours2[i]);
if (a > maxArea) { maxArea = a; maxIdx = i; }
}
if (maxIdx >= 0 && maxArea > imgArea * 0.05)
{
RotatedRect rr = Cv2.MinAreaRect(contours2[maxIdx]);
Point2f[] pts = rr.Points();
docContour = new OpenCvSharp.Point[4];
for (int i = 0; i < 4; i++)
{
docContour[i] = new OpenCvSharp.Point((int)pts[i].X, (int)pts[i].Y);
}
}
edged2.Dispose();
kd.Dispose();
}
gray.Dispose();
if (resized != src) resized.Dispose();
if (docContour == null) return src; // 实在找不到,原样返回
// 将坐标映射回原始尺寸
Point2f[] srcPts = new Point2f[4];
for (int i = 0; i < 4; i++)
{
srcPts[i] = new Point2f(
(float)(docContour[i].X / scale),
(float)(docContour[i].Y / scale));
}
// 对四个角点排序:左上、右上、右下、左下
srcPts = OrderPoints(srcPts);
// 计算目标矩形尺寸
double widthTop = Distance(srcPts[0], srcPts[1]);
double widthBot = Distance(srcPts[3], srcPts[2]);
int maxW = (int)Math.Max(widthTop, widthBot);
double heightLeft = Distance(srcPts[0], srcPts[3]);
double heightRight = Distance(srcPts[1], srcPts[2]);
int maxH = (int)Math.Max(heightLeft, heightRight);
if (maxW < 100 || maxH < 100) return src; // 太小不处理
Point2f[] dstPts = new Point2f[] {
new Point2f(0, 0),
new Point2f(maxW - 1, 0),
new Point2f(maxW - 1, maxH - 1),
new Point2f(0, maxH - 1)
};
Mat M = Cv2.GetPerspectiveTransform(srcPts, dstPts);
Mat warped = new Mat();
Cv2.WarpPerspective(src, warped, M, new OpenCvSharp.Size(maxW, maxH),
InterpolationFlags.Linear, BorderTypes.Constant, Scalar.White);
M.Dispose();
src.Dispose();
return warped;
}
///
/// 对四个点排序:左上、右上、右下、左下
///
private static Point2f[] OrderPoints(Point2f[] pts)
{
// 按 x+y 排序找左上和右下
Point2f[] sorted = new Point2f[4];
List list = new List(pts);
// 左上 = x+y 最小,右下 = x+y 最大
list.Sort(delegate(Point2f a, Point2f b) {
return (a.X + a.Y).CompareTo(b.X + b.Y);
});
sorted[0] = list[0]; // 左上
sorted[2] = list[3]; // 右下
// 右上 = x-y 最大,左下 = x-y 最小
list.Sort(delegate(Point2f a, Point2f b) {
return (a.X - a.Y).CompareTo(b.X - b.Y);
});
sorted[1] = list[3]; // 右上
sorted[3] = list[0]; // 左下
return sorted;
}
private static double Distance(Point2f a, Point2f b)
{
double dx = a.X - b.X;
double dy = a.Y - b.Y;
return Math.Sqrt(dx * dx + dy * dy);
}
// ==========================================
// 第2步:倾斜校正(Deskew)
// ==========================================
private static Mat Deskew(Mat src)
{
Mat gray = new Mat();
Cv2.CvtColor(src, gray, ColorConversionCodes.BGR2GRAY);
// 二值化后检测边缘,比直接Canny更稳定
Mat thresh = new Mat();
Cv2.AdaptiveThreshold(gray, thresh, 255,
AdaptiveThresholdTypes.GaussianC,
ThresholdTypes.BinaryInv, 15, 5);
Mat edges = new Mat();
Cv2.Canny(thresh, edges, 50, 150, 3);
thresh.Dispose();
// 膨胀使文字行连成线段
Mat dk = Cv2.GetStructuringElement(MorphShapes.Rect, new OpenCvSharp.Size(15, 1));
Cv2.Dilate(edges, edges, dk);
dk.Dispose();
// 霍夫直线检测 — 降低阈值,缩短最小线段长度
LineSegmentPoint[] lines = Cv2.HoughLinesP(edges, 1, Math.PI / 180.0,
50, 50, 10);
gray.Dispose();
edges.Dispose();
if (lines == null || lines.Length == 0) return src;
// 收集所有线段角度,按线段长度加权
List angles = new List();
List weights = new List();
for (int i = 0; i < lines.Length; i++)
{
double dx = lines[i].P2.X - lines[i].P1.X;
double dy = lines[i].P2.Y - lines[i].P1.Y;
double len = Math.Sqrt(dx * dx + dy * dy);
double angle = Math.Atan2(dy, dx) * 180.0 / Math.PI;
// 只关注接近水平的线(-30° ~ 30°)
if (Math.Abs(angle) < 30)
{
angles.Add(angle);
weights.Add(len);
}
}
if (angles.Count == 0) return src;
// 加权中位数:按长度排序后取加权中间位置
// 简化:先按角度排序,取中位数
angles.Sort();
double medianAngle = angles[angles.Count / 2];
// 降低阈值:只要 > 0.1° 就校正,上限放宽到 20°
if (Math.Abs(medianAngle) < 0.1 || Math.Abs(medianAngle) > 20)
return src;
// 旋转校正
Point2f center = new Point2f(src.Width / 2f, src.Height / 2f);
Mat rotMat = Cv2.GetRotationMatrix2D(center, medianAngle, 1.0);
Mat rotated = new Mat();
Cv2.WarpAffine(src, rotated, rotMat, src.Size(),
InterpolationFlags.Cubic, BorderTypes.Constant, Scalar.White);
rotMat.Dispose();
src.Dispose();
return rotated;
}
// ==========================================
// 第3步:图像增强
// 减法提取墨迹深度 → 非线性加深 → 锐化
// ==========================================
private static Mat EnhanceDocument(Mat src)
{
// --- a: 转灰度 ---
Mat gray = new Mat();
Cv2.CvtColor(src, gray, ColorConversionCodes.BGR2GRAY);
// --- b: 背景估计 ---
int blurSize = Math.Max(gray.Width, gray.Height) / 8;
if (blurSize % 2 == 0) blurSize++;
if (blurSize < 51) blurSize = 51;
Mat background = new Mat();
Cv2.GaussianBlur(gray, background, new OpenCvSharp.Size(blurSize, blurSize), 0);
// --- c: 减法提取墨迹 + 非线性加深 ---
int w = gray.Width;
int h = gray.Height;
byte[] grayData = new byte[w * h];
byte[] bgData = new byte[w * h];
byte[] resultData = new byte[w * h];
Marshal.Copy(gray.Data, grayData, 0, grayData.Length);
Marshal.Copy(background.Data, bgData, 0, bgData.Length);
background.Dispose();
// 用百分位数而非最大值来计算增益(排除极端值)
// 先收集所有正墨迹值
int[] inkHist = new int[256];
for (int i = 0; i < grayData.Length; i++)
{
int ink = bgData[i] - grayData[i];
if (ink < 0) ink = 0;
if (ink > 255) ink = 255;
inkHist[ink]++;
}
// 找 95 百分位数作为参考墨迹深度
int totalPixels = w * h;
int target95 = (int)(totalPixels * 0.95);
int cumulative = 0;
int ink95 = 10;
for (int i = 0; i < 256; i++)
{
cumulative += inkHist[i];
if (cumulative >= target95)
{
ink95 = Math.Max(i, 5);
break;
}
}
// 增益:让95百分位的墨迹深度映射到接近纯黑
// 用非线性映射(幂函数)让浅墨迹也能变深
for (int i = 0; i < grayData.Length; i++)
{
int ink = bgData[i] - grayData[i];
if (ink < 0) ink = 0;
// 归一化到 0-1
double inkNorm = (double)ink / ink95;
if (inkNorm > 1.0) inkNorm = 1.0;
// 非线性加深:pow(x, 0.45) 让浅墨迹也变深
// 0.45 < 1 所以小值被放大(浅色文字变深)
double darkness = Math.Pow(inkNorm, 0.45);
// 映射到灰度:0=白(255), 1=黑(0)
int val = (int)(255.0 * (1.0 - darkness));
if (val < 0) val = 0;
if (val > 255) val = 255;
resultData[i] = (byte)val;
}
Mat enhanced = new Mat(h, w, MatType.CV_8U);
Marshal.Copy(resultData, 0, enhanced.Data, resultData.Length);
// --- d: USM锐化 ---
Mat blurred = new Mat();
Cv2.GaussianBlur(enhanced, blurred, new OpenCvSharp.Size(0, 0), 1.0);
Mat sharpened = new Mat();
Cv2.AddWeighted(enhanced, 1.8, blurred, -0.8, 0, sharpened);
blurred.Dispose();
enhanced.Dispose();
// --- e: 对比度加强 ---
// 文字更黑,背景更白
Cv2.ConvertScaleAbs(sharpened, sharpened, 1.3, -20);
// --- f: 白底清理 ---
Cv2.Threshold(sharpened, sharpened, 220, 255, ThresholdTypes.Trunc);
Cv2.ConvertScaleAbs(sharpened, sharpened, 255.0 / 220.0, 0);
// 转回3通道
Mat output = new Mat();
Cv2.CvtColor(sharpened, output, ColorConversionCodes.GRAY2BGR);
gray.Dispose();
sharpened.Dispose();
src.Dispose();
return output;
}
}
///
/// PDF 读写工具类
///
public static class PdfHelper
{
///
/// 从PDF提取所有页面中的嵌入图像
///
public static List ExtractImagesFromPdf(string pdfPath)
{
List images = new List();
PdfReader reader = new PdfReader(pdfPath);
for (int pageNum = 1; pageNum <= reader.NumberOfPages; pageNum++)
{
PdfDictionary page = reader.GetPageN(pageNum);
PdfDictionary resources = page.GetAsDict(PdfName.RESOURCES);
if (resources == null) continue;
PdfDictionary xObjects = resources.GetAsDict(PdfName.XOBJECT);
if (xObjects == null) continue;
foreach (PdfName name in xObjects.Keys)
{
PdfObject obj = xObjects.Get(name);
if (!obj.IsIndirect()) continue;
PdfDictionary tg = (PdfDictionary)PdfReader.GetPdfObject(obj);
PdfName subtype = tg.GetAsName(PdfName.SUBTYPE);
if (!PdfName.IMAGE.Equals(subtype)) continue;
try
{
int xrefIdx = ((PRIndirectReference)obj).Number;
PdfObject pdfObj = reader.GetPdfObject(xrefIdx);
PdfStream stream = (PdfStream)pdfObj;
byte[] rawBytes = PdfReader.GetStreamBytesRaw((PRStream)stream);
PdfName filter = stream.GetAsName(PdfName.FILTER);
if (PdfName.DCTDECODE.Equals(filter))
{
// JPEG 直接解码
using (MemoryStream ms = new MemoryStream(rawBytes))
{
Bitmap bmp = new Bitmap(ms);
images.Add(new Bitmap(bmp));
}
}
else if (PdfName.FLATEDECODE.Equals(filter) || filter == null)
{
byte[] decoded = PdfReader.GetStreamBytes((PRStream)stream);
int width = tg.GetAsNumber(PdfName.WIDTH).IntValue;
int height = tg.GetAsNumber(PdfName.HEIGHT).IntValue;
int bpc = tg.GetAsNumber(PdfName.BITSPERCOMPONENT).IntValue;
PdfName cs = tg.GetAsName(PdfName.COLORSPACE);
Bitmap bmp = RawToBitmap(decoded, width, height, bpc, cs);
if (bmp != null) images.Add(bmp);
}
else
{
byte[] decoded = PdfReader.GetStreamBytes((PRStream)stream);
try
{
using (MemoryStream ms = new MemoryStream(decoded))
{
images.Add(new Bitmap(new Bitmap(ms)));
}
}
catch { }
}
}
catch { continue; }
}
}
reader.Close();
if (images.Count == 0)
{
throw new Exception("无法从PDF中提取图像。该PDF可能是纯文本或使用了不支持的编码。");
}
return images;
}
private static Bitmap RawToBitmap(byte[] data, int w, int h, int bpc, PdfName colorSpace)
{
if (bpc != 8) return null;
bool isRgb = PdfName.DEVICERGB.Equals(colorSpace);
bool isGray = PdfName.DEVICEGRAY.Equals(colorSpace);
if (isRgb && data.Length >= w * h * 3)
{
Bitmap bmp = new Bitmap(w, h, PixelFormat.Format24bppRgb);
BitmapData bd = bmp.LockBits(
new System.Drawing.Rectangle(0, 0, w, h),
ImageLockMode.WriteOnly, PixelFormat.Format24bppRgb);
for (int y = 0; y < h; y++)
{
for (int x = 0; x < w; x++)
{
int si = (y * w + x) * 3;
int di = y * bd.Stride + x * 3;
Marshal.WriteByte(bd.Scan0, di, data[si + 2]);
Marshal.WriteByte(bd.Scan0, di + 1, data[si + 1]);
Marshal.WriteByte(bd.Scan0, di + 2, data[si]);
}
}
bmp.UnlockBits(bd);
return bmp;
}
else if (isGray && data.Length >= w * h)
{
Bitmap bmp = new Bitmap(w, h, PixelFormat.Format24bppRgb);
BitmapData bd = bmp.LockBits(
new System.Drawing.Rectangle(0, 0, w, h),
ImageLockMode.WriteOnly, PixelFormat.Format24bppRgb);
for (int y = 0; y < h; y++)
{
for (int x = 0; x < w; x++)
{
byte g = data[y * w + x];
int di = y * bd.Stride + x * 3;
Marshal.WriteByte(bd.Scan0, di, g);
Marshal.WriteByte(bd.Scan0, di + 1, g);
Marshal.WriteByte(bd.Scan0, di + 2, g);
}
}
bmp.UnlockBits(bd);
return bmp;
}
return null;
}
///
/// 将多张图像保存为单个PDF
///
public static void SaveImagesToPdf(List images, string outputPath, int jpegQuality)
{
Document doc = null;
PdfWriter writer = null;
FileStream fs = null;
try
{
fs = new FileStream(outputPath, FileMode.Create);
Bitmap first = images[0];
doc = new Document(new iTextSharp.text.Rectangle(first.Width, first.Height), 0, 0, 0, 0);
writer = PdfWriter.GetInstance(doc, fs);
doc.Open();
for (int i = 0; i < images.Count; i++)
{
Bitmap bmp = images[i];
if (i > 0)
{
doc.SetPageSize(new iTextSharp.text.Rectangle(bmp.Width, bmp.Height));
doc.NewPage();
}
byte[] imgBytes;
using (MemoryStream ms = new MemoryStream())
{
EncoderParameters ep = new EncoderParameters(1);
ep.Param[0] = new EncoderParameter(
System.Drawing.Imaging.Encoder.Quality, (long)jpegQuality);
ImageCodecInfo codec = GetJpegCodec();
bmp.Save(ms, codec, ep);
imgBytes = ms.ToArray();
}
iTextSharp.text.Image pdfImg = iTextSharp.text.Image.GetInstance(imgBytes);
pdfImg.SetAbsolutePosition(0, 0);
pdfImg.ScaleAbsolute(bmp.Width, bmp.Height);
doc.Add(pdfImg);
}
}
finally
{
if (doc != null && doc.IsOpen()) doc.Close();
if (writer != null) writer.Close();
if (fs != null) fs.Close();
}
}
private static ImageCodecInfo GetJpegCodec()
{
ImageCodecInfo[] codecs = ImageCodecInfo.GetImageDecoders();
for (int i = 0; i < codecs.Length; i++)
{
if (codecs[i].FormatID == ImageFormat.Jpeg.Guid) return codecs[i];
}
return null;
}
}
///
/// 简化版界面 - 一键处理,无需选模式
///
public class ScannerForm : Form
{
private ListBox lstFiles;
private Button btnAddImages, btnAddPdf, btnRemove, btnClear;
private Button btnMoveUp, btnMoveDown, btnProcess;
private NumericUpDown nudQuality;
private ProgressBar progressBar;
private Label lblStatus;
private PictureBox picPreview;
private List inputFiles = new List();
private bool isPdfInput = false;
public ScannerForm()
{
InitUI();
}
private void InitUI()
{
this.Text = "文档扫描增强工具 v2";
this.Size = new System.Drawing.Size(720, 600);
this.StartPosition = FormStartPosition.CenterScreen;
this.FormBorderStyle = FormBorderStyle.FixedDialog;
this.MaximizeBox = false;
this.BackColor = System.Drawing.Color.FromArgb(245, 245, 250);
// 文件列表标题
Label lblFiles = new Label();
lblFiles.Text = "输入文件:";
lblFiles.Location = new System.Drawing.Point(15, 12);
lblFiles.AutoSize = true;
lblFiles.Font = new System.Drawing.Font("微软雅黑", 9f, System.Drawing.FontStyle.Bold);
this.Controls.Add(lblFiles);
// 文件列表
lstFiles = new ListBox();
lstFiles.Location = new System.Drawing.Point(15, 35);
lstFiles.Size = new System.Drawing.Size(460, 180);
lstFiles.Font = new System.Drawing.Font("Consolas", 9f);
lstFiles.HorizontalScrollbar = true;
lstFiles.SelectedIndexChanged += delegate(object s, EventArgs ev) {
int idx = lstFiles.SelectedIndex;
if (idx >= 0 && idx < inputFiles.Count && !isPdfInput)
{
try
{
if (picPreview.Image != null) picPreview.Image.Dispose();
picPreview.Image = new Bitmap(inputFiles[idx]);
}
catch { }
}
};
this.Controls.Add(lstFiles);
// 右侧按钮
int bx = 490;
int bw = 100;
btnAddImages = new Button();
btnAddImages.Text = "添加图片";
btnAddImages.Location = new System.Drawing.Point(bx, 35);
btnAddImages.Size = new System.Drawing.Size(bw, 32);
btnAddImages.Click += BtnAddImages_Click;
this.Controls.Add(btnAddImages);
btnAddPdf = new Button();
btnAddPdf.Text = "选择PDF";
btnAddPdf.Location = new System.Drawing.Point(bx + 110, 35);
btnAddPdf.Size = new System.Drawing.Size(bw, 32);
btnAddPdf.Click += BtnAddPdf_Click;
this.Controls.Add(btnAddPdf);
btnRemove = new Button();
btnRemove.Text = "移除选中";
btnRemove.Location = new System.Drawing.Point(bx, 75);
btnRemove.Size = new System.Drawing.Size(bw, 32);
btnRemove.Click += delegate {
if (lstFiles.SelectedIndex >= 0)
{
int idx = lstFiles.SelectedIndex;
inputFiles.RemoveAt(idx);
lstFiles.Items.RemoveAt(idx);
if (inputFiles.Count == 0) isPdfInput = false;
}
};
this.Controls.Add(btnRemove);
btnClear = new Button();
btnClear.Text = "清空列表";
btnClear.Location = new System.Drawing.Point(bx + 110, 75);
btnClear.Size = new System.Drawing.Size(bw, 32);
btnClear.Click += delegate {
inputFiles.Clear();
lstFiles.Items.Clear();
isPdfInput = false;
if (picPreview.Image != null) picPreview.Image.Dispose();
picPreview.Image = null;
lblStatus.Text = "已清空";
};
this.Controls.Add(btnClear);
btnMoveUp = new Button();
btnMoveUp.Text = "↑ 上移";
btnMoveUp.Location = new System.Drawing.Point(bx, 115);
btnMoveUp.Size = new System.Drawing.Size(bw, 32);
btnMoveUp.Click += delegate {
int idx = lstFiles.SelectedIndex;
if (idx > 0) SwapItems(idx, idx - 1);
};
this.Controls.Add(btnMoveUp);
btnMoveDown = new Button();
btnMoveDown.Text = "↓ 下移";
btnMoveDown.Location = new System.Drawing.Point(bx + 110, 115);
btnMoveDown.Size = new System.Drawing.Size(bw, 32);
btnMoveDown.Click += delegate {
int idx = lstFiles.SelectedIndex;
if (idx >= 0 && idx < lstFiles.Items.Count - 1) SwapItems(idx, idx + 1);
};
this.Controls.Add(btnMoveDown);
// JPEG质量
GroupBox grp = new GroupBox();
grp.Text = "输出设置";
grp.Location = new System.Drawing.Point(15, 222);
grp.Size = new System.Drawing.Size(685, 55);
grp.Font = new System.Drawing.Font("微软雅黑", 9f);
this.Controls.Add(grp);
Label lblQ = new Label();
lblQ.Text = "PDF图像质量:";
lblQ.Location = new System.Drawing.Point(15, 23);
lblQ.AutoSize = true;
grp.Controls.Add(lblQ);
nudQuality = new NumericUpDown();
nudQuality.Location = new System.Drawing.Point(110, 20);
nudQuality.Size = new System.Drawing.Size(60, 25);
nudQuality.Minimum = 50;
nudQuality.Maximum = 100;
nudQuality.Value = 92;
grp.Controls.Add(nudQuality);
Label lblQH = new Label();
lblQH.Text = "(自动执行:透视矫正 → 倾斜校正 → 增强美化,一步到位)";
lblQH.Location = new System.Drawing.Point(180, 23);
lblQH.AutoSize = true;
lblQH.ForeColor = System.Drawing.Color.FromArgb(100, 100, 100);
lblQH.Font = new System.Drawing.Font("微软雅黑", 8f);
grp.Controls.Add(lblQH);
// 预览
Label lblPrev = new Label();
lblPrev.Text = "预览:";
lblPrev.Location = new System.Drawing.Point(15, 285);
lblPrev.AutoSize = true;
lblPrev.Font = new System.Drawing.Font("微软雅黑", 9f, System.Drawing.FontStyle.Bold);
this.Controls.Add(lblPrev);
picPreview = new PictureBox();
picPreview.Location = new System.Drawing.Point(15, 308);
picPreview.Size = new System.Drawing.Size(685, 190);
picPreview.SizeMode = PictureBoxSizeMode.Zoom;
picPreview.BorderStyle = BorderStyle.FixedSingle;
picPreview.BackColor = System.Drawing.Color.White;
this.Controls.Add(picPreview);
// 底部
progressBar = new ProgressBar();
progressBar.Location = new System.Drawing.Point(15, 510);
progressBar.Size = new System.Drawing.Size(480, 25);
this.Controls.Add(progressBar);
lblStatus = new Label();
lblStatus.Text = "就绪 - 添加文件后点击处理";
lblStatus.Location = new System.Drawing.Point(15, 538);
lblStatus.Size = new System.Drawing.Size(480, 20);
lblStatus.ForeColor = System.Drawing.Color.DarkGray;
this.Controls.Add(lblStatus);
btnProcess = new Button();
btnProcess.Text = "一键处理并导出PDF";
btnProcess.Location = new System.Drawing.Point(510, 508);
btnProcess.Size = new System.Drawing.Size(190, 35);
btnProcess.Font = new System.Drawing.Font("微软雅黑", 10f, System.Drawing.FontStyle.Bold);
btnProcess.BackColor = System.Drawing.Color.FromArgb(0, 120, 215);
btnProcess.ForeColor = System.Drawing.Color.White;
btnProcess.FlatStyle = FlatStyle.Flat;
btnProcess.Click += BtnProcess_Click;
this.Controls.Add(btnProcess);
}
private void SwapItems(int a, int b)
{
string tf = inputFiles[a];
inputFiles[a] = inputFiles[b];
inputFiles[b] = tf;
object ti = lstFiles.Items[a];
lstFiles.Items[a] = lstFiles.Items[b];
lstFiles.Items[b] = ti;
lstFiles.SelectedIndex = b;
}
private void BtnAddImages_Click(object sender, EventArgs e)
{
if (isPdfInput)
{
MessageBox.Show("已选择PDF,请先清空列表。", "提示");
return;
}
OpenFileDialog ofd = new OpenFileDialog();
ofd.Title = "选择图片";
ofd.Filter = "图片|*.jpg;*.jpeg;*.png;*.bmp;*.tiff;*.tif;*.gif|所有文件|*.*";
ofd.Multiselect = true;
if (ofd.ShowDialog() == DialogResult.OK)
{
foreach (string f in ofd.FileNames)
{
if (!inputFiles.Contains(f))
{
inputFiles.Add(f);
lstFiles.Items.Add(Path.GetFileName(f));
}
}
lblStatus.Text = string.Format("共 {0} 个文件", inputFiles.Count);
}
}
private void BtnAddPdf_Click(object sender, EventArgs e)
{
if (inputFiles.Count > 0 && !isPdfInput)
{
MessageBox.Show("已添加图片,请先清空列表。", "提示");
return;
}
OpenFileDialog ofd = new OpenFileDialog();
ofd.Title = "选择PDF";
ofd.Filter = "PDF|*.pdf";
if (ofd.ShowDialog() == DialogResult.OK)
{
inputFiles.Clear();
lstFiles.Items.Clear();
inputFiles.Add(ofd.FileName);
lstFiles.Items.Add(Path.GetFileName(ofd.FileName));
isPdfInput = true;
lblStatus.Text = string.Format("已选择: {0}", Path.GetFileName(ofd.FileName));
}
}
private void BtnProcess_Click(object sender, EventArgs e)
{
if (inputFiles.Count == 0)
{
MessageBox.Show("请先添加文件!", "提示");
return;
}
SaveFileDialog sfd = new SaveFileDialog();
sfd.Title = "保存处理后的PDF";
sfd.Filter = "PDF|*.pdf";
sfd.FileName = "scanned_output.pdf";
if (sfd.ShowDialog() != DialogResult.OK) return;
string outputPath = sfd.FileName;
int quality = (int)nudQuality.Value;
SetUI(false);
try
{
// 加载源图像
List sourceImages = new List();
if (isPdfInput)
{
lblStatus.Text = "正在从PDF提取图像...";
Application.DoEvents();
sourceImages = PdfHelper.ExtractImagesFromPdf(inputFiles[0]);
}
else
{
for (int i = 0; i < inputFiles.Count; i++)
{
lblStatus.Text = string.Format("加载 {0}/{1}...", i + 1, inputFiles.Count);
Application.DoEvents();
sourceImages.Add(new Bitmap(inputFiles[i]));
}
}
progressBar.Maximum = sourceImages.Count;
progressBar.Value = 0;
// 逐张处理
List results = new List();
for (int i = 0; i < sourceImages.Count; i++)
{
lblStatus.Text = string.Format("处理第 {0}/{1} 页(矫正+增强)...", i + 1, sourceImages.Count);
Application.DoEvents();
// Bitmap → 内存PNG → Mat → 处理 → Mat → 内存PNG → Bitmap
Mat mat = BitmapToMat(sourceImages[i]);
Mat processed = DocumentScanner.ProcessImage(mat);
Bitmap resultBmp = MatToBitmap(processed);
results.Add(resultBmp);
// 预览
if (picPreview.Image != null) picPreview.Image.Dispose();
picPreview.Image = new Bitmap(resultBmp,
new System.Drawing.Size(picPreview.Width, picPreview.Height));
Application.DoEvents();
mat.Dispose();
processed.Dispose();
progressBar.Value = i + 1;
}
// 生成PDF
lblStatus.Text = "正在生成PDF...";
Application.DoEvents();
PdfHelper.SaveImagesToPdf(results, outputPath, quality);
// 清理
foreach (Bitmap b in sourceImages) b.Dispose();
foreach (Bitmap b in results) b.Dispose();
lblStatus.Text = string.Format("完成!已保存: {0}", Path.GetFileName(outputPath));
DialogResult dr = MessageBox.Show(
string.Format("处理完成!共 {0} 页。\n\n是否打开所在目录?", results.Count),
"完成", MessageBoxButtons.YesNo, MessageBoxIcon.Information);
if (dr == DialogResult.Yes)
{
System.Diagnostics.Process.Start("explorer.exe", "/select," + outputPath);
}
}
catch (Exception ex)
{
MessageBox.Show("出错: " + ex.Message, "错误", MessageBoxButtons.OK, MessageBoxIcon.Error);
lblStatus.Text = "失败: " + ex.Message;
}
finally
{
SetUI(true);
}
}
private void SetUI(bool enabled)
{
btnAddImages.Enabled = enabled;
btnAddPdf.Enabled = enabled;
btnRemove.Enabled = enabled;
btnClear.Enabled = enabled;
btnMoveUp.Enabled = enabled;
btnMoveDown.Enabled = enabled;
btnProcess.Enabled = enabled;
}
///
/// Bitmap → Mat(通过内存PNG中转,绕开 System.Drawing.Common 类型冲突)
///
private static Mat BitmapToMat(Bitmap bmp)
{
using (MemoryStream ms = new MemoryStream())
{
bmp.Save(ms, System.Drawing.Imaging.ImageFormat.Png);
byte[] buf = ms.ToArray();
return Cv2.ImDecode(buf, ImreadModes.Color);
}
}
///
/// Mat → Bitmap(通过内存PNG中转)
///
private static Bitmap MatToBitmap(Mat mat)
{
byte[] buf;
Cv2.ImEncode(".png", mat, out buf);
using (MemoryStream ms = new MemoryStream(buf))
{
return new Bitmap(ms);
}
}
}
///
/// Quicker 入口 - 运行线程选择: 后台线程(STA)
///
public static void Exec(Quicker.Public.IStepContext context)
{
// 在调用任何 OpenCvSharp 方法之前,手动加载原生 DLL
// 从 OpenCvSharp.dll 所在目录查找 OpenCvSharpExtern.dll
try
{
string asmPath = typeof(Cv2).Assembly.Location;
string asmDir = Path.GetDirectoryName(asmPath);
string externPath = Path.Combine(asmDir, "OpenCvSharpExtern.dll");
if (File.Exists(externPath))
{
LoadLibrary(externPath);
}
else
{
// 尝试 runtimes 子目录(NuGet 标准结构)
string runtimePath = Path.Combine(asmDir, "runtimes", "win-x64", "native", "OpenCvSharpExtern.dll");
if (File.Exists(runtimePath))
{
LoadLibrary(runtimePath);
}
}
}
catch { }
Application.EnableVisualStyles();
Application.SetCompatibleTextRenderingDefault(false);
Application.Run(new ScannerForm());
}
[DllImport("kernel32.dll", SetLastError = true)]
private static extern IntPtr LoadLibrary(string dllToLoad);