Files
camscanner/CamScanner.cs

1135 lines
39 KiB
C#
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// 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;
/// <summary>
/// 文档扫描处理核心 - 全自动流水线
/// 透视矫正 → 倾斜校正 → 图像增强,一步到位
/// </summary>
public static class DocumentScanner
{
/// <summary>
/// 主处理入口:对单张图像执行完整扫描增强流水线
/// </summary>
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;
}
/// <summary>
/// 对四个点排序:左上、右上、右下、左下
/// </summary>
private static Point2f[] OrderPoints(Point2f[] pts)
{
// 按 x+y 排序找左上和右下
Point2f[] sorted = new Point2f[4];
List<Point2f> list = new List<Point2f>(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<double> angles = new List<double>();
List<double> weights = new List<double>();
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步图像增强
// 除法归一化result = (gray / background) * 255
// 除法对阴影天然免疫阴影处gray和bg同比例变暗比值不变
// 高斯模糊做背景估计(不会有底色问题)
// ==========================================
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: 除法归一化 ---
// result = (gray / background) * 255
// 文字处gray < bg → 比值 < 1 → 结果 < 255偏暗
// 背景处gray ≈ bg → 比值 ≈ 1 → 结果 ≈ 255白色
// 阴影处gray和bg同比例变暗 → 比值仍 ≈ 1 → 结果仍 ≈ 255白色
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();
for (int i = 0; i < grayData.Length; i++)
{
int bg = bgData[i];
if (bg < 1) bg = 1;
int val = (int)((double)grayData[i] / bg * 255.0);
if (val > 255) val = 255;
resultData[i] = (byte)val;
}
Mat normU8 = new Mat(h, w, MatType.CV_8U);
Marshal.Copy(resultData, 0, normU8.Data, resultData.Length);
// --- d: 非线性对比度增强 ---
// gamma 曲线压暗,保留笔画间灰度层次
// 输出范围压到 0-150给白底清理留空间
byte[] lut = new byte[256];
for (int i = 0; i < 256; i++)
{
double x = i / 255.0;
double y = Math.Pow(x, 2.5);
int val = (int)(y * 150.0);
if (val > 255) val = 255;
lut[i] = (byte)val;
}
Mat lutMat = new Mat(1, 256, MatType.CV_8U, lut);
Mat contrasted = new Mat();
Cv2.LUT(normU8, lutMat, contrasted);
lutMat.Dispose();
normU8.Dispose();
// --- e: USM锐化 ---
Mat blurred = new Mat();
Cv2.GaussianBlur(contrasted, blurred, new OpenCvSharp.Size(0, 0), 1.0);
Mat sharpened = new Mat();
Cv2.AddWeighted(contrasted, 1.8, blurred, -0.8, 0, sharpened);
blurred.Dispose();
contrasted.Dispose();
// --- f: 白底清理 ---
// 用 LUT<60 保持原值文字60-150 快速过渡到白色,>150 纯白
byte[] whiteLut = new byte[256];
for (int i = 0; i < 256; i++)
{
if (i <= 60)
{
whiteLut[i] = (byte)i; // 文字保持不变
}
else if (i <= 150)
{
// 平滑过渡到白色
double t = (double)(i - 60) / 90.0;
whiteLut[i] = (byte)(60 + (int)(t * t * 195)); // 60→255
}
else
{
whiteLut[i] = 255; // 纯白
}
}
Mat whiteLutMat = new Mat(1, 256, MatType.CV_8U, whiteLut);
Mat cleaned = new Mat();
Cv2.LUT(sharpened, whiteLutMat, cleaned);
whiteLutMat.Dispose();
sharpened.Dispose();
// --- g: 边缘阴影清除 ---
// 从四边向内扫描原始灰度图,连续暗像素区域标记为阴影→纯白
// 除法归一化对均匀阴影免疫但边缘深色阴影bg也被拉低
// 除法后比值偏低变成灰色,需要额外清除
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 py = cy1; py < cy2; py++)
{
for (int px = cx1; px < cx2; px++)
{
centerSum += grayData[py * w + px];
centerCount++;
}
}
int paperBright = (int)(centerSum / Math.Max(centerCount, 1));
int darkThresh = (int)(paperBright * 0.65);
int maxScanDepth = Math.Max(w, h) / 3; // 最多扫描1/3深度
int tolerance = 5; // 允许跳过最多5个亮像素继续扫描
byte[] cleanedData = new byte[w * h];
Marshal.Copy(cleaned.Data, cleanedData, 0, cleanedData.Length);
// 上边缘
for (int x = 0; x < w; x++)
{
int skipCount = 0;
int lastDark = -1;
for (int y = 0; y < Math.Min(maxScanDepth, h); y++)
{
if (grayData[y * w + x] < darkThresh)
{
lastDark = y;
skipCount = 0;
}
else
{
skipCount++;
if (skipCount > tolerance) break;
}
}
for (int y = 0; y <= lastDark; y++)
cleanedData[y * w + x] = 255;
}
// 下边缘
for (int x = 0; x < w; x++)
{
int skipCount = 0;
int lastDark = h;
for (int y = h - 1; y >= Math.Max(0, h - maxScanDepth); y--)
{
if (grayData[y * w + x] < darkThresh)
{
lastDark = y;
skipCount = 0;
}
else
{
skipCount++;
if (skipCount > tolerance) break;
}
}
for (int y = h - 1; y >= lastDark; y--)
cleanedData[y * w + x] = 255;
}
// 左边缘
for (int py = 0; py < h; py++)
{
int skipCount = 0;
int lastDark = -1;
for (int x = 0; x < Math.Min(maxScanDepth, w); x++)
{
if (grayData[py * w + x] < darkThresh)
{
lastDark = x;
skipCount = 0;
}
else
{
skipCount++;
if (skipCount > tolerance) break;
}
}
for (int x = 0; x <= lastDark; x++)
cleanedData[py * w + x] = 255;
}
// 右边缘
for (int py = 0; py < h; py++)
{
int skipCount = 0;
int lastDark = w;
for (int x = w - 1; x >= Math.Max(0, w - maxScanDepth); x--)
{
if (grayData[py * w + x] < darkThresh)
{
lastDark = x;
skipCount = 0;
}
else
{
skipCount++;
if (skipCount > tolerance) break;
}
}
for (int x = w - 1; x >= lastDark; x--)
cleanedData[py * w + x] = 255;
}
Marshal.Copy(cleanedData, 0, cleaned.Data, cleanedData.Length);
// 转回3通道
Mat output = new Mat();
Cv2.CvtColor(cleaned, output, ColorConversionCodes.GRAY2BGR);
gray.Dispose();
cleaned.Dispose();
src.Dispose();
return output;
}
}
/// <summary>
/// PDF 读写工具类
/// </summary>
public static class PdfHelper
{
/// <summary>
/// 从PDF提取所有页面中的嵌入图像
/// </summary>
public static List<Bitmap> ExtractImagesFromPdf(string pdfPath)
{
List<Bitmap> images = new List<Bitmap>();
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;
}
/// <summary>
/// 将多张图像保存为单个PDF
/// </summary>
public static void SaveImagesToPdf(List<Bitmap> images, string outputPath)
{
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())
{
bmp.Save(ms, System.Drawing.Imaging.ImageFormat.Png);
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();
}
}
}
/// <summary>
/// 简化版界面 - 一键处理,无需选模式
/// </summary>
public class ScannerForm : Form
{
private ListBox lstFiles;
private Button btnAddImages, btnAddPdf, btnRemove, btnClear;
private Button btnMoveUp, btnMoveDown, btnProcess;
private ProgressBar progressBar;
private Label lblStatus;
private PictureBox picPreview;
private List<string> inputFiles = new List<string>();
private bool isPdfInput = false;
private Quicker.Public.IStepContext _context;
public ScannerForm(Quicker.Public.IStepContext context)
{
_context = context;
InitUI();
}
private void InitUI()
{
this.Text = "文档扫描王";
this.Size = new System.Drawing.Size(750, 820);
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);
// 提示
Label lblHint = new Label();
lblHint.Text = "(自动执行:透视矫正 → 倾斜校正 → 增强美化,一步到位)";
lblHint.Location = new System.Drawing.Point(15, 225);
lblHint.AutoSize = true;
lblHint.ForeColor = System.Drawing.Color.FromArgb(100, 100, 100);
lblHint.Font = new System.Drawing.Font("微软雅黑", 8f);
this.Controls.Add(lblHint);
// 预览(正方形)
Label lblPrev = new Label();
lblPrev.Text = "预览:";
lblPrev.Location = new System.Drawing.Point(15, 248);
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, 270);
picPreview.Size = new System.Drawing.Size(710, 440);
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, 722);
progressBar.Size = new System.Drawing.Size(510, 25);
this.Controls.Add(progressBar);
lblStatus = new Label();
lblStatus.Text = "就绪 - 添加文件后点击处理";
lblStatus.Location = new System.Drawing.Point(15, 752);
lblStatus.Size = new System.Drawing.Size(510, 20);
lblStatus.ForeColor = System.Drawing.Color.DarkGray;
this.Controls.Add(lblStatus);
btnProcess = new Button();
btnProcess.Text = "一键处理并导出PDF";
btnProcess.Location = new System.Drawing.Point(540, 720);
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;
}
// 默认输出文件名
string defaultName;
if (isPdfInput)
{
defaultName = "modify_" + Path.GetFileName(inputFiles[0]);
}
else
{
defaultName = DateTime.Now.ToString("yyyyMMddHHmmss") + ".pdf";
}
SaveFileDialog sfd = new SaveFileDialog();
sfd.Title = "保存处理后的PDF";
sfd.Filter = "PDF|*.pdf";
sfd.FileName = defaultName;
if (sfd.ShowDialog() != DialogResult.OK) return;
string outputPath = sfd.FileName;
SetUI(false);
try
{
// 加载源图像
List<Bitmap> sourceImages = new List<Bitmap>();
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<Bitmap> results = new List<Bitmap>();
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);
// 清理
foreach (Bitmap b in sourceImages) b.Dispose();
foreach (Bitmap b in results) b.Dispose();
lblStatus.Text = string.Format("完成!已保存: {0}", Path.GetFileName(outputPath));
// 调用子程序 "over",传参 files 为输出文件路径
var param = new Dictionary<string, object>();
param.Add("files", outputPath);
_context.RunSpAsync("over", param);
}
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;
}
/// <summary>
/// Bitmap → Mat通过内存PNG中转绕开 System.Drawing.Common 类型冲突)
/// </summary>
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);
}
}
/// <summary>
/// Mat → Bitmap通过内存PNG中转
/// </summary>
private static Bitmap MatToBitmap(Mat mat)
{
byte[] buf;
Cv2.ImEncode(".png", mat, out buf);
using (MemoryStream ms = new MemoryStream(buf))
{
return new Bitmap(ms);
}
}
}
/// <summary>
/// Quicker 入口 - 运行线程选择: 后台线程(STA)
/// </summary>
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(context));
}
[DllImport("kernel32.dll", SetLastError = true)]
private static extern IntPtr LoadLibrary(string dllToLoad);