dsr 0.0.0 → 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (5) hide show
  1. checksums.yaml +4 -4
  2. data/dsr.gemspec +4 -5
  3. data/lib/dsr/recognizer.rb +147 -0
  4. data/lib/dsr.rb +38 -8
  5. metadata +12 -25
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: e94dd71eedb7e3989dde0e7a35c73db47c97891211ab3620ef668bf6fb163dcb
4
- data.tar.gz: af6324245ad6c157dc3a700f00121860c75bb828dc096ffcd4a4c1e15ae0dd96
3
+ metadata.gz: 621fa913c6e55e6c86613100c173069d3dded944de48c3d930b0852ee70733e8
4
+ data.tar.gz: a5086a089ea4da28aa6b5547d55718a87c292e388357b6d1df2943f885ef1347
5
5
  SHA512:
6
- metadata.gz: 1a607b1cfaaec5321348face51201ec20baa16958775485f3a86fe67a1eb703be6a58bcf555045dbb181397644eb42240fbe28a38b4dd79f361de4a287e23897
7
- data.tar.gz: 66fefe0318e13f2b2d60f60dd125af1577313ac038d1fc91215016fac21066f91abbb58ce91a6f1eabfcb2b39513c800f563cac567f3539e162b1ec05cfddf10
6
+ metadata.gz: 11af653e502854581722067ac9070e4234f20f77969f4ca30aa364e8b717b7aea1a82b5aee9105985ebf7090c8516167e32d147a058f73dab50c1f2373af7c64
7
+ data.tar.gz: 428a9d20a0df7c3310ca70c91fe88283888a5c9460fbed434d6f1ab90fd3f6dbc8e9de1e9327ed3ed4103e665cf78e8c83b24402ff6d0231adaaa42ba987196e
data/dsr.gemspec CHANGED
@@ -1,15 +1,14 @@
1
1
  Gem::Specification.new do |spec|
2
2
  spec.name = "dsr"
3
- spec.version = "0.0.0"
4
- spec.summary = "[WIP] Document Structure Recognizer -- currently a collection of common routines I use to build A.I.s"
3
+ spec.version = "1.0.0"
4
+ spec.summary = "Document Structure Recognizer -- a collection of common routines to build AIs"
5
5
 
6
6
  spec.author = "Victor Maslov aka Nakilon"
7
7
  spec.email = "nakilon@gmail.com"
8
8
  spec.license = "MIT"
9
9
  spec.metadata = {"source_code_uri" => "https://github.com/nakilon/dsr"}
10
10
 
11
- spec.add_dependency "nakischema"
12
- spec.add_dependency "hexapdf"
11
+ spec.add_dependency "pcbr"
13
12
 
14
- spec.files = %w{ LICENSE dsr.gemspec lib/dsr.rb }
13
+ spec.files = %w{ LICENSE dsr.gemspec lib/dsr.rb lib/dsr/recognizer.rb }
15
14
  end
@@ -0,0 +1,147 @@
1
+ using(
2
+ ::Module.new do
3
+ AssertOneError = ::Class.new ::RuntimeError
4
+ refine ::Array do
5
+ def mean &block
6
+ sum(&block).fdiv size
7
+ end
8
+ def assert_one
9
+ return at 0 if 1 == size
10
+ raise ::Common::AssertOneError, "size: #{size}"
11
+ end
12
+ end
13
+ end
14
+ )
15
+
16
+ module DSR
17
+ module Recognizer
18
+
19
+ # joins two arrays of objects into a Hash of parents and children, sorted by passed block
20
+ # it is possible for parent to have no children
21
+ # examples:
22
+ # Discord search results HTML page -- based on `node.rect.y`, channel names become keys, messages become values
23
+ # @param keys [Array] these become keys
24
+ # @param values [Array] these become values
25
+ # @return [Hash{Object => Array<Object>}
26
+ def self.detect_many_after_many keys, values, &block
27
+ fail if keys.any?(&:nil?) # for 'if key' in the algorithm loop
28
+ # fail if keys.dup.uniq!
29
+ # fail if values.dup.uniq!
30
+ fail if (keys + values).map(&block).uniq!
31
+ key = nil
32
+ h = {}
33
+ vs = []
34
+ for e, from in (
35
+ keys.map{ |_| [_, :from_keys] } +
36
+ values.map{ |_| [_, :from_values] }
37
+ ).sort_by{ |e,| block.call e }
38
+ if :from_keys == from
39
+ h[key] = vs if key
40
+ key, vs = e, []
41
+ else
42
+ fail unless key
43
+ vs.push e
44
+ end
45
+ end
46
+ h[key] = vs if key
47
+ h
48
+ end
49
+
50
+ # sorts an array by a block and splits in chunks
51
+ # gauranteed to split into at least two chunks
52
+ # if you pass `ratio` then recursively splits if there is another `gap_size > prev_gap_size * ratio`
53
+ # if you pass `goal` then tries ratios from 0.0 to 1.0 to find the goal number of chunks
54
+ # if you pass nothing then it uses PCBR to chose the ratio
55
+ # for both `goal` and nothing variants it checks the result to be the same for all fitting ratios
56
+ # `ratio: 0` is effectively the same as just Ruby stdlib sort and chunk by block
57
+ # @param _array [Array<Object>]
58
+ # @param ratio [Float]
59
+ # @param goal [Integer]
60
+ # @yieldparam item [Object] an item of `_array`
61
+ # @yieldreturn [Numeric] something that can subtract from each other to measure a gap
62
+ def self.detect_distant_groups _array, goal: nil, ratio: nil
63
+
64
+ array = ::Struct.new(:item, :value, :i, :dist_left).then do |struct|
65
+ _array.map{ |e| struct.new e, yield(e) }.sort_by(&:value)
66
+ end
67
+ array.each_with_index{ |e, i| e.i = i }
68
+ array.each_cons(2){ |e1, e2| e2.dist_left = e2.value - e1.value }
69
+ f = lambda do |from, to, r: ratio, threshold: 0|
70
+ # next [] if from > to
71
+ next [[array[from]]] if from == to
72
+ max = array[from + 1..to].max_by(&:dist_left)
73
+ # next [] unless max
74
+ if threshold * r < max.dist_left
75
+ [*f[from, max.i - 1, r: r, threshold: max.dist_left], *f[max.i, to, r: r, threshold: max.dist_left]]
76
+ else
77
+ [array[from..to]]
78
+ end
79
+ end
80
+
81
+ if ratio
82
+ f[0, array.size - 1].map{ |_| _.map &:item }
83
+ elsif goal
84
+ 0.step(1, 0.01).map do |r|
85
+ f[0, array.size - 1, r: r].map{ |_| _.map &:item }
86
+ end.tap do |ratios|
87
+ fail unless ratios.first.size == array.map(&:value).uniq.size
88
+ ratios.each_cons(2){ |a, b| fail if a.size < b.size }
89
+ end.select{ |_| _.size == goal }.tap do |splits|
90
+ fail if 1 < splits.uniq.size
91
+ end.first
92
+ else
93
+ require "pcbr"
94
+ pcbr = ::PCBR.new
95
+ 0.step(1, 0.01).map.with_index do |r, i|
96
+ [i, f[0, array.size - 1, r: r].map{ |_| _.map &:item }]
97
+ end.tap do |ratios|
98
+ fail unless ratios.first.last.size == array.map(&:value).uniq.size
99
+ ratios.each_cons(2){ |(_, a), (_, b)| fail if a.size < b.size }
100
+ end.chunk{ |_, split| split.size }.each do |_, splits|
101
+ # v = [splits.first.first, splits.size]
102
+ v = [
103
+ splits.first.first,
104
+ -splits.map(&:last).mean do |split|
105
+ split.map(&:size).then do |sizes|
106
+ sizes.map do |size|
107
+ (sizes.max - sizes.min).then do |_|
108
+ # otherwise it makes Infinity, then NaN
109
+ _.zero? ? size : size.fdiv(_)
110
+ end
111
+ end.then do |sizes|
112
+ # σ²
113
+ sizes.mean{ |_| (_ - sizes.mean) ** 2 }
114
+ end
115
+ end
116
+ end
117
+ ]
118
+ pcbr.store splits.map(&:last), v
119
+ end
120
+ (best, _, score) = pcbr.table.reverse.max_by(&:last)
121
+ fail unless 0 < score
122
+ fail if 1 < best.uniq.size
123
+ best.first
124
+ end
125
+
126
+ end
127
+
128
+ # https://en.wikipedia.org/wiki/Injective_function
129
+ # picks the closest item from `codomain` for each `domain` element
130
+ # raises exception if picks intersect
131
+ # @param domain [Array]
132
+ # @param codomain [Array]
133
+ # @return [Array<Array(Object, Object)>]
134
+ # @yieldparam domain_item, codomain_item [Object]
135
+ # @yieldreturn [Numeric] distance
136
+ def self.injective_mapping domain, codomain
137
+ domain.map do |a|
138
+ [a, codomain.each_index.group_by{ |i| yield a, codomain[i] }.min.last.assert_one]
139
+ end.tap do |_|
140
+ fail if _.map(&:last).uniq!
141
+ end.map do |a, b|
142
+ [a, codomain[b]]
143
+ end
144
+ end
145
+
146
+ end
147
+ end
data/lib/dsr.rb CHANGED
@@ -1,11 +1,13 @@
1
1
  module DSR
2
2
 
3
- Struct = ::Struct.new :text, :left, :bottom, :right, :top, :width, :height
4
- private_constant :Struct
3
+ StructLinkable = ::Struct.new :ref, :left, :bottom, :right, :top
4
+ private_constant :StructLinkable
5
+ StructWithText = ::Struct.new :text, *StructLinkable.members.drop(1), :width, :height
6
+ private_constant :StructWithText
5
7
 
6
8
  class Texts < Array
7
- def find_all_by_text text
8
- self.class.new select{ |_| text == _.text }
9
+ def find_all text_or_regex
10
+ self.class.new select{ |_| text_or_regex === _.text }
9
11
  end
10
12
  def select_intersecting_vertically_with item
11
13
  self.class.new (self-[item]).select{ |_| _.bottom >= item.top && _.top <= item.bottom }
@@ -41,7 +43,7 @@ module DSR
41
43
  }
42
44
  }
43
45
  end["textAnnotations"].map do |text|
44
- Struct.new text["description"],
46
+ StructWithText.new text["description"],
45
47
  text["boundingPoly"]["vertices"].map{ |_| _["x"] }.min,
46
48
  text["boundingPoly"]["vertices"].map{ |_| _["y"] }.max,
47
49
  text["boundingPoly"]["vertices"].map{ |_| _["x"] }.max,
@@ -51,6 +53,7 @@ module DSR
51
53
  end )
52
54
  end
53
55
 
56
+ # TODO: refactor or deprecate?
54
57
  def self.link headers, array, direction, alignment, *priority
55
58
  l, r = case direction
56
59
  when :horizontal ; %i{ left right }
@@ -59,8 +62,8 @@ module DSR
59
62
  end
60
63
  headers = headers.sort_by(&l).map(&:dup)
61
64
  headers.each_cons(2){ |a, b| a[r], b[l] = [a[r], b[l]].max, [a[r], b[l]].min }
62
- headers.first[l] = -Float::INFINITY
63
- headers.last[r] = +Float::INFINITY
65
+ headers.first[l] = -::Float::INFINITY
66
+ headers.last[r] = +::Float::INFINITY
64
67
  headers.unshift headers.delete_at headers.index{ |_| priority.include? _.text } unless priority.empty? # TODO: document/explain this
65
68
  array.sort_by(&l).each_with_object([]) do |cell, a|
66
69
  i = headers.public_send(alignment){ |_| (_[l].._[r]).include?((cell[l]+cell[r])/2) }
@@ -68,6 +71,7 @@ module DSR
68
71
  a[i] << cell
69
72
  end
70
73
  end
74
+
71
75
  def self.pdf2struct object
72
76
  require "hexapdf"
73
77
  processor = Class.new HexaPDF::Content::Processor do
@@ -78,7 +82,7 @@ module DSR
78
82
  end
79
83
  def show_text str
80
84
  boxes = decode_text_with_positioning str
81
- @texts.push Struct.new boxes.string,
85
+ @texts.push StructWithText.new boxes.string,
82
86
  boxes.lower_left[0], -boxes.lower_left[1],
83
87
  boxes.upper_right[0], -boxes.upper_right[1],
84
88
  boxes.upper_right[0] - boxes.lower_left[0],
@@ -103,4 +107,30 @@ module DSR
103
107
  end.reject &:empty?
104
108
  end
105
109
 
110
+ def self.nodes2struct nodes
111
+ nodes.map do |node|
112
+ StructLinkable.new(node, *::JSON.load(node.page.evaluate(<<~HEREDOC, node)))
113
+ ( function(node) {
114
+ var x = scrollX, y = scrollY;
115
+ var rect = JSON.parse(JSON.stringify(node.getBoundingClientRect()));
116
+ rect.top += scrollY;
117
+ rect.left += scrollX;
118
+ var t = JSON.stringify( [rect.left, rect.bottom, rect.right, rect.top] );
119
+ return t;
120
+ } )(arguments[0])
121
+ HEREDOC
122
+ end
123
+ end
124
+
125
+ def self.capybara2struct nodes
126
+ nodes.map do |node|
127
+ rect = node.rect
128
+ StructLinkable.new node,
129
+ rect.x,
130
+ rect.y + rect.height,
131
+ rect.x + rect.width,
132
+ rect.y
133
+ end
134
+ end
135
+
106
136
  end
metadata CHANGED
@@ -1,17 +1,17 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: dsr
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.0.0
4
+ version: 1.0.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Victor Maslov aka Nakilon
8
- autorequire:
8
+ autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2023-08-16 00:00:00.000000000 Z
11
+ date: 2026-07-30 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
- name: nakischema
14
+ name: pcbr
15
15
  requirement: !ruby/object:Gem::Requirement
16
16
  requirements:
17
17
  - - ">="
@@ -24,21 +24,7 @@ dependencies:
24
24
  - - ">="
25
25
  - !ruby/object:Gem::Version
26
26
  version: '0'
27
- - !ruby/object:Gem::Dependency
28
- name: hexapdf
29
- requirement: !ruby/object:Gem::Requirement
30
- requirements:
31
- - - ">="
32
- - !ruby/object:Gem::Version
33
- version: '0'
34
- type: :runtime
35
- prerelease: false
36
- version_requirements: !ruby/object:Gem::Requirement
37
- requirements:
38
- - - ">="
39
- - !ruby/object:Gem::Version
40
- version: '0'
41
- description:
27
+ description:
42
28
  email: nakilon@gmail.com
43
29
  executables: []
44
30
  extensions: []
@@ -47,12 +33,13 @@ files:
47
33
  - LICENSE
48
34
  - dsr.gemspec
49
35
  - lib/dsr.rb
50
- homepage:
36
+ - lib/dsr/recognizer.rb
37
+ homepage:
51
38
  licenses:
52
39
  - MIT
53
40
  metadata:
54
41
  source_code_uri: https://github.com/nakilon/dsr
55
- post_install_message:
42
+ post_install_message:
56
43
  rdoc_options: []
57
44
  require_paths:
58
45
  - lib
@@ -67,9 +54,9 @@ required_rubygems_version: !ruby/object:Gem::Requirement
67
54
  - !ruby/object:Gem::Version
68
55
  version: '0'
69
56
  requirements: []
70
- rubygems_version: 3.3.25
71
- signing_key:
57
+ rubygems_version: 3.2.0
58
+ signing_key:
72
59
  specification_version: 4
73
- summary: "[WIP] Document Structure Recognizer -- currently a collection of common
74
- routines I use to build A.I.s"
60
+ summary: Document Structure Recognizer -- a collection of common routines to build
61
+ AIs
75
62
  test_files: []