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




