qr_reader 0.0.1 → 0.0.2

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.
@@ -0,0 +1,151 @@
1
+ # frozen_string_literal: true
2
+
3
+ module QrReader
4
+ module Formats
5
+ module PDF
6
+ # Text drawing for the vector pass (ISO 32000-1, 9.3 and 9.4): when
7
+ # paths are collected, ContentScanner positions every glyph and hands
8
+ # the module-like ones (barcode fonts) to the PathCollector; Type 3
9
+ # glyph procedures are run like forms.
10
+ class ContentScanner
11
+ TEXT_OPERATORS = %w[BT ET Tf Tm Td TD T* TL Tc Tw Tz Ts Tr Tj TJ ' "].to_h { |op| [op, true] }.freeze
12
+ # Type 3 glyph procedures run per page (each is a content stream).
13
+ MAX_TYPE3_GLYPHS = 20_000
14
+ # Elements of strings and arrays parsed from one page's content.
15
+ TEXT_ELEMENTS = 1_000_000
16
+
17
+ private
18
+
19
+ # The String, hex String or Array operand at the scanner position,
20
+ # or nil (garbage is skipped).
21
+ def text_operand(s, data)
22
+ @text_parser ||= {}
23
+ parser = @text_parser[data.object_id] ||= Parser.new(data, @text_budget ||= [TEXT_ELEMENTS])
24
+ parser.pos = s.pos
25
+ value = parser.parse
26
+ s.pos = parser.pos
27
+ value.is_a?(String) || value.is_a?(Array) ? value : nil
28
+ rescue ParseError
29
+ s.pos += 1
30
+ nil
31
+ end
32
+
33
+ def text_operator(token, operands, ctm, resources, depth)
34
+ st = @paths.state
35
+ case token
36
+ when "BT" then @tm = @tlm = IDENTITY
37
+ when "Tf"
38
+ name, size = operands.last(2)
39
+ st.font = name.is_a?(String) && name.start_with?("/") ? font(resources, Parser.name_symbol(name)) : nil
40
+ st.font_size = size.to_f
41
+ when "Tm"
42
+ m = operands.last(6).map { |v| text_number(v) }
43
+ @tm = @tlm = m if m.size == 6 && m.none?(&:nil?)
44
+ when "Td", "TD"
45
+ tx, ty = operands.last(2).map { |v| text_number(v) }
46
+ if tx && ty
47
+ st.leading = -ty if token == "TD"
48
+ next_line(tx, ty)
49
+ end
50
+ when "T*" then next_line(0.0, -st.leading)
51
+ when "TL" then st.leading = text_number(operands.last) || st.leading
52
+ when "Tc" then st.char_spacing = text_number(operands.last) || st.char_spacing
53
+ when "Tw" then st.word_spacing = text_number(operands.last) || st.word_spacing
54
+ when "Tz" then st.scaling = text_number(operands.last) || st.scaling
55
+ when "Ts" then st.rise = text_number(operands.last) || st.rise
56
+ when "Tr" then st.render = text_number(operands.last)&.to_i || st.render
57
+ when "Tj" then show(operands.last, ctm, resources, depth)
58
+ when "'"
59
+ next_line(0.0, -st.leading)
60
+ show(operands.last, ctm, resources, depth)
61
+ when "\""
62
+ aw, ac, string = operands.last(3)
63
+ st.word_spacing = text_number(aw) || st.word_spacing
64
+ st.char_spacing = text_number(ac) || st.char_spacing
65
+ next_line(0.0, -st.leading)
66
+ show(string, ctm, resources, depth)
67
+ when "TJ"
68
+ items = operands.last
69
+ return unless items.is_a?(Array)
70
+
71
+ items.each do |item|
72
+ if item.is_a?(String)
73
+ show(item, ctm, resources, depth)
74
+ elsif (n = text_number(item))
75
+ advance(-n / 1000.0 * st.font_size * st.scaling / 100.0)
76
+ end
77
+ end
78
+ end
79
+ rescue Error, *INTERNAL_ERRORS
80
+ nil
81
+ end
82
+
83
+ def text_number(value)
84
+ case value
85
+ when String then value.start_with?("/") ? nil : (v = value.to_f).finite? ? v : nil
86
+ else Util.number(value)
87
+ end
88
+ end
89
+
90
+ def next_line(tx, ty)
91
+ @tlm = multiply([1.0, 0.0, 0.0, 1.0, tx, ty], @tlm || IDENTITY)
92
+ @tm = @tlm
93
+ end
94
+
95
+ # Moves the text matrix +tx+ along the line.
96
+ def advance(tx)
97
+ m = @tm || IDENTITY
98
+ @tm = [m[0], m[1], m[2], m[3], tx * m[0] + m[4], tx * m[1] + m[5]]
99
+ end
100
+
101
+ def font(resources, name)
102
+ fonts = @doc.dict_value(@doc.dict_value(resources)&.[](:Font)) or return nil
103
+ ref = fonts[name] or return nil
104
+ cache = @doc.fonts
105
+ key = ref.is_a?(Ref) ? ref : @doc.resolve(ref).object_id
106
+ cache.key?(key) ? cache[key] : (cache[key] = Font.load(@doc, ref))
107
+ end
108
+
109
+ def show(string, ctm, resources, depth)
110
+ st = @paths.state
111
+ font = st.font
112
+ return unless string.is_a?(String) && font
113
+
114
+ size = st.font_size
115
+ h = st.scaling / 100.0
116
+ visible = ![3, 7].include?(st.render)
117
+ font.each_code(string) do |code, w0, space|
118
+ if visible
119
+ trm = multiply(multiply([size * h, 0.0, 0.0, size, 0.0, st.rise], @tm || IDENTITY), ctm)
120
+ if font.type3?
121
+ type3_glyph(font, code, trm, resources, depth)
122
+ elsif (polys = font.glyph(code))
123
+ @paths.add_glyph(polys, trm)
124
+ end
125
+ end
126
+ advance((w0 * size + st.char_spacing + (space ? st.word_spacing : 0.0)) * h)
127
+ end
128
+ end
129
+
130
+ # Runs a Type 3 glyph procedure (its images are not collected).
131
+ def type3_glyph(font, code, trm, resources, depth)
132
+ return if depth >= MAX_FORM_DEPTH || (@type3_runs = (@type3_runs || 0) + 1) > MAX_TYPE3_GLYPHS
133
+
134
+ content = font.procedure(code) or return
135
+ tm = @tm
136
+ tlm = @tlm
137
+ @paths.save
138
+ @in_glyph = (@in_glyph || 0) + 1
139
+ scan(content, font.resources || resources, multiply(font.matrix, trm), depth + 1)
140
+ ensure
141
+ if content
142
+ @in_glyph -= 1
143
+ @paths.restore
144
+ @tm = tm
145
+ @tlm = tlm
146
+ end
147
+ end
148
+ end
149
+ end
150
+ end
151
+ end