diff --git a/CamScanner.cs b/CamScanner.cs
new file mode 100644
index 0000000..271df07
--- /dev/null
+++ b/CamScanner.cs
@@ -0,0 +1,1045 @@
+// 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);