// 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; } } // 计算"正常纸面亮度":取灰度图中心区域的均值 int cx1 = w / 4; int cx2 = w * 3 / 4; int cy1 = h / 4; int cy2 = h * 3 / 4; long centerSum = 0; int centerCount = 0; for (int y = cy1; y < cy2; y++) { for (int x = cx1; x < cx2; x++) { centerSum += grayData[y * w + x]; centerCount++; } } int paperBright = (int)(centerSum / Math.Max(centerCount, 1)); // 墨迹映射(带阴影/折痕保护) // 文字特征:背景亮(接近paperBright),gray比背景低 → ink大 // 折痕/阴影特征:gray本身就暗,背景估计也被拉低 → ink也可能大 // 区分方法:看背景估计值是否接近正常纸面亮度 // 如果 bgData[i] 接近 paperBright → 正常文字区域,正常处理 // 如果 bgData[i] 远低于 paperBright → 阴影/折痕区域,抑制墨迹深度 double bgThreshRatio = 0.75; // 背景低于纸面亮度75%就开始抑制 for (int i = 0; i < grayData.Length; i++) { int ink = bgData[i] - grayData[i]; if (ink < 0) ink = 0; double inkNorm = (double)ink / ink95; if (inkNorm > 1.0) inkNorm = 1.0; double darkness = Math.Pow(inkNorm, 0.45); // 阴影/折痕抑制:背景越暗,墨迹输出越浅 double bgRatio = (double)bgData[i] / Math.Max(paperBright, 1); if (bgRatio < bgThreshRatio) { // 背景很暗 → 阴影区域,大幅抑制 // 线性衰减:bgRatio从0.75→0时,darkness从原值→0 double suppress = bgRatio / bgThreshRatio; darkness = darkness * suppress * suppress; // 平方衰减,更激进 } int val = (int)(255.0 * (1.0 - darkness)); if (val < 0) val = 0; if (val > 255) val = 255; resultData[i] = (byte)val; } // --- 边缘阴影修复:从边缘向内扫描连续暗像素 --- byte[] shadowFlag = new byte[w * h]; int darkThresh = (int)(paperBright * 0.55); int maxScanDepth = Math.Max(w, h) / 8; // 上边缘 for (int x = 0; x < w; x++) { for (int y = 0; y < Math.Min(maxScanDepth, h); y++) { if (grayData[y * w + x] < darkThresh) shadowFlag[y * w + x] = 1; else break; // 遇到亮像素就停止 } } // 下边缘 for (int x = 0; x < w; x++) { for (int y = h - 1; y >= Math.Max(0, h - maxScanDepth); y--) { if (grayData[y * w + x] < darkThresh) shadowFlag[y * w + x] = 1; else break; } } // 左边缘 for (int y = 0; y < h; y++) { for (int x = 0; x < Math.Min(maxScanDepth, w); x++) { if (grayData[y * w + x] < darkThresh) shadowFlag[y * w + x] = 1; else break; } } // 右边缘 for (int y = 0; y < h; y++) { for (int x = w - 1; x >= Math.Max(0, w - maxScanDepth); x--) { if (grayData[y * w + x] < darkThresh) shadowFlag[y * w + x] = 1; else break; } } // 把阴影区域的结果设为白色 for (int i = 0; i < resultData.Length; i++) { if (shadowFlag[i] == 1) resultData[i] = 255; } 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);