Files
camscanner/CamScanner.cs
尘曲 c62db4af45 重写折痕/阴影检测:用局部方差区分文字和折痕
放弃基于背景亮度阈值的方案(会误伤折痕附近的文字)。
改用频域特征区分:
- 文字 = 高频信号,笔画边缘锐利,局部灰度方差高
- 折痕/阴影 = 低频信号,缓慢渐变,局部灰度方差低
计算灰度图的局部方差(|gray-localMean|的局部均值),
低方差+有墨迹的像素判定为折痕/阴影,按方差比例推向白色。

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-19 16:56:02 +08:00

1167 lines
41 KiB
C#
Raw 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步图像增强
// 减法提取墨迹深度 → 非线性加深 → 锐化
// ==========================================
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));
// 墨迹映射(带折痕/阴影抑制)
//
// 核心区分逻辑:文字 vs 折痕/阴影
// 文字 = 高频信号,笔画边缘锐利,局部灰度变化剧烈
// 折痕/阴影 = 低频信号,缓慢渐变,局部灰度变化平缓
//
// 方法:先正常生成墨迹图,然后用"墨迹图的局部方差"来区分
// 高方差区域 = 文字(保留)
// 低方差但偏暗区域 = 折痕/阴影(推向白色)
// 先生成原始墨迹图(不做任何抑制)
byte[] inkData = new byte[w * h];
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);
int val = (int)(255.0 * (1.0 - darkness));
if (val < 0) val = 0;
if (val > 255) val = 255;
resultData[i] = (byte)val;
inkData[i] = (byte)(255 - val); // 墨迹强度0=无墨, 255=纯黑
}
// 计算局部方差图(用灰度图,不是墨迹图)
// 文字区域:灰度变化大(笔画边缘),方差高
// 折痕区域:灰度缓慢变化,方差低
// 用简化方法:|gray - 局部均值| 的局部均值 ≈ 局部标准差
Mat grayMat = new Mat(h, w, MatType.CV_8U, grayData);
Mat localMean = new Mat();
int varKernSize = 15;
Cv2.Blur(grayMat, localMean, new OpenCvSharp.Size(varKernSize, varKernSize));
// |gray - localMean|
Mat diff = new Mat();
Cv2.Absdiff(grayMat, localMean, diff);
localMean.Dispose();
// 对 diff 再做一次均值模糊,得到局部方差的近似
Mat localVar = new Mat();
Cv2.Blur(diff, localVar, new OpenCvSharp.Size(varKernSize, varKernSize));
diff.Dispose();
grayMat.Dispose();
byte[] varData = new byte[w * h];
Marshal.Copy(localVar.Data, varData, 0, varData.Length);
localVar.Dispose();
// 用局部方差来决定是否抑制
// 高方差(> 阈值)= 文字,保留
// 低方差 + 有墨迹 = 折痕/阴影,抑制
int varThresh = 8; // 方差阈值,低于此值认为是平坦区域
for (int i = 0; i < resultData.Length; i++)
{
if (inkData[i] > 10 && varData[i] < varThresh)
{
// 有墨迹但局部方差低 → 折痕/阴影,推向白色
// 抑制程度和方差成正比:方差越低抑制越强
double suppressRatio = (double)varData[i] / varThresh;
int origVal = resultData[i];
resultData[i] = (byte)(origVal + (int)((255 - origVal) * (1.0 - suppressRatio)));
}
}
// --- 边缘阴影修复:从边缘向内扫描连续暗像素 ---
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;
}
}
/// <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, 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;
}
}
/// <summary>
/// 简化版界面 - 一键处理,无需选模式
/// </summary>
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<string> inputFiles = new List<string>();
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<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, 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;
}
/// <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());
}
[DllImport("kernel32.dll", SetLastError = true)]
private static extern IntPtr LoadLibrary(string dllToLoad);