From 2ecbd00723495950a981be630cbc4c00904739f8 Mon Sep 17 00:00:00 2001 From: liana Date: Sun, 1 Jan 2023 17:50:33 +0300 Subject: [PATCH 1/8] added python script for table extraction --- .../checker/Checker.kt | 1 + .../mundaneassignmentpolice/wrapper/PDFBox.kt | 8 ++++ .../wrapper/TableExtractionScript.py | 48 +++++++++++++++++++ 3 files changed, 57 insertions(+) create mode 100644 src/main/kotlin/com/github/darderion/mundaneassignmentpolice/wrapper/TableExtractionScript.py diff --git a/src/main/kotlin/com/github/darderion/mundaneassignmentpolice/checker/Checker.kt b/src/main/kotlin/com/github/darderion/mundaneassignmentpolice/checker/Checker.kt index 8a0bd302..21026cd3 100644 --- a/src/main/kotlin/com/github/darderion/mundaneassignmentpolice/checker/Checker.kt +++ b/src/main/kotlin/com/github/darderion/mundaneassignmentpolice/checker/Checker.kt @@ -8,6 +8,7 @@ class Checker { fun getRuleViolations(pdfName: String, ruleSet: RuleSet) = getRuleViolations(pdfName, ruleSet.rules) fun getRuleViolations(pdfName: String, rules: List): List { val document = PDFBox().getPDF(pdfName) + PDFBox().getTables(pdfName) if (document.areas == null) return listOf( RuleViolation( listOf(document.text.first()), diff --git a/src/main/kotlin/com/github/darderion/mundaneassignmentpolice/wrapper/PDFBox.kt b/src/main/kotlin/com/github/darderion/mundaneassignmentpolice/wrapper/PDFBox.kt index f4ca5596..94fb2360 100644 --- a/src/main/kotlin/com/github/darderion/mundaneassignmentpolice/wrapper/PDFBox.kt +++ b/src/main/kotlin/com/github/darderion/mundaneassignmentpolice/wrapper/PDFBox.kt @@ -197,6 +197,14 @@ class PDFBox { return linkedSetOf(*images.map(::imgToBase64String).toTypedArray()) } + fun getTables(fileName: String){ + ProcessBuilder("python3", + "src/main/kotlin/com/github/darderion/mundaneassignmentpolice/wrapper/TableExtractionScript.py", + "extraction", + fileName) + .start() + } + private fun getImagesFromResources(resources: PDResources): List { val images: MutableList = ArrayList() for (xObjectName in resources.xObjectNames) { diff --git a/src/main/kotlin/com/github/darderion/mundaneassignmentpolice/wrapper/TableExtractionScript.py b/src/main/kotlin/com/github/darderion/mundaneassignmentpolice/wrapper/TableExtractionScript.py new file mode 100644 index 00000000..5f8d7536 --- /dev/null +++ b/src/main/kotlin/com/github/darderion/mundaneassignmentpolice/wrapper/TableExtractionScript.py @@ -0,0 +1,48 @@ +# coding: utf8 +import os +import PyPDF2 +import camelot +import pandas +import sys + + +def extraction(path): + file_name = path.replace('uploads/', '') + try: + PyPDF2.PdfFileReader(open(path, "rb")) + except PyPDF2._utils.PdfStreamError: + print("invalid PDF file") + else: + if not path.endswith('.pdf'): + os.rename(path, path+'.pdf') + path += '.pdf' + if not os.path.isdir(f'uploads/tables/{file_name}'): + os.mkdir(f'uploads/tables/{file_name}') + + tables = camelot.read_pdf(path, stream=True, pages='all') + + for k in range(len(tables)): + min_x, min_y, max_x, max_y = 596, 842, 0, 0 + for i in range(len(tables[k].cells)): + for j in range(len(tables[k].cells[i])): + min_x = min(min_x, tables[k].cells[i][j].x1) + min_y = min(min_y, tables[k].cells[i][j].y1) + max_x = max(max_x, tables[k].cells[i][j].x2) + max_y = max(max_y, tables[k].cells[i][j].y2) + tables[k].df = pandas.concat([tables[k].df, + pandas.DataFrame(['table information', + 'page', tables[k].page, + 'table area', min_x, min_y, max_x, max_y, + 'rows', len(tables[k].rows), + 'columns', len(tables[k].cols)] + )], + ignore_index=True) + tables.export(f'uploads/tables/{file_name}/{file_name}.csv', + f='csv', + compress=False) + if path.endswith('.pdf'): + os.rename(path, path.replace('.pdf','')) + + +if __name__ == '__main__': + globals()[sys.argv[1]](sys.argv[2]) From 09bfffcd60eb8d04af42045ecf34effda5e297d4 Mon Sep 17 00:00:00 2001 From: liana Date: Thu, 5 Jan 2023 17:55:18 +0300 Subject: [PATCH 2/8] added Table extraction from .csv --- pom.xml | 7 +- .../checker/Checker.kt | 1 - .../pdfdocument/PDFDocument.kt | 2 + .../pdfdocument/tables/Table.kt | 16 ++++ .../mundaneassignmentpolice/wrapper/PDFBox.kt | 81 ++++++++++++++++--- .../wrapper/TableExtractionScript.py | 12 +-- 6 files changed, 98 insertions(+), 21 deletions(-) create mode 100644 src/main/kotlin/com/github/darderion/mundaneassignmentpolice/pdfdocument/tables/Table.kt diff --git a/pom.xml b/pom.xml index c6177209..05953c1a 100644 --- a/pom.xml +++ b/pom.xml @@ -15,7 +15,7 @@ Web-app that assists in checking students' assignments 11 - 1.5.31 + 1.6.21 @@ -85,6 +85,11 @@ 5.0.0.M1 test + + org.jetbrains.kotlinx + dataframe + 0.8.0-dev-1005 + diff --git a/src/main/kotlin/com/github/darderion/mundaneassignmentpolice/checker/Checker.kt b/src/main/kotlin/com/github/darderion/mundaneassignmentpolice/checker/Checker.kt index 21026cd3..8a0bd302 100644 --- a/src/main/kotlin/com/github/darderion/mundaneassignmentpolice/checker/Checker.kt +++ b/src/main/kotlin/com/github/darderion/mundaneassignmentpolice/checker/Checker.kt @@ -8,7 +8,6 @@ class Checker { fun getRuleViolations(pdfName: String, ruleSet: RuleSet) = getRuleViolations(pdfName, ruleSet.rules) fun getRuleViolations(pdfName: String, rules: List): List { val document = PDFBox().getPDF(pdfName) - PDFBox().getTables(pdfName) if (document.areas == null) return listOf( RuleViolation( listOf(document.text.first()), diff --git a/src/main/kotlin/com/github/darderion/mundaneassignmentpolice/pdfdocument/PDFDocument.kt b/src/main/kotlin/com/github/darderion/mundaneassignmentpolice/pdfdocument/PDFDocument.kt index a7c7e71b..e499ddd5 100644 --- a/src/main/kotlin/com/github/darderion/mundaneassignmentpolice/pdfdocument/PDFDocument.kt +++ b/src/main/kotlin/com/github/darderion/mundaneassignmentpolice/pdfdocument/PDFDocument.kt @@ -1,10 +1,12 @@ package com.github.darderion.mundaneassignmentpolice.pdfdocument +import com.github.darderion.mundaneassignmentpolice.pdfdocument.tables.Table import com.github.darderion.mundaneassignmentpolice.pdfdocument.text.Line import mu.KotlinLogging class PDFDocument(val name: String = "PDF", val text: List, + val tables: List, val width: Double = defaultPageWidth, val height: Double = defaultPageHeight ) { diff --git a/src/main/kotlin/com/github/darderion/mundaneassignmentpolice/pdfdocument/tables/Table.kt b/src/main/kotlin/com/github/darderion/mundaneassignmentpolice/pdfdocument/tables/Table.kt new file mode 100644 index 00000000..1389e7ce --- /dev/null +++ b/src/main/kotlin/com/github/darderion/mundaneassignmentpolice/pdfdocument/tables/Table.kt @@ -0,0 +1,16 @@ +package com.github.darderion.mundaneassignmentpolice.pdfdocument.tables + +import com.github.darderion.mundaneassignmentpolice.pdfdocument.text.Word +import org.jetbrains.kotlinx.dataframe.DataFrame + +data class Table(val page : Int, + val x1 : Double, + val y1 : Double, + val x2 : Double, + val y2 : Double, + val rowCount : Int, + val colCount : Int, + val df: DataFrame, + var tableText: MutableList, + var lineIndexes: MutableList +) \ No newline at end of file diff --git a/src/main/kotlin/com/github/darderion/mundaneassignmentpolice/wrapper/PDFBox.kt b/src/main/kotlin/com/github/darderion/mundaneassignmentpolice/wrapper/PDFBox.kt index 94fb2360..6f561619 100644 --- a/src/main/kotlin/com/github/darderion/mundaneassignmentpolice/wrapper/PDFBox.kt +++ b/src/main/kotlin/com/github/darderion/mundaneassignmentpolice/wrapper/PDFBox.kt @@ -1,6 +1,7 @@ package com.github.darderion.mundaneassignmentpolice.wrapper import com.github.darderion.mundaneassignmentpolice.pdfdocument.PDFDocument +import com.github.darderion.mundaneassignmentpolice.pdfdocument.tables.Table import com.github.darderion.mundaneassignmentpolice.pdfdocument.text.* import com.github.darderion.mundaneassignmentpolice.utils.imgToBase64String import org.apache.pdfbox.pdmodel.PDDocument @@ -11,6 +12,12 @@ import org.apache.pdfbox.pdmodel.font.PDType1Font import org.apache.pdfbox.pdmodel.graphics.form.PDFormXObject import org.apache.pdfbox.pdmodel.graphics.image.PDImageXObject import org.apache.pdfbox.text.PDFTextStripper +import org.jetbrains.kotlinx.dataframe.AnyFrame +import org.jetbrains.kotlinx.dataframe.DataFrame +import org.jetbrains.kotlinx.dataframe.api.filter +import org.jetbrains.kotlinx.dataframe.api.first +import org.jetbrains.kotlinx.dataframe.api.select +import org.jetbrains.kotlinx.dataframe.io.read import java.awt.Color import java.awt.image.RenderedImage import java.io.* @@ -90,6 +97,8 @@ class PDFBox { * @return PDFDocument */ fun getPDF(fileName: String): PDFDocument { + val tables = getTables(fileName) + val pdfText: MutableList = mutableListOf() val document = getDocument(fileName) @@ -117,7 +126,7 @@ class PDFBox { var font: Font? var word: String var symb: Symbol - val words: MutableList = mutableListOf() + var words: MutableList = mutableListOf() var contentIndex: Int var contentItem: String var coordinates = Coordinate(0, 0) @@ -164,15 +173,30 @@ class PDFBox { } } if (font == null && word.isEmpty()) font = Font(0.0f) - words.add(Word(word, font!!, coordinates)) - - Line(line, pageIndex, lineIndex, words.toList()) + words.add(Word(word, font!!, coordinates)) + tables.forEach { table -> + words = words.filter { + if (isWordInTable(pageIndex, it, table)) { + table.tableText.add(it) + if (!(table.lineIndexes.contains(lineIndex))) + table.lineIndexes.add(lineIndex) + } + !isWordInTable(pageIndex, it, table) + }.toMutableList() + } + Line(line, pageIndex, lineIndex, words.toList()) }) } document.close() - return PDFDocument(fileName, pdfText, size.width.toDouble(), size.height.toDouble()) + return PDFDocument(fileName, pdfText, tables, size.width.toDouble(), size.height.toDouble()) + } + + private fun isWordInTable(page: Int,word: Word, table: Table): Boolean{ + return page == table.page - 1 && + word.position.x >= table.x1 && word.position.y <= table.y1 && + word.position.x <= table.x2 && word.position.y >= table.y2 } fun getPDFSize(fileName: String): Int { @@ -197,14 +221,6 @@ class PDFBox { return linkedSetOf(*images.map(::imgToBase64String).toTypedArray()) } - fun getTables(fileName: String){ - ProcessBuilder("python3", - "src/main/kotlin/com/github/darderion/mundaneassignmentpolice/wrapper/TableExtractionScript.py", - "extraction", - fileName) - .start() - } - private fun getImagesFromResources(resources: PDResources): List { val images: MutableList = ArrayList() for (xObjectName in resources.xObjectNames) { @@ -217,4 +233,43 @@ class PDFBox { } return images } + + private fun getTables(path: String): List
{ + ProcessBuilder("python3", "TableExtractionScript.py", "extraction", path).start() + + val fileName = path.replace("uploads/","") + val tables = mutableListOf
() + + File("uploads/tables/$fileName/").walkBottomUp().filter { it.isFile }.forEach { + val df = DataFrame.read(it) + tables.add(extractTable(df)) + } + return tables + } + + private val defaultPageHeight = 842.0 + + private val pageTableIndex = 2 + private val x1TableIndex = 4 + private val y1TableIndex = 5 + private val x2TableIndex = 6 + private val y2TableIndex = 7 + private val rowTableIndex = 9 + private val colTableIndex = 11 + + private fun extractTable(df: AnyFrame): Table{ + val indexTableInf = df.select{ cols(0) }.first { it[0] == "table information"}.index() + val tableInf = df.select{cols(0)}.filter { it.index() >= indexTableInf } + + val page = tableInf[pageTableIndex][0].toString().toInt() + val x1 = tableInf[x1TableIndex][0].toString().toDouble() + val y1 = defaultPageHeight - tableInf[y1TableIndex][0].toString().toDouble() + val x2 = tableInf[x2TableIndex][0].toString().toDouble() + val y2 = defaultPageHeight - tableInf[y2TableIndex][0].toString().toDouble() + val rowCount = tableInf[rowTableIndex][0].toString().toInt() + val colCount = tableInf[colTableIndex][0].toString().toInt() + val tableData = df.filter { it.index() Date: Fri, 3 Mar 2023 17:54:13 +0300 Subject: [PATCH 3/8] added .............. --- package-lock.json | 2 +- .../checker/Checker.kt | 11 +++++ .../checker/RuleViolation.kt | 7 +++ .../checker/rule/symbol/BasicSymbolRule.kt | 1 + .../checker/rule/table/TableRule.kt | 33 +++++++++++++ .../checker/rule/table/TableRuleBuilder.kt | 20 ++++++++ .../checker/rule/word/BasicWordRule.kt | 1 + .../controller/APIController.kt | 4 +- .../pdfdocument/Annotations.kt | 12 ++++- .../pdfdocument/tables/Cell.kt | 9 ++++ .../pdfdocument/tables/Table.kt | 46 ++++++++++++++----- .../mundaneassignmentpolice/rules/RuleSet.kt | 14 +++++- .../mundaneassignmentpolice/rules/Rules.kt | 6 +++ .../statistics/StatisticsBuilder.kt | 2 +- .../mundaneassignmentpolice/wrapper/PDFBox.kt | 27 +++++------ .../tables}/TableExtractionScript.py | 16 +++++-- .../TestsConfiguration.kt | 2 +- .../pdfdocument/PDFDocumentTests.kt | 4 +- 18 files changed, 181 insertions(+), 36 deletions(-) create mode 100644 src/main/kotlin/com/github/darderion/mundaneassignmentpolice/checker/rule/table/TableRule.kt create mode 100644 src/main/kotlin/com/github/darderion/mundaneassignmentpolice/checker/rule/table/TableRuleBuilder.kt create mode 100644 src/main/kotlin/com/github/darderion/mundaneassignmentpolice/pdfdocument/tables/Cell.kt rename src/main/{kotlin/com/github/darderion/mundaneassignmentpolice/wrapper => scripts/tables}/TableExtractionScript.py (77%) diff --git a/package-lock.json b/package-lock.json index 8a23ca84..40eb5db9 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,5 +1,5 @@ { - "name": "mundane-assignment-police", + "name": "map", "lockfileVersion": 2, "requires": true, "packages": {} diff --git a/src/main/kotlin/com/github/darderion/mundaneassignmentpolice/checker/Checker.kt b/src/main/kotlin/com/github/darderion/mundaneassignmentpolice/checker/Checker.kt index 8a0bd302..73a2ca99 100644 --- a/src/main/kotlin/com/github/darderion/mundaneassignmentpolice/checker/Checker.kt +++ b/src/main/kotlin/com/github/darderion/mundaneassignmentpolice/checker/Checker.kt @@ -1,7 +1,9 @@ package com.github.darderion.mundaneassignmentpolice.checker import com.github.darderion.mundaneassignmentpolice.checker.rule.Rule +import com.github.darderion.mundaneassignmentpolice.checker.rule.table.TableRule import com.github.darderion.mundaneassignmentpolice.rules.RuleSet +import com.github.darderion.mundaneassignmentpolice.rules.TableRuleSet import com.github.darderion.mundaneassignmentpolice.wrapper.PDFBox class Checker { @@ -16,6 +18,15 @@ class Checker { ) ) + return rules.map { + it.process(document) + }.flatten().toSet().toList() + } + fun getRuleTableViolations(pdfName: String, ruleSet: TableRuleSet) = getRuleTableViolations(pdfName, ruleSet.rules) + + fun getRuleTableViolations(pdfName: String, rules: List): List{ + val document = PDFBox().getPDF(pdfName) + return rules.map { it.process(document) }.flatten().toSet().toList() diff --git a/src/main/kotlin/com/github/darderion/mundaneassignmentpolice/checker/RuleViolation.kt b/src/main/kotlin/com/github/darderion/mundaneassignmentpolice/checker/RuleViolation.kt index 1782c96e..d8eb8246 100644 --- a/src/main/kotlin/com/github/darderion/mundaneassignmentpolice/checker/RuleViolation.kt +++ b/src/main/kotlin/com/github/darderion/mundaneassignmentpolice/checker/RuleViolation.kt @@ -1,5 +1,6 @@ package com.github.darderion.mundaneassignmentpolice.checker +import com.github.darderion.mundaneassignmentpolice.pdfdocument.tables.Cell import com.github.darderion.mundaneassignmentpolice.pdfdocument.text.Line enum class RuleViolationType { @@ -13,3 +14,9 @@ data class RuleViolation( ) { // override fun toString() = if (lines.count() == 1) "[${lines.first().line}, p.${lines.first().page}] --> '$message'" else "" } + +data class RuleTableViolation( + val cells: List, + val message: String, + val type: RuleViolationType +) diff --git a/src/main/kotlin/com/github/darderion/mundaneassignmentpolice/checker/rule/symbol/BasicSymbolRule.kt b/src/main/kotlin/com/github/darderion/mundaneassignmentpolice/checker/rule/symbol/BasicSymbolRule.kt index 41b5405c..c1414fe3 100644 --- a/src/main/kotlin/com/github/darderion/mundaneassignmentpolice/checker/rule/symbol/BasicSymbolRule.kt +++ b/src/main/kotlin/com/github/darderion/mundaneassignmentpolice/checker/rule/symbol/BasicSymbolRule.kt @@ -39,6 +39,7 @@ class BasicSymbolRule( when (direction) { LEFT -> sideTexts.removeAt(1) RIGHT -> sideTexts.removeAt(0) + else -> {} } val neighbors = (if (notIgnoredNeighbors.isNotEmpty()) sideTexts diff --git a/src/main/kotlin/com/github/darderion/mundaneassignmentpolice/checker/rule/table/TableRule.kt b/src/main/kotlin/com/github/darderion/mundaneassignmentpolice/checker/rule/table/TableRule.kt new file mode 100644 index 00000000..dc5dd0ec --- /dev/null +++ b/src/main/kotlin/com/github/darderion/mundaneassignmentpolice/checker/rule/table/TableRule.kt @@ -0,0 +1,33 @@ +package com.github.darderion.mundaneassignmentpolice.checker.rule.table + +import com.github.darderion.mundaneassignmentpolice.checker.RuleTableViolation +import com.github.darderion.mundaneassignmentpolice.checker.RuleViolation +import com.github.darderion.mundaneassignmentpolice.checker.RuleViolationType +import com.github.darderion.mundaneassignmentpolice.checker.rule.Rule +import com.github.darderion.mundaneassignmentpolice.pdfdocument.PDFDocument +import com.github.darderion.mundaneassignmentpolice.pdfdocument.PDFRegion +import com.github.darderion.mundaneassignmentpolice.pdfdocument.tables.Table +import com.github.darderion.mundaneassignmentpolice.pdfdocument.tables.Cell + +class TableRule ( + val predicates: MutableList<(Table) -> List>, + val type: RuleViolationType, + val area: PDFRegion, + val name: String + ){ + fun process(document: PDFDocument): List { + val rulesTablesViolations: MutableSet = mutableSetOf() + + predicates.forEach { predicate -> + rulesTablesViolations.addAll( + document.tables.map { + predicate(it) + }.filter { it.isNotEmpty() }.map { + RuleTableViolation(it, name, type) + } + ) + } + + return rulesTablesViolations.toList() + } + } diff --git a/src/main/kotlin/com/github/darderion/mundaneassignmentpolice/checker/rule/table/TableRuleBuilder.kt b/src/main/kotlin/com/github/darderion/mundaneassignmentpolice/checker/rule/table/TableRuleBuilder.kt new file mode 100644 index 00000000..dd81968a --- /dev/null +++ b/src/main/kotlin/com/github/darderion/mundaneassignmentpolice/checker/rule/table/TableRuleBuilder.kt @@ -0,0 +1,20 @@ +package com.github.darderion.mundaneassignmentpolice.checker.rule.table + +import com.github.darderion.mundaneassignmentpolice.checker.RuleViolationType +import com.github.darderion.mundaneassignmentpolice.pdfdocument.PDFRegion +import com.github.darderion.mundaneassignmentpolice.pdfdocument.list.PDFList +import com.github.darderion.mundaneassignmentpolice.pdfdocument.tables.Table +import com.github.darderion.mundaneassignmentpolice.pdfdocument.tables.Cell +import com.github.darderion.mundaneassignmentpolice.pdfdocument.text.Line + +class TableRuleBuilder { + private val predicates: MutableList<(Table) -> List> = mutableListOf() + private var type: RuleViolationType = RuleViolationType.Error + private var region: PDFRegion = PDFRegion.EVERYWHERE + private var name: String = "Rule name" + + fun called(name: String) = this.also { this.name = name } + + fun disallow(predicate: (table: Table) -> List) = this.also { predicates.add(predicate) } + fun getRule() = TableRule(predicates, type, region, name) +} \ No newline at end of file diff --git a/src/main/kotlin/com/github/darderion/mundaneassignmentpolice/checker/rule/word/BasicWordRule.kt b/src/main/kotlin/com/github/darderion/mundaneassignmentpolice/checker/rule/word/BasicWordRule.kt index e5310909..e0f0ddd0 100644 --- a/src/main/kotlin/com/github/darderion/mundaneassignmentpolice/checker/rule/word/BasicWordRule.kt +++ b/src/main/kotlin/com/github/darderion/mundaneassignmentpolice/checker/rule/word/BasicWordRule.kt @@ -39,6 +39,7 @@ class BasicWordRule( when (direction) { Direction.LEFT -> sideWords.removeAt(1) Direction.RIGHT -> sideWords.removeAt(0) + else -> {} } val filteredSideWords = sideWords diff --git a/src/main/kotlin/com/github/darderion/mundaneassignmentpolice/controller/APIController.kt b/src/main/kotlin/com/github/darderion/mundaneassignmentpolice/controller/APIController.kt index 26959cb3..78d1c4cd 100644 --- a/src/main/kotlin/com/github/darderion/mundaneassignmentpolice/controller/APIController.kt +++ b/src/main/kotlin/com/github/darderion/mundaneassignmentpolice/controller/APIController.kt @@ -25,6 +25,7 @@ const val url = developmentURL class APIController { val pdfBox = PDFBox() val ruleSet = RULE_SET_RU + val tableRuleSet = TABLE_RULE_SET_RU @GetMapping("/api/viewPDFText") fun getPDFText(@RequestParam pdfName: String) = @@ -82,7 +83,8 @@ class APIController { } else pdf.text.filter { it.page == page && it.index >= lines.first() && it.index <= lines.last() } - .sortedBy { line -> line.index } + .sortedBy { line -> line.index }, + Checker().getRuleTableViolations(fileName, tableRuleSet).map { it.cells }.flatten() ) logger.info("File created: $pdf2") diff --git a/src/main/kotlin/com/github/darderion/mundaneassignmentpolice/pdfdocument/Annotations.kt b/src/main/kotlin/com/github/darderion/mundaneassignmentpolice/pdfdocument/Annotations.kt index 721b9d2e..f9420da6 100644 --- a/src/main/kotlin/com/github/darderion/mundaneassignmentpolice/pdfdocument/Annotations.kt +++ b/src/main/kotlin/com/github/darderion/mundaneassignmentpolice/pdfdocument/Annotations.kt @@ -1,6 +1,7 @@ package com.github.darderion.mundaneassignmentpolice.pdfdocument import com.github.darderion.mundaneassignmentpolice.controller.pdfFolder +import com.github.darderion.mundaneassignmentpolice.pdfdocument.tables.Cell import com.github.darderion.mundaneassignmentpolice.pdfdocument.text.Coordinate import com.github.darderion.mundaneassignmentpolice.pdfdocument.text.Line import com.github.darderion.mundaneassignmentpolice.wrapper.PDFBox @@ -9,7 +10,7 @@ import java.nio.file.Paths class Annotations { companion object { - fun underline(pdf: PDFDocument, lines: List): String { + fun underline(pdf: PDFDocument, lines: List = listOf(), cells: List = listOf()): String { var document = PDFBox().getDocument(pdf.name) lines.forEach { line -> document = PDFBox().addLine(document, line.page, @@ -17,6 +18,14 @@ class Annotations { (pdf.width - (line.position.x + 50)).toInt() ) } + + cells.forEach { cell -> + document = PDFBox().addLine(document, cell.page, + Coordinate(cell.leftCorner.x, cell.leftCorner.y), + (cell.rightCorner.x - cell.leftCorner.x).toInt() + ) + } + Files.createDirectories(Paths.get("${pdfFolder}ruleviolations/")) val fileName = "${pdfFolder}ruleviolations/${ pdf.name.split('/')[pdf.name.split('/').count() - 1].replace(".pdf", "") @@ -24,5 +33,6 @@ class Annotations { document.save(fileName) return fileName } + } } diff --git a/src/main/kotlin/com/github/darderion/mundaneassignmentpolice/pdfdocument/tables/Cell.kt b/src/main/kotlin/com/github/darderion/mundaneassignmentpolice/pdfdocument/tables/Cell.kt new file mode 100644 index 00000000..b5097e66 --- /dev/null +++ b/src/main/kotlin/com/github/darderion/mundaneassignmentpolice/pdfdocument/tables/Cell.kt @@ -0,0 +1,9 @@ +package com.github.darderion.mundaneassignmentpolice.pdfdocument.tables +import com.github.darderion.mundaneassignmentpolice.pdfdocument.text.Coordinate + +class Cell( + val page: Int, + val cellText: List, + val leftCorner: Coordinate, + val rightCorner: Coordinate +) \ No newline at end of file diff --git a/src/main/kotlin/com/github/darderion/mundaneassignmentpolice/pdfdocument/tables/Table.kt b/src/main/kotlin/com/github/darderion/mundaneassignmentpolice/pdfdocument/tables/Table.kt index 1389e7ce..b7e7716e 100644 --- a/src/main/kotlin/com/github/darderion/mundaneassignmentpolice/pdfdocument/tables/Table.kt +++ b/src/main/kotlin/com/github/darderion/mundaneassignmentpolice/pdfdocument/tables/Table.kt @@ -1,16 +1,40 @@ package com.github.darderion.mundaneassignmentpolice.pdfdocument.tables -import com.github.darderion.mundaneassignmentpolice.pdfdocument.text.Word +import com.github.darderion.mundaneassignmentpolice.pdfdocument.text.Coordinate import org.jetbrains.kotlinx.dataframe.DataFrame +import org.jetbrains.kotlinx.dataframe.api.forEach +import org.jetbrains.kotlinx.dataframe.api.forEachColumn data class Table(val page : Int, - val x1 : Double, - val y1 : Double, - val x2 : Double, - val y2 : Double, - val rowCount : Int, - val colCount : Int, - val df: DataFrame, - var tableText: MutableList, - var lineIndexes: MutableList -) \ No newline at end of file + val x1 : Double, + val y1 : Double, + val x2 : Double, + val y2 : Double, + val rowCount : Int, + val colCount : Int, + val df: DataFrame, + var cells: MutableList +){ + init { + df.forEachColumn { it.forEach { getCell(it.toString()) } } + } + + private val defaultPageHeight = 842.0 + private val x1CellIndex = 3 + private val y1CellIndex = 6 + private val x2CellIndex = 9 + private val y2CellIndex = 12 + private fun getCell(text: String){ + + val coordinates = text.lines().first().split(" ") + + val x1 = coordinates[x1CellIndex].toDouble() + val y1 = defaultPageHeight - coordinates[y1CellIndex].toDouble() + val x2 = coordinates[x2CellIndex].toDouble() + val y2 = defaultPageHeight - coordinates[y2CellIndex].toDouble() + + val cellText = text.lines().filterIndexed { index, _ -> index != 0 } + + cells.add(Cell(page, cellText, Coordinate(x1,y1), Coordinate(x2,y2))) + } +} diff --git a/src/main/kotlin/com/github/darderion/mundaneassignmentpolice/rules/RuleSet.kt b/src/main/kotlin/com/github/darderion/mundaneassignmentpolice/rules/RuleSet.kt index ec5708da..aec65b1a 100644 --- a/src/main/kotlin/com/github/darderion/mundaneassignmentpolice/rules/RuleSet.kt +++ b/src/main/kotlin/com/github/darderion/mundaneassignmentpolice/rules/RuleSet.kt @@ -1,10 +1,11 @@ package com.github.darderion.mundaneassignmentpolice.rules import com.github.darderion.mundaneassignmentpolice.checker.rule.Rule +import com.github.darderion.mundaneassignmentpolice.checker.rule.table.TableRule val RULE_SET_RU = RuleSet( mutableListOf( - RULE_LITLINK, + /*RULE_LITLINK, RULE_SHORT_DASH, RULE_MEDIUM_DASH, RULE_LONG_DASH, @@ -22,9 +23,18 @@ val RULE_SET_RU = RuleSet( RULE_VARIOUS_ABBREVIATIONS, RULE_SECTIONS_ORDER, RULE_LOW_QUALITY_CONFERENCES, + + */ ) - + RULES_SPACE_AROUND_BRACKETS + /*+ RULES_SPACE_AROUND_BRACKETS + RULES_SMALL_NUMBERS + + */ ) +val TABLE_RULE_SET_RU = TableRuleSet( + listOf( TABLE_RULE, + ) +) +class TableRuleSet(val rules: List) class RuleSet(val rules: List) {} diff --git a/src/main/kotlin/com/github/darderion/mundaneassignmentpolice/rules/Rules.kt b/src/main/kotlin/com/github/darderion/mundaneassignmentpolice/rules/Rules.kt index 346e4a12..54e121ad 100644 --- a/src/main/kotlin/com/github/darderion/mundaneassignmentpolice/rules/Rules.kt +++ b/src/main/kotlin/com/github/darderion/mundaneassignmentpolice/rules/Rules.kt @@ -7,6 +7,7 @@ import com.github.darderion.mundaneassignmentpolice.checker.rule.regex.RegexRule import com.github.darderion.mundaneassignmentpolice.checker.rule.symbol.SymbolRuleBuilder import com.github.darderion.mundaneassignmentpolice.checker.rule.symbol.and import com.github.darderion.mundaneassignmentpolice.checker.rule.symbol.or +import com.github.darderion.mundaneassignmentpolice.checker.rule.table.TableRuleBuilder import com.github.darderion.mundaneassignmentpolice.checker.rule.tableofcontent.TableOfContentRuleBuilder import com.github.darderion.mundaneassignmentpolice.checker.rule.url.URLRuleBuilder import com.github.darderion.mundaneassignmentpolice.checker.rule.url.then @@ -416,3 +417,8 @@ val RULE_LOW_QUALITY_CONFERENCES = URLRuleBuilder() .any { conference -> url.text.contains(conference) } }.map { it to it.lines } }.getRule() + +val TABLE_RULE = TableRuleBuilder() + .called("Первая клетка") + .disallow { listOf( it.cells[0])} + .getRule() diff --git a/src/main/kotlin/com/github/darderion/mundaneassignmentpolice/statisticsservice/statistics/StatisticsBuilder.kt b/src/main/kotlin/com/github/darderion/mundaneassignmentpolice/statisticsservice/statistics/StatisticsBuilder.kt index dde691cf..85caad11 100644 --- a/src/main/kotlin/com/github/darderion/mundaneassignmentpolice/statisticsservice/statistics/StatisticsBuilder.kt +++ b/src/main/kotlin/com/github/darderion/mundaneassignmentpolice/statisticsservice/statistics/StatisticsBuilder.kt @@ -14,7 +14,7 @@ import kotlin.math.min class StatisticsBuilder { private val MIN_TOTAL_SHARE_WORDS = 0.75 - private val fileStopWordsName="src/main/resources/StopWords.txt" + private val fileStopWordsName="src/main/python/src/main/resources/StopWords.txt" fun getWordsStatistic(pdf : PDFDocument) : WordsStatistic { diff --git a/src/main/kotlin/com/github/darderion/mundaneassignmentpolice/wrapper/PDFBox.kt b/src/main/kotlin/com/github/darderion/mundaneassignmentpolice/wrapper/PDFBox.kt index 6f561619..1667f7ae 100644 --- a/src/main/kotlin/com/github/darderion/mundaneassignmentpolice/wrapper/PDFBox.kt +++ b/src/main/kotlin/com/github/darderion/mundaneassignmentpolice/wrapper/PDFBox.kt @@ -15,7 +15,7 @@ import org.apache.pdfbox.text.PDFTextStripper import org.jetbrains.kotlinx.dataframe.AnyFrame import org.jetbrains.kotlinx.dataframe.DataFrame import org.jetbrains.kotlinx.dataframe.api.filter -import org.jetbrains.kotlinx.dataframe.api.first +import org.jetbrains.kotlinx.dataframe.api.last import org.jetbrains.kotlinx.dataframe.api.select import org.jetbrains.kotlinx.dataframe.io.read import java.awt.Color @@ -175,14 +175,7 @@ class PDFBox { if (font == null && word.isEmpty()) font = Font(0.0f) words.add(Word(word, font!!, coordinates)) tables.forEach { table -> - words = words.filter { - if (isWordInTable(pageIndex, it, table)) { - table.tableText.add(it) - if (!(table.lineIndexes.contains(lineIndex))) - table.lineIndexes.add(lineIndex) - } - !isWordInTable(pageIndex, it, table) - }.toMutableList() + words = words.filter { !isWordInTable(pageIndex, it, table) }.toMutableList() } Line(line, pageIndex, lineIndex, words.toList()) }) @@ -235,12 +228,16 @@ class PDFBox { } private fun getTables(path: String): List
{ - ProcessBuilder("python3", "TableExtractionScript.py", "extraction", path).start() + + ProcessBuilder("python3", "../../../../../../../main/scripts/tables/TableExtractionScript.py", "extraction", path).start() val fileName = path.replace("uploads/","") val tables = mutableListOf
() - File("uploads/tables/$fileName/").walkBottomUp().filter { it.isFile }.forEach { + val pat = System.getProperty("user.dir") + println("Working Directory = $pat") + + File("../../../../../../../../uploads/tables/$fileName/").walkBottomUp().filter { it.isFile }.forEach { val df = DataFrame.read(it) tables.add(extractTable(df)) } @@ -258,7 +255,7 @@ class PDFBox { private val colTableIndex = 11 private fun extractTable(df: AnyFrame): Table{ - val indexTableInf = df.select{ cols(0) }.first { it[0] == "table information"}.index() + val indexTableInf = df.select{ cols(0) }.last { it[0] == "table information"}.index() val tableInf = df.select{cols(0)}.filter { it.index() >= indexTableInf } val page = tableInf[pageTableIndex][0].toString().toInt() @@ -270,6 +267,10 @@ class PDFBox { val colCount = tableInf[colTableIndex][0].toString().toInt() val tableData = df.filter { it.index() Date: Fri, 17 Mar 2023 21:34:31 +0300 Subject: [PATCH 4/8] fixed TableExtractionScript.py launch --- .../pdfdocument/tables/Table.kt | 13 ++++--- .../mundaneassignmentpolice/wrapper/PDFBox.kt | 37 +++++++++++-------- .../{tables => }/TableExtractionScript.py | 5 +-- 3 files changed, 31 insertions(+), 24 deletions(-) rename src/main/scripts/{tables => }/TableExtractionScript.py (96%) diff --git a/src/main/kotlin/com/github/darderion/mundaneassignmentpolice/pdfdocument/tables/Table.kt b/src/main/kotlin/com/github/darderion/mundaneassignmentpolice/pdfdocument/tables/Table.kt index b7e7716e..1ff58b86 100644 --- a/src/main/kotlin/com/github/darderion/mundaneassignmentpolice/pdfdocument/tables/Table.kt +++ b/src/main/kotlin/com/github/darderion/mundaneassignmentpolice/pdfdocument/tables/Table.kt @@ -19,11 +19,6 @@ data class Table(val page : Int, df.forEachColumn { it.forEach { getCell(it.toString()) } } } - private val defaultPageHeight = 842.0 - private val x1CellIndex = 3 - private val y1CellIndex = 6 - private val x2CellIndex = 9 - private val y2CellIndex = 12 private fun getCell(text: String){ val coordinates = text.lines().first().split(" ") @@ -37,4 +32,12 @@ data class Table(val page : Int, cells.add(Cell(page, cellText, Coordinate(x1,y1), Coordinate(x2,y2))) } + + companion object { + private const val defaultPageHeight = 842.0 + private const val x1CellIndex = 2 + private const val y1CellIndex = 5 + private const val x2CellIndex = 8 + private const val y2CellIndex = 11 + } } diff --git a/src/main/kotlin/com/github/darderion/mundaneassignmentpolice/wrapper/PDFBox.kt b/src/main/kotlin/com/github/darderion/mundaneassignmentpolice/wrapper/PDFBox.kt index 1667f7ae..12200539 100644 --- a/src/main/kotlin/com/github/darderion/mundaneassignmentpolice/wrapper/PDFBox.kt +++ b/src/main/kotlin/com/github/darderion/mundaneassignmentpolice/wrapper/PDFBox.kt @@ -228,32 +228,25 @@ class PDFBox { } private fun getTables(path: String): List
{ - - ProcessBuilder("python3", "../../../../../../../main/scripts/tables/TableExtractionScript.py", "extraction", path).start() - + val workingDirPath = System.getProperty("user.home") + "/map" val fileName = path.replace("uploads/","") val tables = mutableListOf
() - val pat = System.getProperty("user.dir") - println("Working Directory = $pat") + ProcessBuilder("src/main/scripts/venv/bin/python3", + "src/main/scripts/TableExtractionScript.py", + "extraction", path) + .directory(File(workingDirPath)) + .redirectOutput(ProcessBuilder.Redirect.INHERIT) + .start() + .waitFor() - File("../../../../../../../../uploads/tables/$fileName/").walkBottomUp().filter { it.isFile }.forEach { + File("$workingDirPath/uploads/tables/$fileName/").walkBottomUp().filter { it.isFile }.forEach { val df = DataFrame.read(it) tables.add(extractTable(df)) } return tables } - private val defaultPageHeight = 842.0 - - private val pageTableIndex = 2 - private val x1TableIndex = 4 - private val y1TableIndex = 5 - private val x2TableIndex = 6 - private val y2TableIndex = 7 - private val rowTableIndex = 9 - private val colTableIndex = 11 - private fun extractTable(df: AnyFrame): Table{ val indexTableInf = df.select{ cols(0) }.last { it[0] == "table information"}.index() val tableInf = df.select{cols(0)}.filter { it.index() >= indexTableInf } @@ -273,4 +266,16 @@ class PDFBox { tableData, mutableListOf()) } + + companion object { + private const val defaultPageHeight = 842.0 + + private const val pageTableIndex = 2 + private const val x1TableIndex = 4 + private const val y1TableIndex = 5 + private const val x2TableIndex = 6 + private const val y2TableIndex = 7 + private const val rowTableIndex = 9 + private const val colTableIndex = 11 + } } diff --git a/src/main/scripts/tables/TableExtractionScript.py b/src/main/scripts/TableExtractionScript.py similarity index 96% rename from src/main/scripts/tables/TableExtractionScript.py rename to src/main/scripts/TableExtractionScript.py index c45f49a4..4fbb907e 100644 --- a/src/main/scripts/tables/TableExtractionScript.py +++ b/src/main/scripts/TableExtractionScript.py @@ -6,9 +6,8 @@ def extraction(path): - print(os.getcwd()) - #os.chdir('../../../../') - #print(os.getcwd()) + + os.chdir(os.path.expanduser("~/map/")) file_name = path.replace('uploads/', '') try: From 4b54f48f12609e85f39f71fdbff5a2e449769b31 Mon Sep 17 00:00:00 2001 From: liana Date: Wed, 5 Apr 2023 22:48:03 +0300 Subject: [PATCH 5/8] added endPosition in Line, modified underline function. words from tables removed from pdfText. table text split into lines. table initialization changed. --- .../checker/Checker.kt | 11 --- .../checker/RuleViolation.kt | 6 -- .../checker/rule/list/ListRule.kt | 3 +- .../checker/rule/table/TableRule.kt | 23 +++-- .../checker/rule/table/TableRuleBuilder.kt | 6 +- .../controller/APIController.kt | 4 +- .../pdfdocument/Annotations.kt | 13 +-- .../pdfdocument/list/PDFList.kt | 16 ++-- .../pdfdocument/tables/Cell.kt | 6 +- .../pdfdocument/tables/Table.kt | 56 ++++++++---- .../pdfdocument/text/Line.kt | 11 ++- .../mundaneassignmentpolice/rules/RuleSet.kt | 9 +- .../mundaneassignmentpolice/rules/Rules.kt | 11 ++- .../statistics/StatisticsBuilder.kt | 2 +- .../mundaneassignmentpolice/wrapper/PDFBox.kt | 91 +++++++++---------- .../TableExtractionScript.py | 4 +- 16 files changed, 133 insertions(+), 139 deletions(-) rename src/main/{scripts => python}/TableExtractionScript.py (95%) diff --git a/src/main/kotlin/com/github/darderion/mundaneassignmentpolice/checker/Checker.kt b/src/main/kotlin/com/github/darderion/mundaneassignmentpolice/checker/Checker.kt index 73a2ca99..8a0bd302 100644 --- a/src/main/kotlin/com/github/darderion/mundaneassignmentpolice/checker/Checker.kt +++ b/src/main/kotlin/com/github/darderion/mundaneassignmentpolice/checker/Checker.kt @@ -1,9 +1,7 @@ package com.github.darderion.mundaneassignmentpolice.checker import com.github.darderion.mundaneassignmentpolice.checker.rule.Rule -import com.github.darderion.mundaneassignmentpolice.checker.rule.table.TableRule import com.github.darderion.mundaneassignmentpolice.rules.RuleSet -import com.github.darderion.mundaneassignmentpolice.rules.TableRuleSet import com.github.darderion.mundaneassignmentpolice.wrapper.PDFBox class Checker { @@ -18,15 +16,6 @@ class Checker { ) ) - return rules.map { - it.process(document) - }.flatten().toSet().toList() - } - fun getRuleTableViolations(pdfName: String, ruleSet: TableRuleSet) = getRuleTableViolations(pdfName, ruleSet.rules) - - fun getRuleTableViolations(pdfName: String, rules: List): List{ - val document = PDFBox().getPDF(pdfName) - return rules.map { it.process(document) }.flatten().toSet().toList() diff --git a/src/main/kotlin/com/github/darderion/mundaneassignmentpolice/checker/RuleViolation.kt b/src/main/kotlin/com/github/darderion/mundaneassignmentpolice/checker/RuleViolation.kt index d8eb8246..fd37b0e1 100644 --- a/src/main/kotlin/com/github/darderion/mundaneassignmentpolice/checker/RuleViolation.kt +++ b/src/main/kotlin/com/github/darderion/mundaneassignmentpolice/checker/RuleViolation.kt @@ -1,6 +1,5 @@ package com.github.darderion.mundaneassignmentpolice.checker -import com.github.darderion.mundaneassignmentpolice.pdfdocument.tables.Cell import com.github.darderion.mundaneassignmentpolice.pdfdocument.text.Line enum class RuleViolationType { @@ -15,8 +14,3 @@ data class RuleViolation( // override fun toString() = if (lines.count() == 1) "[${lines.first().line}, p.${lines.first().page}] --> '$message'" else "" } -data class RuleTableViolation( - val cells: List, - val message: String, - val type: RuleViolationType -) diff --git a/src/main/kotlin/com/github/darderion/mundaneassignmentpolice/checker/rule/list/ListRule.kt b/src/main/kotlin/com/github/darderion/mundaneassignmentpolice/checker/rule/list/ListRule.kt index e8e2a712..ec2f7b2b 100644 --- a/src/main/kotlin/com/github/darderion/mundaneassignmentpolice/checker/rule/list/ListRule.kt +++ b/src/main/kotlin/com/github/darderion/mundaneassignmentpolice/checker/rule/list/ListRule.kt @@ -8,6 +8,7 @@ import com.github.darderion.mundaneassignmentpolice.pdfdocument.PDFArea.TABLE_OF import com.github.darderion.mundaneassignmentpolice.pdfdocument.PDFDocument import com.github.darderion.mundaneassignmentpolice.pdfdocument.PDFRegion import com.github.darderion.mundaneassignmentpolice.pdfdocument.list.PDFList +import com.github.darderion.mundaneassignmentpolice.pdfdocument.text.Coordinate import com.github.darderion.mundaneassignmentpolice.pdfdocument.text.Line class ListRule( @@ -26,7 +27,7 @@ class ListRule( document.areas!!.tableOfContents.map { document.text.filter { it.area == TABLE_OF_CONTENT }.firstOrNull { line -> line.content.contains(it) - }?: Line(0, 0, 0, listOf(), TABLE_OF_CONTENT) + }?: Line(0, 0, 0, listOf(), TABLE_OF_CONTENT, Coordinate(0,0)) } ) diff --git a/src/main/kotlin/com/github/darderion/mundaneassignmentpolice/checker/rule/table/TableRule.kt b/src/main/kotlin/com/github/darderion/mundaneassignmentpolice/checker/rule/table/TableRule.kt index dc5dd0ec..697401e4 100644 --- a/src/main/kotlin/com/github/darderion/mundaneassignmentpolice/checker/rule/table/TableRule.kt +++ b/src/main/kotlin/com/github/darderion/mundaneassignmentpolice/checker/rule/table/TableRule.kt @@ -1,33 +1,32 @@ package com.github.darderion.mundaneassignmentpolice.checker.rule.table -import com.github.darderion.mundaneassignmentpolice.checker.RuleTableViolation import com.github.darderion.mundaneassignmentpolice.checker.RuleViolation import com.github.darderion.mundaneassignmentpolice.checker.RuleViolationType import com.github.darderion.mundaneassignmentpolice.checker.rule.Rule import com.github.darderion.mundaneassignmentpolice.pdfdocument.PDFDocument import com.github.darderion.mundaneassignmentpolice.pdfdocument.PDFRegion import com.github.darderion.mundaneassignmentpolice.pdfdocument.tables.Table -import com.github.darderion.mundaneassignmentpolice.pdfdocument.tables.Cell +import com.github.darderion.mundaneassignmentpolice.pdfdocument.text.Line class TableRule ( - val predicates: MutableList<(Table) -> List>, - val type: RuleViolationType, - val area: PDFRegion, - val name: String - ){ - fun process(document: PDFDocument): List { - val rulesTablesViolations: MutableSet = mutableSetOf() + val predicates: MutableList<(Table) -> List>, + type: RuleViolationType, + area: PDFRegion, + name: String + ): Rule(area, name, type){ + override fun process(document: PDFDocument): List { + val rulesViolations: MutableSet = mutableSetOf() predicates.forEach { predicate -> - rulesTablesViolations.addAll( + rulesViolations.addAll( document.tables.map { predicate(it) }.filter { it.isNotEmpty() }.map { - RuleTableViolation(it, name, type) + RuleViolation(it, name, type) } ) } - return rulesTablesViolations.toList() + return rulesViolations.toList() } } diff --git a/src/main/kotlin/com/github/darderion/mundaneassignmentpolice/checker/rule/table/TableRuleBuilder.kt b/src/main/kotlin/com/github/darderion/mundaneassignmentpolice/checker/rule/table/TableRuleBuilder.kt index dd81968a..c9acd21f 100644 --- a/src/main/kotlin/com/github/darderion/mundaneassignmentpolice/checker/rule/table/TableRuleBuilder.kt +++ b/src/main/kotlin/com/github/darderion/mundaneassignmentpolice/checker/rule/table/TableRuleBuilder.kt @@ -2,19 +2,17 @@ package com.github.darderion.mundaneassignmentpolice.checker.rule.table import com.github.darderion.mundaneassignmentpolice.checker.RuleViolationType import com.github.darderion.mundaneassignmentpolice.pdfdocument.PDFRegion -import com.github.darderion.mundaneassignmentpolice.pdfdocument.list.PDFList import com.github.darderion.mundaneassignmentpolice.pdfdocument.tables.Table -import com.github.darderion.mundaneassignmentpolice.pdfdocument.tables.Cell import com.github.darderion.mundaneassignmentpolice.pdfdocument.text.Line class TableRuleBuilder { - private val predicates: MutableList<(Table) -> List> = mutableListOf() + private val predicates: MutableList<(Table) -> List> = mutableListOf() private var type: RuleViolationType = RuleViolationType.Error private var region: PDFRegion = PDFRegion.EVERYWHERE private var name: String = "Rule name" fun called(name: String) = this.also { this.name = name } - fun disallow(predicate: (table: Table) -> List) = this.also { predicates.add(predicate) } + fun disallow(predicate: (table: Table) -> List) = this.also { predicates.add(predicate) } fun getRule() = TableRule(predicates, type, region, name) } \ No newline at end of file diff --git a/src/main/kotlin/com/github/darderion/mundaneassignmentpolice/controller/APIController.kt b/src/main/kotlin/com/github/darderion/mundaneassignmentpolice/controller/APIController.kt index 78d1c4cd..26959cb3 100644 --- a/src/main/kotlin/com/github/darderion/mundaneassignmentpolice/controller/APIController.kt +++ b/src/main/kotlin/com/github/darderion/mundaneassignmentpolice/controller/APIController.kt @@ -25,7 +25,6 @@ const val url = developmentURL class APIController { val pdfBox = PDFBox() val ruleSet = RULE_SET_RU - val tableRuleSet = TABLE_RULE_SET_RU @GetMapping("/api/viewPDFText") fun getPDFText(@RequestParam pdfName: String) = @@ -83,8 +82,7 @@ class APIController { } else pdf.text.filter { it.page == page && it.index >= lines.first() && it.index <= lines.last() } - .sortedBy { line -> line.index }, - Checker().getRuleTableViolations(fileName, tableRuleSet).map { it.cells }.flatten() + .sortedBy { line -> line.index } ) logger.info("File created: $pdf2") diff --git a/src/main/kotlin/com/github/darderion/mundaneassignmentpolice/pdfdocument/Annotations.kt b/src/main/kotlin/com/github/darderion/mundaneassignmentpolice/pdfdocument/Annotations.kt index f9420da6..edc86d07 100644 --- a/src/main/kotlin/com/github/darderion/mundaneassignmentpolice/pdfdocument/Annotations.kt +++ b/src/main/kotlin/com/github/darderion/mundaneassignmentpolice/pdfdocument/Annotations.kt @@ -1,7 +1,6 @@ package com.github.darderion.mundaneassignmentpolice.pdfdocument import com.github.darderion.mundaneassignmentpolice.controller.pdfFolder -import com.github.darderion.mundaneassignmentpolice.pdfdocument.tables.Cell import com.github.darderion.mundaneassignmentpolice.pdfdocument.text.Coordinate import com.github.darderion.mundaneassignmentpolice.pdfdocument.text.Line import com.github.darderion.mundaneassignmentpolice.wrapper.PDFBox @@ -10,21 +9,15 @@ import java.nio.file.Paths class Annotations { companion object { - fun underline(pdf: PDFDocument, lines: List = listOf(), cells: List = listOf()): String { + fun underline(pdf: PDFDocument, lines: List): String { var document = PDFBox().getDocument(pdf.name) lines.forEach { line -> document = PDFBox().addLine(document, line.page, - Coordinate(line.position.x to (pdf.height - (line.text.maxOf { it.position.y } + 2))), - (pdf.width - (line.position.x + 50)).toInt() + Coordinate(line.startPosition.x to (pdf.height - (line.text.maxOf { it.position.y } + 2))), + (line.endPosition.x - line.startPosition.x).toInt() ) } - cells.forEach { cell -> - document = PDFBox().addLine(document, cell.page, - Coordinate(cell.leftCorner.x, cell.leftCorner.y), - (cell.rightCorner.x - cell.leftCorner.x).toInt() - ) - } Files.createDirectories(Paths.get("${pdfFolder}ruleviolations/")) val fileName = "${pdfFolder}ruleviolations/${ diff --git a/src/main/kotlin/com/github/darderion/mundaneassignmentpolice/pdfdocument/list/PDFList.kt b/src/main/kotlin/com/github/darderion/mundaneassignmentpolice/pdfdocument/list/PDFList.kt index c55826eb..3ce27e68 100644 --- a/src/main/kotlin/com/github/darderion/mundaneassignmentpolice/pdfdocument/list/PDFList.kt +++ b/src/main/kotlin/com/github/darderion/mundaneassignmentpolice/pdfdocument/list/PDFList.kt @@ -54,7 +54,7 @@ data class PDFList(val value: MutableList = mutableListOf(), val nodes: Mu */ fun getLists(lines: List): List> { // Adding a line to process a text that has no lines after a list - val lines = lines + Line(-1, -1, -1, listOf(Word("NOT A LIST ITEM", Font(0.0f), Coordinate(1000, -1)))) + val lines = lines + Line(-1, -1, -1, listOf(Word("NOT A LIST ITEM", Font(0.0f), Coordinate(1000, -1))), null, Coordinate(0,0)) val lists: MutableList> = mutableListOf() val stack: Stack> = Stack() @@ -69,11 +69,11 @@ data class PDFList(val value: MutableList = mutableListOf(), val nodes: Mu stack.push(stack.peek().nodes.first()) } } else { - previousPosition = stack.peek().value.first().position - if (previousPosition hasSameXAs line.position) { // 1. lorem OR lorem + previousPosition = stack.peek().value.first().startPosition + if (previousPosition hasSameXAs line.startPosition) { // 1. lorem OR lorem stack.peek().value.add(line) // lorem lorem } else { - if (previousPosition.x < line.position.x) { + if (previousPosition.x < line.startPosition.x) { if (isListItem(line)) { stack.peek().nodes.add(PDFList(line.drop(2))) // lorem stack.push(stack.peek().nodes.last()) // 1. lorem @@ -83,17 +83,17 @@ data class PDFList(val value: MutableList = mutableListOf(), val nodes: Mu } } else { // lorem OR lorem OR ... lorem OR ... lorem while (!( stack.isEmpty() || // lorem 2. lorem lorem 2. lorem - (isListItem(line) && previousPosition hasSameXAs line.drop(2).position) || - previousPosition hasSameXAs line.position)) { + (isListItem(line) && previousPosition hasSameXAs line.drop(2).startPosition) || + previousPosition hasSameXAs line.startPosition)) { previousList = stack.pop() if (stack.isNotEmpty()) { - previousPosition = stack.peek().value.first().position + previousPosition = stack.peek().value.first().startPosition } } if (stack.isEmpty()) { lists.add(previousList!!) } else { - if (previousPosition hasSameXAs line.position) { // lorem + if (previousPosition hasSameXAs line.startPosition) { // lorem stack.peek().value.add(line) // lorem } else { stack.pop() diff --git a/src/main/kotlin/com/github/darderion/mundaneassignmentpolice/pdfdocument/tables/Cell.kt b/src/main/kotlin/com/github/darderion/mundaneassignmentpolice/pdfdocument/tables/Cell.kt index b5097e66..880cd0ac 100644 --- a/src/main/kotlin/com/github/darderion/mundaneassignmentpolice/pdfdocument/tables/Cell.kt +++ b/src/main/kotlin/com/github/darderion/mundaneassignmentpolice/pdfdocument/tables/Cell.kt @@ -1,9 +1,11 @@ package com.github.darderion.mundaneassignmentpolice.pdfdocument.tables import com.github.darderion.mundaneassignmentpolice.pdfdocument.text.Coordinate +import com.github.darderion.mundaneassignmentpolice.pdfdocument.text.Line -class Cell( +data class Cell( val page: Int, - val cellText: List, + val cellText: MutableList, + var cellLines: MutableList, val leftCorner: Coordinate, val rightCorner: Coordinate ) \ No newline at end of file diff --git a/src/main/kotlin/com/github/darderion/mundaneassignmentpolice/pdfdocument/tables/Table.kt b/src/main/kotlin/com/github/darderion/mundaneassignmentpolice/pdfdocument/tables/Table.kt index 1ff58b86..b354d139 100644 --- a/src/main/kotlin/com/github/darderion/mundaneassignmentpolice/pdfdocument/tables/Table.kt +++ b/src/main/kotlin/com/github/darderion/mundaneassignmentpolice/pdfdocument/tables/Table.kt @@ -1,36 +1,48 @@ package com.github.darderion.mundaneassignmentpolice.pdfdocument.tables -import com.github.darderion.mundaneassignmentpolice.pdfdocument.text.Coordinate +import com.github.darderion.mundaneassignmentpolice.pdfdocument.text.* +import com.github.darderion.mundaneassignmentpolice.wrapper.PDFBox +import org.jetbrains.kotlinx.dataframe.AnyFrame import org.jetbrains.kotlinx.dataframe.DataFrame -import org.jetbrains.kotlinx.dataframe.api.forEach -import org.jetbrains.kotlinx.dataframe.api.forEachColumn - -data class Table(val page : Int, - val x1 : Double, - val y1 : Double, - val x2 : Double, - val y2 : Double, - val rowCount : Int, - val colCount : Int, - val df: DataFrame, - var cells: MutableList -){ +import org.jetbrains.kotlinx.dataframe.api.* + +class Table(val df: DataFrame){ + + val page : Int + val x1 : Double + val y1 : Double + val x2 : Double + val y2 : Double + val rowCount : Int + val colCount : Int + val cells: MutableList = mutableListOf() init { - df.forEachColumn { it.forEach { getCell(it.toString()) } } + val indexTableInf = df.select{ cols(0) }.last { it[0] == "table information"}.index() + val tableInf = df.select{cols(0)}.filter { it.index() >= indexTableInf } + + this.page = tableInf[pageTableIndex][0].toString().toInt() - 1 + this.x1 = tableInf[x1TableIndex][0].toString().toDouble() + this.y1 = defaultPageHeight - tableInf[y1TableIndex][0].toString().toDouble() + this.x2 = tableInf[x2TableIndex][0].toString().toDouble() + this.y2 = defaultPageHeight - tableInf[y2TableIndex][0].toString().toDouble() + this.rowCount = tableInf[rowTableIndex][0].toString().toInt() + this.colCount = tableInf[colTableIndex][0].toString().toInt() + val tableData = df.filter { it.index() < indexTableInf } + + tableData.forEachColumn { it.forEach { getCell(it.toString()) } } } private fun getCell(text: String){ val coordinates = text.lines().first().split(" ") - val x1 = coordinates[x1CellIndex].toDouble() val y1 = defaultPageHeight - coordinates[y1CellIndex].toDouble() val x2 = coordinates[x2CellIndex].toDouble() val y2 = defaultPageHeight - coordinates[y2CellIndex].toDouble() - val cellText = text.lines().filterIndexed { index, _ -> index != 0 } + val cellText = text.lines().filterIndexed{ index, _ -> index > 0 }.toMutableList() - cells.add(Cell(page, cellText, Coordinate(x1,y1), Coordinate(x2,y2))) + cells.add(Cell(page, cellText, mutableListOf(), Coordinate(x1,y1), Coordinate(x2,y2))) } companion object { @@ -39,5 +51,13 @@ data class Table(val page : Int, private const val y1CellIndex = 5 private const val x2CellIndex = 8 private const val y2CellIndex = 11 + + private const val pageTableIndex = 2 + private const val x1TableIndex = 4 + private const val y1TableIndex = 5 + private const val x2TableIndex = 6 + private const val y2TableIndex = 7 + private const val rowTableIndex = 9 + private const val colTableIndex = 11 } } diff --git a/src/main/kotlin/com/github/darderion/mundaneassignmentpolice/pdfdocument/text/Line.kt b/src/main/kotlin/com/github/darderion/mundaneassignmentpolice/pdfdocument/text/Line.kt index a7003cc6..021e112a 100644 --- a/src/main/kotlin/com/github/darderion/mundaneassignmentpolice/pdfdocument/text/Line.kt +++ b/src/main/kotlin/com/github/darderion/mundaneassignmentpolice/pdfdocument/text/Line.kt @@ -3,12 +3,12 @@ package com.github.darderion.mundaneassignmentpolice.pdfdocument.text import com.github.darderion.mundaneassignmentpolice.pdfdocument.PDFArea data class Line(val index: Int, val page: Int, val documentIndex: Int, - val text: List, var area: PDFArea? = null + val text: List, var area: PDFArea? = null, var endPosition: Coordinate ) { val content: String get() = text.joinToString("") { it.text } - val position: Coordinate + val startPosition: Coordinate get() = if (text.isNotEmpty()) text.first().position else Coordinate(0, 0) val first: String? @@ -17,7 +17,10 @@ data class Line(val index: Int, val page: Int, val documentIndex: Int, val second: String? get() = if (text.count() > 1) text[1].text else null - override fun toString() = "[$documentIndex -- $index, p.$page, $area, ${position.x}] --> '$content'" + override fun toString() = "[$documentIndex -- $index, p.$page, $area, ${startPosition.x}] --> '$content'" - fun drop(numberOfItems: Int) = Line(index, page, documentIndex, text.drop(numberOfItems), area) + fun drop(numberOfItems: Int) = Line(index, page, documentIndex, text.drop(numberOfItems), area, Coordinate(0,0)) + companion object{ + private const val defaultPageWidth = 595.22 + } } diff --git a/src/main/kotlin/com/github/darderion/mundaneassignmentpolice/rules/RuleSet.kt b/src/main/kotlin/com/github/darderion/mundaneassignmentpolice/rules/RuleSet.kt index aec65b1a..5195f93b 100644 --- a/src/main/kotlin/com/github/darderion/mundaneassignmentpolice/rules/RuleSet.kt +++ b/src/main/kotlin/com/github/darderion/mundaneassignmentpolice/rules/RuleSet.kt @@ -1,10 +1,11 @@ package com.github.darderion.mundaneassignmentpolice.rules import com.github.darderion.mundaneassignmentpolice.checker.rule.Rule -import com.github.darderion.mundaneassignmentpolice.checker.rule.table.TableRule val RULE_SET_RU = RuleSet( mutableListOf( + TABLE_RULE, + /*RULE_LITLINK, RULE_SHORT_DASH, RULE_MEDIUM_DASH, @@ -31,10 +32,4 @@ val RULE_SET_RU = RuleSet( */ ) - -val TABLE_RULE_SET_RU = TableRuleSet( - listOf( TABLE_RULE, - ) -) -class TableRuleSet(val rules: List) class RuleSet(val rules: List) {} diff --git a/src/main/kotlin/com/github/darderion/mundaneassignmentpolice/rules/Rules.kt b/src/main/kotlin/com/github/darderion/mundaneassignmentpolice/rules/Rules.kt index 54e121ad..568e2cb9 100644 --- a/src/main/kotlin/com/github/darderion/mundaneassignmentpolice/rules/Rules.kt +++ b/src/main/kotlin/com/github/darderion/mundaneassignmentpolice/rules/Rules.kt @@ -16,6 +16,7 @@ import com.github.darderion.mundaneassignmentpolice.checker.rule.word.WordRuleBu import com.github.darderion.mundaneassignmentpolice.checker.rule.word.or import com.github.darderion.mundaneassignmentpolice.pdfdocument.PDFArea import com.github.darderion.mundaneassignmentpolice.pdfdocument.PDFRegion +import com.github.darderion.mundaneassignmentpolice.pdfdocument.text.Line import com.github.darderion.mundaneassignmentpolice.utils.InvalidOperationException import com.github.darderion.mundaneassignmentpolice.utils.LowQualityConferencesUtil import com.github.darderion.mundaneassignmentpolice.utils.ResourcesUtil @@ -419,6 +420,10 @@ val RULE_LOW_QUALITY_CONFERENCES = URLRuleBuilder() }.getRule() val TABLE_RULE = TableRuleBuilder() - .called("Первая клетка") - .disallow { listOf( it.cells[0])} - .getRule() + .called("Все клетки") + .disallow { table -> + val lines = mutableListOf() + table.cells.forEach { cell -> lines.addAll(cell.cellLines) } + lines + } + .getRule() \ No newline at end of file diff --git a/src/main/kotlin/com/github/darderion/mundaneassignmentpolice/statisticsservice/statistics/StatisticsBuilder.kt b/src/main/kotlin/com/github/darderion/mundaneassignmentpolice/statisticsservice/statistics/StatisticsBuilder.kt index 85caad11..dde691cf 100644 --- a/src/main/kotlin/com/github/darderion/mundaneassignmentpolice/statisticsservice/statistics/StatisticsBuilder.kt +++ b/src/main/kotlin/com/github/darderion/mundaneassignmentpolice/statisticsservice/statistics/StatisticsBuilder.kt @@ -14,7 +14,7 @@ import kotlin.math.min class StatisticsBuilder { private val MIN_TOTAL_SHARE_WORDS = 0.75 - private val fileStopWordsName="src/main/python/src/main/resources/StopWords.txt" + private val fileStopWordsName="src/main/resources/StopWords.txt" fun getWordsStatistic(pdf : PDFDocument) : WordsStatistic { diff --git a/src/main/kotlin/com/github/darderion/mundaneassignmentpolice/wrapper/PDFBox.kt b/src/main/kotlin/com/github/darderion/mundaneassignmentpolice/wrapper/PDFBox.kt index 12200539..ec1f0a78 100644 --- a/src/main/kotlin/com/github/darderion/mundaneassignmentpolice/wrapper/PDFBox.kt +++ b/src/main/kotlin/com/github/darderion/mundaneassignmentpolice/wrapper/PDFBox.kt @@ -1,6 +1,7 @@ package com.github.darderion.mundaneassignmentpolice.wrapper import com.github.darderion.mundaneassignmentpolice.pdfdocument.PDFDocument +import com.github.darderion.mundaneassignmentpolice.pdfdocument.tables.Cell import com.github.darderion.mundaneassignmentpolice.pdfdocument.tables.Table import com.github.darderion.mundaneassignmentpolice.pdfdocument.text.* import com.github.darderion.mundaneassignmentpolice.utils.imgToBase64String @@ -12,11 +13,7 @@ import org.apache.pdfbox.pdmodel.font.PDType1Font import org.apache.pdfbox.pdmodel.graphics.form.PDFormXObject import org.apache.pdfbox.pdmodel.graphics.image.PDImageXObject import org.apache.pdfbox.text.PDFTextStripper -import org.jetbrains.kotlinx.dataframe.AnyFrame import org.jetbrains.kotlinx.dataframe.DataFrame -import org.jetbrains.kotlinx.dataframe.api.filter -import org.jetbrains.kotlinx.dataframe.api.last -import org.jetbrains.kotlinx.dataframe.api.select import org.jetbrains.kotlinx.dataframe.io.read import java.awt.Color import java.awt.image.RenderedImage @@ -173,21 +170,50 @@ class PDFBox { } } if (font == null && word.isEmpty()) font = Font(0.0f) - words.add(Word(word, font!!, coordinates)) - tables.forEach { table -> - words = words.filter { !isWordInTable(pageIndex, it, table) }.toMutableList() + words.add(Word(word, font!!, coordinates)) + + tables.filter { table -> table.page == pageIndex }.forEach { table -> + words = words.filter { word -> !isWordInTable(pageIndex, word, table) } + .filter { it.text.isNotEmpty() }.toMutableList() + } + + if (document.pages[pageIndex].resources.xObjectNames.count() != 0){ + Line(line, pageIndex, lineIndex, words.toList(),null,Coordinate(0,0)) + } + else{ + Line(line, pageIndex, lineIndex, words.toList(),null,stripper.symbols[stripperIndex-1].position)} + } + ) + + var line = text.lines().size + tables.forEach { table -> + if (table.page == pageIndex) + table.cells.forEach { cell -> + val cellLines = mutableListOf() + cellLines.addAll(cell.cellText.filter { it.isNotEmpty() }.map { content -> + words.clear() + content.split(" ").forEach { + words.add(Word(it, Font(12f), cell.leftCorner)) + } + lineIndex += 1 + line += 1 + val tableLine = Line(line, pageIndex, lineIndex, words.toList(), + endPosition = Coordinate(cell.rightCorner.x, cell.rightCorner.y)) + cell.cellLines = cellLines + tableLine + } + + ) } - Line(line, pageIndex, lineIndex, words.toList()) - }) + } } - document.close() return PDFDocument(fileName, pdfText, tables, size.width.toDouble(), size.height.toDouble()) } - private fun isWordInTable(page: Int,word: Word, table: Table): Boolean{ - return page == table.page - 1 && + private fun isWordInTable(page: Int, word: Word, table: Table): Boolean { + return page == table.page && word.position.x >= table.x1 && word.position.y <= table.y1 && word.position.x <= table.x2 && word.position.y >= table.y2 } @@ -232,9 +258,11 @@ class PDFBox { val fileName = path.replace("uploads/","") val tables = mutableListOf
() - ProcessBuilder("src/main/scripts/venv/bin/python3", - "src/main/scripts/TableExtractionScript.py", - "extraction", path) + ProcessBuilder( + "src/main/python/venv/bin/python3", + "src/main/python/TableExtractionScript.py", + "extraction", path + ) .directory(File(workingDirPath)) .redirectOutput(ProcessBuilder.Redirect.INHERIT) .start() @@ -242,40 +270,9 @@ class PDFBox { File("$workingDirPath/uploads/tables/$fileName/").walkBottomUp().filter { it.isFile }.forEach { val df = DataFrame.read(it) - tables.add(extractTable(df)) + tables.add(Table(df)) } return tables } - private fun extractTable(df: AnyFrame): Table{ - val indexTableInf = df.select{ cols(0) }.last { it[0] == "table information"}.index() - val tableInf = df.select{cols(0)}.filter { it.index() >= indexTableInf } - - val page = tableInf[pageTableIndex][0].toString().toInt() - val x1 = tableInf[x1TableIndex][0].toString().toDouble() - val y1 = defaultPageHeight - tableInf[y1TableIndex][0].toString().toDouble() - val x2 = tableInf[x2TableIndex][0].toString().toDouble() - val y2 = defaultPageHeight - tableInf[y2TableIndex][0].toString().toDouble() - val rowCount = tableInf[rowTableIndex][0].toString().toInt() - val colCount = tableInf[colTableIndex][0].toString().toInt() - val tableData = df.filter { it.index() Date: Mon, 24 Apr 2023 15:08:47 +0300 Subject: [PATCH 6/8] added camelot --- .../pdfdocument/tables/Table.kt | 6 + .../mundaneassignmentpolice/wrapper/PDFBox.kt | 38 +- src/main/python/TableExtractionScript.py | 4 +- src/main/python/camelot/__init__.py | 21 + src/main/python/camelot/__main__.py | 14 + src/main/python/camelot/__version__.py | 23 + src/main/python/camelot/backends/__init__.py | 3 + .../camelot/backends/ghostscript_backend.py | 47 + .../camelot/backends/image_conversion.py | 40 + .../camelot/backends/poppler_backend.py | 22 + src/main/python/camelot/cli.py | 304 ++++++ src/main/python/camelot/core.py | 764 ++++++++++++++ src/main/python/camelot/handlers.py | 180 ++++ src/main/python/camelot/image_processing.py | 222 +++++ src/main/python/camelot/io.py | 119 +++ src/main/python/camelot/parsers/__init__.py | 4 + src/main/python/camelot/parsers/base.py | 20 + src/main/python/camelot/parsers/lattice.py | 435 ++++++++ src/main/python/camelot/parsers/stream.py | 468 +++++++++ src/main/python/camelot/plotting.py | 225 +++++ src/main/python/camelot/utils.py | 938 ++++++++++++++++++ 21 files changed, 3886 insertions(+), 11 deletions(-) mode change 100644 => 100755 src/main/python/TableExtractionScript.py create mode 100755 src/main/python/camelot/__init__.py create mode 100644 src/main/python/camelot/__main__.py create mode 100644 src/main/python/camelot/__version__.py create mode 100644 src/main/python/camelot/backends/__init__.py create mode 100644 src/main/python/camelot/backends/ghostscript_backend.py create mode 100644 src/main/python/camelot/backends/image_conversion.py create mode 100644 src/main/python/camelot/backends/poppler_backend.py create mode 100644 src/main/python/camelot/cli.py create mode 100644 src/main/python/camelot/core.py create mode 100644 src/main/python/camelot/handlers.py create mode 100644 src/main/python/camelot/image_processing.py create mode 100644 src/main/python/camelot/io.py create mode 100644 src/main/python/camelot/parsers/__init__.py create mode 100644 src/main/python/camelot/parsers/base.py create mode 100644 src/main/python/camelot/parsers/lattice.py create mode 100644 src/main/python/camelot/parsers/stream.py create mode 100644 src/main/python/camelot/plotting.py create mode 100644 src/main/python/camelot/utils.py diff --git a/src/main/kotlin/com/github/darderion/mundaneassignmentpolice/pdfdocument/tables/Table.kt b/src/main/kotlin/com/github/darderion/mundaneassignmentpolice/pdfdocument/tables/Table.kt index b354d139..f41c97cc 100644 --- a/src/main/kotlin/com/github/darderion/mundaneassignmentpolice/pdfdocument/tables/Table.kt +++ b/src/main/kotlin/com/github/darderion/mundaneassignmentpolice/pdfdocument/tables/Table.kt @@ -45,6 +45,12 @@ class Table(val df: DataFrame){ cells.add(Cell(page, cellText, mutableListOf(), Coordinate(x1,y1), Coordinate(x2,y2))) } + fun getLines(): List{ + val lines = mutableListOf() + cells.forEach{ lines.addAll(it.cellLines) } + return lines + } + companion object { private const val defaultPageHeight = 842.0 private const val x1CellIndex = 2 diff --git a/src/main/kotlin/com/github/darderion/mundaneassignmentpolice/wrapper/PDFBox.kt b/src/main/kotlin/com/github/darderion/mundaneassignmentpolice/wrapper/PDFBox.kt index ec1f0a78..01a5b4fd 100644 --- a/src/main/kotlin/com/github/darderion/mundaneassignmentpolice/wrapper/PDFBox.kt +++ b/src/main/kotlin/com/github/darderion/mundaneassignmentpolice/wrapper/PDFBox.kt @@ -1,7 +1,6 @@ package com.github.darderion.mundaneassignmentpolice.wrapper import com.github.darderion.mundaneassignmentpolice.pdfdocument.PDFDocument -import com.github.darderion.mundaneassignmentpolice.pdfdocument.tables.Cell import com.github.darderion.mundaneassignmentpolice.pdfdocument.tables.Table import com.github.darderion.mundaneassignmentpolice.pdfdocument.text.* import com.github.darderion.mundaneassignmentpolice.utils.imgToBase64String @@ -18,6 +17,14 @@ import org.jetbrains.kotlinx.dataframe.io.read import java.awt.Color import java.awt.image.RenderedImage import java.io.* +import java.nio.file.Files +import java.nio.file.LinkOption +import java.util.* +import java.util.concurrent.TimeUnit +import kotlin.collections.ArrayList +import kotlin.collections.HashMap +import kotlin.collections.LinkedHashSet +import kotlin.io.path.Path class PDFBox { @@ -200,6 +207,7 @@ class PDFBox { val tableLine = Line(line, pageIndex, lineIndex, words.toList(), endPosition = Coordinate(cell.rightCorner.x, cell.rightCorner.y)) cell.cellLines = cellLines + pdfText.add(tableLine) tableLine } @@ -254,24 +262,34 @@ class PDFBox { } private fun getTables(path: String): List
{ + val d: Long = Date().time + val workingDirPath = System.getProperty("user.home") + "/map" val fileName = path.replace("uploads/","") val tables = mutableListOf
() - ProcessBuilder( - "src/main/python/venv/bin/python3", - "src/main/python/TableExtractionScript.py", - "extraction", path - ) - .directory(File(workingDirPath)) - .redirectOutput(ProcessBuilder.Redirect.INHERIT) - .start() - .waitFor() + if (!Files.exists(Path("$workingDirPath/uploads/tables/$fileName"), LinkOption.NOFOLLOW_LINKS)) { + + + ProcessBuilder( + "src/main/python/venv/bin/python3", + "src/main/python/TableExtractionScript.py", + "extraction", path + ) + .directory(File(workingDirPath)) + .redirectOutput(ProcessBuilder.Redirect.INHERIT) + .start() + .waitFor() + } File("$workingDirPath/uploads/tables/$fileName/").walkBottomUp().filter { it.isFile }.forEach { val df = DataFrame.read(it) tables.add(Table(df)) } + + val e: Long = Date().time + println(e - d) + println(tables.size) return tables } diff --git a/src/main/python/TableExtractionScript.py b/src/main/python/TableExtractionScript.py old mode 100644 new mode 100755 index 1b8e7528..ba2efab3 --- a/src/main/python/TableExtractionScript.py +++ b/src/main/python/TableExtractionScript.py @@ -1,4 +1,6 @@ import PyPDF2 +import sys +sys.path.insert(0, '../src') import camelot import pandas import sys @@ -21,7 +23,7 @@ def extraction(path): if not os.path.isdir(f'uploads/tables/{file_name}'): os.mkdir(f'uploads/tables/{file_name}') - tables = camelot.read_pdf(path, stream=True, pages='all') + tables = camelot.read_pdf(path, latice=True, pages='all') for k in range(len(tables)): left_x, left_y, right_x, right_y = 596, 896, 0, 0 diff --git a/src/main/python/camelot/__init__.py b/src/main/python/camelot/__init__.py new file mode 100755 index 00000000..bc4beb62 --- /dev/null +++ b/src/main/python/camelot/__init__.py @@ -0,0 +1,21 @@ +# -*- coding: utf-8 -*- + +import logging + +from .__version__ import __version__ +from .io import read_pdf +from .plotting import PlotMethods + + +# set up logging +logger = logging.getLogger("camelot") + +format_string = "%(asctime)s - %(levelname)s - %(message)s" +formatter = logging.Formatter(format_string, datefmt="%Y-%m-%dT%H:%M:%S") +handler = logging.StreamHandler() +handler.setFormatter(formatter) + +logger.addHandler(handler) + +# instantiate plot method +plot = PlotMethods() diff --git a/src/main/python/camelot/__main__.py b/src/main/python/camelot/__main__.py new file mode 100644 index 00000000..ac90c95f --- /dev/null +++ b/src/main/python/camelot/__main__.py @@ -0,0 +1,14 @@ +# -*- coding: utf-8 -*- + + +__all__ = ("main",) + + +def main(): + from src.main.python.camelot.cli import cli + + cli() + + +if __name__ == "__main__": + main() diff --git a/src/main/python/camelot/__version__.py b/src/main/python/camelot/__version__.py new file mode 100644 index 00000000..72364b92 --- /dev/null +++ b/src/main/python/camelot/__version__.py @@ -0,0 +1,23 @@ +# -*- coding: utf-8 -*- + +VERSION = (0, 11, 0) +PRERELEASE = None # alpha, beta or rc +REVISION = None + + +def generate_version(version, prerelease=None, revision=None): + version_parts = [".".join(map(str, version))] + if prerelease is not None: + version_parts.append(f"-{prerelease}") + if revision is not None: + version_parts.append(f".{revision}") + return "".join(version_parts) + + +__title__ = "camelot-py" +__description__ = "PDF Table Extraction for Humans." +__url__ = "http://camelot-py.readthedocs.io/" +__version__ = generate_version(VERSION, prerelease=PRERELEASE, revision=REVISION) +__author__ = "Vinayak Mehta" +__author_email__ = "vmehta94@gmail.com" +__license__ = "MIT License" diff --git a/src/main/python/camelot/backends/__init__.py b/src/main/python/camelot/backends/__init__.py new file mode 100644 index 00000000..8d0b91e9 --- /dev/null +++ b/src/main/python/camelot/backends/__init__.py @@ -0,0 +1,3 @@ +# -*- coding: utf-8 -*- + +from .image_conversion import ImageConversionBackend diff --git a/src/main/python/camelot/backends/ghostscript_backend.py b/src/main/python/camelot/backends/ghostscript_backend.py new file mode 100644 index 00000000..1de7da19 --- /dev/null +++ b/src/main/python/camelot/backends/ghostscript_backend.py @@ -0,0 +1,47 @@ +# -*- coding: utf-8 -*- + +import sys +import ctypes +from ctypes.util import find_library + + +def installed_posix(): + library = find_library("gs") + return library is not None + + +def installed_windows(): + library = find_library( + "".join(("gsdll", str(ctypes.sizeof(ctypes.c_voidp) * 8), ".dll")) + ) + return library is not None + + +class GhostscriptBackend(object): + def installed(self): + if sys.platform in ["linux", "darwin"]: + return installed_posix() + elif sys.platform == "win32": + return installed_windows() + else: + return installed_posix() + + def convert(self, pdf_path, png_path, resolution=300): + if not self.installed(): + raise OSError( + "Ghostscript is not installed. You can install it using the instructions" + " here: https://camelot-py.readthedocs.io/en/master/user/install-deps.html" + ) + + import ghostscript + + gs_command = [ + "gs", + "-q", + "-sDEVICE=png16m", + "-o", + png_path, + f"-r{resolution}", + pdf_path, + ] + ghostscript.Ghostscript(*gs_command) diff --git a/src/main/python/camelot/backends/image_conversion.py b/src/main/python/camelot/backends/image_conversion.py new file mode 100644 index 00000000..7d2c4d7a --- /dev/null +++ b/src/main/python/camelot/backends/image_conversion.py @@ -0,0 +1,40 @@ +# -*- coding: utf-8 -*- + +from .poppler_backend import PopplerBackend +from .ghostscript_backend import GhostscriptBackend + +BACKENDS = {"poppler": PopplerBackend, "ghostscript": GhostscriptBackend} + + +class ImageConversionBackend(object): + def __init__(self, backend="poppler", use_fallback=True): + if backend not in BACKENDS.keys(): + raise ValueError(f"Image conversion backend '{backend}' not supported") + + self.backend = backend + self.use_fallback = use_fallback + self.fallbacks = list(filter(lambda x: x != backend, BACKENDS.keys())) + + def convert(self, pdf_path, png_path): + try: + converter = BACKENDS[self.backend]() + converter.convert(pdf_path, png_path) + except Exception as e: + import sys + + if self.use_fallback: + for fallback in self.fallbacks: + try: + converter = BACKENDS[fallback]() + converter.convert(pdf_path, png_path) + except Exception as e: + raise type(e)( + str(e) + f" with image conversion backend '{fallback}'" + ).with_traceback(sys.exc_info()[2]) + continue + else: + break + else: + raise type(e)( + str(e) + f" with image conversion backend '{self.backend}'" + ).with_traceback(sys.exc_info()[2]) diff --git a/src/main/python/camelot/backends/poppler_backend.py b/src/main/python/camelot/backends/poppler_backend.py new file mode 100644 index 00000000..41033729 --- /dev/null +++ b/src/main/python/camelot/backends/poppler_backend.py @@ -0,0 +1,22 @@ +# -*- coding: utf-8 -*- + +import shutil +import subprocess + + +class PopplerBackend(object): + def convert(self, pdf_path, png_path): + pdftopng_executable = shutil.which("pdftopng") + if pdftopng_executable is None: + raise OSError( + "pdftopng is not installed. You can install it using the 'pip install pdftopng' command." + ) + + pdftopng_command = [pdftopng_executable, pdf_path, png_path] + + try: + subprocess.check_output( + " ".join(pdftopng_command), stderr=subprocess.STDOUT, shell=True + ) + except subprocess.CalledProcessError as e: + raise ValueError(e.output) diff --git a/src/main/python/camelot/cli.py b/src/main/python/camelot/cli.py new file mode 100644 index 00000000..546a32d8 --- /dev/null +++ b/src/main/python/camelot/cli.py @@ -0,0 +1,304 @@ +# -*- coding: utf-8 -*- + +import logging + +import click + +try: + import matplotlib.pyplot as plt +except ImportError: + _HAS_MPL = False +else: + _HAS_MPL = True + +from . import __version__, read_pdf, plot + + +logger = logging.getLogger("camelot") +logger.setLevel(logging.INFO) + + +class Config(object): + def __init__(self): + self.config = {} + + def set_config(self, key, value): + self.config[key] = value + + +pass_config = click.make_pass_decorator(Config) + + +@click.group(name="camelot") +@click.version_option(version=__version__) +@click.option("-q", "--quiet", is_flag=False, help="Suppress logs and warnings.") +@click.option( + "-p", + "--pages", + default="1", + help="Comma-separated page numbers." " Example: 1,3,4 or 1,4-end or all.", +) +@click.option("-pw", "--password", help="Password for decryption.") +@click.option("-o", "--output", help="Output file path.") +@click.option( + "-f", + "--format", + type=click.Choice(["csv", "excel", "html", "json", "markdown", "sqlite"]), + help="Output file format.", +) +@click.option("-z", "--zip", is_flag=True, help="Create ZIP archive.") +@click.option( + "-split", + "--split_text", + is_flag=True, + help="Split text that spans across multiple cells.", +) +@click.option( + "-flag", + "--flag_size", + is_flag=True, + help="Flag text based on" " font size. Useful to detect super/subscripts.", +) +@click.option( + "-strip", + "--strip_text", + help="Characters that should be stripped from a string before" + " assigning it to a cell.", +) +@click.option( + "-M", + "--margins", + nargs=3, + default=(1.0, 0.5, 0.1), + help="PDFMiner char_margin, line_margin and word_margin.", +) +@click.pass_context +def cli(ctx, *args, **kwargs): + """Camelot: PDF Table Extraction for Humans""" + ctx.obj = Config() + for key, value in kwargs.items(): + ctx.obj.set_config(key, value) + + +@cli.command("lattice") +@click.option( + "-R", + "--table_regions", + default=[], + multiple=True, + help="Page regions to analyze. Example: x1,y1,x2,y2" + " where x1, y1 -> left-top and x2, y2 -> right-bottom.", +) +@click.option( + "-T", + "--table_areas", + default=[], + multiple=True, + help="Table areas to process. Example: x1,y1,x2,y2" + " where x1, y1 -> left-top and x2, y2 -> right-bottom.", +) +@click.option( + "-back", "--process_background", is_flag=True, help="Process background lines." +) +@click.option( + "-scale", + "--line_scale", + default=15, + help="Line size scaling factor. The larger the value," + " the smaller the detected lines.", +) +@click.option( + "-copy", + "--copy_text", + default=[], + type=click.Choice(["h", "v"]), + multiple=True, + help="Direction in which text in a spanning cell" " will be copied over.", +) +@click.option( + "-shift", + "--shift_text", + default=["l", "t"], + type=click.Choice(["", "l", "r", "t", "b"]), + multiple=True, + help="Direction in which text in a spanning cell will flow.", +) +@click.option( + "-l", + "--line_tol", + default=2, + help="Tolerance parameter used to merge close vertical" " and horizontal lines.", +) +@click.option( + "-j", + "--joint_tol", + default=2, + help="Tolerance parameter used to decide whether" + " the detected lines and points lie close to each other.", +) +@click.option( + "-block", + "--threshold_blocksize", + default=15, + help="For adaptive thresholding, size of a pixel" + " neighborhood that is used to calculate a threshold value for" + " the pixel. Example: 3, 5, 7, and so on.", +) +@click.option( + "-const", + "--threshold_constant", + default=-2, + help="For adaptive thresholding, constant subtracted" + " from the mean or weighted mean. Normally, it is positive but" + " may be zero or negative as well.", +) +@click.option( + "-I", + "--iterations", + default=0, + help="Number of times for erosion/dilation will be applied.", +) +@click.option( + "-res", + "--resolution", + default=300, + help="Resolution used for PDF to PNG conversion.", +) +@click.option( + "-plot", + "--plot_type", + type=click.Choice(["text", "grid", "contour", "joint", "line"]), + help="Plot elements found on PDF page for visual debugging.", +) +@click.argument("filepath", type=click.Path(exists=True)) +@pass_config +def lattice(c, *args, **kwargs): + """Use lines between text to parse the table.""" + conf = c.config + pages = conf.pop("pages") + output = conf.pop("output") + f = conf.pop("format") + compress = conf.pop("zip") + quiet = conf.pop("quiet") + plot_type = kwargs.pop("plot_type") + filepath = kwargs.pop("filepath") + kwargs.update(conf) + + table_regions = list(kwargs["table_regions"]) + kwargs["table_regions"] = None if not table_regions else table_regions + table_areas = list(kwargs["table_areas"]) + kwargs["table_areas"] = None if not table_areas else table_areas + copy_text = list(kwargs["copy_text"]) + kwargs["copy_text"] = None if not copy_text else copy_text + kwargs["shift_text"] = list(kwargs["shift_text"]) + + if plot_type is not None: + if not _HAS_MPL: + raise ImportError("matplotlib is required for plotting.") + else: + if output is None: + raise click.UsageError("Please specify output file path using --output") + if f is None: + raise click.UsageError("Please specify output file format using --format") + + tables = read_pdf( + filepath, pages=pages, flavor="lattice", suppress_stdout=quiet, **kwargs + ) + click.echo(f"Found {tables.n} tables") + if plot_type is not None: + for table in tables: + plot(table, kind=plot_type) + plt.show() + else: + tables.export(output, f=f, compress=compress) + + +@cli.command("stream") +@click.option( + "-R", + "--table_regions", + default=[], + multiple=True, + help="Page regions to analyze. Example: x1,y1,x2,y2" + " where x1, y1 -> left-top and x2, y2 -> right-bottom.", +) +@click.option( + "-T", + "--table_areas", + default=[], + multiple=True, + help="Table areas to process. Example: x1,y1,x2,y2" + " where x1, y1 -> left-top and x2, y2 -> right-bottom.", +) +@click.option( + "-C", + "--columns", + default=[], + multiple=True, + help="X coordinates of column separators.", +) +@click.option( + "-e", + "--edge_tol", + default=50, + help="Tolerance parameter" " for extending textedges vertically.", +) +@click.option( + "-r", + "--row_tol", + default=2, + help="Tolerance parameter" " used to combine text vertically, to generate rows.", +) +@click.option( + "-c", + "--column_tol", + default=0, + help="Tolerance parameter" + " used to combine text horizontally, to generate columns.", +) +@click.option( + "-plot", + "--plot_type", + type=click.Choice(["text", "grid", "contour", "textedge"]), + help="Plot elements found on PDF page for visual debugging.", +) +@click.argument("filepath", type=click.Path(exists=True)) +@pass_config +def stream(c, *args, **kwargs): + """Use spaces between text to parse the table.""" + conf = c.config + pages = conf.pop("pages") + output = conf.pop("output") + f = conf.pop("format") + compress = conf.pop("zip") + quiet = conf.pop("quiet") + plot_type = kwargs.pop("plot_type") + filepath = kwargs.pop("filepath") + kwargs.update(conf) + + table_regions = list(kwargs["table_regions"]) + kwargs["table_regions"] = None if not table_regions else table_regions + table_areas = list(kwargs["table_areas"]) + kwargs["table_areas"] = None if not table_areas else table_areas + columns = list(kwargs["columns"]) + kwargs["columns"] = None if not columns else columns + + if plot_type is not None: + if not _HAS_MPL: + raise ImportError("matplotlib is required for plotting.") + else: + if output is None: + raise click.UsageError("Please specify output file path using --output") + if f is None: + raise click.UsageError("Please specify output file format using --format") + + tables = read_pdf( + filepath, pages=pages, flavor="stream", suppress_stdout=quiet, **kwargs + ) + click.echo(f"Found {tables.n} tables") + if plot_type is not None: + for table in tables: + plot(table, kind=plot_type) + plt.show() + else: + tables.export(output, f=f, compress=compress) diff --git a/src/main/python/camelot/core.py b/src/main/python/camelot/core.py new file mode 100644 index 00000000..58a98efd --- /dev/null +++ b/src/main/python/camelot/core.py @@ -0,0 +1,764 @@ +# -*- coding: utf-8 -*- + +import os +import sqlite3 +import zipfile +import tempfile +from itertools import chain +from operator import itemgetter + +import numpy as np +import pandas as pd + + +# minimum number of vertical textline intersections for a textedge +# to be considered valid +TEXTEDGE_REQUIRED_ELEMENTS = 4 +# padding added to table area on the left, right and bottom +TABLE_AREA_PADDING = 10 + + +class TextEdge(object): + """Defines a text edge coordinates relative to a left-bottom + origin. (PDF coordinate space) + + Parameters + ---------- + x : float + x-coordinate of the text edge. + y0 : float + y-coordinate of bottommost point. + y1 : float + y-coordinate of topmost point. + align : string, optional (default: 'left') + {'left', 'right', 'middle'} + + Attributes + ---------- + intersections: int + Number of intersections with horizontal text rows. + is_valid: bool + A text edge is valid if it intersections with at least + TEXTEDGE_REQUIRED_ELEMENTS horizontal text rows. + + """ + + def __init__(self, x, y0, y1, align="left"): + self.x = x + self.y0 = y0 + self.y1 = y1 + self.align = align + self.intersections = 0 + self.is_valid = False + + def __repr__(self): + x = round(self.x, 2) + y0 = round(self.y0, 2) + y1 = round(self.y1, 2) + return ( + f"" + ) + + def update_coords(self, x, y0, edge_tol=50): + """Updates the text edge's x and bottom y coordinates and sets + the is_valid attribute. + """ + if np.isclose(self.y0, y0, atol=edge_tol): + self.x = (self.intersections * self.x + x) / float(self.intersections + 1) + self.y0 = y0 + self.intersections += 1 + # a textedge is valid only if it extends uninterrupted + # over a required number of textlines + if self.intersections > TEXTEDGE_REQUIRED_ELEMENTS: + self.is_valid = True + + +class TextEdges(object): + """Defines a dict of left, right and middle text edges found on + the PDF page. The dict has three keys based on the alignments, + and each key's value is a list of camelot.core.TextEdge objects. + """ + + def __init__(self, edge_tol=50): + self.edge_tol = edge_tol + self._textedges = {"left": [], "right": [], "middle": []} + + @staticmethod + def get_x_coord(textline, align): + """Returns the x coordinate of a text row based on the + specified alignment. + """ + x_left = textline.x0 + x_right = textline.x1 + x_middle = x_left + (x_right - x_left) / 2.0 + x_coord = {"left": x_left, "middle": x_middle, "right": x_right} + return x_coord[align] + + def find(self, x_coord, align): + """Returns the index of an existing text edge using + the specified x coordinate and alignment. + """ + for i, te in enumerate(self._textedges[align]): + if np.isclose(te.x, x_coord, atol=0.5): + return i + return None + + def add(self, textline, align): + """Adds a new text edge to the current dict.""" + x = self.get_x_coord(textline, align) + y0 = textline.y0 + y1 = textline.y1 + te = TextEdge(x, y0, y1, align=align) + self._textedges[align].append(te) + + def update(self, textline): + """Updates an existing text edge in the current dict.""" + for align in ["left", "right", "middle"]: + x_coord = self.get_x_coord(textline, align) + idx = self.find(x_coord, align) + if idx is None: + self.add(textline, align) + else: + self._textedges[align][idx].update_coords( + x_coord, textline.y0, edge_tol=self.edge_tol + ) + + def generate(self, textlines): + """Generates the text edges dict based on horizontal text + rows. + """ + for tl in textlines: + if len(tl.get_text().strip()) > 1: # TODO: hacky + self.update(tl) + + def get_relevant(self): + """Returns the list of relevant text edges (all share the same + alignment) based on which list intersects horizontal text rows + the most. + """ + intersections_sum = { + "left": sum( + te.intersections for te in self._textedges["left"] if te.is_valid + ), + "right": sum( + te.intersections for te in self._textedges["right"] if te.is_valid + ), + "middle": sum( + te.intersections for te in self._textedges["middle"] if te.is_valid + ), + } + + # TODO: naive + # get vertical textedges that intersect maximum number of + # times with horizontal textlines + relevant_align = max(intersections_sum.items(), key=itemgetter(1))[0] + return self._textedges[relevant_align] + + def get_table_areas(self, textlines, relevant_textedges): + """Returns a dict of interesting table areas on the PDF page + calculated using relevant text edges. + """ + + def pad(area, average_row_height): + x0 = area[0] - TABLE_AREA_PADDING + y0 = area[1] - TABLE_AREA_PADDING + x1 = area[2] + TABLE_AREA_PADDING + # add a constant since table headers can be relatively up + y1 = area[3] + average_row_height * 5 + return (x0, y0, x1, y1) + + # sort relevant textedges in reading order + relevant_textedges.sort(key=lambda te: (-te.y0, te.x)) + + table_areas = {} + for te in relevant_textedges: + if te.is_valid: + if not table_areas: + table_areas[(te.x, te.y0, te.x, te.y1)] = None + else: + found = None + for area in table_areas: + # check for overlap + if te.y1 >= area[1] and te.y0 <= area[3]: + found = area + break + if found is None: + table_areas[(te.x, te.y0, te.x, te.y1)] = None + else: + table_areas.pop(found) + updated_area = ( + found[0], + min(te.y0, found[1]), + max(found[2], te.x), + max(found[3], te.y1), + ) + table_areas[updated_area] = None + + # extend table areas based on textlines that overlap + # vertically. it's possible that these textlines were + # eliminated during textedges generation since numbers and + # chars/words/sentences are often aligned differently. + # drawback: table areas that have paragraphs on their sides + # will include the paragraphs too. + sum_textline_height = 0 + for tl in textlines: + sum_textline_height += tl.y1 - tl.y0 + found = None + for area in table_areas: + # check for overlap + if tl.y0 >= area[1] and tl.y1 <= area[3]: + found = area + break + if found is not None: + table_areas.pop(found) + updated_area = ( + min(tl.x0, found[0]), + min(tl.y0, found[1]), + max(found[2], tl.x1), + max(found[3], tl.y1), + ) + table_areas[updated_area] = None + average_textline_height = sum_textline_height / float(len(textlines)) + + # add some padding to table areas + table_areas_padded = {} + for area in table_areas: + table_areas_padded[pad(area, average_textline_height)] = None + + return table_areas_padded + + +class Cell(object): + """Defines a cell in a table with coordinates relative to a + left-bottom origin. (PDF coordinate space) + + Parameters + ---------- + x1 : float + x-coordinate of left-bottom point. + y1 : float + y-coordinate of left-bottom point. + x2 : float + x-coordinate of right-top point. + y2 : float + y-coordinate of right-top point. + + Attributes + ---------- + lb : tuple + Tuple representing left-bottom coordinates. + lt : tuple + Tuple representing left-top coordinates. + rb : tuple + Tuple representing right-bottom coordinates. + rt : tuple + Tuple representing right-top coordinates. + left : bool + Whether or not cell is bounded on the left. + right : bool + Whether or not cell is bounded on the right. + top : bool + Whether or not cell is bounded on the top. + bottom : bool + Whether or not cell is bounded on the bottom. + hspan : bool + Whether or not cell spans horizontally. + vspan : bool + Whether or not cell spans vertically. + text : string + Text assigned to cell. + + """ + + def __init__(self, x1, y1, x2, y2): + self.x1 = x1 + self.y1 = y1 + self.x2 = x2 + self.y2 = y2 + self.lb = (x1, y1) + self.lt = (x1, y2) + self.rb = (x2, y1) + self.rt = (x2, y2) + self.left = False + self.right = False + self.top = False + self.bottom = False + self.hspan = False + self.vspan = False + self._text = "" + + def __repr__(self): + x1 = round(self.x1) + y1 = round(self.y1) + x2 = round(self.x2) + y2 = round(self.y2) + return f"" + + @property + def text(self): + return self._text + + @text.setter + def text(self, t): + self._text = "".join([self._text, t]) + + @property + def bound(self): + """The number of sides on which the cell is bounded.""" + return self.top + self.bottom + self.left + self.right + + +class Table(object): + """Defines a table with coordinates relative to a left-bottom + origin. (PDF coordinate space) + + Parameters + ---------- + cols : list + List of tuples representing column x-coordinates in increasing + order. + rows : list + List of tuples representing row y-coordinates in decreasing + order. + + Attributes + ---------- + df : :class:`pandas.DataFrame` + shape : tuple + Shape of the table. + accuracy : float + Accuracy with which text was assigned to the cell. + whitespace : float + Percentage of whitespace in the table. + order : int + Table number on PDF page. + page : int + PDF page number. + + """ + + def __init__(self, cols, rows): + self.cols = cols + self.rows = rows + self.cells = [[Cell(c[0], r[1], c[1], r[0]) for c in cols] for r in rows] + self.df = None + self.shape = (0, 0) + self.accuracy = 0 + self.whitespace = 0 + self.order = None + self.page = None + + def __repr__(self): + return f"<{self.__class__.__name__} shape={self.shape}>" + + def __lt__(self, other): + if self.page == other.page: + if self.order < other.order: + return True + if self.page < other.page: + return True + + @property + def data(self): + """Returns two-dimensional list of strings in table.""" + d = [] + for row in self.cells: + d.append([cell.text.strip() for cell in row]) + return d + + @property + def parsing_report(self): + """Returns a parsing report with %accuracy, %whitespace, + table number on page and page number. + """ + # pretty? + report = { + "accuracy": round(self.accuracy, 2), + "whitespace": round(self.whitespace, 2), + "order": self.order, + "page": self.page, + } + return report + + def set_all_edges(self): + """Sets all table edges to True.""" + for row in self.cells: + for cell in row: + cell.left = cell.right = cell.top = cell.bottom = True + return self + + def set_edges(self, vertical, horizontal, joint_tol=2): + """Sets a cell's edges to True depending on whether the cell's + coordinates overlap with the line's coordinates within a + tolerance. + + Parameters + ---------- + vertical : list + List of detected vertical lines. + horizontal : list + List of detected horizontal lines. + + """ + for v in vertical: + # find closest x coord + # iterate over y coords and find closest start and end points + i = [ + i + for i, t in enumerate(self.cols) + if np.isclose(v[0], t[0], atol=joint_tol) + ] + j = [ + j + for j, t in enumerate(self.rows) + if np.isclose(v[3], t[0], atol=joint_tol) + ] + k = [ + k + for k, t in enumerate(self.rows) + if np.isclose(v[1], t[0], atol=joint_tol) + ] + if not j: + continue + J = j[0] + if i == [0]: # only left edge + L = i[0] + if k: + K = k[0] + while J < K: + self.cells[J][L].left = True + J += 1 + else: + K = len(self.rows) + while J < K: + self.cells[J][L].left = True + J += 1 + elif i == []: # only right edge + L = len(self.cols) - 1 + if k: + K = k[0] + while J < K: + self.cells[J][L].right = True + J += 1 + else: + K = len(self.rows) + while J < K: + self.cells[J][L].right = True + J += 1 + else: # both left and right edges + L = i[0] + if k: + K = k[0] + while J < K: + self.cells[J][L].left = True + self.cells[J][L - 1].right = True + J += 1 + else: + K = len(self.rows) + while J < K: + self.cells[J][L].left = True + self.cells[J][L - 1].right = True + J += 1 + + for h in horizontal: + # find closest y coord + # iterate over x coords and find closest start and end points + i = [ + i + for i, t in enumerate(self.rows) + if np.isclose(h[1], t[0], atol=joint_tol) + ] + j = [ + j + for j, t in enumerate(self.cols) + if np.isclose(h[0], t[0], atol=joint_tol) + ] + k = [ + k + for k, t in enumerate(self.cols) + if np.isclose(h[2], t[0], atol=joint_tol) + ] + if not j: + continue + J = j[0] + if i == [0]: # only top edge + L = i[0] + if k: + K = k[0] + while J < K: + self.cells[L][J].top = True + J += 1 + else: + K = len(self.cols) + while J < K: + self.cells[L][J].top = True + J += 1 + elif i == []: # only bottom edge + L = len(self.rows) - 1 + if k: + K = k[0] + while J < K: + self.cells[L][J].bottom = True + J += 1 + else: + K = len(self.cols) + while J < K: + self.cells[L][J].bottom = True + J += 1 + else: # both top and bottom edges + L = i[0] + if k: + K = k[0] + while J < K: + self.cells[L][J].top = True + self.cells[L - 1][J].bottom = True + J += 1 + else: + K = len(self.cols) + while J < K: + self.cells[L][J].top = True + self.cells[L - 1][J].bottom = True + J += 1 + + return self + + def set_border(self): + """Sets table border edges to True.""" + for r in range(len(self.rows)): + self.cells[r][0].left = True + self.cells[r][len(self.cols) - 1].right = True + for c in range(len(self.cols)): + self.cells[0][c].top = True + self.cells[len(self.rows) - 1][c].bottom = True + return self + + def set_span(self): + """Sets a cell's hspan or vspan attribute to True depending + on whether the cell spans horizontally or vertically. + """ + for row in self.cells: + for cell in row: + left = cell.left + right = cell.right + top = cell.top + bottom = cell.bottom + if cell.bound == 4: + continue + elif cell.bound == 3: + if not left and (right and top and bottom): + cell.hspan = True + elif not right and (left and top and bottom): + cell.hspan = True + elif not top and (left and right and bottom): + cell.vspan = True + elif not bottom and (left and right and top): + cell.vspan = True + elif cell.bound == 2: + if left and right and (not top and not bottom): + cell.vspan = True + elif top and bottom and (not left and not right): + cell.hspan = True + elif cell.bound in [0, 1]: + cell.vspan = True + cell.hspan = True + return self + + def to_csv(self, path, **kwargs): + """Writes Table to a comma-separated values (csv) file. + + For kwargs, check :meth:`pandas.DataFrame.to_csv`. + + Parameters + ---------- + path : str + Output filepath. + + """ + kw = {"encoding": "utf-8", "index": False, "header": False, "quoting": 1} + kw.update(kwargs) + self.df.to_csv(path, **kw) + + def to_json(self, path, **kwargs): + """Writes Table to a JSON file. + + For kwargs, check :meth:`pandas.DataFrame.to_json`. + + Parameters + ---------- + path : str + Output filepath. + + """ + kw = {"orient": "records"} + kw.update(kwargs) + json_string = self.df.to_json(**kw) + with open(path, "w") as f: + f.write(json_string) + + def to_excel(self, path, **kwargs): + """Writes Table to an Excel file. + + For kwargs, check :meth:`pandas.DataFrame.to_excel`. + + Parameters + ---------- + path : str + Output filepath. + + """ + kw = { + "sheet_name": f"page-{self.page}-table-{self.order}", + "encoding": "utf-8", + } + kw.update(kwargs) + writer = pd.ExcelWriter(path) + self.df.to_excel(writer, **kw) + writer.save() + + def to_html(self, path, **kwargs): + """Writes Table to an HTML file. + + For kwargs, check :meth:`pandas.DataFrame.to_html`. + + Parameters + ---------- + path : str + Output filepath. + + """ + html_string = self.df.to_html(**kwargs) + with open(path, "w", encoding="utf-8") as f: + f.write(html_string) + + def to_markdown(self, path, **kwargs): + """Writes Table to a Markdown file. + + For kwargs, check :meth:`pandas.DataFrame.to_markdown`. + + Parameters + ---------- + path : str + Output filepath. + + """ + md_string = self.df.to_markdown(**kwargs) + with open(path, "w", encoding="utf-8") as f: + f.write(md_string) + + def to_sqlite(self, path, **kwargs): + """Writes Table to sqlite database. + + For kwargs, check :meth:`pandas.DataFrame.to_sql`. + + Parameters + ---------- + path : str + Output filepath. + + """ + kw = {"if_exists": "replace", "index": False} + kw.update(kwargs) + conn = sqlite3.connect(path) + table_name = f"page-{self.page}-table-{self.order}" + self.df.to_sql(table_name, conn, **kw) + conn.commit() + conn.close() + + +class TableList(object): + """Defines a list of camelot.core.Table objects. Each table can + be accessed using its index. + + Attributes + ---------- + n : int + Number of tables in the list. + + """ + + def __init__(self, tables): + self._tables = tables + + def __repr__(self): + return f"<{self.__class__.__name__} n={self.n}>" + + def __len__(self): + return len(self._tables) + + def __getitem__(self, idx): + return self._tables[idx] + + @staticmethod + def _format_func(table, f): + return getattr(table, f"to_{f}") + + @property + def n(self): + return len(self) + + def _write_file(self, f=None, **kwargs): + dirname = kwargs.get("dirname") + root = kwargs.get("root") + ext = kwargs.get("ext") + for table in self._tables: + filename = f"{root}-page-{table.page}-table-{table.order}{ext}" + filepath = os.path.join(dirname, filename) + to_format = self._format_func(table, f) + to_format(filepath) + + def _compress_dir(self, **kwargs): + path = kwargs.get("path") + dirname = kwargs.get("dirname") + root = kwargs.get("root") + ext = kwargs.get("ext") + zipname = os.path.join(os.path.dirname(path), root) + ".zip" + with zipfile.ZipFile(zipname, "w", allowZip64=True) as z: + for table in self._tables: + filename = f"{root}-page-{table.page}-table-{table.order}{ext}" + filepath = os.path.join(dirname, filename) + z.write(filepath, os.path.basename(filepath)) + + def export(self, path, f="csv", compress=False): + """Exports the list of tables to specified file format. + + Parameters + ---------- + path : str + Output filepath. + f : str + File format. Can be csv, excel, html, json, markdown or sqlite. + compress : bool + Whether or not to add files to a ZIP archive. + + """ + dirname = os.path.dirname(path) + basename = os.path.basename(path) + root, ext = os.path.splitext(basename) + if compress: + dirname = tempfile.mkdtemp() + + kwargs = {"path": path, "dirname": dirname, "root": root, "ext": ext} + + if f in ["csv", "html", "json", "markdown"]: + self._write_file(f=f, **kwargs) + if compress: + self._compress_dir(**kwargs) + elif f == "excel": + filepath = os.path.join(dirname, basename) + writer = pd.ExcelWriter(filepath) + for table in self._tables: + sheet_name = f"page-{table.page}-table-{table.order}" + table.df.to_excel(writer, sheet_name=sheet_name, encoding="utf-8") + writer.save() + if compress: + zipname = os.path.join(os.path.dirname(path), root) + ".zip" + with zipfile.ZipFile(zipname, "w", allowZip64=True) as z: + z.write(filepath, os.path.basename(filepath)) + elif f == "sqlite": + filepath = os.path.join(dirname, basename) + for table in self._tables: + table.to_sqlite(filepath) + if compress: + zipname = os.path.join(os.path.dirname(path), root) + ".zip" + with zipfile.ZipFile(zipname, "w", allowZip64=True) as z: + z.write(filepath, os.path.basename(filepath)) diff --git a/src/main/python/camelot/handlers.py b/src/main/python/camelot/handlers.py new file mode 100644 index 00000000..5f07e5d0 --- /dev/null +++ b/src/main/python/camelot/handlers.py @@ -0,0 +1,180 @@ +# -*- coding: utf-8 -*- + +import os +import sys + +from pypdf import PdfReader, PdfWriter + +from .core import TableList +from .parsers import Stream, Lattice +from .utils import ( + TemporaryDirectory, + get_page_layout, + get_text_objects, + get_rotation, + is_url, + download_url, +) + + +class PDFHandler(object): + """Handles all operations like temp directory creation, splitting + file into single page PDFs, parsing each PDF and then removing the + temp directory. + + Parameters + ---------- + filepath : str + Filepath or URL of the PDF file. + pages : str, optional (default: '1') + Comma-separated page numbers. + Example: '1,3,4' or '1,4-end' or 'all'. + password : str, optional (default: None) + Password for decryption. + + """ + + def __init__(self, filepath, pages="1", password=None): + if is_url(filepath): + filepath = download_url(filepath) + self.filepath = filepath + if not filepath.lower().endswith(".pdf"): + raise NotImplementedError("File format not supported") + + if password is None: + self.password = "" + else: + self.password = password + if sys.version_info[0] < 3: + self.password = self.password.encode("ascii") + self.pages = self._get_pages(pages) + + def _get_pages(self, pages): + """Converts pages string to list of ints. + + Parameters + ---------- + filepath : str + Filepath or URL of the PDF file. + pages : str, optional (default: '1') + Comma-separated page numbers. + Example: '1,3,4' or '1,4-end' or 'all'. + + Returns + ------- + P : list + List of int page numbers. + + """ + page_numbers = [] + + if pages == "1": + page_numbers.append({"start": 1, "end": 1}) + else: + with open(self.filepath, "rb") as f: + infile = PdfReader(f, strict=False) + + if infile.is_encrypted: + infile.decrypt(self.password) + + if pages == "all": + page_numbers.append({"start": 1, "end": len(infile.pages)}) + else: + for r in pages.split(","): + if "-" in r: + a, b = r.split("-") + if b == "end": + b = len(infile.pages) + page_numbers.append({"start": int(a), "end": int(b)}) + else: + page_numbers.append({"start": int(r), "end": int(r)}) + + P = [] + for p in page_numbers: + P.extend(range(p["start"], p["end"] + 1)) + return sorted(set(P)) + + def _save_page(self, filepath, page, temp): + """Saves specified page from PDF into a temporary directory. + + Parameters + ---------- + filepath : str + Filepath or URL of the PDF file. + page : int + Page number. + temp : str + Tmp directory. + + """ + with open(filepath, "rb") as fileobj: + infile = PdfReader(fileobj, strict=False) + if infile.is_encrypted: + infile.decrypt(self.password) + fpath = os.path.join(temp, f"page-{page}.pdf") + froot, fext = os.path.splitext(fpath) + p = infile.pages[page - 1] + outfile = PdfWriter() + outfile.add_page(p) + with open(fpath, "wb") as f: + outfile.write(f) + layout, dim = get_page_layout(fpath) + # fix rotated PDF + chars = get_text_objects(layout, ltype="char") + horizontal_text = get_text_objects(layout, ltype="horizontal_text") + vertical_text = get_text_objects(layout, ltype="vertical_text") + rotation = get_rotation(chars, horizontal_text, vertical_text) + if rotation != "": + fpath_new = "".join([froot.replace("page", "p"), "_rotated", fext]) + os.rename(fpath, fpath_new) + instream = open(fpath_new, "rb") + infile = PdfReader(instream, strict=False) + if infile.is_encrypted: + infile.decrypt(self.password) + outfile = PdfWriter() + p = infile.pages[0] + if rotation == "anticlockwise": + p.rotate(90) + elif rotation == "clockwise": + p.rotate(-90) + outfile.add_page(p) + with open(fpath, "wb") as f: + outfile.write(f) + instream.close() + + def parse( + self, flavor="lattice", suppress_stdout=False, layout_kwargs={}, **kwargs + ): + """Extracts tables by calling parser.get_tables on all single + page PDFs. + + Parameters + ---------- + flavor : str (default: 'lattice') + The parsing method to use ('lattice' or 'stream'). + Lattice is used by default. + suppress_stdout : str (default: False) + Suppress logs and warnings. + layout_kwargs : dict, optional (default: {}) + A dict of `pdfminer.layout.LAParams `_ kwargs. + kwargs : dict + See camelot.read_pdf kwargs. + + Returns + ------- + tables : camelot.core.TableList + List of tables found in PDF. + + """ + tables = [] + with TemporaryDirectory() as tempdir: + for p in self.pages: + self._save_page(self.filepath, p, tempdir) + pages = [os.path.join(tempdir, f"page-{p}.pdf") for p in self.pages] + parser = Lattice(**kwargs) if flavor == "lattice" else Stream(**kwargs) + for p in pages: + t = parser.extract_tables( + p, suppress_stdout=suppress_stdout, layout_kwargs=layout_kwargs + ) + tables.extend(t) + return TableList(sorted(tables)) diff --git a/src/main/python/camelot/image_processing.py b/src/main/python/camelot/image_processing.py new file mode 100644 index 00000000..08acb23e --- /dev/null +++ b/src/main/python/camelot/image_processing.py @@ -0,0 +1,222 @@ +# -*- coding: utf-8 -*- + +import cv2 +import numpy as np + + +def adaptive_threshold(imagename, process_background=False, blocksize=15, c=-2): + """Thresholds an image using OpenCV's adaptiveThreshold. + + Parameters + ---------- + imagename : string + Path to image file. + process_background : bool, optional (default: False) + Whether or not to process lines that are in background. + blocksize : int, optional (default: 15) + Size of a pixel neighborhood that is used to calculate a + threshold value for the pixel: 3, 5, 7, and so on. + + For more information, refer `OpenCV's adaptiveThreshold `_. + c : int, optional (default: -2) + Constant subtracted from the mean or weighted mean. + Normally, it is positive but may be zero or negative as well. + + For more information, refer `OpenCV's adaptiveThreshold `_. + + Returns + ------- + img : object + numpy.ndarray representing the original image. + threshold : object + numpy.ndarray representing the thresholded image. + + """ + img = cv2.imread(imagename) + gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY) + + if process_background: + threshold = cv2.adaptiveThreshold( + gray, 255, cv2.ADAPTIVE_THRESH_GAUSSIAN_C, cv2.THRESH_BINARY, blocksize, c + ) + else: + threshold = cv2.adaptiveThreshold( + np.invert(gray), + 255, + cv2.ADAPTIVE_THRESH_GAUSSIAN_C, + cv2.THRESH_BINARY, + blocksize, + c, + ) + return img, threshold + + +def find_lines( + threshold, regions=None, direction="horizontal", line_scale=15, iterations=0 +): + """Finds horizontal and vertical lines by applying morphological + transformations on an image. + + Parameters + ---------- + threshold : object + numpy.ndarray representing the thresholded image. + regions : list, optional (default: None) + List of page regions that may contain tables of the form x1,y1,x2,y2 + where (x1, y1) -> left-top and (x2, y2) -> right-bottom + in image coordinate space. + direction : string, optional (default: 'horizontal') + Specifies whether to find vertical or horizontal lines. + line_scale : int, optional (default: 15) + Factor by which the page dimensions will be divided to get + smallest length of lines that should be detected. + + The larger this value, smaller the detected lines. Making it + too large will lead to text being detected as lines. + iterations : int, optional (default: 0) + Number of times for erosion/dilation is applied. + + For more information, refer `OpenCV's dilate `_. + + Returns + ------- + dmask : object + numpy.ndarray representing pixels where vertical/horizontal + lines lie. + lines : list + List of tuples representing vertical/horizontal lines with + coordinates relative to a left-top origin in + image coordinate space. + + """ + lines = [] + + if direction == "vertical": + size = threshold.shape[0] // line_scale + el = cv2.getStructuringElement(cv2.MORPH_RECT, (1, size)) + elif direction == "horizontal": + size = threshold.shape[1] // line_scale + el = cv2.getStructuringElement(cv2.MORPH_RECT, (size, 1)) + elif direction is None: + raise ValueError("Specify direction as either 'vertical' or 'horizontal'") + + if regions is not None: + region_mask = np.zeros(threshold.shape) + for region in regions: + x, y, w, h = region + region_mask[y : y + h, x : x + w] = 1 + threshold = np.multiply(threshold, region_mask) + + threshold = cv2.erode(threshold, el) + threshold = cv2.dilate(threshold, el) + dmask = cv2.dilate(threshold, el, iterations=iterations) + + try: + _, contours, _ = cv2.findContours( + threshold.astype(np.uint8), cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE + ) + except ValueError: + # for opencv backward compatibility + contours, _ = cv2.findContours( + threshold.astype(np.uint8), cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE + ) + + for c in contours: + x, y, w, h = cv2.boundingRect(c) + x1, x2 = x, x + w + y1, y2 = y, y + h + if direction == "vertical": + lines.append(((x1 + x2) // 2, y2, (x1 + x2) // 2, y1)) + elif direction == "horizontal": + lines.append((x1, (y1 + y2) // 2, x2, (y1 + y2) // 2)) + + return dmask, lines + + +def find_contours(vertical, horizontal): + """Finds table boundaries using OpenCV's findContours. + + Parameters + ---------- + vertical : object + numpy.ndarray representing pixels where vertical lines lie. + horizontal : object + numpy.ndarray representing pixels where horizontal lines lie. + + Returns + ------- + cont : list + List of tuples representing table boundaries. Each tuple is of + the form (x, y, w, h) where (x, y) -> left-top, w -> width and + h -> height in image coordinate space. + + """ + mask = vertical + horizontal + + try: + __, contours, __ = cv2.findContours( + mask.astype(np.uint8), cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE + ) + except ValueError: + # for opencv backward compatibility + contours, __ = cv2.findContours( + mask.astype(np.uint8), cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE + ) + # sort in reverse based on contour area and use first 10 contours + contours = sorted(contours, key=cv2.contourArea, reverse=True)[:10] + + cont = [] + for c in contours: + c_poly = cv2.approxPolyDP(c, 3, True) + x, y, w, h = cv2.boundingRect(c_poly) + cont.append((x, y, w, h)) + return cont + + +def find_joints(contours, vertical, horizontal): + """Finds joints/intersections present inside each table boundary. + + Parameters + ---------- + contours : list + List of tuples representing table boundaries. Each tuple is of + the form (x, y, w, h) where (x, y) -> left-top, w -> width and + h -> height in image coordinate space. + vertical : object + numpy.ndarray representing pixels where vertical lines lie. + horizontal : object + numpy.ndarray representing pixels where horizontal lines lie. + + Returns + ------- + tables : dict + Dict with table boundaries as keys and list of intersections + in that boundary as their value. + Keys are of the form (x1, y1, x2, y2) where (x1, y1) -> lb + and (x2, y2) -> rt in image coordinate space. + + """ + joints = np.multiply(vertical, horizontal) + tables = {} + for c in contours: + x, y, w, h = c + roi = joints[y : y + h, x : x + w] + try: + __, jc, __ = cv2.findContours( + roi.astype(np.uint8), cv2.RETR_CCOMP, cv2.CHAIN_APPROX_SIMPLE + ) + except ValueError: + # for opencv backward compatibility + jc, __ = cv2.findContours( + roi.astype(np.uint8), cv2.RETR_CCOMP, cv2.CHAIN_APPROX_SIMPLE + ) + if len(jc) <= 4: # remove contours with less than 4 joints + continue + joint_coords = [] + for j in jc: + jx, jy, jw, jh = cv2.boundingRect(j) + c1, c2 = x + (2 * jx + jw) // 2, y + (2 * jy + jh) // 2 + joint_coords.append((c1, c2)) + tables[(x, y + h, x + w, y)] = joint_coords + + return tables diff --git a/src/main/python/camelot/io.py b/src/main/python/camelot/io.py new file mode 100644 index 00000000..a27a7c66 --- /dev/null +++ b/src/main/python/camelot/io.py @@ -0,0 +1,119 @@ +# -*- coding: utf-8 -*- + +import warnings + +from .handlers import PDFHandler +from .utils import validate_input, remove_extra + + +def read_pdf( + filepath, + pages="1", + password=None, + flavor="lattice", + suppress_stdout=False, + layout_kwargs={}, + **kwargs +): + """Read PDF and return extracted tables. + + Note: kwargs annotated with ^ can only be used with flavor='stream' + and kwargs annotated with * can only be used with flavor='lattice'. + + Parameters + ---------- + filepath : str + Filepath or URL of the PDF file. + pages : str, optional (default: '1') + Comma-separated page numbers. + Example: '1,3,4' or '1,4-end' or 'all'. + password : str, optional (default: None) + Password for decryption. + flavor : str (default: 'lattice') + The parsing method to use ('lattice' or 'stream'). + Lattice is used by default. + suppress_stdout : bool, optional (default: True) + Print all logs and warnings. + layout_kwargs : dict, optional (default: {}) + A dict of `pdfminer.layout.LAParams `_ kwargs. + table_areas : list, optional (default: None) + List of table area strings of the form x1,y1,x2,y2 + where (x1, y1) -> left-top and (x2, y2) -> right-bottom + in PDF coordinate space. + columns^ : list, optional (default: None) + List of column x-coordinates strings where the coordinates + are comma-separated. + split_text : bool, optional (default: False) + Split text that spans across multiple cells. + flag_size : bool, optional (default: False) + Flag text based on font size. Useful to detect + super/subscripts. Adds around flagged text. + strip_text : str, optional (default: '') + Characters that should be stripped from a string before + assigning it to a cell. + row_tol^ : int, optional (default: 2) + Tolerance parameter used to combine text vertically, + to generate rows. + column_tol^ : int, optional (default: 0) + Tolerance parameter used to combine text horizontally, + to generate columns. + process_background* : bool, optional (default: False) + Process background lines. + line_scale* : int, optional (default: 15) + Line size scaling factor. The larger the value the smaller + the detected lines. Making it very large will lead to text + being detected as lines. + copy_text* : list, optional (default: None) + {'h', 'v'} + Direction in which text in a spanning cell will be copied + over. + shift_text* : list, optional (default: ['l', 't']) + {'l', 'r', 't', 'b'} + Direction in which text in a spanning cell will flow. + line_tol* : int, optional (default: 2) + Tolerance parameter used to merge close vertical and horizontal + lines. + joint_tol* : int, optional (default: 2) + Tolerance parameter used to decide whether the detected lines + and points lie close to each other. + threshold_blocksize* : int, optional (default: 15) + Size of a pixel neighborhood that is used to calculate a + threshold value for the pixel: 3, 5, 7, and so on. + + For more information, refer `OpenCV's adaptiveThreshold `_. + threshold_constant* : int, optional (default: -2) + Constant subtracted from the mean or weighted mean. + Normally, it is positive but may be zero or negative as well. + + For more information, refer `OpenCV's adaptiveThreshold `_. + iterations* : int, optional (default: 0) + Number of times for erosion/dilation is applied. + + For more information, refer `OpenCV's dilate `_. + resolution* : int, optional (default: 300) + Resolution used for PDF to PNG conversion. + + Returns + ------- + tables : camelot.core.TableList + + """ + if flavor not in ["lattice", "stream"]: + raise NotImplementedError( + "Unknown flavor specified." " Use either 'lattice' or 'stream'" + ) + + with warnings.catch_warnings(): + if suppress_stdout: + warnings.simplefilter("ignore") + + validate_input(kwargs, flavor=flavor) + p = PDFHandler(filepath, pages=pages, password=password) + kwargs = remove_extra(kwargs, flavor=flavor) + tables = p.parse( + flavor=flavor, + suppress_stdout=suppress_stdout, + layout_kwargs=layout_kwargs, + **kwargs + ) + return tables diff --git a/src/main/python/camelot/parsers/__init__.py b/src/main/python/camelot/parsers/__init__.py new file mode 100644 index 00000000..5cc66051 --- /dev/null +++ b/src/main/python/camelot/parsers/__init__.py @@ -0,0 +1,4 @@ +# -*- coding: utf-8 -*- + +from .stream import Stream +from .lattice import Lattice diff --git a/src/main/python/camelot/parsers/base.py b/src/main/python/camelot/parsers/base.py new file mode 100644 index 00000000..aeba056f --- /dev/null +++ b/src/main/python/camelot/parsers/base.py @@ -0,0 +1,20 @@ +# -*- coding: utf-8 -*- + +import os + +from ..utils import get_page_layout, get_text_objects + + +class BaseParser(object): + """Defines a base parser.""" + + def _generate_layout(self, filename, layout_kwargs): + self.filename = filename + self.layout_kwargs = layout_kwargs + self.layout, self.dimensions = get_page_layout(filename, **layout_kwargs) + self.images = get_text_objects(self.layout, ltype="image") + self.horizontal_text = get_text_objects(self.layout, ltype="horizontal_text") + self.vertical_text = get_text_objects(self.layout, ltype="vertical_text") + self.pdf_width, self.pdf_height = self.dimensions + self.rootname, __ = os.path.splitext(self.filename) + self.imagename = "".join([self.rootname, ".png"]) diff --git a/src/main/python/camelot/parsers/lattice.py b/src/main/python/camelot/parsers/lattice.py new file mode 100644 index 00000000..a1752272 --- /dev/null +++ b/src/main/python/camelot/parsers/lattice.py @@ -0,0 +1,435 @@ +# -*- coding: utf-8 -*- + +import os +import sys +import copy +import locale +import logging +import warnings + +import numpy as np +import pandas as pd + +from .base import BaseParser +from ..core import Table +from ..utils import ( + scale_image, + scale_pdf, + segments_in_bbox, + text_in_bbox, + merge_close_lines, + get_table_index, + compute_accuracy, + compute_whitespace, +) +from ..image_processing import ( + adaptive_threshold, + find_lines, + find_contours, + find_joints, +) +from ..backends.image_conversion import BACKENDS + + +logger = logging.getLogger("camelot") + + +class Lattice(BaseParser): + """Lattice method of parsing looks for lines between text + to parse the table. + + Parameters + ---------- + table_regions : list, optional (default: None) + List of page regions that may contain tables of the form x1,y1,x2,y2 + where (x1, y1) -> left-top and (x2, y2) -> right-bottom + in PDF coordinate space. + table_areas : list, optional (default: None) + List of table area strings of the form x1,y1,x2,y2 + where (x1, y1) -> left-top and (x2, y2) -> right-bottom + in PDF coordinate space. + process_background : bool, optional (default: False) + Process background lines. + line_scale : int, optional (default: 15) + Line size scaling factor. The larger the value the smaller + the detected lines. Making it very large will lead to text + being detected as lines. + copy_text : list, optional (default: None) + {'h', 'v'} + Direction in which text in a spanning cell will be copied + over. + shift_text : list, optional (default: ['l', 't']) + {'l', 'r', 't', 'b'} + Direction in which text in a spanning cell will flow. + split_text : bool, optional (default: False) + Split text that spans across multiple cells. + flag_size : bool, optional (default: False) + Flag text based on font size. Useful to detect + super/subscripts. Adds around flagged text. + strip_text : str, optional (default: '') + Characters that should be stripped from a string before + assigning it to a cell. + line_tol : int, optional (default: 2) + Tolerance parameter used to merge close vertical and horizontal + lines. + joint_tol : int, optional (default: 2) + Tolerance parameter used to decide whether the detected lines + and points lie close to each other. + threshold_blocksize : int, optional (default: 15) + Size of a pixel neighborhood that is used to calculate a + threshold value for the pixel: 3, 5, 7, and so on. + + For more information, refer `OpenCV's adaptiveThreshold `_. + threshold_constant : int, optional (default: -2) + Constant subtracted from the mean or weighted mean. + Normally, it is positive but may be zero or negative as well. + + For more information, refer `OpenCV's adaptiveThreshold `_. + iterations : int, optional (default: 0) + Number of times for erosion/dilation is applied. + + For more information, refer `OpenCV's dilate `_. + resolution : int, optional (default: 300) + Resolution used for PDF to PNG conversion. + + """ + + def __init__( + self, + table_regions=None, + table_areas=None, + process_background=False, + line_scale=15, + copy_text=None, + shift_text=["l", "t"], + split_text=False, + flag_size=False, + strip_text="", + line_tol=2, + joint_tol=2, + threshold_blocksize=15, + threshold_constant=-2, + iterations=0, + resolution=300, + backend="ghostscript", + **kwargs, + ): + self.table_regions = table_regions + self.table_areas = table_areas + self.process_background = process_background + self.line_scale = line_scale + self.copy_text = copy_text + self.shift_text = shift_text + self.split_text = split_text + self.flag_size = flag_size + self.strip_text = strip_text + self.line_tol = line_tol + self.joint_tol = joint_tol + self.threshold_blocksize = threshold_blocksize + self.threshold_constant = threshold_constant + self.iterations = iterations + self.resolution = resolution + self.backend = Lattice._get_backend(backend) + + @staticmethod + def _get_backend(backend): + def implements_convert(): + methods = [ + method for method in dir(backend) if method.startswith("__") is False + ] + return "convert" in methods + + if isinstance(backend, str): + if backend not in BACKENDS.keys(): + raise NotImplementedError( + f"Unknown backend '{backend}' specified. Please use either 'poppler' or 'ghostscript'." + ) + + if backend == "ghostscript": + warnings.warn( + "'ghostscript' will be replaced by 'poppler' as the default image conversion" + " backend in v0.12.0. You can try out 'poppler' with backend='poppler'.", + DeprecationWarning, + ) + + return BACKENDS[backend]() + else: + if not implements_convert(): + raise NotImplementedError( + f"'{backend}' must implement a 'convert' method" + ) + + return backend + + @staticmethod + def _reduce_index(t, idx, shift_text): + """Reduces index of a text object if it lies within a spanning + cell. + + Parameters + ---------- + table : camelot.core.Table + idx : list + List of tuples of the form (r_idx, c_idx, text). + shift_text : list + {'l', 'r', 't', 'b'} + Select one or more strings from above and pass them as a + list to specify where the text in a spanning cell should + flow. + + Returns + ------- + indices : list + List of tuples of the form (r_idx, c_idx, text) where + r_idx and c_idx are new row and column indices for text. + + """ + indices = [] + for r_idx, c_idx, text in idx: + for d in shift_text: + if d == "l": + if t.cells[r_idx][c_idx].hspan: + while not t.cells[r_idx][c_idx].left: + c_idx -= 1 + if d == "r": + if t.cells[r_idx][c_idx].hspan: + while not t.cells[r_idx][c_idx].right: + c_idx += 1 + if d == "t": + if t.cells[r_idx][c_idx].vspan: + while not t.cells[r_idx][c_idx].top: + r_idx -= 1 + if d == "b": + if t.cells[r_idx][c_idx].vspan: + while not t.cells[r_idx][c_idx].bottom: + r_idx += 1 + indices.append((r_idx, c_idx, text)) + return indices + + @staticmethod + def _copy_spanning_text(t, copy_text=None): + """Copies over text in empty spanning cells. + + Parameters + ---------- + t : camelot.core.Table + copy_text : list, optional (default: None) + {'h', 'v'} + Select one or more strings from above and pass them as a list + to specify the direction in which text should be copied over + when a cell spans multiple rows or columns. + + Returns + ------- + t : camelot.core.Table + + """ + for f in copy_text: + if f == "h": + for i in range(len(t.cells)): + for j in range(len(t.cells[i])): + if t.cells[i][j].text.strip() == "": + if t.cells[i][j].hspan and not t.cells[i][j].left: + t.cells[i][j].text = t.cells[i][j - 1].text + elif f == "v": + for i in range(len(t.cells)): + for j in range(len(t.cells[i])): + if t.cells[i][j].text.strip() == "": + if t.cells[i][j].vspan and not t.cells[i][j].top: + t.cells[i][j].text = t.cells[i - 1][j].text + return t + + def _generate_table_bbox(self): + def scale_areas(areas): + scaled_areas = [] + for area in areas: + x1, y1, x2, y2 = area.split(",") + x1 = float(x1) + y1 = float(y1) + x2 = float(x2) + y2 = float(y2) + x1, y1, x2, y2 = scale_pdf((x1, y1, x2, y2), image_scalers) + scaled_areas.append((x1, y1, abs(x2 - x1), abs(y2 - y1))) + return scaled_areas + + self.image, self.threshold = adaptive_threshold( + self.imagename, + process_background=self.process_background, + blocksize=self.threshold_blocksize, + c=self.threshold_constant, + ) + + image_width = self.image.shape[1] + image_height = self.image.shape[0] + image_width_scaler = image_width / float(self.pdf_width) + image_height_scaler = image_height / float(self.pdf_height) + pdf_width_scaler = self.pdf_width / float(image_width) + pdf_height_scaler = self.pdf_height / float(image_height) + image_scalers = (image_width_scaler, image_height_scaler, self.pdf_height) + pdf_scalers = (pdf_width_scaler, pdf_height_scaler, image_height) + + if self.table_areas is None: + regions = None + if self.table_regions is not None: + regions = scale_areas(self.table_regions) + + vertical_mask, vertical_segments = find_lines( + self.threshold, + regions=regions, + direction="vertical", + line_scale=self.line_scale, + iterations=self.iterations, + ) + horizontal_mask, horizontal_segments = find_lines( + self.threshold, + regions=regions, + direction="horizontal", + line_scale=self.line_scale, + iterations=self.iterations, + ) + + contours = find_contours(vertical_mask, horizontal_mask) + table_bbox = find_joints(contours, vertical_mask, horizontal_mask) + else: + vertical_mask, vertical_segments = find_lines( + self.threshold, + direction="vertical", + line_scale=self.line_scale, + iterations=self.iterations, + ) + horizontal_mask, horizontal_segments = find_lines( + self.threshold, + direction="horizontal", + line_scale=self.line_scale, + iterations=self.iterations, + ) + + areas = scale_areas(self.table_areas) + table_bbox = find_joints(areas, vertical_mask, horizontal_mask) + + self.table_bbox_unscaled = copy.deepcopy(table_bbox) + + self.table_bbox, self.vertical_segments, self.horizontal_segments = scale_image( + table_bbox, vertical_segments, horizontal_segments, pdf_scalers + ) + + def _generate_columns_and_rows(self, table_idx, tk): + # select elements which lie within table_bbox + t_bbox = {} + v_s, h_s = segments_in_bbox( + tk, self.vertical_segments, self.horizontal_segments + ) + t_bbox["horizontal"] = text_in_bbox(tk, self.horizontal_text) + t_bbox["vertical"] = text_in_bbox(tk, self.vertical_text) + + t_bbox["horizontal"].sort(key=lambda x: (-x.y0, x.x0)) + t_bbox["vertical"].sort(key=lambda x: (x.x0, -x.y0)) + + self.t_bbox = t_bbox + + cols, rows = zip(*self.table_bbox[tk]) + cols, rows = list(cols), list(rows) + cols.extend([tk[0], tk[2]]) + rows.extend([tk[1], tk[3]]) + # sort horizontal and vertical segments + cols = merge_close_lines(sorted(cols), line_tol=self.line_tol) + rows = merge_close_lines(sorted(rows, reverse=True), line_tol=self.line_tol) + # make grid using x and y coord of shortlisted rows and cols + cols = [(cols[i], cols[i + 1]) for i in range(0, len(cols) - 1)] + rows = [(rows[i], rows[i + 1]) for i in range(0, len(rows) - 1)] + + return cols, rows, v_s, h_s + + def _generate_table(self, table_idx, cols, rows, **kwargs): + v_s = kwargs.get("v_s") + h_s = kwargs.get("h_s") + if v_s is None or h_s is None: + raise ValueError("No segments found on {}".format(self.rootname)) + + table = Table(cols, rows) + # set table edges to True using ver+hor lines + table = table.set_edges(v_s, h_s, joint_tol=self.joint_tol) + # set table border edges to True + table = table.set_border() + # set spanning cells to True + table = table.set_span() + + pos_errors = [] + # TODO: have a single list in place of two directional ones? + # sorted on x-coordinate based on reading order i.e. LTR or RTL + for direction in ["vertical", "horizontal"]: + for t in self.t_bbox[direction]: + indices, error = get_table_index( + table, + t, + direction, + split_text=self.split_text, + flag_size=self.flag_size, + strip_text=self.strip_text, + ) + if indices[:2] != (-1, -1): + pos_errors.append(error) + indices = Lattice._reduce_index( + table, indices, shift_text=self.shift_text + ) + for r_idx, c_idx, text in indices: + table.cells[r_idx][c_idx].text = text + accuracy = compute_accuracy([[100, pos_errors]]) + + if self.copy_text is not None: + table = Lattice._copy_spanning_text(table, copy_text=self.copy_text) + + data = table.data + table.df = pd.DataFrame(data) + table.shape = table.df.shape + + whitespace = compute_whitespace(data) + table.flavor = "lattice" + table.accuracy = accuracy + table.whitespace = whitespace + table.order = table_idx + 1 + table.page = int(os.path.basename(self.rootname).replace("page-", "")) + + # for plotting + _text = [] + _text.extend([(t.x0, t.y0, t.x1, t.y1) for t in self.horizontal_text]) + _text.extend([(t.x0, t.y0, t.x1, t.y1) for t in self.vertical_text]) + table._text = _text + table._image = (self.image, self.table_bbox_unscaled) + table._segments = (self.vertical_segments, self.horizontal_segments) + table._textedges = None + + return table + + def extract_tables(self, filename, suppress_stdout=False, layout_kwargs={}): + self._generate_layout(filename, layout_kwargs) + if not suppress_stdout: + logger.info("Processing {}".format(os.path.basename(self.rootname))) + + if not self.horizontal_text: + if self.images: + warnings.warn( + "{} is image-based, camelot only works on" + " text-based pages.".format(os.path.basename(self.rootname)) + ) + else: + warnings.warn( + "No tables found on {}".format(os.path.basename(self.rootname)) + ) + return [] + + self.backend.convert(self.filename, self.imagename) + + self._generate_table_bbox() + + _tables = [] + # sort tables based on y-coord + for table_idx, tk in enumerate( + sorted(self.table_bbox.keys(), key=lambda x: x[1], reverse=True) + ): + cols, rows, v_s, h_s = self._generate_columns_and_rows(table_idx, tk) + table = self._generate_table(table_idx, cols, rows, v_s=v_s, h_s=h_s) + table._bbox = tk + _tables.append(table) + + return _tables diff --git a/src/main/python/camelot/parsers/stream.py b/src/main/python/camelot/parsers/stream.py new file mode 100644 index 00000000..c7b21daf --- /dev/null +++ b/src/main/python/camelot/parsers/stream.py @@ -0,0 +1,468 @@ +# -*- coding: utf-8 -*- + +import os +import logging +import warnings + +import numpy as np +import pandas as pd + +from .base import BaseParser +from ..core import TextEdges, Table +from ..utils import text_in_bbox, get_table_index, compute_accuracy, compute_whitespace + + +logger = logging.getLogger("camelot") + + +class Stream(BaseParser): + """Stream method of parsing looks for spaces between text + to parse the table. + + If you want to specify columns when specifying multiple table + areas, make sure that the length of both lists are equal. + + Parameters + ---------- + table_regions : list, optional (default: None) + List of page regions that may contain tables of the form x1,y1,x2,y2 + where (x1, y1) -> left-top and (x2, y2) -> right-bottom + in PDF coordinate space. + table_areas : list, optional (default: None) + List of table area strings of the form x1,y1,x2,y2 + where (x1, y1) -> left-top and (x2, y2) -> right-bottom + in PDF coordinate space. + columns : list, optional (default: None) + List of column x-coordinates strings where the coordinates + are comma-separated. + split_text : bool, optional (default: False) + Split text that spans across multiple cells. + flag_size : bool, optional (default: False) + Flag text based on font size. Useful to detect + super/subscripts. Adds around flagged text. + strip_text : str, optional (default: '') + Characters that should be stripped from a string before + assigning it to a cell. + edge_tol : int, optional (default: 50) + Tolerance parameter for extending textedges vertically. + row_tol : int, optional (default: 2) + Tolerance parameter used to combine text vertically, + to generate rows. + column_tol : int, optional (default: 0) + Tolerance parameter used to combine text horizontally, + to generate columns. + + """ + + def __init__( + self, + table_regions=None, + table_areas=None, + columns=None, + split_text=False, + flag_size=False, + strip_text="", + edge_tol=50, + row_tol=2, + column_tol=0, + **kwargs, + ): + self.table_regions = table_regions + self.table_areas = table_areas + self.columns = columns + self._validate_columns() + self.split_text = split_text + self.flag_size = flag_size + self.strip_text = strip_text + self.edge_tol = edge_tol + self.row_tol = row_tol + self.column_tol = column_tol + + @staticmethod + def _text_bbox(t_bbox): + """Returns bounding box for the text present on a page. + + Parameters + ---------- + t_bbox : dict + Dict with two keys 'horizontal' and 'vertical' with lists of + LTTextLineHorizontals and LTTextLineVerticals respectively. + + Returns + ------- + text_bbox : tuple + Tuple (x0, y0, x1, y1) in pdf coordinate space. + + """ + xmin = min([t.x0 for direction in t_bbox for t in t_bbox[direction]]) + ymin = min([t.y0 for direction in t_bbox for t in t_bbox[direction]]) + xmax = max([t.x1 for direction in t_bbox for t in t_bbox[direction]]) + ymax = max([t.y1 for direction in t_bbox for t in t_bbox[direction]]) + text_bbox = (xmin, ymin, xmax, ymax) + return text_bbox + + @staticmethod + def _group_rows(text, row_tol=2): + """Groups PDFMiner text objects into rows vertically + within a tolerance. + + Parameters + ---------- + text : list + List of PDFMiner text objects. + row_tol : int, optional (default: 2) + + Returns + ------- + rows : list + Two-dimensional list of text objects grouped into rows. + + """ + row_y = 0 + rows = [] + temp = [] + + for t in text: + # is checking for upright necessary? + # if t.get_text().strip() and all([obj.upright for obj in t._objs if + # type(obj) is LTChar]): + if t.get_text().strip(): + if not np.isclose(row_y, t.y0, atol=row_tol): + rows.append(sorted(temp, key=lambda t: t.x0)) + temp = [] + row_y = t.y0 + temp.append(t) + + rows.append(sorted(temp, key=lambda t: t.x0)) + if len(rows) > 1: + __ = rows.pop(0) # TODO: hacky + return rows + + @staticmethod + def _merge_columns(l, column_tol=0): + """Merges column boundaries horizontally if they overlap + or lie within a tolerance. + + Parameters + ---------- + l : list + List of column x-coordinate tuples. + column_tol : int, optional (default: 0) + + Returns + ------- + merged : list + List of merged column x-coordinate tuples. + + """ + merged = [] + for higher in l: + if not merged: + merged.append(higher) + else: + lower = merged[-1] + if column_tol >= 0: + if higher[0] <= lower[1] or np.isclose( + higher[0], lower[1], atol=column_tol + ): + upper_bound = max(lower[1], higher[1]) + lower_bound = min(lower[0], higher[0]) + merged[-1] = (lower_bound, upper_bound) + else: + merged.append(higher) + elif column_tol < 0: + if higher[0] <= lower[1]: + if np.isclose(higher[0], lower[1], atol=abs(column_tol)): + merged.append(higher) + else: + upper_bound = max(lower[1], higher[1]) + lower_bound = min(lower[0], higher[0]) + merged[-1] = (lower_bound, upper_bound) + else: + merged.append(higher) + return merged + + @staticmethod + def _join_rows(rows_grouped, text_y_max, text_y_min): + """Makes row coordinates continuous. + + Parameters + ---------- + rows_grouped : list + Two-dimensional list of text objects grouped into rows. + text_y_max : int + text_y_min : int + + Returns + ------- + rows : list + List of continuous row y-coordinate tuples. + + """ + row_mids = [ + sum([(t.y0 + t.y1) / 2 for t in r]) / len(r) if len(r) > 0 else 0 + for r in rows_grouped + ] + rows = [(row_mids[i] + row_mids[i - 1]) / 2 for i in range(1, len(row_mids))] + rows.insert(0, text_y_max) + rows.append(text_y_min) + rows = [(rows[i], rows[i + 1]) for i in range(0, len(rows) - 1)] + return rows + + @staticmethod + def _add_columns(cols, text, row_tol): + """Adds columns to existing list by taking into account + the text that lies outside the current column x-coordinates. + + Parameters + ---------- + cols : list + List of column x-coordinate tuples. + text : list + List of PDFMiner text objects. + ytol : int + + Returns + ------- + cols : list + Updated list of column x-coordinate tuples. + + """ + if text: + text = Stream._group_rows(text, row_tol=row_tol) + elements = [len(r) for r in text] + new_cols = [ + (t.x0, t.x1) for r in text if len(r) == max(elements) for t in r + ] + cols.extend(Stream._merge_columns(sorted(new_cols))) + return cols + + @staticmethod + def _join_columns(cols, text_x_min, text_x_max): + """Makes column coordinates continuous. + + Parameters + ---------- + cols : list + List of column x-coordinate tuples. + text_x_min : int + text_y_max : int + + Returns + ------- + cols : list + Updated list of column x-coordinate tuples. + + """ + cols = sorted(cols) + cols = [(cols[i][0] + cols[i - 1][1]) / 2 for i in range(1, len(cols))] + cols.insert(0, text_x_min) + cols.append(text_x_max) + cols = [(cols[i], cols[i + 1]) for i in range(0, len(cols) - 1)] + return cols + + def _validate_columns(self): + if self.table_areas is not None and self.columns is not None: + if len(self.table_areas) != len(self.columns): + raise ValueError("Length of table_areas and columns" " should be equal") + + def _nurminen_table_detection(self, textlines): + """A general implementation of the table detection algorithm + described by Anssi Nurminen's master's thesis. + Link: https://dspace.cc.tut.fi/dpub/bitstream/handle/123456789/21520/Nurminen.pdf?sequence=3 + + Assumes that tables are situated relatively far apart + vertically. + """ + # TODO: add support for arabic text #141 + # sort textlines in reading order + textlines.sort(key=lambda x: (-x.y0, x.x0)) + textedges = TextEdges(edge_tol=self.edge_tol) + # generate left, middle and right textedges + textedges.generate(textlines) + # select relevant edges + relevant_textedges = textedges.get_relevant() + self.textedges.extend(relevant_textedges) + # guess table areas using textlines and relevant edges + table_bbox = textedges.get_table_areas(textlines, relevant_textedges) + # treat whole page as table area if no table areas found + if not len(table_bbox): + table_bbox = {(0, 0, self.pdf_width, self.pdf_height): None} + + return table_bbox + + def _generate_table_bbox(self): + self.textedges = [] + if self.table_areas is None: + hor_text = self.horizontal_text + if self.table_regions is not None: + # filter horizontal text + hor_text = [] + for region in self.table_regions: + x1, y1, x2, y2 = region.split(",") + x1 = float(x1) + y1 = float(y1) + x2 = float(x2) + y2 = float(y2) + region_text = text_in_bbox((x1, y2, x2, y1), self.horizontal_text) + hor_text.extend(region_text) + # find tables based on nurminen's detection algorithm + table_bbox = self._nurminen_table_detection(hor_text) + else: + table_bbox = {} + for area in self.table_areas: + x1, y1, x2, y2 = area.split(",") + x1 = float(x1) + y1 = float(y1) + x2 = float(x2) + y2 = float(y2) + table_bbox[(x1, y2, x2, y1)] = None + self.table_bbox = table_bbox + + def _generate_columns_and_rows(self, table_idx, tk): + # select elements which lie within table_bbox + t_bbox = {} + t_bbox["horizontal"] = text_in_bbox(tk, self.horizontal_text) + t_bbox["vertical"] = text_in_bbox(tk, self.vertical_text) + + t_bbox["horizontal"].sort(key=lambda x: (-x.y0, x.x0)) + t_bbox["vertical"].sort(key=lambda x: (x.x0, -x.y0)) + + self.t_bbox = t_bbox + + text_x_min, text_y_min, text_x_max, text_y_max = self._text_bbox(self.t_bbox) + rows_grouped = self._group_rows(self.t_bbox["horizontal"], row_tol=self.row_tol) + rows = self._join_rows(rows_grouped, text_y_max, text_y_min) + elements = [len(r) for r in rows_grouped] + + if self.columns is not None and self.columns[table_idx] != "": + # user has to input boundary columns too + # take (0, pdf_width) by default + # similar to else condition + # len can't be 1 + cols = self.columns[table_idx].split(",") + cols = [float(c) for c in cols] + cols.insert(0, text_x_min) + cols.append(text_x_max) + cols = [(cols[i], cols[i + 1]) for i in range(0, len(cols) - 1)] + else: + # calculate mode of the list of number of elements in + # each row to guess the number of columns + if not len(elements): + cols = [(text_x_min, text_x_max)] + else: + ncols = max(set(elements), key=elements.count) + if ncols == 1: + # if mode is 1, the page usually contains not tables + # but there can be cases where the list can be skewed, + # try to remove all 1s from list in this case and + # see if the list contains elements, if yes, then use + # the mode after removing 1s + elements = list(filter(lambda x: x != 1, elements)) + if len(elements): + ncols = max(set(elements), key=elements.count) + else: + warnings.warn(f"No tables found in table area {table_idx + 1}") + cols = [ + (t.x0, t.x1) for r in rows_grouped if len(r) == ncols for t in r + ] + cols = self._merge_columns(sorted(cols), column_tol=self.column_tol) + inner_text = [] + for i in range(1, len(cols)): + left = cols[i - 1][1] + right = cols[i][0] + inner_text.extend( + [ + t + for direction in self.t_bbox + for t in self.t_bbox[direction] + if t.x0 > left and t.x1 < right + ] + ) + outer_text = [ + t + for direction in self.t_bbox + for t in self.t_bbox[direction] + if t.x0 > cols[-1][1] or t.x1 < cols[0][0] + ] + inner_text.extend(outer_text) + cols = self._add_columns(cols, inner_text, self.row_tol) + cols = self._join_columns(cols, text_x_min, text_x_max) + + return cols, rows + + def _generate_table(self, table_idx, cols, rows, **kwargs): + table = Table(cols, rows) + table = table.set_all_edges() + + pos_errors = [] + # TODO: have a single list in place of two directional ones? + # sorted on x-coordinate based on reading order i.e. LTR or RTL + for direction in ["vertical", "horizontal"]: + for t in self.t_bbox[direction]: + indices, error = get_table_index( + table, + t, + direction, + split_text=self.split_text, + flag_size=self.flag_size, + strip_text=self.strip_text, + ) + if indices[:2] != (-1, -1): + pos_errors.append(error) + for r_idx, c_idx, text in indices: + table.cells[r_idx][c_idx].text = text + accuracy = compute_accuracy([[100, pos_errors]]) + + data = table.data + table.df = pd.DataFrame(data) + table.shape = table.df.shape + + whitespace = compute_whitespace(data) + table.flavor = "stream" + table.accuracy = accuracy + table.whitespace = whitespace + table.order = table_idx + 1 + table.page = int(os.path.basename(self.rootname).replace("page-", "")) + + # for plotting + _text = [] + _text.extend([(t.x0, t.y0, t.x1, t.y1) for t in self.horizontal_text]) + _text.extend([(t.x0, t.y0, t.x1, t.y1) for t in self.vertical_text]) + table._text = _text + table._image = None + table._segments = None + table._textedges = self.textedges + + return table + + def extract_tables(self, filename, suppress_stdout=False, layout_kwargs={}): + self._generate_layout(filename, layout_kwargs) + base_filename = os.path.basename(self.rootname) + + if not suppress_stdout: + logger.info(f"Processing {base_filename}") + + if not self.horizontal_text: + if self.images: + warnings.warn( + f"{base_filename} is image-based, camelot only works on" + " text-based pages." + ) + else: + warnings.warn(f"No tables found on {base_filename}") + return [] + + self._generate_table_bbox() + + _tables = [] + # sort tables based on y-coord + for table_idx, tk in enumerate( + sorted(self.table_bbox.keys(), key=lambda x: x[1], reverse=True) + ): + cols, rows = self._generate_columns_and_rows(table_idx, tk) + table = self._generate_table(table_idx, cols, rows) + table._bbox = tk + _tables.append(table) + + return _tables diff --git a/src/main/python/camelot/plotting.py b/src/main/python/camelot/plotting.py new file mode 100644 index 00000000..f5b6afe9 --- /dev/null +++ b/src/main/python/camelot/plotting.py @@ -0,0 +1,225 @@ +# -*- coding: utf-8 -*- + +try: + import matplotlib.pyplot as plt + import matplotlib.patches as patches +except ImportError: + _HAS_MPL = False +else: + _HAS_MPL = True + + +class PlotMethods(object): + def __call__(self, table, kind="text", filename=None): + """Plot elements found on PDF page based on kind + specified, useful for debugging and playing with different + parameters to get the best output. + + Parameters + ---------- + table: camelot.core.Table + A Camelot Table. + kind : str, optional (default: 'text') + {'text', 'grid', 'contour', 'joint', 'line'} + The element type for which a plot should be generated. + filepath: str, optional (default: None) + Absolute path for saving the generated plot. + + Returns + ------- + fig : matplotlib.fig.Figure + + """ + if not _HAS_MPL: + raise ImportError("matplotlib is required for plotting.") + + if table.flavor == "lattice" and kind in ["textedge"]: + raise NotImplementedError(f"Lattice flavor does not support kind='{kind}'") + elif table.flavor == "stream" and kind in ["joint", "line"]: + raise NotImplementedError(f"Stream flavor does not support kind='{kind}'") + + plot_method = getattr(self, kind) + fig = plot_method(table) + + if filename is not None: + fig.savefig(filename) + return None + + return fig + + def text(self, table): + """Generates a plot for all text elements present + on the PDF page. + + Parameters + ---------- + table : camelot.core.Table + + Returns + ------- + fig : matplotlib.fig.Figure + + """ + fig = plt.figure() + ax = fig.add_subplot(111, aspect="equal") + xs, ys = [], [] + for t in table._text: + xs.extend([t[0], t[2]]) + ys.extend([t[1], t[3]]) + ax.add_patch(patches.Rectangle((t[0], t[1]), t[2] - t[0], t[3] - t[1])) + ax.set_xlim(min(xs) - 10, max(xs) + 10) + ax.set_ylim(min(ys) - 10, max(ys) + 10) + return fig + + def grid(self, table): + """Generates a plot for the detected table grids + on the PDF page. + + Parameters + ---------- + table : camelot.core.Table + + Returns + ------- + fig : matplotlib.fig.Figure + + """ + fig = plt.figure() + ax = fig.add_subplot(111, aspect="equal") + for row in table.cells: + for cell in row: + if cell.left: + ax.plot([cell.lb[0], cell.lt[0]], [cell.lb[1], cell.lt[1]]) + if cell.right: + ax.plot([cell.rb[0], cell.rt[0]], [cell.rb[1], cell.rt[1]]) + if cell.top: + ax.plot([cell.lt[0], cell.rt[0]], [cell.lt[1], cell.rt[1]]) + if cell.bottom: + ax.plot([cell.lb[0], cell.rb[0]], [cell.lb[1], cell.rb[1]]) + return fig + + def contour(self, table): + """Generates a plot for all table boundaries present + on the PDF page. + + Parameters + ---------- + table : camelot.core.Table + + Returns + ------- + fig : matplotlib.fig.Figure + + """ + try: + img, table_bbox = table._image + _FOR_LATTICE = True + except TypeError: + img, table_bbox = (None, {table._bbox: None}) + _FOR_LATTICE = False + fig = plt.figure() + ax = fig.add_subplot(111, aspect="equal") + + xs, ys = [], [] + if not _FOR_LATTICE: + for t in table._text: + xs.extend([t[0], t[2]]) + ys.extend([t[1], t[3]]) + ax.add_patch( + patches.Rectangle( + (t[0], t[1]), t[2] - t[0], t[3] - t[1], color="blue" + ) + ) + + for t in table_bbox.keys(): + ax.add_patch( + patches.Rectangle( + (t[0], t[1]), t[2] - t[0], t[3] - t[1], fill=False, color="red" + ) + ) + if not _FOR_LATTICE: + xs.extend([t[0], t[2]]) + ys.extend([t[1], t[3]]) + ax.set_xlim(min(xs) - 10, max(xs) + 10) + ax.set_ylim(min(ys) - 10, max(ys) + 10) + + if _FOR_LATTICE: + ax.imshow(img) + return fig + + def textedge(self, table): + """Generates a plot for relevant textedges. + + Parameters + ---------- + table : camelot.core.Table + + Returns + ------- + fig : matplotlib.fig.Figure + + """ + fig = plt.figure() + ax = fig.add_subplot(111, aspect="equal") + xs, ys = [], [] + for t in table._text: + xs.extend([t[0], t[2]]) + ys.extend([t[1], t[3]]) + ax.add_patch( + patches.Rectangle((t[0], t[1]), t[2] - t[0], t[3] - t[1], color="blue") + ) + ax.set_xlim(min(xs) - 10, max(xs) + 10) + ax.set_ylim(min(ys) - 10, max(ys) + 10) + + for te in table._textedges: + ax.plot([te.x, te.x], [te.y0, te.y1]) + + return fig + + def joint(self, table): + """Generates a plot for all line intersections present + on the PDF page. + + Parameters + ---------- + table : camelot.core.Table + + Returns + ------- + fig : matplotlib.fig.Figure + + """ + img, table_bbox = table._image + fig = plt.figure() + ax = fig.add_subplot(111, aspect="equal") + x_coord = [] + y_coord = [] + for k in table_bbox.keys(): + for coord in table_bbox[k]: + x_coord.append(coord[0]) + y_coord.append(coord[1]) + ax.plot(x_coord, y_coord, "ro") + ax.imshow(img) + return fig + + def line(self, table): + """Generates a plot for all line segments present + on the PDF page. + + Parameters + ---------- + table : camelot.core.Table + + Returns + ------- + fig : matplotlib.fig.Figure + + """ + fig = plt.figure() + ax = fig.add_subplot(111, aspect="equal") + vertical, horizontal = table._segments + for v in vertical: + ax.plot([v[0], v[2]], [v[1], v[3]]) + for h in horizontal: + ax.plot([h[0], h[2]], [h[1], h[3]]) + return fig diff --git a/src/main/python/camelot/utils.py b/src/main/python/camelot/utils.py new file mode 100644 index 00000000..404c00b2 --- /dev/null +++ b/src/main/python/camelot/utils.py @@ -0,0 +1,938 @@ +# -*- coding: utf-8 -*- + +import os +import re +import random +import shutil +import string +import tempfile +import warnings +from itertools import groupby +from operator import itemgetter + +import numpy as np +from pdfminer.pdfparser import PDFParser +from pdfminer.pdfdocument import PDFDocument +from pdfminer.pdfpage import PDFPage +from pdfminer.pdfpage import PDFTextExtractionNotAllowed +from pdfminer.pdfinterp import PDFResourceManager +from pdfminer.pdfinterp import PDFPageInterpreter +from pdfminer.converter import PDFPageAggregator +from pdfminer.layout import ( + LAParams, + LTAnno, + LTChar, + LTTextLineHorizontal, + LTTextLineVertical, + LTImage, +) + +from urllib.request import Request, urlopen +from urllib.parse import urlparse as parse_url +from urllib.parse import uses_relative, uses_netloc, uses_params + + +_VALID_URLS = set(uses_relative + uses_netloc + uses_params) +_VALID_URLS.discard("") + + +# https://github.com/pandas-dev/pandas/blob/master/pandas/io/common.py +def is_url(url): + """Check to see if a URL has a valid protocol. + + Parameters + ---------- + url : str or unicode + + Returns + ------- + isurl : bool + If url has a valid protocol return True otherwise False. + + """ + try: + return parse_url(url).scheme in _VALID_URLS + except Exception: + return False + + +def random_string(length): + ret = "" + while length: + ret += random.choice( + string.digits + string.ascii_lowercase + string.ascii_uppercase + ) + length -= 1 + return ret + + +def download_url(url): + """Download file from specified URL. + + Parameters + ---------- + url : str or unicode + + Returns + ------- + filepath : str or unicode + Temporary filepath. + + """ + filename = f"{random_string(6)}.pdf" + with tempfile.NamedTemporaryFile("wb", delete=False) as f: + headers = {"User-Agent": "Mozilla/5.0"} + request = Request(url, None, headers) + obj = urlopen(request) + content_type = obj.info().get_content_type() + if content_type != "application/pdf": + raise NotImplementedError("File format not supported") + f.write(obj.read()) + filepath = os.path.join(os.path.dirname(f.name), filename) + shutil.move(f.name, filepath) + return filepath + + +stream_kwargs = ["columns", "edge_tol", "row_tol", "column_tol"] +lattice_kwargs = [ + "process_background", + "line_scale", + "copy_text", + "shift_text", + "line_tol", + "joint_tol", + "threshold_blocksize", + "threshold_constant", + "iterations", + "resolution", +] + + +def validate_input(kwargs, flavor="lattice"): + def check_intersection(parser_kwargs, input_kwargs): + isec = set(parser_kwargs).intersection(set(input_kwargs.keys())) + if isec: + raise ValueError( + f"{','.join(sorted(isec))} cannot be used with flavor='{flavor}'" + ) + + if flavor == "lattice": + check_intersection(stream_kwargs, kwargs) + else: + check_intersection(lattice_kwargs, kwargs) + + +def remove_extra(kwargs, flavor="lattice"): + if flavor == "lattice": + for key in kwargs.keys(): + if key in stream_kwargs: + kwargs.pop(key) + else: + for key in kwargs.keys(): + if key in lattice_kwargs: + kwargs.pop(key) + return kwargs + + +# https://stackoverflow.com/a/22726782 +class TemporaryDirectory(object): + def __enter__(self): + self.name = tempfile.mkdtemp() + return self.name + + def __exit__(self, exc_type, exc_value, traceback): + shutil.rmtree(self.name) + + +def translate(x1, x2): + """Translates x2 by x1. + + Parameters + ---------- + x1 : float + x2 : float + + Returns + ------- + x2 : float + + """ + x2 += x1 + return x2 + + +def scale(x, s): + """Scales x by scaling factor s. + + Parameters + ---------- + x : float + s : float + + Returns + ------- + x : float + + """ + x *= s + return x + + +def scale_pdf(k, factors): + """Translates and scales pdf coordinate space to image + coordinate space. + + Parameters + ---------- + k : tuple + Tuple (x1, y1, x2, y2) representing table bounding box where + (x1, y1) -> lt and (x2, y2) -> rb in PDFMiner coordinate + space. + factors : tuple + Tuple (scaling_factor_x, scaling_factor_y, pdf_y) where the + first two elements are scaling factors and pdf_y is height of + pdf. + + Returns + ------- + knew : tuple + Tuple (x1, y1, x2, y2) representing table bounding box where + (x1, y1) -> lt and (x2, y2) -> rb in OpenCV coordinate + space. + + """ + x1, y1, x2, y2 = k + scaling_factor_x, scaling_factor_y, pdf_y = factors + x1 = scale(x1, scaling_factor_x) + y1 = scale(abs(translate(-pdf_y, y1)), scaling_factor_y) + x2 = scale(x2, scaling_factor_x) + y2 = scale(abs(translate(-pdf_y, y2)), scaling_factor_y) + knew = (int(x1), int(y1), int(x2), int(y2)) + return knew + + +def scale_image(tables, v_segments, h_segments, factors): + """Translates and scales image coordinate space to pdf + coordinate space. + + Parameters + ---------- + tables : dict + Dict with table boundaries as keys and list of intersections + in that boundary as value. + v_segments : list + List of vertical line segments. + h_segments : list + List of horizontal line segments. + factors : tuple + Tuple (scaling_factor_x, scaling_factor_y, img_y) where the + first two elements are scaling factors and img_y is height of + image. + + Returns + ------- + tables_new : dict + v_segments_new : dict + h_segments_new : dict + + """ + scaling_factor_x, scaling_factor_y, img_y = factors + tables_new = {} + for k in tables.keys(): + x1, y1, x2, y2 = k + x1 = scale(x1, scaling_factor_x) + y1 = scale(abs(translate(-img_y, y1)), scaling_factor_y) + x2 = scale(x2, scaling_factor_x) + y2 = scale(abs(translate(-img_y, y2)), scaling_factor_y) + j_x, j_y = zip(*tables[k]) + j_x = [scale(j, scaling_factor_x) for j in j_x] + j_y = [scale(abs(translate(-img_y, j)), scaling_factor_y) for j in j_y] + joints = zip(j_x, j_y) + tables_new[(x1, y1, x2, y2)] = joints + + v_segments_new = [] + for v in v_segments: + x1, x2 = scale(v[0], scaling_factor_x), scale(v[2], scaling_factor_x) + y1, y2 = ( + scale(abs(translate(-img_y, v[1])), scaling_factor_y), + scale(abs(translate(-img_y, v[3])), scaling_factor_y), + ) + v_segments_new.append((x1, y1, x2, y2)) + + h_segments_new = [] + for h in h_segments: + x1, x2 = scale(h[0], scaling_factor_x), scale(h[2], scaling_factor_x) + y1, y2 = ( + scale(abs(translate(-img_y, h[1])), scaling_factor_y), + scale(abs(translate(-img_y, h[3])), scaling_factor_y), + ) + h_segments_new.append((x1, y1, x2, y2)) + + return tables_new, v_segments_new, h_segments_new + + +def get_rotation(chars, horizontal_text, vertical_text): + """Detects if text in table is rotated or not using the current + transformation matrix (CTM) and returns its orientation. + + Parameters + ---------- + horizontal_text : list + List of PDFMiner LTTextLineHorizontal objects. + vertical_text : list + List of PDFMiner LTTextLineVertical objects. + ltchar : list + List of PDFMiner LTChar objects. + + Returns + ------- + rotation : string + '' if text in table is upright, 'anticlockwise' if + rotated 90 degree anticlockwise and 'clockwise' if + rotated 90 degree clockwise. + + """ + rotation = "" + hlen = len([t for t in horizontal_text if t.get_text().strip()]) + vlen = len([t for t in vertical_text if t.get_text().strip()]) + if hlen < vlen: + clockwise = sum(t.matrix[1] < 0 and t.matrix[2] > 0 for t in chars) + anticlockwise = sum(t.matrix[1] > 0 and t.matrix[2] < 0 for t in chars) + rotation = "anticlockwise" if clockwise < anticlockwise else "clockwise" + return rotation + + +def segments_in_bbox(bbox, v_segments, h_segments): + """Returns all line segments present inside a bounding box. + + Parameters + ---------- + bbox : tuple + Tuple (x1, y1, x2, y2) representing a bounding box where + (x1, y1) -> lb and (x2, y2) -> rt in PDFMiner coordinate + space. + v_segments : list + List of vertical line segments. + h_segments : list + List of vertical horizontal segments. + + Returns + ------- + v_s : list + List of vertical line segments that lie inside table. + h_s : list + List of horizontal line segments that lie inside table. + + """ + lb = (bbox[0], bbox[1]) + rt = (bbox[2], bbox[3]) + v_s = [ + v + for v in v_segments + if v[1] > lb[1] - 2 and v[3] < rt[1] + 2 and lb[0] - 2 <= v[0] <= rt[0] + 2 + ] + h_s = [ + h + for h in h_segments + if h[0] > lb[0] - 2 and h[2] < rt[0] + 2 and lb[1] - 2 <= h[1] <= rt[1] + 2 + ] + return v_s, h_s + + +def text_in_bbox(bbox, text): + """Returns all text objects present inside a bounding box. + + Parameters + ---------- + bbox : tuple + Tuple (x1, y1, x2, y2) representing a bounding box where + (x1, y1) -> lb and (x2, y2) -> rt in the PDF coordinate + space. + text : List of PDFMiner text objects. + + Returns + ------- + t_bbox : list + List of PDFMiner text objects that lie inside table, discarding the overlapping ones + + """ + lb = (bbox[0], bbox[1]) + rt = (bbox[2], bbox[3]) + t_bbox = [ + t + for t in text + if lb[0] - 2 <= (t.x0 + t.x1) / 2.0 <= rt[0] + 2 + and lb[1] - 2 <= (t.y0 + t.y1) / 2.0 <= rt[1] + 2 + ] + + # Avoid duplicate text by discarding overlapping boxes + rest = {t for t in t_bbox} + for ba in t_bbox: + for bb in rest.copy(): + if ba == bb: + continue + if bbox_intersect(ba, bb): + # if the intersection is larger than 80% of ba's size, we keep the longest + if (bbox_intersection_area(ba, bb) / bbox_area(ba)) > 0.8: + if bbox_longer(bb, ba): + rest.discard(ba) + unique_boxes = list(rest) + + return unique_boxes + + +def bbox_intersection_area(ba, bb) -> float: + """Returns area of the intersection of the bounding boxes of two PDFMiner objects. + + Parameters + ---------- + ba : PDFMiner text object + bb : PDFMiner text object + + Returns + ------- + intersection_area : float + Area of the intersection of the bounding boxes of both objects + + """ + x_left = max(ba.x0, bb.x0) + y_top = min(ba.y1, bb.y1) + x_right = min(ba.x1, bb.x1) + y_bottom = max(ba.y0, bb.y0) + + if x_right < x_left or y_bottom > y_top: + return 0.0 + + intersection_area = (x_right - x_left) * (y_top - y_bottom) + return intersection_area + + +def bbox_area(bb) -> float: + """Returns area of the bounding box of a PDFMiner object. + + Parameters + ---------- + bb : PDFMiner text object + + Returns + ------- + area : float + Area of the bounding box of the object + + """ + return (bb.x1 - bb.x0) * (bb.y1 - bb.y0) + + +def bbox_intersect(ba, bb) -> bool: + """Returns True if the bounding boxes of two PDFMiner objects intersect. + + Parameters + ---------- + ba : PDFMiner text object + bb : PDFMiner text object + + Returns + ------- + overlaps : bool + True if the bounding boxes intersect + + """ + return ba.x1 >= bb.x0 and bb.x1 >= ba.x0 and ba.y1 >= bb.y0 and bb.y1 >= ba.y0 + + +def bbox_longer(ba, bb) -> bool: + """Returns True if the bounding box of the first PDFMiner object is longer or equal to the second. + + Parameters + ---------- + ba : PDFMiner text object + bb : PDFMiner text object + + Returns + ------- + longer : bool + True if the bounding box of the first object is longer or equal + + """ + return (ba.x1 - ba.x0) >= (bb.x1 - bb.x0) + + +def merge_close_lines(ar, line_tol=2): + """Merges lines which are within a tolerance by calculating a + moving mean, based on their x or y axis projections. + + Parameters + ---------- + ar : list + line_tol : int, optional (default: 2) + + Returns + ------- + ret : list + + """ + ret = [] + for a in ar: + if not ret: + ret.append(a) + else: + temp = ret[-1] + if np.isclose(temp, a, atol=line_tol): + temp = (temp + a) / 2.0 + ret[-1] = temp + else: + ret.append(a) + return ret + + +def text_strip(text, strip=""): + """Strips any characters in `strip` that are present in `text`. + Parameters + ---------- + text : str + Text to process and strip. + strip : str, optional (default: '') + Characters that should be stripped from `text`. + Returns + ------- + stripped : str + """ + if not strip: + return text + + stripped = re.sub( + fr"[{''.join(map(re.escape, strip))}]", "", text, flags=re.UNICODE + ) + return stripped + + +# TODO: combine the following functions into a TextProcessor class which +# applies corresponding transformations sequentially +# (inspired from sklearn.pipeline.Pipeline) + + +def flag_font_size(textline, direction, strip_text=""): + """Flags super/subscripts in text by enclosing them with . + May give false positives. + + Parameters + ---------- + textline : list + List of PDFMiner LTChar objects. + direction : string + Direction of the PDFMiner LTTextLine object. + strip_text : str, optional (default: '') + Characters that should be stripped from a string before + assigning it to a cell. + + Returns + ------- + fstring : string + + """ + if direction == "horizontal": + d = [ + (t.get_text(), np.round(t.height, decimals=6)) + for t in textline + if not isinstance(t, LTAnno) + ] + elif direction == "vertical": + d = [ + (t.get_text(), np.round(t.width, decimals=6)) + for t in textline + if not isinstance(t, LTAnno) + ] + l = [np.round(size, decimals=6) for text, size in d] + if len(set(l)) > 1: + flist = [] + min_size = min(l) + for key, chars in groupby(d, itemgetter(1)): + if key == min_size: + fchars = [t[0] for t in chars] + if "".join(fchars).strip(): + fchars.insert(0, "") + fchars.append("") + flist.append("".join(fchars)) + else: + fchars = [t[0] for t in chars] + if "".join(fchars).strip(): + flist.append("".join(fchars)) + fstring = "".join(flist) + else: + fstring = "".join([t.get_text() for t in textline]) + return text_strip(fstring, strip_text) + + +def split_textline(table, textline, direction, flag_size=False, strip_text=""): + """Splits PDFMiner LTTextLine into substrings if it spans across + multiple rows/columns. + + Parameters + ---------- + table : camelot.core.Table + textline : object + PDFMiner LTTextLine object. + direction : string + Direction of the PDFMiner LTTextLine object. + flag_size : bool, optional (default: False) + Whether or not to highlight a substring using + if its size is different from rest of the string. (Useful for + super and subscripts.) + strip_text : str, optional (default: '') + Characters that should be stripped from a string before + assigning it to a cell. + + Returns + ------- + grouped_chars : list + List of tuples of the form (idx, text) where idx is the index + of row/column and text is the an lttextline substring. + + """ + idx = 0 + cut_text = [] + bbox = textline.bbox + try: + if direction == "horizontal" and not textline.is_empty(): + x_overlap = [ + i + for i, x in enumerate(table.cols) + if x[0] <= bbox[2] and bbox[0] <= x[1] + ] + r_idx = [ + j + for j, r in enumerate(table.rows) + if r[1] <= (bbox[1] + bbox[3]) / 2 <= r[0] + ] + r = r_idx[0] + x_cuts = [ + (c, table.cells[r][c].x2) for c in x_overlap if table.cells[r][c].right + ] + if not x_cuts: + x_cuts = [(x_overlap[0], table.cells[r][-1].x2)] + for obj in textline._objs: + row = table.rows[r] + for cut in x_cuts: + if isinstance(obj, LTChar): + if ( + row[1] <= (obj.y0 + obj.y1) / 2 <= row[0] + and (obj.x0 + obj.x1) / 2 <= cut[1] + ): + cut_text.append((r, cut[0], obj)) + break + else: + # TODO: add test + if cut == x_cuts[-1]: + cut_text.append((r, cut[0] + 1, obj)) + elif isinstance(obj, LTAnno): + cut_text.append((r, cut[0], obj)) + elif direction == "vertical" and not textline.is_empty(): + y_overlap = [ + j + for j, y in enumerate(table.rows) + if y[1] <= bbox[3] and bbox[1] <= y[0] + ] + c_idx = [ + i + for i, c in enumerate(table.cols) + if c[0] <= (bbox[0] + bbox[2]) / 2 <= c[1] + ] + c = c_idx[0] + y_cuts = [ + (r, table.cells[r][c].y1) for r in y_overlap if table.cells[r][c].bottom + ] + if not y_cuts: + y_cuts = [(y_overlap[0], table.cells[-1][c].y1)] + for obj in textline._objs: + col = table.cols[c] + for cut in y_cuts: + if isinstance(obj, LTChar): + if ( + col[0] <= (obj.x0 + obj.x1) / 2 <= col[1] + and (obj.y0 + obj.y1) / 2 >= cut[1] + ): + cut_text.append((cut[0], c, obj)) + break + else: + # TODO: add test + if cut == y_cuts[-1]: + cut_text.append((cut[0] - 1, c, obj)) + elif isinstance(obj, LTAnno): + cut_text.append((cut[0], c, obj)) + except IndexError: + return [(-1, -1, textline.get_text())] + grouped_chars = [] + for key, chars in groupby(cut_text, itemgetter(0, 1)): + if flag_size: + grouped_chars.append( + ( + key[0], + key[1], + flag_font_size( + [t[2] for t in chars], direction, strip_text=strip_text + ), + ) + ) + else: + gchars = [t[2].get_text() for t in chars] + grouped_chars.append( + (key[0], key[1], text_strip("".join(gchars), strip_text)) + ) + return grouped_chars + + +def get_table_index( + table, t, direction, split_text=False, flag_size=False, strip_text="" +): + """Gets indices of the table cell where given text object lies by + comparing their y and x-coordinates. + + Parameters + ---------- + table : camelot.core.Table + t : object + PDFMiner LTTextLine object. + direction : string + Direction of the PDFMiner LTTextLine object. + split_text : bool, optional (default: False) + Whether or not to split a text line if it spans across + multiple cells. + flag_size : bool, optional (default: False) + Whether or not to highlight a substring using + if its size is different from rest of the string. (Useful for + super and subscripts) + strip_text : str, optional (default: '') + Characters that should be stripped from a string before + assigning it to a cell. + + Returns + ------- + indices : list + List of tuples of the form (r_idx, c_idx, text) where r_idx + and c_idx are row and column indices. + error : float + Assignment error, percentage of text area that lies outside + a cell. + +-------+ + | | + | [Text bounding box] + | | + +-------+ + + """ + r_idx, c_idx = [-1] * 2 + for r in range(len(table.rows)): + if (t.y0 + t.y1) / 2.0 < table.rows[r][0] and (t.y0 + t.y1) / 2.0 > table.rows[ + r + ][1]: + lt_col_overlap = [] + for c in table.cols: + if c[0] <= t.x1 and c[1] >= t.x0: + left = t.x0 if c[0] <= t.x0 else c[0] + right = t.x1 if c[1] >= t.x1 else c[1] + lt_col_overlap.append(abs(left - right) / abs(c[0] - c[1])) + else: + lt_col_overlap.append(-1) + if len(list(filter(lambda x: x != -1, lt_col_overlap))) == 0: + text = t.get_text().strip("\n") + text_range = (t.x0, t.x1) + col_range = (table.cols[0][0], table.cols[-1][1]) + warnings.warn( + f"{text} {text_range} does not lie in column range {col_range}" + ) + r_idx = r + c_idx = lt_col_overlap.index(max(lt_col_overlap)) + break + + # error calculation + y0_offset, y1_offset, x0_offset, x1_offset = [0] * 4 + if t.y0 > table.rows[r_idx][0]: + y0_offset = abs(t.y0 - table.rows[r_idx][0]) + if t.y1 < table.rows[r_idx][1]: + y1_offset = abs(t.y1 - table.rows[r_idx][1]) + if t.x0 < table.cols[c_idx][0]: + x0_offset = abs(t.x0 - table.cols[c_idx][0]) + if t.x1 > table.cols[c_idx][1]: + x1_offset = abs(t.x1 - table.cols[c_idx][1]) + X = 1.0 if abs(t.x0 - t.x1) == 0.0 else abs(t.x0 - t.x1) + Y = 1.0 if abs(t.y0 - t.y1) == 0.0 else abs(t.y0 - t.y1) + charea = X * Y + error = ((X * (y0_offset + y1_offset)) + (Y * (x0_offset + x1_offset))) / charea + + if split_text: + return ( + split_textline( + table, t, direction, flag_size=flag_size, strip_text=strip_text + ), + error, + ) + else: + if flag_size: + return ( + [ + ( + r_idx, + c_idx, + flag_font_size(t._objs, direction, strip_text=strip_text), + ) + ], + error, + ) + else: + return [(r_idx, c_idx, text_strip(t.get_text(), strip_text))], error + + +def compute_accuracy(error_weights): + """Calculates a score based on weights assigned to various + parameters and their error percentages. + + Parameters + ---------- + error_weights : list + Two-dimensional list of the form [[p1, e1], [p2, e2], ...] + where pn is the weight assigned to list of errors en. + Sum of pn should be equal to 100. + + Returns + ------- + score : float + + """ + SCORE_VAL = 100 + try: + score = 0 + if sum([ew[0] for ew in error_weights]) != SCORE_VAL: + raise ValueError("Sum of weights should be equal to 100.") + for ew in error_weights: + weight = ew[0] / len(ew[1]) + for error_percentage in ew[1]: + score += weight * (1 - error_percentage) + except ZeroDivisionError: + score = 0 + return score + + +def compute_whitespace(d): + """Calculates the percentage of empty strings in a + two-dimensional list. + + Parameters + ---------- + d : list + + Returns + ------- + whitespace : float + Percentage of empty cells. + + """ + whitespace = 0 + r_nempty_cells, c_nempty_cells = [], [] + for i in d: + for j in i: + if j.strip() == "": + whitespace += 1 + whitespace = 100 * (whitespace / float(len(d) * len(d[0]))) + return whitespace + + +def get_page_layout( + filename, + line_overlap=0.5, + char_margin=1.0, + line_margin=0.5, + word_margin=0.1, + boxes_flow=0.5, + detect_vertical=True, + all_texts=True, +): + """Returns a PDFMiner LTPage object and page dimension of a single + page pdf. To get the definitions of kwargs, see + https://pdfminersix.rtfd.io/en/latest/reference/composable.html. + + Parameters + ---------- + filename : string + Path to pdf file. + line_overlap : float + char_margin : float + line_margin : float + word_margin : float + boxes_flow : float + detect_vertical : bool + all_texts : bool + + Returns + ------- + layout : object + PDFMiner LTPage object. + dim : tuple + Dimension of pdf page in the form (width, height). + + """ + with open(filename, "rb") as f: + parser = PDFParser(f) + document = PDFDocument(parser) + if not document.is_extractable: + raise PDFTextExtractionNotAllowed( + f"Text extraction is not allowed: {filename}" + ) + laparams = LAParams( + line_overlap=line_overlap, + char_margin=char_margin, + line_margin=line_margin, + word_margin=word_margin, + boxes_flow=boxes_flow, + detect_vertical=detect_vertical, + all_texts=all_texts, + ) + rsrcmgr = PDFResourceManager() + device = PDFPageAggregator(rsrcmgr, laparams=laparams) + interpreter = PDFPageInterpreter(rsrcmgr, device) + for page in PDFPage.create_pages(document): + interpreter.process_page(page) + layout = device.get_result() + width = layout.bbox[2] + height = layout.bbox[3] + dim = (width, height) + return layout, dim + + +def get_text_objects(layout, ltype="char", t=None): + """Recursively parses pdf layout to get a list of + PDFMiner text objects. + + Parameters + ---------- + layout : object + PDFMiner LTPage object. + ltype : string + Specify 'char', 'lh', 'lv' to get LTChar, LTTextLineHorizontal, + and LTTextLineVertical objects respectively. + t : list + + Returns + ------- + t : list + List of PDFMiner text objects. + + """ + if ltype == "char": + LTObject = LTChar + elif ltype == "image": + LTObject = LTImage + elif ltype == "horizontal_text": + LTObject = LTTextLineHorizontal + elif ltype == "vertical_text": + LTObject = LTTextLineVertical + if t is None: + t = [] + try: + for obj in layout._objs: + if isinstance(obj, LTObject): + t.append(obj) + else: + t += get_text_objects(obj, ltype=ltype) + except AttributeError: + pass + return t From 1e3894d1fb85658b0443d03940b1fa27d278a7bc Mon Sep 17 00:00:00 2001 From: liana Date: Mon, 24 Apr 2023 15:31:14 +0300 Subject: [PATCH 7/8] added changes to camelot --- .../mundaneassignmentpolice/wrapper/PDFBox.kt | 5 - src/main/python/TableExtractionScript.py | 2 +- src/main/python/camelot/image_processing.py | 207 ++++++++++++++++-- src/main/python/camelot/parsers/lattice.py | 29 +++ 4 files changed, 222 insertions(+), 21 deletions(-) diff --git a/src/main/kotlin/com/github/darderion/mundaneassignmentpolice/wrapper/PDFBox.kt b/src/main/kotlin/com/github/darderion/mundaneassignmentpolice/wrapper/PDFBox.kt index 01a5b4fd..4c0d2e19 100644 --- a/src/main/kotlin/com/github/darderion/mundaneassignmentpolice/wrapper/PDFBox.kt +++ b/src/main/kotlin/com/github/darderion/mundaneassignmentpolice/wrapper/PDFBox.kt @@ -262,7 +262,6 @@ class PDFBox { } private fun getTables(path: String): List
{ - val d: Long = Date().time val workingDirPath = System.getProperty("user.home") + "/map" val fileName = path.replace("uploads/","") @@ -270,7 +269,6 @@ class PDFBox { if (!Files.exists(Path("$workingDirPath/uploads/tables/$fileName"), LinkOption.NOFOLLOW_LINKS)) { - ProcessBuilder( "src/main/python/venv/bin/python3", "src/main/python/TableExtractionScript.py", @@ -287,9 +285,6 @@ class PDFBox { tables.add(Table(df)) } - val e: Long = Date().time - println(e - d) - println(tables.size) return tables } diff --git a/src/main/python/TableExtractionScript.py b/src/main/python/TableExtractionScript.py index ba2efab3..0804bd4e 100755 --- a/src/main/python/TableExtractionScript.py +++ b/src/main/python/TableExtractionScript.py @@ -23,7 +23,7 @@ def extraction(path): if not os.path.isdir(f'uploads/tables/{file_name}'): os.mkdir(f'uploads/tables/{file_name}') - tables = camelot.read_pdf(path, latice=True, pages='all') + tables = camelot.read_pdf(path, latice=True, pages='all', line_scale=30) for k in range(len(tables)): left_x, left_y, right_x, right_y = 596, 896, 0, 0 diff --git a/src/main/python/camelot/image_processing.py b/src/main/python/camelot/image_processing.py index 08acb23e..8affe9b1 100644 --- a/src/main/python/camelot/image_processing.py +++ b/src/main/python/camelot/image_processing.py @@ -3,6 +3,23 @@ import cv2 import numpy as np +def adaptive_threshold_with_img(img, process_background=False, blocksize=15, c=-2): + gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY) + + if process_background: + threshold = cv2.adaptiveThreshold( + gray, 255, cv2.ADAPTIVE_THRESH_GAUSSIAN_C, cv2.THRESH_BINARY, blocksize, c + ) + else: + threshold = cv2.adaptiveThreshold( + np.invert(gray), + 255, + cv2.ADAPTIVE_THRESH_GAUSSIAN_C, + cv2.THRESH_BINARY, + blocksize, + c, + ) + return img, threshold def adaptive_threshold(imagename, process_background=False, blocksize=15, c=-2): """Thresholds an image using OpenCV's adaptiveThreshold. @@ -33,21 +50,7 @@ def adaptive_threshold(imagename, process_background=False, blocksize=15, c=-2): """ img = cv2.imread(imagename) - gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY) - - if process_background: - threshold = cv2.adaptiveThreshold( - gray, 255, cv2.ADAPTIVE_THRESH_GAUSSIAN_C, cv2.THRESH_BINARY, blocksize, c - ) - else: - threshold = cv2.adaptiveThreshold( - np.invert(gray), - 255, - cv2.ADAPTIVE_THRESH_GAUSSIAN_C, - cv2.THRESH_BINARY, - blocksize, - c, - ) + img, threshold = adaptive_threshold_with_img(img, process_background, blocksize, c) return img, threshold @@ -220,3 +223,177 @@ def find_joints(contours, vertical, horizontal): tables[(x, y + h, x + w, y)] = joint_coords return tables + + +def intersectes(r1, r2): + """ Checking the intersection of two ribs. + + :param r1: tuple + (x11, y11, x21, y21) where (x11, y11) -> start coordinates of r1 + and (x21, y21) -> end coordinates of rib1. + :param r2: tuple + (x12, y12, x22, y22) where (x12, y12) -> start coordinates of r2 + and (x22, y22) -> end coordinates of rib2. + :return: boolean + if ribs intersect True else False. + """ + c_m = 10 + x11, y11, x21, y21 = r1[0], r1[1], r1[2], r1[3] + x12, y12, x22, y22 = r2[0], r2[1], r2[2], r2[3] + + if (x11 == x21 and x12 == x22) or (y11 == y21 and y12 == y22): + return False + elif x11 == x21 and y12 == y22: + return x11 + c_m >= x12 and x11 <= x22 + c_m \ + and y12 <= y11 + c_m and y12 >= y21 - c_m + else: + return x12 + c_m >= x11 and x12 <= x21 + c_m \ + and y11 <= y12 + c_m and y11 >= y22 - c_m + + +def draw_v(image, h_lines): + """ + Draws the vertical lines between given horisontal lines, corrects the image. + + :param image: img : object + numpy.ndarray representing the image. + :param h_lines: list + List of tuples representing horizontal lines with coordinates. + :return: img : object + numpy.ndarray representing the new image. + """ + + if len(h_lines) > 0: + + h_lines = sorted(h_lines, key=lambda x: (x[0], x[1])) + + l_x, r_x = h_lines[0][0], h_lines[0][2] + u_y, d_y = h_lines[0][1], h_lines[0][1] + + for i in range(len(h_lines)): + + if l_x == h_lines[i][0] and i != len(h_lines) - 1: + r_x = max(r_x, h_lines[i][2]) + + elif l_x == h_lines[i][0]: + d_y = h_lines[i][3] + cv2.rectangle(image, pt1=(l_x, u_y), pt2=(r_x, d_y), color=(0, 0, 0), thickness=3) + + else: + d_y = h_lines[i - 1][3] + cv2.rectangle(image, pt1=(l_x, u_y), pt2=(r_x, d_y), color=(0, 0, 0), thickness=3) + l_x, r_x = h_lines[i][0], h_lines[i][2] + u_y, d_y = h_lines[i][1], h_lines[i][3] + + + return image + + +def draw_h(image, v_lines): + ''' + Draws the horisontal lines between given vertical lines, corrects the image. + + :param image: img : object + numpy.ndarray representing the image. + :param v_lines: list + List of tuples representing vertical lines with + coordinates. + :return: image : object + numpy.ndarray representing the new image. + ''' + if (len(v_lines) > 0): + v_lines = sorted(v_lines, key=lambda x: (x[3], x[0])) + + u_y, d_y = v_lines[0][3], v_lines[0][1] + + for i in range(len(v_lines)): + + if u_y == v_lines[i][3] and i != len(v_lines) - 1: + d_y = max(d_y, v_lines[i][1]) + + elif u_y == v_lines[i][3]: + d_y = max(d_y, v_lines[i][1]) + cv2.rectangle(image, pt1=(50, u_y), pt2=(image.shape[1] - 50, d_y), color=(0, 0, 0), thickness=3) + + else: + cv2.rectangle(image, pt1=(50, u_y), pt2=(image.shape[1] - 50, d_y), color=(0, 0, 0), thickness=3) + u_y, d_y = v_lines[i][3], v_lines[i][1] + + return image + +def correct_lines(image, v_segments, h_segments): + ''' + + :param image: object + numpy.ndarray representing the image. + :param v_segments: list + List of tuples representing vertical lines with + coordinates. + :param h_segments: list + List of tuples representing horizontal lines with + coordinates. + :return: image : object + numpy.ndarray representing the new image. + ''' + + h_size, v_size = len(h_segments), len(v_segments) + + if h_size > 1 and v_size == 0: + image = draw_v(image, h_segments) + + elif h_size == 0 and v_size > 1: + image = draw_h(image, v_segments) + + elif v_size >= 1 and h_size >= 1: + + ribs = v_segments[:] + h_segments[:] + segments = [[ribs[i]][:] for i in range(len(ribs))] + + for i in range(0, len(ribs) - 1): + for j in range(i+1, len(ribs)): + if intersectes(ribs[i],ribs[j]): + for sg1 in segments: + cur_sg = [] + if ribs[i] in sg1: + cur_sg = sg1 + break + + for sg2 in segments: + del_sg = [] + if ribs[j] in sg2 and cur_sg != sg2: + cur_sg += sg2[:] + del_sg = sg2 + break + if del_sg in segments: + segments.remove(del_sg) + + + s_lines = [] + + for i in range(len(segments)): + + min_x, min_y = segments[i][0][0], segments[i][0][3] + max_x, max_y = segments[i][0][2], segments[i][0][1] + + if len(segments[i]) > 1: + for line in segments[i]: + min_x, min_y = min(min_x, line[0]),min(min_y, line[3]) + max_x, max_y = max(max_x, line[2]), max(max_y,line[1]) + cv2.rectangle(image, pt1=(min_x, min_y), pt2=(max_x, max_y), color=(0, 0, 0), thickness=3) + else: + s_lines += segments[i] + + h_s_lines, v_s_lines = [], [] + + for line in s_lines: + v_s_lines.append(line) if line[0] == line[2] else h_s_lines.append(line) + + image = draw_h(image, v_s_lines) + image = draw_v(image, h_s_lines) + + '''cv2.imshow("Image", image) + cv2.waitKey(0) + cv2.destroyAllWindows() + ''' + return image + diff --git a/src/main/python/camelot/parsers/lattice.py b/src/main/python/camelot/parsers/lattice.py index a1752272..5d8a79c8 100644 --- a/src/main/python/camelot/parsers/lattice.py +++ b/src/main/python/camelot/parsers/lattice.py @@ -27,6 +27,8 @@ find_lines, find_contours, find_joints, + correct_lines, + adaptive_threshold_with_img, ) from ..backends.image_conversion import BACKENDS @@ -288,6 +290,33 @@ def scale_areas(areas): iterations=self.iterations, ) + self.image = correct_lines( + self.image, + vertical_segments, + horizontal_segments + ) + self.image, threshold = adaptive_threshold_with_img( + self.image, + process_background=self.process_background, + blocksize=self.threshold_blocksize, + c=self.threshold_constant + ) + + vertical_mask, vertical_segments = find_lines( + threshold, + regions=regions, + direction="vertical", + line_scale=self.line_scale, + iterations=self.iterations, + ) + horizontal_mask, horizontal_segments = find_lines( + threshold, + regions=regions, + direction="horizontal", + line_scale=self.line_scale, + iterations=self.iterations, + ) + contours = find_contours(vertical_mask, horizontal_mask) table_bbox = find_joints(contours, vertical_mask, horizontal_mask) else: From 922aca791eb544ef5ca030fe3e5ebca5049edd15 Mon Sep 17 00:00:00 2001 From: liana Date: Fri, 28 Apr 2023 18:09:26 +0300 Subject: [PATCH 8/8] added tests for TableExtractionScript.py and camelot --- .../mundaneassignmentpolice/wrapper/PDFBox.kt | 7 +- src/main/python/TableExtractionScript.py | 24 ++-- src/main/python/camelot/handlers.py | 4 +- src/main/python/camelot/image_processing.py | 4 +- src/test/python/TableExtractionScriptTest.py | 60 ++++++++++ src/test/python/camelot/camelot_py.py | 109 ++++++++++++++++++ .../python/camelot/DrawingComplexTables.pdf | Bin 0 -> 81239 bytes .../python/camelot/DrawingHorizontalLines.pdf | Bin 0 -> 46888 bytes .../python/camelot/DrawingVerticalLines.pdf | Bin 0 -> 58814 bytes .../tableextractionscript/OpenNotPDF.docx | Bin 0 -> 12552 bytes .../TableInformation.pdf | Bin 0 -> 35046 bytes 11 files changed, 189 insertions(+), 19 deletions(-) create mode 100644 src/test/python/TableExtractionScriptTest.py create mode 100644 src/test/python/camelot/camelot_py.py create mode 100644 src/test/resources/com/github/darderion/mundaneassignmentpolice/python/camelot/DrawingComplexTables.pdf create mode 100644 src/test/resources/com/github/darderion/mundaneassignmentpolice/python/camelot/DrawingHorizontalLines.pdf create mode 100644 src/test/resources/com/github/darderion/mundaneassignmentpolice/python/camelot/DrawingVerticalLines.pdf create mode 100644 src/test/resources/com/github/darderion/mundaneassignmentpolice/python/tableextractionscript/OpenNotPDF.docx create mode 100644 src/test/resources/com/github/darderion/mundaneassignmentpolice/python/tableextractionscript/TableInformation.pdf diff --git a/src/main/kotlin/com/github/darderion/mundaneassignmentpolice/wrapper/PDFBox.kt b/src/main/kotlin/com/github/darderion/mundaneassignmentpolice/wrapper/PDFBox.kt index 4c0d2e19..3ed05144 100644 --- a/src/main/kotlin/com/github/darderion/mundaneassignmentpolice/wrapper/PDFBox.kt +++ b/src/main/kotlin/com/github/darderion/mundaneassignmentpolice/wrapper/PDFBox.kt @@ -261,7 +261,12 @@ class PDFBox { return images } - private fun getTables(path: String): List
{ + /** + * Returns tables from PDF + * @param path pdf's path + * @return list of Table + */ + fun getTables(path: String): List
{ val workingDirPath = System.getProperty("user.home") + "/map" val fileName = path.replace("uploads/","") diff --git a/src/main/python/TableExtractionScript.py b/src/main/python/TableExtractionScript.py index 0804bd4e..a42773de 100755 --- a/src/main/python/TableExtractionScript.py +++ b/src/main/python/TableExtractionScript.py @@ -1,29 +1,27 @@ import PyPDF2 -import sys -sys.path.insert(0, '../src') -import camelot +from PyPDF2.errors import PdfReadError +import src.main.python.camelot import pandas -import sys import os +import sys +from pathlib import Path +sys.path.insert(0, '../src') -def extraction(path): +def extraction(pdf_path): os.chdir(os.path.expanduser("~/map/")) - file_name = path.replace('uploads/', '') + file_name = Path(pdf_path).stem try: - PyPDF2.PdfFileReader(open(path, 'rb')) - except PyPDF2._utils.PdfStreamError: + PyPDF2.PdfFileReader(open(pdf_path, 'rb')) + except PyPDF2.errors.PdfReadError: print("invalid PDF file") else: - if not path.endswith('.pdf'): - os.rename(path, path+'.pdf') - path += '.pdf' if not os.path.isdir(f'uploads/tables/{file_name}'): os.mkdir(f'uploads/tables/{file_name}') - tables = camelot.read_pdf(path, latice=True, pages='all', line_scale=30) + tables = src.main.python.camelot.read_pdf(pdf_path, latice=True, pages='all', line_scale=30) for k in range(len(tables)): left_x, left_y, right_x, right_y = 596, 896, 0, 0 @@ -49,8 +47,6 @@ def extraction(path): tables.export(f'uploads/tables/{file_name}/{file_name}.csv', f='csv', compress=False) - if path.endswith('.pdf'): - os.rename(path, path.replace('.pdf','')) if __name__ == '__main__': diff --git a/src/main/python/camelot/handlers.py b/src/main/python/camelot/handlers.py index 5f07e5d0..3feadb60 100644 --- a/src/main/python/camelot/handlers.py +++ b/src/main/python/camelot/handlers.py @@ -38,8 +38,8 @@ def __init__(self, filepath, pages="1", password=None): if is_url(filepath): filepath = download_url(filepath) self.filepath = filepath - if not filepath.lower().endswith(".pdf"): - raise NotImplementedError("File format not supported") + #if not filepath.lower().endswith(".pdf"): + # raise NotImplementedError("File format not supported") if password is None: self.password = "" diff --git a/src/main/python/camelot/image_processing.py b/src/main/python/camelot/image_processing.py index 8affe9b1..08aae1b5 100644 --- a/src/main/python/camelot/image_processing.py +++ b/src/main/python/camelot/image_processing.py @@ -245,10 +245,10 @@ def intersectes(r1, r2): return False elif x11 == x21 and y12 == y22: return x11 + c_m >= x12 and x11 <= x22 + c_m \ - and y12 <= y11 + c_m and y12 >= y21 - c_m + and y11 + c_m >= y12 >= y21 - c_m else: return x12 + c_m >= x11 and x12 <= x21 + c_m \ - and y11 <= y12 + c_m and y11 >= y22 - c_m + and y12 + c_m >= y11 >= y22 - c_m def draw_v(image, h_lines): diff --git a/src/test/python/TableExtractionScriptTest.py b/src/test/python/TableExtractionScriptTest.py new file mode 100644 index 00000000..83ffccfa --- /dev/null +++ b/src/test/python/TableExtractionScriptTest.py @@ -0,0 +1,60 @@ +import unittest +import pandas +import contextlib +from pathlib import Path +import io +import os +import sys +import src.main.python.camelot +from src.main.python.TableExtractionScript import extraction + +sys.path.insert(0, '../src') + +class TableExtractionScriptTest(unittest.TestCase): + + def test_open_file(self): + pdf_path = 'src/test//resources/com/github/darderion/mundaneassignmentpolice/python/tableextractionscript/OpenNotPDF.docx' + + s = io.StringIO() + with contextlib.redirect_stdout(s): + extraction(pdf_path) + + self.assertEqual('invalid PDF file\n', s.getvalue()) + + def test_check_table_directory(self): + pdf_path = 'src/test/resources/com/github/darderion/mundaneassignmentpolice/python/tableextractionscript/TableInformation.pdf' + extraction(pdf_path) + self.assertTrue(os.path.exists(f'uploads/tables/{Path(pdf_path).stem}')) + + def test_save_table(self): + pdf_path = 'src/test/resources/com/github/darderion/mundaneassignmentpolice/python/tableextractionscript/TableInformation.pdf' + extraction(pdf_path) + self.assertTrue(os.path.exists('uploads/tables/TableInformation/TableInformation-page-1-table-1.csv')) + + def test_check_table_information(self): + pdf_path = 'src/test/resources/com/github/darderion/mundaneassignmentpolice/python/tableextractionscript/TableInformation.pdf' + extraction(pdf_path) + table = pandas.read_csv(os.path.expanduser("~/map/uploads/tables/TableInformation/TableInformation-page-1-table-1.csv")) + camelot_table = src.main.python.camelot.read_pdf(pdf_path, linescale=30)[0] + self.assertEqual('table data', table.columns[0]) + + self.assertEqual('table information', table['table data'][4]) + + self.assertEqual('page', table['table data'][5]) + self.assertEqual('1', table['table data'][6]) + + self.assertEqual('table area', table['table data'][7]) + self.assertEqual(camelot_table.cells[3][0].x1, float(table['table data'][8])) + self.assertEqual(camelot_table.cells[3][3].x2, float(table['table data'][10])) + self.assertEqual(camelot_table.cells[3][0].y1, float(table['table data'][9])) + self.assertEqual(camelot_table.cells[0][3].y2, float(table['table data'][11])) + + self.assertEqual('rows', table['table data'][12]) + self.assertEqual('4', table['table data'][13]) + + self.assertEqual('columns', table['table data'][14]) + self.assertEqual('4', table['table data'][15]) + + +if __name__ == '__main__': + unittest.main() diff --git a/src/test/python/camelot/camelot_py.py b/src/test/python/camelot/camelot_py.py new file mode 100644 index 00000000..1424c35f --- /dev/null +++ b/src/test/python/camelot/camelot_py.py @@ -0,0 +1,109 @@ +import os +import unittest +import sys +sys.path.insert(0, '../src') +import src.main.python.camelot as camelot +from src.main.python.camelot.image_processing import ( + intersectes +) +os.chdir(os.path.expanduser("~/map/src/test/resources/com/github/darderion/mundaneassignmentpolice/python/camelot")) + + +class DrawingLines(unittest.TestCase): + def test_v_draw(self): + file_name = 'DrawingVerticalLines.pdf' + + tables = camelot.read_pdf(file_name, latice=True, pages='1') + self.assertEqual(0, len(tables)) + + tables = camelot.read_pdf(file_name, latice=True, pages='2') + self.assertEqual(0, len(tables)) + + tables = camelot.read_pdf(file_name, latice=True, pages='3') + self.assertEqual(1, len(tables)) + self.assertEqual(5, len(tables[0].cells)) + self.assertEqual(1, len(tables[0].cols)) + self.assertEqual(5, len(tables[0].rows)) + + tables = camelot.read_pdf(file_name, latice=True, pages='4') + self.assertEqual(3, len(tables)) + + self.assertEqual(2, len(tables[0].cells)) + self.assertEqual(2, len(tables[1].cells)) + self.assertEqual(2, len(tables[2].cells)) + + self.assertEqual(1, len(tables[0].cols)) + self.assertEqual(1, len(tables[1].cols)) + self.assertEqual(1, len(tables[2].cols)) + + self.assertEqual(2, len(tables[0].rows)) + self.assertEqual(2, len(tables[1].rows)) + self.assertEqual(2, len(tables[2].rows)) + + def test_h_draw(self): + file_name = 'DrawingHorizontalLines.pdf' + + tables = camelot.read_pdf(file_name, latice=True, pages='1') + self.assertEqual(0, len(tables)) + + tables = camelot.read_pdf(file_name, latice=True, pages='2') + self.assertEqual(0, len(tables)) + + tables = camelot.read_pdf(file_name, latice=True, pages='3') + self.assertEqual(1, len(tables)) + + tables = camelot.read_pdf(file_name, latice=True, pages='4') + self.assertEqual(1, len(tables)) + + tables = camelot.read_pdf(file_name, latice=True, pages='5') + self.assertEqual(2, len(tables)) + + def test_intersects(self): + # rib1 intersects rib2 at first end + rib1, rib2 = (1, 100, 1, 5), (1, 5, 100, 5) + self.assertEqual(True, intersectes(rib1, rib2)) + + # rib1 intersects rib2 at second end + rib1, rib2 = (1, 100, 100, 100), (100, 100, 100, 5) + self.assertEqual(True, intersectes(rib1, rib2)) + + # horizontal rib1 parallel to horizontal rib2 + rib1, rib2 = (1, 100, 5, 100), (1, 200, 5, 200) + self.assertEqual(False, intersectes(rib1, rib2)) + + # vertical rib1 parallel to vertical rib2 + rib1, rib2 = (1, 100, 1, 200), (10, 100, 10, 200) + self.assertEqual(False, intersectes(rib1, rib2)) + + # rib1 intersects rib2 inside + rib1, rib2 = (1, 5, 100, 5), (50, 100, 50, 2) + self.assertEqual(True, intersectes(rib1, rib2)) + + # rib1 does not intersect rib2 + rib1, rib2 = (5, 10, 100, 10), (50, 60, 50, 40) + self.assertEqual(False, intersectes(rib1, rib2)) + + # rib1 lies on the same line as rib2 and does not intersect rib2 + rib1, rib2 = (5, 10, 100, 10), (150, 10, 160, 10) + self.assertEqual(False, intersectes(rib1, rib2)) + + def test_correct_lines(self): + file_name = 'DrawingComplexTables.pdf' + + tables = camelot.read_pdf(file_name, latice=True, pages='1') + self.assertEqual(1, len(tables)) + + tables = camelot.read_pdf(file_name, latice=True, pages='2') + self.assertEqual(2, len(tables)) + + + tables = camelot.read_pdf(file_name, latice=True, pages='3') + self.assertEqual(2, len(tables)) + + tables = camelot.read_pdf(file_name, latice=True, pages='4') + self.assertEqual(3, len(tables)) + + + +if __name__ == '__main__': + unittest.main() diff --git a/src/test/resources/com/github/darderion/mundaneassignmentpolice/python/camelot/DrawingComplexTables.pdf b/src/test/resources/com/github/darderion/mundaneassignmentpolice/python/camelot/DrawingComplexTables.pdf new file mode 100644 index 0000000000000000000000000000000000000000..9fce1ba45067514b673654c9167c21ee0443c22d GIT binary patch literal 81239 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

&yw=BUsgn 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 literal 0 HcmV?d00001 diff --git a/src/test/resources/com/github/darderion/mundaneassignmentpolice/python/camelot/DrawingVerticalLines.pdf b/src/test/resources/com/github/darderion/mundaneassignmentpolice/python/camelot/DrawingVerticalLines.pdf new file mode 100644 index 0000000000000000000000000000000000000000..c85265828231d6ab24fb5311ff8be26570061af9 GIT binary patch literal 58814 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 literal 0 HcmV?d00001