Files
2026-10-12 16:49:14 +08:00

608 lines
21 KiB
C#
Raw Permalink Blame History

This file contains invisible Unicode characters
This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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.
#if DEBUG
#define DEBUGOCR
#endif
using System;
using System.Collections.Generic;
using System.IO;
using System.Text.RegularExpressions;
using System.Xml;
using iTextSharp.text.pdf;
using PDFPatcher.Common;
using PDFPatcher.Model;
using PDFPatcher.Processor.Imaging;
namespace PDFPatcher.Processor
{
sealed class OcrProcessor
{
internal sealed class Result
{
public ImageDisposition Image { get; }
public List<TextLine> Texts { get; }
public Result(ImageDisposition image) {
Image = image;
Texts = [];
}
}
const int OpenWorkload = 1;
const string __punctuations = @"·.“”,,\.-""~∼。::\p{P}";
static readonly AutoBookmarkOptions __MergeOptions = new AutoBookmarkOptions { MergeAdjacentTitles = true, MergeDifferentSizeTitles = true };
static readonly Regex __ContentPunctuationExpression = new Regex(@"[" + __punctuations + @"][" + __punctuations + @"0一\s]+[" + __punctuations + @"]\s*", RegexOptions.Compiled);
static readonly Regex __ContinuousWhiteSpaceExpression = new Regex(@"[  ]{3,}", RegexOptions.Compiled);
static readonly Regex __WhiteSpaceBetweenChineseCharacters = new Regex(@"([\u4E00-\u9FFF\u3400-\u4DBF])[  ]+(?=[\u4E00-\u9FFF\u3400-\u4DBF])", RegexOptions.Compiled);
private readonly ModiOcr _Ocr;
private readonly float _OcrQuantitativeFactor;
private readonly PdfReader _reader;
private readonly ImageExtractor _ocrImageExp;
private readonly OcrOptions _options;
private IResultWriter _resultWriter;
public OcrProcessor(PdfReader reader, OcrOptions options) : this(options) {
var expOptions = new ImageExtracterOptions() {
OutputPath = Path.GetTempPath(),
FileMask = "\"ocr-" + DateTime.Now.ToString("yyMMddHHmmss") + "-\"0000",
MergeImages = true,
MergeJpgToPng = true,
MinHeight = 100,
MinWidth = 100
};
CleanUpTempFiles(expOptions.OutputPath);
_reader = reader;
_ocrImageExp = new ImageExtractor(expOptions) {
PrintImageLocation = false
};
}
private OcrProcessor(OcrOptions options) {
_Ocr = new ModiOcr {
LangID = options.OcrLangID,
StretchPage = options.StretchPage,
OrientPage = options.OrientPage,
WritingDirection = options.WritingDirection
};
_OcrQuantitativeFactor = options.QuantitativeFactor;
_options = options;
}
private static void CleanUpTempFiles(string folderPath) {
var tf = Directory.GetFiles(folderPath, "ocr-*.tif");
foreach (var file in tf) {
try {
File.Delete(file);
}
catch (Exception) {
}
}
}
internal int EstimateWorkload() {
var n = _reader.NumberOfPages;
var load = 0;
load += OpenWorkload;
var t = PageRangeCollection.Parse(_options.PageRanges, 1, n, true).TotalPages;
load += t > 0 ? t : n;
return load;
}
internal void SetWriter(XmlWriter writer) {
_resultWriter = new XmlResultWriter(writer);
}
internal void SetWriter(TextWriter writer) {
_resultWriter = new TextResultWriter(writer);
}
internal void PerformOcr() {
Tracker.IncrementProgress(OpenWorkload);
var ranges = PageRangeCollection.Parse(_options.PageRanges, 1, _reader.NumberOfPages, true);
__MergeOptions.DetectColumns = _options.DetectColumns;
__MergeOptions.WritingDirection = _options.WritingDirection;
TextLine.DefaultDirection = _options.WritingDirection;
if (FileHelper.IsPathValid(_options.SaveOcredImagePath)) {
File.Delete(_options.SaveOcredImagePath);
}
var el = new List<int>();
foreach (PageRange r in ranges) {
for (int i = r.StartValue; i <= r.EndValue; i++) {
Tracker.TraceMessage("正在识别第 " + i + " 页。");
var or = OcrPage(i, el);
if (or.Count > 0) {
_resultWriter?.BeginWritePage(i);
foreach (var result in or) {
_resultWriter?.BeginWriteImage(result.Image);
if (_options.OutputOriginalOcrResult) {
if (_resultWriter != null) {
foreach (var item in result.Texts) {
_resultWriter.WriteText(item, null);
}
}
}
else {
WriteOcrResult(i, result);
}
_resultWriter?.EndWriteImage();
}
_resultWriter?.EndWritePage();
}
Tracker.IncrementProgress(1);
}
}
if (el.Count > 0) {
Tracker.TraceMessage(Tracker.Category.Alert, String.Concat("有 ", el.Count, " 页在识别过程中出现错误,页码为:", String.Join(", ", el)));
}
}
private void WriteOcrResult(int i, Result result) {
SortRecognizedText(result.Texts, _options);
var pr = _reader.GetPageNRelease(i).GetPageVisibleRectangle();
var tl = _options.WritingDirection != WritingDirection.Unknown
? AutoBookmarkCreator.MergeTextInfoList(pr, result.Texts.ConvertAll((l) => GetMergedTextInfo(result.Image, l)), __MergeOptions) // 按照书写方向重组识别文本
: result.Texts;
foreach (var item in tl) {
var t = item.Text;
var ir = item.Region;
t = CleanUpText(t, _options);
if (_options.PrintOcrResult) {
#if DEBUG
Tracker.TraceMessage(String.Concat(item.Direction.ToString()[0], ir.Top.ToString(" 0000"), ',', ir.Left.ToString("0000"), "(", ir.Width.ToString("0000"), ',', ir.Height.ToString("0000"), ")\t", t));
#else
Tracker.TraceMessage (t);
#endif
}
_resultWriter?.WriteText(item, t);
}
}
/// <summary>
/// 根据识别选项优化输出结果。
/// </summary>
/// <param name="text">文本内容。</param>
/// <param name="options">识别选项。</param>
/// <returns>优化后的文本。</returns>
internal static string CleanUpText(string text, OcrOptions options) {
if (options.DetectContentPunctuations) {
text = __ContentPunctuationExpression.Replace(text, " .... ");
}
if (options.CompressWhiteSpaces) {
text = __ContinuousWhiteSpaceExpression.Replace(text, " ");
}
if (options.RemoveWhiteSpacesBetweenChineseCharacters) {
text = __WhiteSpaceBetweenChineseCharacters.Replace(text, "$1");
}
return text;
}
private IList<Result> OcrPage(int i, List<int> errorList) {
#if DEBUGOCR
Tracker.TraceMessage("导出第 " + i + " 页的图片。");
#endif
_ocrImageExp.ExtractPageImages(_reader, i);
#if DEBUGOCR
Tracker.TraceMessage("完成导出第 " + i + " 页的图片。");
#endif
var or = new List<Result>();
try {
foreach (var item in _ocrImageExp.PosList) {
var r = new Result(item);
OcrPage(r);
or.Add(r);
r = null;
}
}
catch (System.Runtime.InteropServices.COMException ex) {
string err = null;
switch (ex.ErrorCode) {
case -959967087:
err = "页面的图片不包含可识别的文本。";
goto default;
default:
Tracker.TraceMessage(Tracker.Category.Error, "在执行第 " + i + " 页的光学字符识别时出错:");
if (err != null) {
Tracker.TraceMessage(err);
}
else {
Tracker.TraceMessage("错误编号:" + ex.ErrorCode);
Tracker.TraceMessage(ex);
errorList.Add(i);
}
break;
}
}
catch (Exception ex) {
Tracker.TraceMessage(Tracker.Category.Error, "在执行第 " + i + " 页的光学字符识别时出错:");
Tracker.TraceMessage(ex);
errorList.Add(i);
}
finally {
foreach (var item in _ocrImageExp.InfoList) {
if (String.IsNullOrEmpty(item.FileName)) {
continue;
}
try {
File.Delete(item.FileName);
}
catch (Exception ex) {
Tracker.TraceMessage(Tracker.Category.Error, ex.Message);
Tracker.TraceMessage(Tracker.Category.Error, "无法删除识别过程中产生的临时文件:" + item.FileName);
}
}
}
return or;
}
private void OcrPage(Result result) {
var image = result.Image;
var p = image.Image.FileName;
if (String.IsNullOrEmpty(p)) {
return;
}
#if DEBUGOCR
Tracker.TraceMessage("识别图片:" + p);
#endif
#if DEBUG
Tracker.TraceMessage(p);
#endif
result.Texts.Clear();
OcrImageFile(result.Texts, p);
#region Legacy code
// var ll = new List<TextLine> ();
// // 同行合并宽度最小值
// var cw = image.Image.Width / 4;
// // 遍历识别所得的各 TextInfo,使用最小距离聚类方法将其聚类为行
// foreach (var item in or) {
// var ir = item.Region;
// DistanceInfo cd = null; // TextInfo 到 TextLine 的距离
// DistanceInfo md = new DistanceInfo (DistanceInfo.Placement.Unknown, float.MaxValue); // 最小距离
// TextLine ml = null; // 最小距离的 TextLine
//if (item.Text == "哉") {
// var lxx = 1;
//}
// // 求最小距离的 TextLine
// foreach (var li in ll) {
// cd = li.GetDistance (ir);
// if ((cd.Location == DistanceInfo.Placement.Overlapping // 当前项与文本行交叠
// && (md.Location != DistanceInfo.Placement.Overlapping // 最小距离不是交叠
// || cd.Distance < md.Distance) // 当前项与文本行的交叠中心距离小于最小距离
// )
// || ((md.Location == DistanceInfo.Placement.Unknown // 未知最小距离
// || (cd.Location != DistanceInfo.Placement.Overlapping
// && md.Location != DistanceInfo.Placement.Overlapping
// && cd.Distance < md.Distance) // 当前项与文本行的距离小于最小距离
// )
// && (((cd.Location == DistanceInfo.Placement.Left || cd.Location == DistanceInfo.Placement.Right) // 相对位置为水平
// && li.Direction != TextLine.WritingDirection.Vertical // 文本行方向不为纵向
// )
// || ((cd.Location == DistanceInfo.Placement.Up || cd.Location == DistanceInfo.Placement.Down) // 相对位置为垂直
// && li.Direction != TextLine.WritingDirection.Horizontal // 文本行方向不为横向
// )
// )
// && cd.Distance < cw
// )
// ) {
// md = cd;
// ml = li;
// }
// }
// if (ml != null) {
// // 若存在最小距离的 TextLine 且可合并,则将 item 归入 TextLine
// if (md.Location == DistanceInfo.Placement.Overlapping) {
// // 检查是否存在交叠重复的文本
// foreach (var t in ml.Texts) {
// if (t.Region.IntersectWith (item.Region) // item 与 TextLine 中某项交叠
// && (t.Text.Contains (item.Text) || item.Text.Contains (t.Text) // 交叠的项文本和 item 的文本相同
// )
// ) {
// goto Next; // 忽略此项目
// }
// }
// }
// ml.AddText (item);
// }
// else {
// // 否则,用 item 创建新的 TextLine
// ll.Add (new TextLine (item));
// }
// Next:
// continue;
// }
//if (or.Count > 0) {
// float size = 0, size2 = 0, avgSize, maxSize = 0;
// float top = or[0].Region.Top, bottom = or[0].Region.Bottom, left = or[0].Region.Left, right = 0;
// var sb = new System.Text.StringBuilder ();
// int letterCount = 0;
// var rr = new List<TextInfo> ();
// Bound r;
// var end = or.Count - 1;
// for (var i = 0; i <= end; i++) {
// var item = or[i];
// r = item.Region;
// avgSize = letterCount > 0 ? size / letterCount : maxSize;
// AddLine:
// if (r.Top > bottom + 0.2f * (avgSize) || i > end) { // 新行
// size = image.YScale * avgSize;
// if (_OcrQuatitiveFactor > 0) {
// var a = Math.IEEERemainder (size, _OcrQuatitiveFactor);
// var b = Math.IEEERemainder (size + _OcrQuatitiveFactor, _OcrQuatitiveFactor);
// if (a < b) {
// size -= (float)a;
// }
// else {
// size += _OcrQuatitiveFactor - (float)b;
// }
// }
// if (size >= _fontSizeThreshold) {
// var ni = new TextInfo ()
// {
// Text = sb.ToString (),
// Size = size,
// Region = new Bound (
// image.X + image.XScale * left,
// image.Y + image.YScale * (image.Image.Height - bottom),
// image.X + image.XScale * right,
// image.Y + image.YScale * (image.Image.Height - top)),
// Font = -1
// };
// rr.Add (ni);
// }
// maxSize = size = size2 = 0;
// left = r.Left;
// right = r.Right;
// top = r.Top;
// bottom = r.Bottom;
// sb.Length = 0;
// letterCount = 0;
// if (i > end) {
// break;
// }
// }
// if (Char.IsLetter (item.Text[0])) {
// size += item.Size;
// size2 += item.Size * item.Size;
// letterCount++;
// }
// if (item.Size > maxSize) {
// maxSize = item.Size;
// }
// if (r.Top < top) {
// top = r.Top;
// }
// if (r.Bottom > bottom) {
// bottom = r.Bottom;
// }
// if (r.Right > right) {
// right = r.Right;
// }
// sb.Append (item.Text);
// if (i == end) {
// i++;
// goto AddLine;
// }
// }
// this._TextList.AddRange (rr);
//}
#endregion
}
private void OcrImageFile(List<TextLine> result, string p) {
var sp = _options.SaveOcredImagePath;
if (!FileHelper.HasExtension(p, Constants.FileExtensions.Tif)) {
using (var fi = new FreeImageAPI.FreeImageBitmap(p)) {
#if !DEBUG
var t = Path.GetDirectoryName (p) + "\\ocr-" + new Random ().Next ().ToText () +".tif";
#else
var t = "m:\\ocr.tif";
#endif
if (_options.PreserveColor) {
fi.Save(t, FreeImageAPI.FREE_IMAGE_FORMAT.FIF_TIFF);
}
else {
using (var ti = fi.GetColorConvertedInstance(FreeImageAPI.FREE_IMAGE_COLOR_DEPTH.FICD_01_BPP_THRESHOLD)) {
ti.Save(t, FreeImageAPI.FREE_IMAGE_FORMAT.FIF_TIFF);
}
}
_Ocr.Ocr(t, sp, result);
#if !DEBUG
try {
File.Delete (t);
}
catch (Exception) {
Tracker.TraceMessage (Tracker.Category.Notice, "无法删除临时文件:" + t);
}
#endif
}
}
else {
_Ocr.Ocr(p, sp, result);
}
#if DEBUGOCR
Tracker.TraceMessage("完成识别图片:" + p);
#endif
return;
}
/// <summary>
/// 调用图像处理引擎识别位图。如图片中的文本量太少,将无法识别,并会抛出异常。
/// </summary>
/// <param name="bmp">需要识别的图片。</param>
/// <param name="options">识别选项。</param>
/// <exception cref="System.Runtime.InteropServices.COMException">在识别时发生的错误。</exception>
/// <returns>识别后的文本。</returns>
internal static List<TextLine> OcrBitmap(System.Drawing.Bitmap bmp, OcrOptions options) {
const int minSize = 500;
var ocr = new OcrProcessor(options);
var r = new List<TextLine>();
string p = null;
using (var fi = new FreeImageAPI.FreeImageBitmap(bmp)) {
if (fi.Width < minSize || fi.Height < minSize) {
fi.EnlargeCanvas<FreeImageAPI.RGBQUAD>(0, 0, fi.Width < minSize ? minSize : fi.Width, fi.Height < minSize ? minSize : fi.Height, new FreeImageAPI.RGBQUAD(fi.GetPixel(0, 0)));
}
p = FileHelper.CombinePath(Path.GetTempPath(), new Random().Next(Int32.MaxValue).ToText() + Constants.FileExtensions.Tif);
fi.Save(p, FreeImageAPI.FREE_IMAGE_FORMAT.FIF_TIFF);
}
ocr._Ocr.Ocr(p, null, r);
File.Delete(p);
return r;
}
private static TextInfo GetMergedTextInfo(ImageDisposition image, TextLine item) {
var ti = new TextInfo {
Font = null,
Region = item.Region,
Text = item.Text,
Size = (float)Math.Round(item.Direction == WritingDirection.Vertical ? item.Region.Width/* * image.XScale*/ : item.Region.Height/* * image.YScale*/),
LetterWidth = item.GetAverageCharSize()
};
//if (item.Texts.Count > 0) {
// float aw = 0;
// foreach (var t in item.Texts) {
// aw += t.LetterWidth;
// }
// aw /= item.Texts.Count;
// ti.LetterWidth = aw;
//}
return ti;
}
private static void SortRecognizedText(List<TextLine> list, OcrOptions ocrOptions) {
if (ocrOptions.WritingDirection == WritingDirection.Horizontal) {
list.Sort((a, b) => {
var ra = a.Region;
var rb = b.Region;
if (ra.Bottom > rb.Top) {
return 1;
}
if (ra.Top < rb.Bottom) {
return -1;
}
if (ra.IsAlignedWith(rb, WritingDirection.Horizontal)) {
return ra.Center < rb.Center ? -1 : 1;
}
return ra.Middle < rb.Middle ? -1 : 1;
});
}
else if (ocrOptions.WritingDirection == WritingDirection.Vertical) {
list.Sort((a, b) => {
var ra = a.Region;
var rb = b.Region;
if (ra.Left > rb.Right) {
return -1;
}
if (ra.Right < rb.Left) {
return 1;
}
if (ra.IsAlignedWith(rb, WritingDirection.Vertical)) {
return ra.Middle > rb.Middle ? -1 : 1;
}
return ra.Center > rb.Center ? -1 : 1;
});
}
}
interface IResultWriter
{
void BeginWritePage(int i);
void BeginWriteImage(ImageDisposition image);
void WriteText(TextLine text, string optimizedText);
void EndWriteImage();
void EndWritePage();
}
sealed class XmlResultWriter : IResultWriter
{
readonly XmlWriter _writer;
public XmlResultWriter(XmlWriter writer) {
_writer = writer;
}
#region IResultWriter 成员
public void BeginWritePage(int i) {
_writer.WriteStartElement(Constants.Ocr.Result);
_writer.WriteAttributeString(Constants.Content.PageNumber, ValueHelper.ToText(i));
}
public void WriteText(TextLine text, string optimizedText) {
if (optimizedText != null) {
WriteTextItem(optimizedText, text.Region, text.Direction);
return;
}
_writer.WriteComment(text.Text);
foreach (var item in text.Texts) {
WriteTextItem(item.Text, item.Region, WritingDirection.Unknown);
}
}
private void WriteTextItem(string text, Bound ir, WritingDirection direction) {
_writer.WriteStartElement(Constants.Ocr.Content);
_writer.WriteAttributeString(Constants.Ocr.Text, text);
switch (direction) {
case WritingDirection.Horizontal:
_writer.WriteAttributeString(Constants.Coordinates.Direction, Constants.Coordinates.Horizontal);
break;
case WritingDirection.Vertical:
_writer.WriteAttributeString(Constants.Coordinates.Direction, Constants.Coordinates.Vertical);
break;
}
_writer.WriteAttributeString(Constants.Coordinates.Top, ValueHelper.ToText(Math.Round(ir.Top)));
_writer.WriteAttributeString(Constants.Coordinates.Left, ValueHelper.ToText(Math.Round(ir.Left)));
_writer.WriteAttributeString(Constants.Coordinates.Bottom, ValueHelper.ToText(Math.Round(ir.Bottom)));
_writer.WriteAttributeString(Constants.Coordinates.Right, ValueHelper.ToText(Math.Round(ir.Right)));
_writer.WriteEndElement();
}
public void EndWritePage() {
_writer.WriteEndElement();
}
public void BeginWriteImage(ImageDisposition image) {
_writer.WriteStartElement(Constants.Ocr.Image);
_writer.WriteAttributeString(Constants.Coordinates.Width, ValueHelper.ToText(image.Image.Width));
_writer.WriteAttributeString(Constants.Coordinates.Height, ValueHelper.ToText(image.Image.Height));
_writer.WriteAttributeString(Constants.Content.OperandNames.Matrix, PdfHelper.MatrixToString(image.Ctm));
}
public void EndWriteImage() {
_writer.WriteEndElement();
}
#endregion
}
sealed class TextResultWriter : IResultWriter
{
readonly TextWriter _writer;
public TextResultWriter(TextWriter writer) {
_writer = writer;
}
#region IResultWriter 成员
public void BeginWritePage(int i) {
_writer.WriteLine("#识别页码=" + i);
}
public void WriteText(TextLine text, string optimizedText) {
_writer.WriteLine(optimizedText ?? text.Text);
}
public void EndWritePage() {
_writer.WriteLine();
}
public void BeginWriteImage(ImageDisposition image) {
_writer.WriteLine("#识别图片=" + PdfHelper.MatrixToString(image.Ctm));
}
public void EndWriteImage() { }
#endregion
}
}
}