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);