fix: #1 - text blocks by words
This commit is contained in:
@@ -38,7 +38,7 @@ class Pdf2Table(page: PDPage) : PDFGraphicsStreamEngine(page) {
|
|||||||
|
|
||||||
private val svgBuilder = StringBuilder()
|
private val svgBuilder = StringBuilder()
|
||||||
private val currentPath = StringBuilder()
|
private val currentPath = StringBuilder()
|
||||||
private val tableSvg = StringBuilder()
|
val tableSvg = StringBuilder()
|
||||||
|
|
||||||
|
|
||||||
private val currentSubPathLines = mutableListOf<PdfLine>()
|
private val currentSubPathLines = mutableListOf<PdfLine>()
|
||||||
@@ -52,7 +52,7 @@ class Pdf2Table(page: PDPage) : PDFGraphicsStreamEngine(page) {
|
|||||||
val width = mediaBox.width
|
val width = mediaBox.width
|
||||||
val height = mediaBox.height
|
val height = mediaBox.height
|
||||||
return """
|
return """
|
||||||
<svg width="$width" height="$height" viewBox="0 0 $width $height" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
|
<svg width="$width" height="$height" viewBox="0 0 $width $height" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" style='font-size: 50%;'>
|
||||||
<g>${svgBuilder}</g>
|
<g>${svgBuilder}</g>
|
||||||
<g>${tableSvg}</g>
|
<g>${tableSvg}</g>
|
||||||
</svg>
|
</svg>
|
||||||
@@ -211,7 +211,7 @@ class Pdf2Table(page: PDPage) : PDFGraphicsStreamEngine(page) {
|
|||||||
val bottom = ys[ii+1]
|
val bottom = ys[ii+1]
|
||||||
cells.add(TableCell(left, top, right-left, bottom-top))
|
cells.add(TableCell(left, top, right-left, bottom-top))
|
||||||
|
|
||||||
tableSvg.append("<rect fill='${if (i % 2 == 0) "#f" else "#0"}${if (ii % 2 == 0) "f0" else "00"}' x='$left' y='$top' width='${right-left}' height='${bottom-top}'/>\n")
|
tableSvg.append("<rect fill='${if (i % 2 == 0) "#ff" else "#9e"}${if (ii % 2 == 0) "ff9e" else "9e9e"}' x='$left' y='$top' width='${right-left}' height='${bottom-top}'/>\n")
|
||||||
}
|
}
|
||||||
columns.add(TableColumn(left, right-left, cells))
|
columns.add(TableColumn(left, right-left, cells))
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -52,12 +52,6 @@ object PdfScheduleParser {
|
|||||||
svg.processPage(page)
|
svg.processPage(page)
|
||||||
val cols = svg.asTable()
|
val cols = svg.asTable()
|
||||||
|
|
||||||
/** Вывод таблички из PDF в SVG формате + отладочные жлементы из [Pdf2Table.asTable] **/
|
|
||||||
Log.v(SVG_TAG, "====")
|
|
||||||
for(line in svg.asSvg().split("\n")) {
|
|
||||||
Log.v(SVG_TAG, line)
|
|
||||||
}
|
|
||||||
|
|
||||||
if(cols.size != 3) throw RuntimeException("Found columns: ${cols.size}/3")
|
if(cols.size != 3) throw RuntimeException("Found columns: ${cols.size}/3")
|
||||||
|
|
||||||
// Парсинг текста
|
// Парсинг текста
|
||||||
@@ -67,6 +61,11 @@ object PdfScheduleParser {
|
|||||||
textStripper.getText(doc)
|
textStripper.getText(doc)
|
||||||
val texts = textStripper.blocks
|
val texts = textStripper.blocks
|
||||||
|
|
||||||
|
for(text in texts) {
|
||||||
|
svg.tableSvg.appendLine("<text x='${text.x}' y='${text.y}'>${text.text}</text>")
|
||||||
|
svg.tableSvg.appendLine("<circle cx='${text.x}' cy='${text.y}' r='1' fill='red'/>")
|
||||||
|
}
|
||||||
|
|
||||||
val daysColumn = cols[0] // Колонка "День", имеет формат "<день> dd.mm.yy"
|
val daysColumn = cols[0] // Колонка "День", имеет формат "<день> dd.mm.yy"
|
||||||
val intervalsColumn = cols[1] // Колонка "Интервал", имеет формат "HH:mm-HH:mm"
|
val intervalsColumn = cols[1] // Колонка "Интервал", имеет формат "HH:mm-HH:mm"
|
||||||
val lessonsColumn = cols[2] // Колонка с инофрмацией о паре
|
val lessonsColumn = cols[2] // Колонка с инофрмацией о паре
|
||||||
@@ -114,7 +113,10 @@ object PdfScheduleParser {
|
|||||||
group = content(groupCell)
|
group = content(groupCell)
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
if(day.size != 2) continue // XXX: добавить защиту от заголовка и выбрасывать исключения вместо пропуска?
|
if(day.size != 2) {
|
||||||
|
Log.w(TAG, "Day data is not in format <day> <date>: $day")
|
||||||
|
continue // XXX: добавить защиту от заголовка и выбрасывать исключения вместо пропуска?
|
||||||
|
}
|
||||||
|
|
||||||
val date = dateFrom(day[1]) ?: continue
|
val date = dateFrom(day[1]) ?: continue
|
||||||
val intervals = mutableListOf<TimeSlot>()
|
val intervals = mutableListOf<TimeSlot>()
|
||||||
@@ -161,7 +163,7 @@ object PdfScheduleParser {
|
|||||||
val type = LessonType.fromRaw(typeMatch.first().value.trim())
|
val type = LessonType.fromRaw(typeMatch.first().value.trim())
|
||||||
val teacher = raw.substring(typeMatch.last().range.last+1, raw.length - room.length).trim()
|
val teacher = raw.substring(typeMatch.last().range.last+1, raw.length - room.length).trim()
|
||||||
var subgroup: String? = null
|
var subgroup: String? = null
|
||||||
Log.d(TAG, subject + " " + group)
|
Log.d(TAG, "$subject $group")
|
||||||
if(group != null && subject.startsWith("$group,")) {
|
if(group != null && subject.startsWith("$group,")) {
|
||||||
subject = subject.substring(group.length + 1).trim()
|
subject = subject.substring(group.length + 1).trim()
|
||||||
val subgroupMatch = SUBGROUP_REGEX.find(subject)
|
val subgroupMatch = SUBGROUP_REGEX.find(subject)
|
||||||
@@ -192,6 +194,18 @@ object PdfScheduleParser {
|
|||||||
for (text in texts.filter { it.rotation != 0f }) {
|
for (text in texts.filter { it.rotation != 0f }) {
|
||||||
Log.v(TAG, "${text.x.roundToInt()}\t${text.y.roundToInt()}\t${text.text}")
|
Log.v(TAG, "${text.x.roundToInt()}\t${text.y.roundToInt()}\t${text.text}")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** Вывод таблички из PDF в SVG формате + отладочные жлементы из [Pdf2Table.asTable] **/
|
||||||
|
Log.v(SVG_TAG, "====")
|
||||||
|
for(line in svg.asSvg().split("\n")) {
|
||||||
|
if(line.length > 250) {
|
||||||
|
Log.v(SVG_TAG, "length: " + line.length)
|
||||||
|
Log.v(SVG_TAG + "-" + (pageIndex+1), "length: " + line.length)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
Log.v(SVG_TAG, line)
|
||||||
|
Log.v(SVG_TAG + "-" + (pageIndex+1), line)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
Log.e(TAG, "Parsing error", e)
|
Log.e(TAG, "Parsing error", e)
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ import com.tom_roush.pdfbox.text.TextPosition
|
|||||||
* Парсер блоков текста из PDF
|
* Парсер блоков текста из PDF
|
||||||
* @author Agzam
|
* @author Agzam
|
||||||
*/
|
*/
|
||||||
|
|
||||||
class PdfTextParser : PDFTextStripper() {
|
class PdfTextParser : PDFTextStripper() {
|
||||||
|
|
||||||
data class TextBlock(
|
data class TextBlock(
|
||||||
@@ -25,21 +26,51 @@ class PdfTextParser : PDFTextStripper() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
override fun writeString(string: String?, textPositions: MutableList<TextPosition>?) {
|
override fun writeString(string: String?, textPositions: MutableList<TextPosition>?) {
|
||||||
if (!string.isNullOrBlank() && !textPositions.isNullOrEmpty()) {
|
if (textPositions.isNullOrEmpty()) return
|
||||||
val first = textPositions.first()
|
|
||||||
val last = textPositions.last()
|
|
||||||
|
|
||||||
val matrix = first.textMatrix
|
val currentWordChars = mutableListOf<TextPosition>()
|
||||||
val rotation = first.dir
|
|
||||||
|
|
||||||
val totalWidth = (last.xDirAdj + last.widthDirAdj) - first.xDirAdj
|
for (tp in textPositions) {
|
||||||
|
val unicode = tp.unicode
|
||||||
|
|
||||||
val x = matrix.translateX
|
if (unicode == " " || unicode == "\u00A0" || unicode == "\t") {
|
||||||
val y = matrix.translateY - first.heightDir
|
addWordBlock(currentWordChars)
|
||||||
|
currentWordChars.clear()
|
||||||
val height = first.heightDir
|
} else {
|
||||||
|
currentWordChars.add(tp)
|
||||||
blocks.add(TextBlock(string.trim(), x, currentPage.mediaBox.height - y - height, totalWidth, height, rotation))
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
addWordBlock(currentWordChars)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun addWordBlock(wordChars: List<TextPosition>) {
|
||||||
|
if (wordChars.isEmpty()) return
|
||||||
|
|
||||||
|
val first = wordChars.first()
|
||||||
|
val last = wordChars.last()
|
||||||
|
|
||||||
|
val wordText = wordChars.joinToString("") { it.unicode }.trim()
|
||||||
|
if (wordText.isEmpty()) return
|
||||||
|
|
||||||
|
val matrix = first.textMatrix
|
||||||
|
val rotation = first.dir
|
||||||
|
|
||||||
|
val totalWidth = (last.xDirAdj + last.widthDirAdj) - first.xDirAdj
|
||||||
|
|
||||||
|
val x = matrix.translateX
|
||||||
|
val y = matrix.translateY - first.heightDir
|
||||||
|
val height = first.heightDir
|
||||||
|
|
||||||
|
blocks.add(
|
||||||
|
TextBlock(
|
||||||
|
text = wordText,
|
||||||
|
x = x,
|
||||||
|
y = currentPage.mediaBox.height - y - height,
|
||||||
|
width = totalWidth,
|
||||||
|
height = height,
|
||||||
|
rotation = rotation
|
||||||
|
)
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Reference in New Issue
Block a user