1135 lines
39 KiB
C#
1135 lines
39 KiB
C#
// 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);
|