You need to enable JavaScript to run this app.
最新活动
大模型
产品
解决方案
定价
生态与合作
支持与服务
开发者
了解我们

C#解析含上标分数的PDF:解决iTextSharp上标字符乱码问题

解决iTextSharp解析PDF上标字符错误及上标分数的问题

我之前也踩过类似的上标解析乱码坑,大概率是PDF里的特殊印刷字体没有正确的ToUnicode映射,导致iTextSharp把字形ID(GID)错误映射成了©ƒ这类不相关的常规字符。下面分两部分给你落地解决方案:

一、修复上标字符解析错误(转数字/常规文本)

1. 优先利用PDF自带的ToUnicode映射表

很多专业印刷的PDF会附带ToUnicode映射,明确告诉阅读器每个字形对应的Unicode字符。你可以通过iTextSharp的字典和字符映射表手动读取转换:

using iTextSharp.text.pdf;
using iTextSharp.text.pdf.parser;

public string FixSuperscriptMismatch(string rawParsedText, PdfReader reader, int pageNumber)
{
    var pageContent = reader.GetPageContent(pageNumber);
    var tokenizer = new PRTokeniser(new RandomAccessFileOrArray(pageContent));
    PdfDictionary fontDictionary = null;

    // 遍历页面内容定位字体字典
    while (tokenizer.NextToken())
    {
        if (tokenizer.TokenType == PRTokeniser.TK_NAME && tokenizer.StringValue.Equals("/Font"))
        {
            tokenizer.NextToken(); // 跳过/符号
            tokenizer.NextToken(); // 获取字体字典引用ID
            fontDictionary = reader.GetPdfObject(tokenizer.IntValue) as PdfDictionary;
            break;
        }
    }

    if (fontDictionary != null)
    {
        foreach (PdfName fontKey in fontDictionary.Keys)
        {
            if (fontDictionary.Get(fontKey) is PdfDictionary font)
            {
                var toUnicodeEntry = font.Get(PdfName.TOUNICODE);
                if (toUnicodeEntry != null)
                {
                    // 加载ToUnicode映射表
                    var cmap = CMapLocation.LoadCmap(reader.GetStreamBytesRaw((PRIndirectReference)toUnicodeEntry));
                    var reverseMap = cmap.CreateReverseMapping();
                    var fixedText = new StringBuilder();

                    foreach (char c in rawParsedText)
                    {
                        if (reverseMap.TryGetValue(c, out int correctUnicode))
                        {
                            fixedText.Append((char)correctUnicode);
                        }
                        else
                        {
                            fixedText.Append(c);
                        }
                    }
                    return fixedText.ToString();
                }
            }
        }
    }

    // 没有ToUnicode映射时,用自定义替换表应急(根据你的实际错误字符补充)
    var replacementPairs = new Dictionary<char, char>
    {
        {'©', '²'},
        {'ƒ', '¹'},
        {'ª', '³'},
        {'«', '⁴'}
    };

    foreach (var pair in replacementPairs)
    {
        rawParsedText = rawParsedText.Replace(pair.Key, pair.Value);
    }
    return rawParsedText;
}

2. 自定义文本提取策略(基于位置判断上标)

如果上面的方法无效,你可以通过上标的排版特征(字体更小、Y坐标高于基线)来识别并替换:

public class SuperscriptFixStrategy : LocationTextExtractionStrategy
{
    private readonly Dictionary<char, char> _superscriptMap = new Dictionary<char, char>
    {
        {'©', '²'}, {'ƒ', '¹'}, {'ª', '³'}
    };

    public override void RenderText(TextRenderInfo renderInfo)
    {
        // 判断是否为上标:字体大小小于基线字体的80%,且Y坐标高于基线
        var baselineY = renderInfo.GetBaseline().GetStartPoint()[1];
        var descentY = renderInfo.GetDescentLine().GetStartPoint()[1];
        var isSuperscript = baselineY > descentY + renderInfo.GetFontSize() * 0.6;

        if (isSuperscript)
        {
            var fixedText = new StringBuilder();
            foreach (char c in renderInfo.GetText())
            {
                fixedText.Append(_superscriptMap.TryGetValue(c, out char fix) ? fix : c);
            }
            // 用修复后的文本调用基类渲染逻辑
            base.RenderText(new TextRenderInfo(fixedText.ToString(), renderInfo.GetGlyphPositions(),
                renderInfo.GetBaseline(), renderInfo.GetDescentLine(), renderInfo.GetFont(),
                renderInfo.GetFontSize(), renderInfo.GetWidth(), renderInfo.GetSingleSpaceWidth()));
        }
        else
        {
            base.RenderText(renderInfo);
        }
    }
}

使用方式:

var reader = new PdfReader("your-target.pdf");
var strategy = new SuperscriptFixStrategy();
var correctedText = PdfTextExtractor.GetTextFromPage(reader, 1, strategy);
reader.Close();

二、解析上标形式的分数

上标分数通常是「分子上标+分母下标」的排版,我们可以通过文本块的Y坐标、字体大小和X位置来识别并组合:

自定义分数提取策略

public class FractionParseStrategy : LocationTextExtractionStrategy
{
    private List<TextChunk> _textChunks = new List<TextChunk>();

    public override void RenderText(TextRenderInfo renderInfo)
    {
        var baseline = renderInfo.GetBaseline().GetStartPoint();
        _textChunks.Add(new TextChunk(
            renderInfo.GetText(),
            baseline[0], // X坐标
            baseline[1], // Y坐标
            renderInfo.GetFontSize()
        ));
    }

    public string GetTextWithFractions()
    {
        // 按X坐标排序,保证文本顺序正确
        var sortedChunks = _textChunks.OrderBy(c => c.X).ToList();
        var result = new StringBuilder();

        for (int i = 0; i < sortedChunks.Count; i++)
        {
            var currentChunk = sortedChunks[i];
            // 检查下一个块是否为分母(下标)
            if (i + 1 < sortedChunks.Count)
            {
                var nextChunk = sortedChunks[i + 1];
                // 判断条件:分子字体小、Y高;分母字体小、Y低;X位置接近
                bool isFraction = currentChunk.FontSize < 0.8 * sortedChunks.Max(c => c.FontSize)
                                  && nextChunk.FontSize < currentChunk.FontSize * 1.2
                                  && currentChunk.Y > nextChunk.Y + currentChunk.FontSize * 0.5
                                  && Math.Abs(currentChunk.X - nextChunk.X) < currentChunk.FontSize * 0.4;

                if (isFraction)
                {
                    result.Append($"{currentChunk.Text}/{nextChunk.Text}");
                    i++; // 跳过已处理的分母块
                    continue;
                }
            }
            result.Append(currentChunk.Text);
        }
        return result.ToString();
    }

    // 辅助类存储文本块的位置和大小信息
    private class TextChunk
    {
        public string Text { get; }
        public float X { get; }
        public float Y { get; }
        public float FontSize { get; }

        public TextChunk(string text, float x, float y, float fontSize)
        {
            Text = text;
            X = x;
            Y = y;
            FontSize = fontSize;
        }
    }
}

使用示例:

var reader = new PdfReader("fraction-document.pdf");
var strategy = new FractionParseStrategy();
PdfTextExtractor.GetTextFromPage(reader, 1, strategy);
var textWithFractions = strategy.GetTextWithFractions();
reader.Close();

注意事项

  • 不同PDF的排版规范差异很大,你可能需要根据实际文件调整判断阈值(比如字体大小比例、Y坐标差)。
  • 如果PDF是扫描件转的OCR文本,需要先确保OCR的准确性,再处理上标/分数问题。

内容的提问来源于stack exchange,提问作者Shaiju Poozhikkunnu

火山引擎 最新活动