diff --git a/.gitignore b/.gitignore index 392691f..e0817fd 100644 --- a/.gitignore +++ b/.gitignore @@ -23,3 +23,7 @@ go.work.sum # OS .DS_Store Thumbs.db + +# Python bytecode from the benchmark harness +__pycache__/ +*.pyc diff --git a/bench/go.mod b/bench/go.mod index f592b91..e3dfbdb 100644 --- a/bench/go.mod +++ b/bench/go.mod @@ -8,4 +8,18 @@ go 1.25.0 require github.com/hallelx2/pdftable v0.0.0 +require ( + github.com/clipperhouse/uax29/v2 v2.7.0 // indirect + github.com/hhrutter/lzw v1.0.0 // indirect + github.com/hhrutter/pkcs7 v0.2.2 // indirect + github.com/hhrutter/tiff v1.0.3 // indirect + github.com/mattn/go-runewidth v0.0.23 // indirect + github.com/pdfcpu/pdfcpu v0.12.1 // indirect + github.com/pkg/errors v0.9.1 // indirect + golang.org/x/crypto v0.50.0 // indirect + golang.org/x/image v0.39.0 // indirect + golang.org/x/text v0.36.0 // indirect + gopkg.in/yaml.v2 v2.4.0 // indirect +) + replace github.com/hallelx2/pdftable => ../ diff --git a/bench/go.sum b/bench/go.sum new file mode 100644 index 0000000..eb36f86 --- /dev/null +++ b/bench/go.sum @@ -0,0 +1,24 @@ +github.com/clipperhouse/uax29/v2 v2.7.0 h1:+gs4oBZ2gPfVrKPthwbMzWZDaAFPGYK72F0NJv2v7Vk= +github.com/clipperhouse/uax29/v2 v2.7.0/go.mod h1:EFJ2TJMRUaplDxHKj1qAEhCtQPW2tJSwu5BF98AuoVM= +github.com/hhrutter/lzw v1.0.0 h1:laL89Llp86W3rRs83LvKbwYRx6INE8gDn0XNb1oXtm0= +github.com/hhrutter/lzw v1.0.0/go.mod h1:2HC6DJSn/n6iAZfgM3Pg+cP1KxeWc3ezG8bBqW5+WEo= +github.com/hhrutter/pkcs7 v0.2.2 h1:xMoifoVWah1LNym3C0pomEiLmyJyVIBXt/8oTPyPz+8= +github.com/hhrutter/pkcs7 v0.2.2/go.mod h1:aEzKz0+ZAlz7YaEMY47jDHL14hVWD6iXt0AgqgAvWgE= +github.com/hhrutter/tiff v1.0.3 h1:POV5xITOE1Lt5FvP24ylft0LyCmHmc8GkJ1SVlvUyk0= +github.com/hhrutter/tiff v1.0.3/go.mod h1:zZDLVY4cp9za2FLrryAaGszwWYAUM6DrRiBR0l//mxA= +github.com/mattn/go-runewidth v0.0.23 h1:7ykA0T0jkPpzSvMS5i9uoNn2Xy3R383f9HDx3RybWcw= +github.com/mattn/go-runewidth v0.0.23/go.mod h1:XBkDxAl56ILZc9knddidhrOlY5R/pDhgLpndooCuJAs= +github.com/pdfcpu/pdfcpu v0.12.1 h1:HwoN72zJCj+pPbfMDChYBTZrT7SY0VwgUzqeaId3I20= +github.com/pdfcpu/pdfcpu v0.12.1/go.mod h1:7KPpVLMavcpliPrtN6o7Kuk3cFtYq8nii3SJnnsK7ps= +github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= +github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= +golang.org/x/crypto v0.50.0 h1:zO47/JPrL6vsNkINmLoo/PH1gcxpls50DNogFvB5ZGI= +golang.org/x/crypto v0.50.0/go.mod h1:3muZ7vA7PBCE6xgPX7nkzzjiUq87kRItoJQM1Yo8S+Q= +golang.org/x/image v0.39.0 h1:skVYidAEVKgn8lZ602XO75asgXBgLj9G/FE3RbuPFww= +golang.org/x/image v0.39.0/go.mod h1:sIbmppfU+xFLPIG0FoVUTvyBMmgng1/XAMhQ2ft0hpA= +golang.org/x/text v0.36.0 h1:JfKh3XmcRPqZPKevfXVpI1wXPTqbkE5f7JA92a55Yxg= +golang.org/x/text v0.36.0/go.mod h1:NIdBknypM8iqVmPiuco0Dh6P5Jcdk8lJL0CUebqK164= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY= +gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= diff --git a/bench/icdar2013/__pycache__/score.cpython-313.pyc b/bench/icdar2013/__pycache__/score.cpython-313.pyc deleted file mode 100644 index 678893b..0000000 Binary files a/bench/icdar2013/__pycache__/score.cpython-313.pyc and /dev/null differ diff --git a/bench/icdar2013/extract.go b/bench/icdar2013/extract.go index 255cd04..a6f7cac 100644 --- a/bench/icdar2013/extract.go +++ b/bench/icdar2013/extract.go @@ -6,6 +6,7 @@ import ( "flag" "fmt" "os" + "strconv" "github.com/hallelx2/pdftable" ) @@ -15,9 +16,18 @@ type tableOut struct { Rows [][]string `json:"rows"` } +// edgeSet is one page's explicit row/column boundaries, in PDF points. +type edgeSet struct { + V []float64 `json:"v"` + H []float64 `json:"h"` +} + func main() { - strategy := flag.String("strategy", "lines", "lines | text | fallback") + strategy := flag.String("strategy", "lines", + "lines | text | mixed | auto | lines-then-mixed | fallback") merge := flag.Bool("merge", false, "TableSettings.MergeSplitTokens") + oracle := flag.String("oracle", "", + `JSON of per-page explicit edges: {"1":{"v":[..],"h":[..]}}`) flag.Parse() doc, err := pdftable.OpenFile(flag.Arg(0)) @@ -38,27 +48,37 @@ func main() { } lines := mk(pdftable.StrategyLines, pdftable.StrategyLines) text := mk(pdftable.StrategyText, pdftable.StrategyText) - // "mixed" is the booktabs case: horizontal rules give the rows, word - // alignment gives the columns. A table ruled only horizontally has no - // ruling intersections at all, so pure "lines" cannot see it. + // alignment gives the columns. mixed := mk(pdftable.StrategyText, pdftable.StrategyLines) auto := mk(pdftable.StrategyAuto, pdftable.StrategyAuto) var attempts []pdftable.TableSettings switch *strategy { - case "lines": - attempts = []pdftable.TableSettings{lines} case "text": attempts = []pdftable.TableSettings{text} case "mixed": attempts = []pdftable.TableSettings{mixed} - case "lines-then-mixed": - attempts = []pdftable.TableSettings{lines, mixed} case "auto": attempts = []pdftable.TableSettings{auto} - default: // fallback: ruled cells first, whitespace alignment if none + case "lines-then-mixed": + attempts = []pdftable.TableSettings{lines, mixed} + case "fallback": attempts = []pdftable.TableSettings{lines, text} + default: + attempts = []pdftable.TableSettings{lines} + } + + // Oracle mode: the caller supplies the row/column boundaries and + // pdftable only fills the cells. This is exactly the shape of the + // hybrid a layout model would drive — and fed GROUND-TRUTH edges it + // measures the ceiling that hybrid can reach: how good extraction gets + // if detection and gridding were solved perfectly. + var oracleEdges map[string]edgeSet + if *oracle != "" { + if b, err := os.ReadFile(*oracle); err == nil { + _ = json.Unmarshal(b, &oracleEdges) + } } out := []tableOut{} @@ -67,6 +87,23 @@ func main() { if err != nil { continue } + + if oracleEdges != nil { + e, ok := oracleEdges[strconv.Itoa(i)] + if !ok || len(e.V) < 2 || len(e.H) < 2 { + continue + } + s := mk(pdftable.StrategyExplicit, pdftable.StrategyExplicit) + s.ExplicitVerticalLines = e.V + s.ExplicitHorizontalLines = e.H + if tables, err := p.ExtractTables(s); err == nil { + for _, t := range tables { + out = append(out, tableOut{Page: i, Rows: t.Rows}) + } + } + continue + } + for _, s := range attempts { tables, err := p.ExtractTables(s) if err != nil || len(tables) == 0 {