invoice_extractor_arca 0.2.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 (48) hide show
  1. checksums.yaml +7 -0
  2. data/CHANGELOG.md +49 -0
  3. data/LICENSE.txt +21 -0
  4. data/README.md +631 -0
  5. data/exe/arca-invoice-extract +6 -0
  6. data/lib/invoice_extractor_arca/catalogs/copy_types.json +5 -0
  7. data/lib/invoice_extractor_arca/catalogs/currencies.json +65 -0
  8. data/lib/invoice_extractor_arca/catalogs/document_types.json +40 -0
  9. data/lib/invoice_extractor_arca/catalogs/iva_types.json +13 -0
  10. data/lib/invoice_extractor_arca/catalogs/payment_methods.json +10 -0
  11. data/lib/invoice_extractor_arca/catalogs/units_measure.json +47 -0
  12. data/lib/invoice_extractor_arca/catalogs/voucher_types.json +99 -0
  13. data/lib/invoice_extractor_arca/catalogs.rb +34 -0
  14. data/lib/invoice_extractor_arca/cli.rb +247 -0
  15. data/lib/invoice_extractor_arca/command_runner.rb +18 -0
  16. data/lib/invoice_extractor_arca/configuration.rb +42 -0
  17. data/lib/invoice_extractor_arca/dependencies/resolver.rb +118 -0
  18. data/lib/invoice_extractor_arca/error.rb +6 -0
  19. data/lib/invoice_extractor_arca/error_record.rb +39 -0
  20. data/lib/invoice_extractor_arca/errors.rb +34 -0
  21. data/lib/invoice_extractor_arca/extractor.rb +381 -0
  22. data/lib/invoice_extractor_arca/final_payload_builder.rb +67 -0
  23. data/lib/invoice_extractor_arca/ocr/rapidocr_extract.py +198 -0
  24. data/lib/invoice_extractor_arca/ocr/rapidocr_text_extractor.rb +117 -0
  25. data/lib/invoice_extractor_arca/pdf/ocr_text_extractor.rb +131 -0
  26. data/lib/invoice_extractor_arca/pdf/renderer.rb +97 -0
  27. data/lib/invoice_extractor_arca/pdf/text_extractor.rb +182 -0
  28. data/lib/invoice_extractor_arca/qr/payload_parser.rb +124 -0
  29. data/lib/invoice_extractor_arca/qr/reader.rb +53 -0
  30. data/lib/invoice_extractor_arca/qr/result_builder.rb +229 -0
  31. data/lib/invoice_extractor_arca/qr_extractor.rb +156 -0
  32. data/lib/invoice_extractor_arca/result.rb +94 -0
  33. data/lib/invoice_extractor_arca/scanners/factory.rb +88 -0
  34. data/lib/invoice_extractor_arca/scanners/qreader_scan.py +102 -0
  35. data/lib/invoice_extractor_arca/scanners/qreader_scanner.rb +131 -0
  36. data/lib/invoice_extractor_arca/scanners/scanner_chain.rb +73 -0
  37. data/lib/invoice_extractor_arca/scanners/zbar_scanner.rb +52 -0
  38. data/lib/invoice_extractor_arca/text/invoice_parser.rb +517 -0
  39. data/lib/invoice_extractor_arca/text/layout_table_parser.rb +218 -0
  40. data/lib/invoice_extractor_arca/text/locale_value_normalizer.rb +53 -0
  41. data/lib/invoice_extractor_arca/text/normalizer.rb +31 -0
  42. data/lib/invoice_extractor_arca/validation/reconciler.rb +53 -0
  43. data/lib/invoice_extractor_arca/version.rb +5 -0
  44. data/lib/invoice_extractor_arca/voucher_validator.rb +169 -0
  45. data/lib/invoice_extractor_arca.rb +117 -0
  46. data/requirements-ocr.txt +2 -0
  47. data/sig/invoice_extractor_arca.rbs +4 -0
  48. metadata +135 -0
@@ -0,0 +1,218 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "../error_record"
4
+
5
+ module InvoiceExtractorArca
6
+ module Text
7
+ class LayoutTableParser
8
+ HEADERS = [
9
+ [:code, /C[oó]digo/i],
10
+ [:description, /Producto\s*\/\s*Servicio/i],
11
+ [:quantity, /Cantidad/i],
12
+ [:unit_measure, /U\.\s*Medida/i],
13
+ [:unit_price, /Precio\s+Unit\./i],
14
+ [:discount_percentage, /%\s*Bonif\.?/i],
15
+ [:discount_total, /Imp\.\s*Bonif\.?/i],
16
+ [:subtotal, /Subtotal/i]
17
+ ].freeze
18
+
19
+ NUMERIC_FIELDS = %i[
20
+ quantity
21
+ unit_price
22
+ discount_percentage
23
+ discount_total
24
+ subtotal
25
+ ].freeze
26
+
27
+ NUMERIC_PATTERN =
28
+ /\A-?\d+(?:\.\d{3})*(?:,\d+)?\z/
29
+
30
+ def initialize(value_normalizer:)
31
+ @value_normalizer = value_normalizer
32
+ end
33
+
34
+ def parse(raw)
35
+ lines = raw.to_s.lines.map(&:chomp)
36
+ header_index, positions = find_header(lines)
37
+
38
+ unless header_index
39
+ return failure(
40
+ "enrichment_service_table_not_found",
41
+ "Service table header was not found"
42
+ )
43
+ end
44
+
45
+ body = table_body(lines, header_index)
46
+
47
+ {
48
+ rows: build_rows(body, positions),
49
+ diagnostics: []
50
+ }
51
+ end
52
+
53
+ private
54
+
55
+ def find_header(lines)
56
+ lines.each_with_index do |line, index|
57
+ positions = header_positions(line)
58
+ return [index, positions] if positions
59
+ end
60
+
61
+ [nil, nil]
62
+ end
63
+
64
+ def header_positions(line)
65
+ positions = {}
66
+
67
+ HEADERS.each do |name, pattern|
68
+ match = line.match(pattern)
69
+ return nil unless match
70
+
71
+ positions[name] = match.begin(0)
72
+ end
73
+
74
+ offsets = positions.values
75
+ return nil unless offsets == offsets.sort
76
+ return nil unless offsets.uniq.length == offsets.length
77
+
78
+ positions
79
+ end
80
+
81
+ def table_body(lines, header_index)
82
+ lines[(header_index + 1)..].to_a.take_while do |line|
83
+ !line.match?(/^\s*Subtotal\s*:\s*\$/i) &&
84
+ !line.match?(/Importe\s+Otros\s+Tributos\s*:\s*\$/i) &&
85
+ !line.match?(/Importe\s+Total\s*:\s*\$/i)
86
+ end
87
+ end
88
+
89
+ def build_rows(lines, positions)
90
+ rows = []
91
+ current = nil
92
+
93
+ lines.each do |line|
94
+ cells = slice_line(line, positions)
95
+
96
+ if row_start?(cells)
97
+ rows << finalize_row(current) if current
98
+ current = cells
99
+ elsif current && continuation_line?(cells)
100
+ append_description(current, cells[:description])
101
+ end
102
+ end
103
+
104
+ rows << finalize_row(current) if current
105
+ rows
106
+ end
107
+
108
+ def slice_line(line, positions)
109
+ names = positions.keys
110
+
111
+ cells =
112
+ names.each_with_index.each_with_object({}) do |(name, index), result|
113
+ from = positions.fetch(name)
114
+ next_name = names[index + 1]
115
+ to = next_name ? positions.fetch(next_name) : nil
116
+
117
+ result[name] = line[from...to].to_s.rstrip
118
+ end
119
+
120
+ repair_description_overlap(cells)
121
+ end
122
+
123
+ def row_start?(cells)
124
+ required = %i[
125
+ quantity
126
+ unit_price
127
+ discount_percentage
128
+ discount_total
129
+ subtotal
130
+ ]
131
+
132
+ required.all? do |name|
133
+ numeric?(cells.fetch(name))
134
+ end
135
+ end
136
+
137
+ def continuation_line?(cells)
138
+ description_present = !cells[:description].to_s.strip.empty?
139
+
140
+ numeric_cells_empty =
141
+ NUMERIC_FIELDS.all? do |name|
142
+ cells.fetch(name).to_s.strip.empty?
143
+ end
144
+
145
+ description_present && numeric_cells_empty
146
+ end
147
+
148
+ def append_description(row, continuation)
149
+ text = continuation.to_s.strip
150
+ return if text.empty?
151
+
152
+ row[:description] =
153
+ [row[:description].to_s.strip, text]
154
+ .reject(&:empty?)
155
+ .join(" ")
156
+ end
157
+
158
+ def finalize_row(cells)
159
+ cells.each_with_object({}) do |(name, raw), row|
160
+ cleaned = raw.to_s.strip
161
+ normalized = normalize_cell(name, cleaned)
162
+
163
+ row[name] = {
164
+ raw: cleaned,
165
+ normalized: normalized,
166
+ source: "pdf_text",
167
+ method: "layout_column",
168
+ confidence: confidence_for(name, cleaned, normalized)
169
+ }
170
+ end
171
+ end
172
+
173
+ def normalize_cell(name, value)
174
+ return @value_normalizer.decimal(value) if NUMERIC_FIELDS.include?(name)
175
+
176
+ value
177
+ end
178
+
179
+ def confidence_for(name, raw, normalized)
180
+ return "high" unless NUMERIC_FIELDS.include?(name)
181
+ return "low" if raw.empty? || normalized.nil?
182
+
183
+ "high"
184
+ end
185
+
186
+ def numeric?(value)
187
+ value.to_s.strip.match?(NUMERIC_PATTERN)
188
+ end
189
+
190
+ def failure(code, message)
191
+ {
192
+ rows: [],
193
+ diagnostics: [
194
+ ErrorRecord.new(
195
+ code: code,
196
+ message: message,
197
+ severity: "warning",
198
+ component: "enrichment"
199
+ )
200
+ ]
201
+ }
202
+ end
203
+
204
+ def repair_description_overlap(cells)
205
+ code = cells[:code].to_s.strip
206
+ description = cells[:description].to_s
207
+
208
+ if code.match?(/\A[[:alpha:]]\z/) &&
209
+ description.match?(/\A[[:lower:]]/)
210
+ cells[:description] = "#{code}#{description}"
211
+ cells[:code] = ""
212
+ end
213
+
214
+ cells
215
+ end
216
+ end
217
+ end
218
+ end
@@ -0,0 +1,53 @@
1
+ # lib/invoice_extractor_arca/text/locale_value_normalizer.rb
2
+ # frozen_string_literal: true
3
+
4
+ require "bigdecimal"
5
+ require "date"
6
+
7
+ module InvoiceExtractorArca
8
+ module Text
9
+ class LocaleValueNormalizer
10
+ def date(value)
11
+ Date.strptime(value.to_s.strip, "%d/%m/%Y").iso8601
12
+ rescue Date::Error
13
+ nil
14
+ end
15
+
16
+ def digits(value)
17
+ result = value.to_s.gsub(/\D/, "")
18
+ result.empty? ? nil : result
19
+ end
20
+
21
+ def code(value, width)
22
+ result = digits(value)
23
+ return nil unless result && result.length <= width
24
+
25
+ result.rjust(width, "0")
26
+ end
27
+
28
+ def decimal(value)
29
+ input = value.to_s.strip.delete(" ")
30
+ return nil unless input
31
+
32
+ normalized =
33
+ if input.include?(",")
34
+ input.delete(".").sub(",", ".")
35
+ else
36
+ input
37
+ end
38
+
39
+ return nil unless normalized.match?(/\A-?\d+(?:\.\d+)?\z/)
40
+
41
+ format_decimal(BigDecimal(normalized), 2)
42
+ end
43
+
44
+ def format_decimal(value, scale)
45
+ string = value.round(scale).to_s("F")
46
+ whole, fraction = string.split(".", 2)
47
+ fraction = fraction.to_s.ljust(scale, "0")[0, scale]
48
+
49
+ "#{whole}.#{fraction}"
50
+ end
51
+ end
52
+ end
53
+ end
@@ -0,0 +1,31 @@
1
+ # frozen_string_literal: true
2
+
3
+ module InvoiceExtractorArca
4
+ module Text
5
+ class Normalizer
6
+ def normalize(text)
7
+ normalized =
8
+ text
9
+ .to_s
10
+ .encode(
11
+ "UTF-8",
12
+ invalid: :replace,
13
+ undef: :replace,
14
+ replace: ""
15
+ )
16
+ .delete("\u0000")
17
+ .gsub(/\r\n?/, "\n")
18
+ .gsub("\f", "\n\n")
19
+
20
+ normalized =
21
+ normalized
22
+ .lines
23
+ .map(&:rstrip)
24
+ .join("\n")
25
+ .gsub(/\n{3,}/, "\n\n")
26
+
27
+ normalized.strip
28
+ end
29
+ end
30
+ end
31
+ end
@@ -0,0 +1,53 @@
1
+ # lib/invoice_extractor_arca/validation/reconciler.rb
2
+ # frozen_string_literal: true
3
+
4
+ module InvoiceExtractorArca
5
+ module Validation
6
+ class Reconciler
7
+ MAPPINGS = {
8
+ voucher_type_code: :voucher_type_code,
9
+ selling_point: :point_of_sale,
10
+ invoice_number: :voucher_number,
11
+ emission_date: :issued_on,
12
+ biller_cuit: :issuer_cuit,
13
+ recipient_document_type: :recipient_document_type_description,
14
+ recipient_document_number: :recipient_document_number,
15
+ total: :amount,
16
+ authorization_code: :authorization_code
17
+ }.freeze
18
+
19
+ def call(voucher:, enrichment:, parsing_warnings: [])
20
+ comparisons = MAPPINGS.map do |text_field, voucher_field|
21
+ text_value = enrichment.dig(text_field, :normalized)
22
+ qr_value = voucher[voucher_field]
23
+
24
+ status =
25
+ if text_value.nil?
26
+ "not_extracted"
27
+ elsif text_value == qr_value
28
+ "match"
29
+ else
30
+ "mismatch"
31
+ end
32
+
33
+ {
34
+ field: text_field.to_s,
35
+ status: status,
36
+ qr_value: qr_value,
37
+ text_value: text_value,
38
+ severity: (status == "mismatch") ? "warning" : "info"
39
+ }
40
+ end
41
+
42
+ warning =
43
+ parsing_warnings.any? ||
44
+ comparisons.any? { |comparison| comparison[:status] == "mismatch" }
45
+
46
+ {
47
+ status: warning ? "warning" : "ok",
48
+ comparisons: comparisons
49
+ }
50
+ end
51
+ end
52
+ end
53
+ end
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ module InvoiceExtractorArca
4
+ VERSION = "0.2.0"
5
+ end
@@ -0,0 +1,169 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "bigdecimal"
4
+ require "date"
5
+ require_relative "catalogs"
6
+ require_relative "errors"
7
+
8
+ module InvoiceExtractorArca
9
+ class VoucherValidator
10
+ REQUIRED_FIELDS = %w[
11
+ ver
12
+ fecha
13
+ cuit
14
+ ptoVta
15
+ tipoCmp
16
+ nroCmp
17
+ importe
18
+ moneda
19
+ ctz
20
+ tipoDocRec
21
+ nroDocRec
22
+ tipoCodAut
23
+ codAut
24
+ ].freeze
25
+
26
+ def validate_and_normalize(original)
27
+ validate_hash!(original)
28
+ validate_required_fields!(original)
29
+
30
+ issued_on = normalize_date(original["fecha"])
31
+ issuer_cuit = digits(original["cuit"], expected: 11)
32
+ point_of_sale = normalize_code(original["ptoVta"], 5)
33
+ voucher_type_code = normalize_code(original["tipoCmp"], 3)
34
+ voucher_number = normalize_code(original["nroCmp"], 8)
35
+ currency_code = original["moneda"].to_s
36
+ recipient_document_type_code = original["tipoDocRec"].to_s
37
+ recipient_document_number = digits(original["nroDocRec"], max: 20)
38
+ authorization_code = digits(original["codAut"], expected: 14)
39
+
40
+ voucher_type_description =
41
+ catalog_value!(
42
+ Catalogs::VOUCHER_TYPES,
43
+ voucher_type_code,
44
+ "tipoCmp"
45
+ )
46
+
47
+ currency_description =
48
+ catalog_value!(
49
+ Catalogs::CURRENCIES,
50
+ currency_code,
51
+ "moneda"
52
+ )
53
+
54
+ recipient_document_type_description =
55
+ catalog_value!(
56
+ Catalogs::DOCUMENT_TYPES,
57
+ recipient_document_type_code,
58
+ "tipoDocRec"
59
+ )
60
+
61
+ {
62
+ version: normalize_version(original["ver"]),
63
+ issued_on: issued_on,
64
+ issuer_cuit: issuer_cuit,
65
+ point_of_sale: point_of_sale,
66
+ voucher_type_code: voucher_type_code,
67
+ voucher_type_description: voucher_type_description,
68
+ voucher_number: voucher_number,
69
+ invoice_number: "#{point_of_sale}-#{voucher_number}",
70
+ amount: decimal(original["importe"], scale: 2, integer_digits: 13),
71
+ currency_code: currency_code,
72
+ currency_description: currency_description,
73
+ exchange_rate: decimal(original["ctz"], scale: 6, integer_digits: 13),
74
+ recipient_document_type_code: recipient_document_type_code,
75
+ recipient_document_type_description: recipient_document_type_description,
76
+ recipient_document_number: recipient_document_number,
77
+ authorization_type: original["tipoCodAut"].to_s,
78
+ authorization_code: authorization_code
79
+ }
80
+ rescue ArgumentError => e
81
+ raise Errors::InvalidVoucherSchema, e.message
82
+ end
83
+
84
+ private
85
+
86
+ def validate_hash!(original)
87
+ return if original.is_a?(Hash)
88
+
89
+ raise ArgumentError, "Voucher payload must be a JSON object"
90
+ end
91
+
92
+ def validate_required_fields!(original)
93
+ missing = REQUIRED_FIELDS.reject { |field| original.key?(field) }
94
+ return if missing.empty?
95
+
96
+ raise ArgumentError, "Missing fields: #{missing.join(", ")}"
97
+ end
98
+
99
+ def normalize_version(value)
100
+ string = Integer(value).to_s
101
+ raise ArgumentError, "Invalid version" unless string.match?(/\A\d\z/)
102
+
103
+ Integer(string)
104
+ end
105
+
106
+ def normalize_date(value)
107
+ Date.iso8601(value.to_s).iso8601
108
+ rescue Date::Error
109
+ raise ArgumentError, "Invalid issue date"
110
+ end
111
+
112
+ def normalize_code(value, width)
113
+ string = Integer(value).to_s
114
+ raise ArgumentError, "Invalid code length" if string.length > width
115
+
116
+ string.rjust(width, "0")
117
+ end
118
+
119
+ def digits(value, expected: nil, max: nil)
120
+ string = value.to_s.gsub(/\D/, "")
121
+ raise ArgumentError, "Missing digits" if string.empty?
122
+
123
+ if expected && string.length != expected
124
+ raise ArgumentError, "Invalid digit length"
125
+ end
126
+
127
+ if max && string.length > max
128
+ raise ArgumentError, "Invalid digit length"
129
+ end
130
+
131
+ string
132
+ end
133
+
134
+ def decimal(value, scale:, integer_digits:)
135
+ string = value.to_s.tr(",", ".").strip
136
+
137
+ unless string.match?(/\A\d+(\.\d+)?\z/)
138
+ raise ArgumentError, "Invalid decimal value"
139
+ end
140
+
141
+ whole, fraction = string.split(".", 2)
142
+
143
+ if whole.length > integer_digits
144
+ raise ArgumentError, "Invalid decimal integer length"
145
+ end
146
+
147
+ if fraction && fraction.length > scale
148
+ raise ArgumentError, "Invalid decimal scale"
149
+ end
150
+
151
+ format_decimal(BigDecimal(string), scale)
152
+ end
153
+
154
+ def format_decimal(value, scale)
155
+ string = value.round(scale).to_s("F")
156
+ whole, fraction = string.split(".", 2)
157
+ fraction = fraction.to_s.ljust(scale, "0")[0, scale]
158
+
159
+ "#{whole}.#{fraction}"
160
+ end
161
+
162
+ def catalog_value!(catalog, code, field_name)
163
+ value = catalog[code]
164
+ return value unless value.nil?
165
+
166
+ raise ArgumentError, "Unknown #{field_name}: #{code}"
167
+ end
168
+ end
169
+ end
@@ -0,0 +1,117 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "json"
4
+
5
+ require_relative "invoice_extractor_arca/version"
6
+ require_relative "invoice_extractor_arca/configuration"
7
+ require_relative "invoice_extractor_arca/result"
8
+ require_relative "invoice_extractor_arca/error"
9
+ require_relative "invoice_extractor_arca/errors"
10
+ require_relative "invoice_extractor_arca/dependencies/resolver"
11
+ require_relative "invoice_extractor_arca/error_record"
12
+ require_relative "invoice_extractor_arca/command_runner"
13
+ require_relative "invoice_extractor_arca/text/locale_value_normalizer"
14
+ require_relative "invoice_extractor_arca/text/layout_table_parser"
15
+ require_relative "invoice_extractor_arca/text/invoice_parser"
16
+ require_relative "invoice_extractor_arca/validation/reconciler"
17
+ require_relative "invoice_extractor_arca/extractor"
18
+ require_relative "invoice_extractor_arca/scanners/zbar_scanner"
19
+ require_relative "invoice_extractor_arca/scanners/qreader_scanner"
20
+ require_relative "invoice_extractor_arca/scanners/scanner_chain"
21
+ require_relative "invoice_extractor_arca/scanners/factory"
22
+ require_relative "invoice_extractor_arca/pdf/renderer"
23
+ require_relative "invoice_extractor_arca/pdf/ocr_text_extractor"
24
+ require_relative "invoice_extractor_arca/qr/payload_parser"
25
+ require_relative "invoice_extractor_arca/qr/result_builder"
26
+ require_relative "invoice_extractor_arca/qr/reader"
27
+ require_relative "invoice_extractor_arca/qr_extractor"
28
+ require_relative "invoice_extractor_arca/catalogs"
29
+ require_relative "invoice_extractor_arca/voucher_validator"
30
+ require_relative "invoice_extractor_arca/text/normalizer"
31
+ require_relative "invoice_extractor_arca/pdf/text_extractor"
32
+ require_relative "invoice_extractor_arca/ocr/rapidocr_text_extractor"
33
+ require_relative "invoice_extractor_arca/cli"
34
+ require_relative "invoice_extractor_arca/final_payload_builder"
35
+
36
+ module InvoiceExtractorArca
37
+ IMAGE_EXTENSIONS = %w[
38
+ .png
39
+ .jpg
40
+ .jpeg
41
+ .webp
42
+ .bmp
43
+ .tif
44
+ .tiff
45
+ ].freeze
46
+
47
+ module_function
48
+
49
+ def extract(path, **options)
50
+ Extractor.new(**extractor_options(options)).extract(path)
51
+ end
52
+
53
+ def extract_pdf(path, **options)
54
+ validate_pdf_extension!(path)
55
+ extract(path, **options)
56
+ end
57
+
58
+ def extract_image(path, **options)
59
+ validate_image_extension!(path)
60
+ extract(path, **options)
61
+ end
62
+
63
+ def check_dependencies(dependency_paths: {})
64
+ Dependencies::Resolver.new(
65
+ custom_paths: dependency_paths
66
+ ).check_all
67
+ end
68
+
69
+ def check_dependencies!(dependency_paths: {})
70
+ Dependencies::Resolver.new(
71
+ custom_paths: dependency_paths
72
+ ).resolve_all!
73
+ end
74
+
75
+ def extractor_options(options)
76
+ allowed = %i[
77
+ strict
78
+ dependency_paths
79
+ page_range
80
+ resolution
81
+ multiple_qr
82
+ qr_scanner
83
+ python_command
84
+ resolver
85
+ runner
86
+ qr_extractor
87
+ voucher_validator
88
+ enrichment
89
+ text_extractor
90
+ invoice_parser
91
+ reconciler
92
+ image_text_extractor
93
+ pdf_ocr_text_extractor
94
+ final_payload_builder
95
+ ]
96
+
97
+ options.slice(*allowed)
98
+ end
99
+
100
+ private_class_method :extractor_options
101
+
102
+ def validate_pdf_extension!(path)
103
+ return if File.extname(path).downcase == ".pdf"
104
+
105
+ raise Errors::UnsupportedFileType,
106
+ "Expected a PDF file: #{path}"
107
+ end
108
+ private_class_method :validate_pdf_extension!
109
+
110
+ def validate_image_extension!(path)
111
+ return if IMAGE_EXTENSIONS.include?(File.extname(path).downcase)
112
+
113
+ raise Errors::UnsupportedFileType,
114
+ "Expected an image file: #{path}"
115
+ end
116
+ private_class_method :validate_image_extension!
117
+ end
@@ -0,0 +1,2 @@
1
+ rapidocr==3.9.2
2
+ onnxruntime
@@ -0,0 +1,4 @@
1
+ module InvoiceExtractorArca
2
+ VERSION: String
3
+ # See the writing guide of rbs: https://github.com/ruby/rbs#guides
4
+ end