Как получить конкретный абзац в PDF-файл, используя iTextSharp в C#?
Я использую iTextSharp в моем приложении C# winform. Я хочу получить определенный параграф в файле PDF. Это возможно в iTextSharp?
1 ответ
И да и нет.
Сначала нет. Формат PDF не имеет понятия текстовых структур, таких как абзацы, предложения или даже слова, он просто содержит тексты. Тот факт, что два фрагмента текста расположены рядом друг с другом, поэтому мы считаем их структурированными, является человеческим делом. Когда вы видите что-то похожее на трехстрочный абзац в PDF, на самом деле программа, сгенерировавшая PDF-файл, фактически делала задачу, разбивая текст на три несвязанные текстовые строки и затем рисуя каждую строку в определенных координатах x,y. И что еще хуже, в зависимости от того, что хочет дизайнер, каждая строка текста может состоять из небольших фрагментов, которые могут быть словами или даже просто символами. Так может быть draw "the cat in the hat" at 10,10
или это может быть draw "t" at 10,10, then draw "h" at 14,10, then draw "e" at 18,10
и так далее. На самом деле это довольно часто встречается в PDF-файлах из таких программ, как Adobe InDesign.
Теперь да. На самом деле это возможно. Если вы захотите немного поработать, вы сможете заставить iTextSharp делать то, что вы ищете. Есть класс под названием PdfTextExtractor
который имеет метод с именем GetTextFromPage
который получит весь необработанный текст со страницы. Последний параметр этого метода - это объект, который реализует ITextExtractionStrategy
интерфейс. Если вы создаете свой собственный класс, который реализует этот интерфейс, вы можете обрабатывать каждый прогон текста и выполнять свою собственную логику.
В этом интерфейсе есть метод RenderText
который вызывается для каждого прогона текста. Вы получите iTextSharp.text.pdf.parser.TextRenderInfo
объект, из которого вы можете получить исходный текст из прогона, а также другие вещи, такие как текущие координаты, с которых он начинается, текущий шрифт и т. д. Поскольку визуальная строка текста может состоять из нескольких прогонов, вы можете использовать этот метод для сравните базовую линию прогона (начальную координату x) с предыдущим прогоном, чтобы определить, является ли он частью той же визуальной линии.
Ниже приведен пример реализации этого интерфейса:
public class TextAsParagraphsExtractionStrategy : iTextSharp.text.pdf.parser.ITextExtractionStrategy {
//Text buffer
private StringBuilder result = new StringBuilder();
//Store last used properties
private Vector lastBaseLine;
//Buffer of lines of text and their Y coordinates. NOTE, these should be exposed as properties instead of fields but are left as is for simplicity's sake
public List<string> strings = new List<String>();
public List<float> baselines = new List<float>();
//This is called whenever a run of text is encountered
public void RenderText(iTextSharp.text.pdf.parser.TextRenderInfo renderInfo) {
//This code assumes that if the baseline changes then we're on a newline
Vector curBaseline = renderInfo.GetBaseline().GetStartPoint();
//See if the baseline has changed
if ((this.lastBaseLine != null) && (curBaseline[Vector.I2] != lastBaseLine[Vector.I2])) {
//See if we have text and not just whitespace
if ((!String.IsNullOrWhiteSpace(this.result.ToString()))) {
//Mark the previous line as done by adding it to our buffers
this.baselines.Add(this.lastBaseLine[Vector.I2]);
this.strings.Add(this.result.ToString());
}
//Reset our "line" buffer
this.result.Clear();
}
//Append the current text to our line buffer
this.result.Append(renderInfo.GetText());
//Reset the last used line
this.lastBaseLine = curBaseline;
}
public string GetResultantText() {
//One last time, see if there's anything left in the buffer
if ((!String.IsNullOrWhiteSpace(this.result.ToString()))) {
this.baselines.Add(this.lastBaseLine[Vector.I2]);
this.strings.Add(this.result.ToString());
}
//We're not going to use this method to return a string, instead after callers should inspect this class's strings and baselines fields.
return null;
}
//Not needed, part of interface contract
public void BeginTextBlock() { }
public void EndTextBlock() { }
public void RenderImage(ImageRenderInfo renderInfo) { }
}
Чтобы назвать это, мы бы сделали:
PdfReader reader = new PdfReader(workingFile);
TextAsParagraphsExtractionStrategy S = new TextAsParagraphsExtractionStrategy();
iTextSharp.text.pdf.parser.PdfTextExtractor.GetTextFromPage(reader, 1, S);
for (int i = 0; i < S.strings.Count; i++) {
Console.WriteLine("Line {0,-5}: {1}", S.baselines[i], S.strings[i]);
}
Мы на самом деле выбрасываем значение из GetTextFromPage
и вместо того, чтобы осмотреть работника baselines
а также strings
поля массива. Следующим шагом для этого будет сравнение базовых линий и попытка определить, как сгруппировать строки, чтобы они стали абзацами.
Должен отметить, что не все абзацы имеют интервал, который отличается от отдельных строк текста. Например, если вы запустите созданный ниже PDF-файл с помощью приведенного выше кода, вы увидите, что каждая строка текста находится на расстоянии 18 пунктов друг от друга, независимо от того, образует ли строка новый абзац или нет. Если вы откроете PDF-файл, который он создает в Acrobat, и закроете все, кроме первой буквы каждой строки, вы увидите, что ваш глаз даже не может определить разницу между разрывом строки и разрывом абзаца.
using (FileStream fs = new FileStream(workingFile, FileMode.Create, FileAccess.Write, FileShare.None)) {
using (Document doc = new Document(PageSize.LETTER)) {
using (PdfWriter writer = PdfWriter.GetInstance(doc, fs)) {
doc.Open();
doc.Add(new Paragraph("Lorem ipsum dolor sit amet, consectetuer adipiscing elit. Maecenas porttitor congue massa. Fusce posuere, magna sed pulvinar ultricies, purus lectus malesuada libero, sit amet commodo magna eros quis urna."));
doc.Add(new Paragraph("This"));
doc.Add(new Paragraph("Is"));
doc.Add(new Paragraph("A"));
doc.Add(new Paragraph("Test"));
doc.Close();
}
}
}