acro_that 0.1.8 → 1.0.1
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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +1 -4
- data/Gemfile.lock +1 -1
- data/README.md +86 -3
- data/docs/clear_fields.md +10 -10
- data/lib/acro_that/actions/add_field.rb +33 -448
- data/lib/acro_that/actions/update_field.rb +168 -38
- data/lib/acro_that/dict_scan.rb +26 -0
- data/lib/acro_that/document.rb +33 -12
- data/lib/acro_that/fields/base.rb +371 -0
- data/lib/acro_that/fields/checkbox.rb +164 -0
- data/lib/acro_that/fields/radio.rb +220 -0
- data/lib/acro_that/{actions/add_signature_appearance.rb → fields/signature.rb} +64 -93
- data/lib/acro_that/fields/text.rb +31 -0
- data/lib/acro_that/version.rb +1 -1
- data/lib/acro_that.rb +10 -2
- metadata +7 -3
|
@@ -0,0 +1,371 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module AcroThat
|
|
4
|
+
module Fields
|
|
5
|
+
# Base class for field types with shared functionality
|
|
6
|
+
module Base
|
|
7
|
+
include Actions::Base
|
|
8
|
+
|
|
9
|
+
attr_reader :document, :name, :options, :metadata, :field_type, :field_value
|
|
10
|
+
|
|
11
|
+
def initialize(document, name, options = {})
|
|
12
|
+
@document = document
|
|
13
|
+
@name = name
|
|
14
|
+
@options = normalize_hash_keys(options)
|
|
15
|
+
@metadata = normalize_hash_keys(@options[:metadata] || {})
|
|
16
|
+
@field_type = determine_field_type
|
|
17
|
+
@field_value = @options[:value] || ""
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
def x
|
|
21
|
+
@options[:x] || 100
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
def y
|
|
25
|
+
@options[:y] || 500
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
def width
|
|
29
|
+
@options[:width] || 100
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
def height
|
|
33
|
+
@options[:height] || 20
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
def page_num
|
|
37
|
+
@options[:page] || 1
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
private
|
|
41
|
+
|
|
42
|
+
def normalize_hash_keys(hash)
|
|
43
|
+
return hash unless hash.is_a?(Hash)
|
|
44
|
+
|
|
45
|
+
hash.each_with_object({}) do |(key, value), normalized|
|
|
46
|
+
sym_key = key.is_a?(Symbol) ? key : key.to_sym
|
|
47
|
+
normalized[sym_key] = value.is_a?(Hash) ? normalize_hash_keys(value) : value
|
|
48
|
+
end
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
def determine_field_type
|
|
52
|
+
type_input = @options[:type] || "/Tx"
|
|
53
|
+
case type_input
|
|
54
|
+
when :text, "text", "/Tx", "/tx"
|
|
55
|
+
"/Tx"
|
|
56
|
+
when :button, "button", "/Btn", "/btn"
|
|
57
|
+
"/Btn"
|
|
58
|
+
when :radio, "radio"
|
|
59
|
+
"/Btn"
|
|
60
|
+
when :checkbox, "checkbox"
|
|
61
|
+
"/Btn"
|
|
62
|
+
when :choice, "choice", "/Ch", "/ch"
|
|
63
|
+
"/Ch"
|
|
64
|
+
when :signature, "signature", "/Sig", "/sig"
|
|
65
|
+
"/Sig"
|
|
66
|
+
else
|
|
67
|
+
type_input.to_s
|
|
68
|
+
end
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
def create_field_dictionary(value, type)
|
|
72
|
+
dict = "<<\n"
|
|
73
|
+
dict += " /FT #{type}\n"
|
|
74
|
+
dict += " /T #{DictScan.encode_pdf_string(@name)}\n"
|
|
75
|
+
|
|
76
|
+
# Apply /Ff from metadata, or use default 0
|
|
77
|
+
field_flags = @metadata[:Ff] || 0
|
|
78
|
+
dict += " /Ff #{field_flags}\n"
|
|
79
|
+
|
|
80
|
+
dict += " /DA (/Helv 0 Tf 0 g)\n"
|
|
81
|
+
|
|
82
|
+
# Check if this is a radio button (has Radio flag set)
|
|
83
|
+
is_radio_field = field_flags.anybits?(32_768)
|
|
84
|
+
|
|
85
|
+
# For signature fields with image data, don't set /V (appearance stream will be added separately)
|
|
86
|
+
# For radio buttons, /V should be the export value name (e.g., "/email", "/phone")
|
|
87
|
+
# For checkboxes, set /V to normalized value (Yes/Off)
|
|
88
|
+
# For other fields, set /V normally
|
|
89
|
+
should_set_value = if type == "/Sig" && value && !value.empty?
|
|
90
|
+
!(value.is_a?(String) && (value.start_with?("data:image/") || (value.length > 50 && value.match?(%r{^[A-Za-z0-9+/]*={0,2}$}))))
|
|
91
|
+
else
|
|
92
|
+
true
|
|
93
|
+
end
|
|
94
|
+
|
|
95
|
+
# For radio buttons: use export value as PDF name (e.g., "/email")
|
|
96
|
+
# For checkboxes: normalize to "Yes" or "Off"
|
|
97
|
+
# For other fields: use value as-is
|
|
98
|
+
normalized_field_value = if is_radio_field && value && !value.to_s.empty?
|
|
99
|
+
# Encode export value as PDF name (escapes special characters like parentheses)
|
|
100
|
+
DictScan.encode_pdf_name(value)
|
|
101
|
+
elsif type == "/Btn" && value
|
|
102
|
+
value_str = value.to_s
|
|
103
|
+
is_checked = ["Yes", "/Yes", "true"].include?(value_str) || value == true
|
|
104
|
+
is_checked ? "Yes" : "Off"
|
|
105
|
+
else
|
|
106
|
+
value
|
|
107
|
+
end
|
|
108
|
+
|
|
109
|
+
# For radio buttons, /V should only be set if explicitly selected
|
|
110
|
+
# For checkboxes, /V should be a PDF name to match /AS format
|
|
111
|
+
# For other fields, encode as PDF string
|
|
112
|
+
if should_set_value && normalized_field_value && !normalized_field_value.to_s.empty?
|
|
113
|
+
# For radio buttons, only set /V if selected option is explicitly set to true
|
|
114
|
+
if is_radio_field
|
|
115
|
+
# Only set /V for radio buttons if selected option is true
|
|
116
|
+
if [true, "true"].include?(@options[:selected]) && normalized_field_value.to_s.start_with?("/")
|
|
117
|
+
dict += " /V #{normalized_field_value}\n"
|
|
118
|
+
end
|
|
119
|
+
elsif type == "/Btn"
|
|
120
|
+
# For checkboxes (button fields that aren't radio), encode value as PDF name
|
|
121
|
+
# to match the /AS appearance state format (/Yes or /Off)
|
|
122
|
+
dict += " /V #{DictScan.encode_pdf_name(normalized_field_value)}\n"
|
|
123
|
+
else
|
|
124
|
+
dict += " /V #{DictScan.encode_pdf_string(normalized_field_value)}\n"
|
|
125
|
+
end
|
|
126
|
+
end
|
|
127
|
+
|
|
128
|
+
# Apply other metadata entries (excluding Ff which we handled above)
|
|
129
|
+
@metadata.each do |key, val|
|
|
130
|
+
next if key == :Ff
|
|
131
|
+
|
|
132
|
+
pdf_key = DictScan.format_pdf_key(key)
|
|
133
|
+
pdf_value = DictScan.format_pdf_value(val)
|
|
134
|
+
dict += " #{pdf_key} #{pdf_value}\n"
|
|
135
|
+
end
|
|
136
|
+
|
|
137
|
+
dict += ">>"
|
|
138
|
+
dict
|
|
139
|
+
end
|
|
140
|
+
|
|
141
|
+
def create_widget_annotation_with_parent(_widget_obj_num, parent_ref, page_ref, x, y, width, height, type, value,
|
|
142
|
+
is_radio: false)
|
|
143
|
+
rect_array = "[#{x} #{y} #{x + width} #{y + height}]"
|
|
144
|
+
widget = "<<\n"
|
|
145
|
+
widget += " /Type /Annot\n"
|
|
146
|
+
widget += " /Subtype /Widget\n"
|
|
147
|
+
widget += " /Parent #{parent_ref[0]} #{parent_ref[1]} R\n"
|
|
148
|
+
widget += " /P #{page_ref[0]} #{page_ref[1]} R\n" if page_ref
|
|
149
|
+
|
|
150
|
+
widget += " /FT #{type}\n"
|
|
151
|
+
if is_radio
|
|
152
|
+
widget += " /T #{DictScan.encode_pdf_string(@name)}\n"
|
|
153
|
+
else
|
|
154
|
+
|
|
155
|
+
should_set_value = if type == "/Sig" && value && !value.empty?
|
|
156
|
+
!(value.is_a?(String) && (value.start_with?("data:image/") || (value.length > 50 && value.match?(%r{^[A-Za-z0-9+/]*={0,2}$}))))
|
|
157
|
+
elsif type == "/Btn"
|
|
158
|
+
true
|
|
159
|
+
else
|
|
160
|
+
true
|
|
161
|
+
end
|
|
162
|
+
|
|
163
|
+
if type == "/Btn" && should_set_value
|
|
164
|
+
# For checkboxes, encode value as PDF name to match /AS appearance state format
|
|
165
|
+
value_str = value.to_s
|
|
166
|
+
is_checked = ["Yes", "/Yes", "true"].include?(value_str) || value == true
|
|
167
|
+
checkbox_value = is_checked ? "Yes" : "Off"
|
|
168
|
+
widget += " /V #{DictScan.encode_pdf_name(checkbox_value)}\n"
|
|
169
|
+
elsif should_set_value && value && !value.empty?
|
|
170
|
+
widget += " /V #{DictScan.encode_pdf_string(value)}\n"
|
|
171
|
+
end
|
|
172
|
+
end
|
|
173
|
+
|
|
174
|
+
widget += " /Rect #{rect_array}\n"
|
|
175
|
+
widget += " /F 4\n"
|
|
176
|
+
|
|
177
|
+
widget += if is_radio
|
|
178
|
+
" /MK << /BC [0.0] /BG [1.0] >>\n"
|
|
179
|
+
else
|
|
180
|
+
" /DA (/Helv 0 Tf 0 g)\n"
|
|
181
|
+
end
|
|
182
|
+
|
|
183
|
+
@metadata.each do |key, val|
|
|
184
|
+
pdf_key = DictScan.format_pdf_key(key)
|
|
185
|
+
pdf_value = DictScan.format_pdf_value(val)
|
|
186
|
+
next if ["/F", "/V"].include?(pdf_key)
|
|
187
|
+
next if is_radio && ["/Ff", "/DA"].include?(pdf_key)
|
|
188
|
+
|
|
189
|
+
widget += " #{pdf_key} #{pdf_value}\n"
|
|
190
|
+
end
|
|
191
|
+
|
|
192
|
+
widget += ">>"
|
|
193
|
+
widget
|
|
194
|
+
end
|
|
195
|
+
|
|
196
|
+
def add_field_to_acroform_with_defaults(field_obj_num)
|
|
197
|
+
af_ref = acroform_ref
|
|
198
|
+
return false unless af_ref
|
|
199
|
+
|
|
200
|
+
af_body = get_object_body_with_patch(af_ref)
|
|
201
|
+
# Use +"" instead of dup to create a mutable copy without keeping reference to original
|
|
202
|
+
patched = af_body.to_s
|
|
203
|
+
|
|
204
|
+
# Step 1: Add field to /Fields array
|
|
205
|
+
fields_array_ref = DictScan.value_token_after("/Fields", patched)
|
|
206
|
+
|
|
207
|
+
if fields_array_ref && fields_array_ref =~ /\A(\d+)\s+(\d+)\s+R/
|
|
208
|
+
arr_ref = [Integer(::Regexp.last_match(1)), Integer(::Regexp.last_match(2))]
|
|
209
|
+
arr_body = get_object_body_with_patch(arr_ref)
|
|
210
|
+
new_body = DictScan.add_ref_to_array(arr_body, [field_obj_num, 0])
|
|
211
|
+
apply_patch(arr_ref, new_body, arr_body)
|
|
212
|
+
elsif patched.include?("/Fields")
|
|
213
|
+
patched = DictScan.add_ref_to_inline_array(patched, "/Fields", [field_obj_num, 0])
|
|
214
|
+
else
|
|
215
|
+
patched = DictScan.upsert_key_value(patched, "/Fields", "[#{field_obj_num} 0 R]")
|
|
216
|
+
end
|
|
217
|
+
|
|
218
|
+
# Step 2: Ensure /NeedAppearances false (we provide custom appearance streams)
|
|
219
|
+
# Setting to false tells viewers to use our custom appearances instead of generating defaults
|
|
220
|
+
# If we don't set this or set it to true, viewers will ignore our custom appearances and
|
|
221
|
+
# generate their own default appearances (e.g., circular radio buttons instead of our squares)
|
|
222
|
+
patched = if patched.include?("/NeedAppearances")
|
|
223
|
+
# Replace existing /NeedAppearances with false
|
|
224
|
+
DictScan.replace_key_value(patched, "/NeedAppearances", "false")
|
|
225
|
+
else
|
|
226
|
+
DictScan.upsert_key_value(patched, "/NeedAppearances", "false")
|
|
227
|
+
end
|
|
228
|
+
|
|
229
|
+
# Step 2.5: Remove /XFA if present
|
|
230
|
+
if patched.include?("/XFA")
|
|
231
|
+
xfa_pattern = %r{/XFA(?=[\s(<\[/])}
|
|
232
|
+
if patched.match(xfa_pattern)
|
|
233
|
+
xfa_value = DictScan.value_token_after("/XFA", patched)
|
|
234
|
+
if xfa_value
|
|
235
|
+
xfa_match = patched.match(xfa_pattern)
|
|
236
|
+
if xfa_match
|
|
237
|
+
key_start = xfa_match.begin(0)
|
|
238
|
+
value_start = xfa_match.end(0)
|
|
239
|
+
value_start += 1 while value_start < patched.length && patched[value_start] =~ /\s/
|
|
240
|
+
value_end = value_start + xfa_value.length
|
|
241
|
+
value_end += 1 while value_end < patched.length && patched[value_end] =~ /\s/
|
|
242
|
+
before = patched[0...key_start]
|
|
243
|
+
before = before.rstrip
|
|
244
|
+
after = patched[value_end..]
|
|
245
|
+
patched = "#{before} #{after.lstrip}".strip
|
|
246
|
+
patched = patched.gsub(/\s+/, " ")
|
|
247
|
+
end
|
|
248
|
+
end
|
|
249
|
+
end
|
|
250
|
+
end
|
|
251
|
+
|
|
252
|
+
# Step 3: Ensure /DR /Font has /Helv mapping
|
|
253
|
+
unless patched.include?("/DR") && patched.include?("/Helv")
|
|
254
|
+
font_obj_num = next_fresh_object_number
|
|
255
|
+
font_body = "<<\n /Type /Font\n /Subtype /Type1\n /BaseFont /Helvetica\n>>"
|
|
256
|
+
document.instance_variable_get(:@patches) << { ref: [font_obj_num, 0], body: font_body }
|
|
257
|
+
|
|
258
|
+
if patched.include?("/DR")
|
|
259
|
+
dr_tok = DictScan.value_token_after("/DR", patched)
|
|
260
|
+
if dr_tok && dr_tok.start_with?("<<")
|
|
261
|
+
unless dr_tok.include?("/Font")
|
|
262
|
+
new_dr_tok = dr_tok.chomp(">>") + " /Font << /Helv #{font_obj_num} 0 R >>\n>>"
|
|
263
|
+
patched = patched.sub(dr_tok) { |_| new_dr_tok }
|
|
264
|
+
end
|
|
265
|
+
else
|
|
266
|
+
patched = DictScan.replace_key_value(patched, "/DR", "<< /Font << /Helv #{font_obj_num} 0 R >> >>")
|
|
267
|
+
end
|
|
268
|
+
else
|
|
269
|
+
patched = DictScan.upsert_key_value(patched, "/DR", "<< /Font << /Helv #{font_obj_num} 0 R >> >>")
|
|
270
|
+
end
|
|
271
|
+
end
|
|
272
|
+
|
|
273
|
+
apply_patch(af_ref, patched, af_body)
|
|
274
|
+
true
|
|
275
|
+
end
|
|
276
|
+
|
|
277
|
+
def find_page_ref(page_num)
|
|
278
|
+
find_page_by_number(page_num)
|
|
279
|
+
end
|
|
280
|
+
|
|
281
|
+
def add_widget_to_page(widget_obj_num, page_num)
|
|
282
|
+
target_page_ref = find_page_ref(page_num)
|
|
283
|
+
return false unless target_page_ref
|
|
284
|
+
|
|
285
|
+
page_body = get_object_body_with_patch(target_page_ref)
|
|
286
|
+
|
|
287
|
+
new_body = if page_body =~ %r{/Annots\s*\[(.*?)\]}m
|
|
288
|
+
result = DictScan.add_ref_to_inline_array(page_body, "/Annots", [widget_obj_num, 0])
|
|
289
|
+
if result && result != page_body
|
|
290
|
+
result
|
|
291
|
+
else
|
|
292
|
+
annots_array = ::Regexp.last_match(1)
|
|
293
|
+
ref_token = "#{widget_obj_num} 0 R"
|
|
294
|
+
new_annots = if annots_array.strip.empty?
|
|
295
|
+
"[#{ref_token}]"
|
|
296
|
+
else
|
|
297
|
+
"[#{annots_array} #{ref_token}]"
|
|
298
|
+
end
|
|
299
|
+
page_body.sub(%r{/Annots\s*\[.*?\]}, "/Annots #{new_annots}")
|
|
300
|
+
end
|
|
301
|
+
elsif page_body =~ %r{/Annots\s+(\d+)\s+(\d+)\s+R}
|
|
302
|
+
annots_array_ref = [Integer(::Regexp.last_match(1)), Integer(::Regexp.last_match(2))]
|
|
303
|
+
annots_array_body = get_object_body_with_patch(annots_array_ref)
|
|
304
|
+
|
|
305
|
+
ref_token = "#{widget_obj_num} 0 R"
|
|
306
|
+
if annots_array_body
|
|
307
|
+
new_annots_body = if annots_array_body.strip == "[]"
|
|
308
|
+
"[#{ref_token}]"
|
|
309
|
+
elsif annots_array_body.strip.start_with?("[") && annots_array_body.strip.end_with?("]")
|
|
310
|
+
without_brackets = annots_array_body.strip[1..-2].strip
|
|
311
|
+
"[#{without_brackets} #{ref_token}]"
|
|
312
|
+
else
|
|
313
|
+
"[#{annots_array_body} #{ref_token}]"
|
|
314
|
+
end
|
|
315
|
+
|
|
316
|
+
apply_patch(annots_array_ref, new_annots_body, annots_array_body)
|
|
317
|
+
page_body
|
|
318
|
+
else
|
|
319
|
+
page_body.sub(%r{/Annots\s+\d+\s+\d+\s+R}, "/Annots [#{ref_token}]")
|
|
320
|
+
end
|
|
321
|
+
else
|
|
322
|
+
ref_token = "#{widget_obj_num} 0 R"
|
|
323
|
+
if page_body.include?(">>")
|
|
324
|
+
page_body.reverse.sub(">>".reverse, "/Annots [#{ref_token}]>>".reverse).reverse
|
|
325
|
+
else
|
|
326
|
+
page_body + " /Annots [#{ref_token}]"
|
|
327
|
+
end
|
|
328
|
+
end
|
|
329
|
+
|
|
330
|
+
apply_patch(target_page_ref, new_body, page_body) if new_body && new_body != page_body
|
|
331
|
+
true
|
|
332
|
+
end
|
|
333
|
+
|
|
334
|
+
def add_widget_to_parent_kids(parent_ref, widget_obj_num)
|
|
335
|
+
parent_body = get_object_body_with_patch(parent_ref)
|
|
336
|
+
return unless parent_body
|
|
337
|
+
|
|
338
|
+
kids_array_ref = DictScan.value_token_after("/Kids", parent_body)
|
|
339
|
+
|
|
340
|
+
if kids_array_ref && kids_array_ref =~ /\A(\d+)\s+(\d+)\s+R\z/
|
|
341
|
+
arr_ref = [Integer(::Regexp.last_match(1)), Integer(::Regexp.last_match(2))]
|
|
342
|
+
arr_body = get_object_body_with_patch(arr_ref)
|
|
343
|
+
new_body = DictScan.add_ref_to_array(arr_body, [widget_obj_num, 0])
|
|
344
|
+
apply_patch(arr_ref, new_body, arr_body)
|
|
345
|
+
elsif kids_array_ref && kids_array_ref.start_with?("[")
|
|
346
|
+
new_body = DictScan.add_ref_to_inline_array(parent_body, "/Kids", [widget_obj_num, 0])
|
|
347
|
+
apply_patch(parent_ref, new_body, parent_body) if new_body && new_body != parent_body
|
|
348
|
+
else
|
|
349
|
+
new_body = DictScan.upsert_key_value(parent_body, "/Kids", "[#{widget_obj_num} 0 R]")
|
|
350
|
+
apply_patch(parent_ref, new_body, parent_body) if new_body && new_body != parent_body
|
|
351
|
+
end
|
|
352
|
+
end
|
|
353
|
+
|
|
354
|
+
def build_form_xobject(content_stream, width, height)
|
|
355
|
+
dict = "<<\n"
|
|
356
|
+
dict += " /Type /XObject\n"
|
|
357
|
+
dict += " /Subtype /Form\n"
|
|
358
|
+
dict += " /BBox [0 0 #{width} #{height}]\n"
|
|
359
|
+
dict += " /Matrix [1 0 0 1 0 0]\n"
|
|
360
|
+
dict += " /Resources << >>\n"
|
|
361
|
+
dict += " /Length #{content_stream.bytesize}\n"
|
|
362
|
+
dict += ">>\n"
|
|
363
|
+
dict += "stream\n"
|
|
364
|
+
dict += content_stream
|
|
365
|
+
dict += "\nendstream"
|
|
366
|
+
|
|
367
|
+
dict
|
|
368
|
+
end
|
|
369
|
+
end
|
|
370
|
+
end
|
|
371
|
+
end
|
|
@@ -0,0 +1,164 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module AcroThat
|
|
4
|
+
module Fields
|
|
5
|
+
# Handles checkbox field creation
|
|
6
|
+
class Checkbox
|
|
7
|
+
include Base
|
|
8
|
+
|
|
9
|
+
attr_reader :field_obj_num
|
|
10
|
+
|
|
11
|
+
def call
|
|
12
|
+
@field_obj_num = next_fresh_object_number
|
|
13
|
+
widget_obj_num = @field_obj_num + 1
|
|
14
|
+
|
|
15
|
+
field_body = create_field_dictionary(@field_value, @field_type)
|
|
16
|
+
page_ref = find_page_ref(page_num)
|
|
17
|
+
|
|
18
|
+
widget_body = create_widget_annotation_with_parent(widget_obj_num, [@field_obj_num, 0], page_ref, x, y, width,
|
|
19
|
+
height, @field_type, @field_value)
|
|
20
|
+
|
|
21
|
+
document.instance_variable_get(:@patches) << { ref: [@field_obj_num, 0], body: field_body }
|
|
22
|
+
document.instance_variable_get(:@patches) << { ref: [widget_obj_num, 0], body: widget_body }
|
|
23
|
+
|
|
24
|
+
add_field_to_acroform_with_defaults(@field_obj_num)
|
|
25
|
+
add_widget_to_page(widget_obj_num, page_num)
|
|
26
|
+
|
|
27
|
+
add_checkbox_appearance(widget_obj_num)
|
|
28
|
+
|
|
29
|
+
true
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
private
|
|
33
|
+
|
|
34
|
+
def add_checkbox_appearance(widget_obj_num)
|
|
35
|
+
yes_obj_num = next_fresh_object_number
|
|
36
|
+
off_obj_num = yes_obj_num + 1
|
|
37
|
+
|
|
38
|
+
yes_body = create_checkbox_yes_appearance(width, height)
|
|
39
|
+
document.instance_variable_get(:@patches) << { ref: [yes_obj_num, 0], body: yes_body }
|
|
40
|
+
|
|
41
|
+
off_body = create_checkbox_off_appearance(width, height)
|
|
42
|
+
document.instance_variable_get(:@patches) << { ref: [off_obj_num, 0], body: off_body }
|
|
43
|
+
|
|
44
|
+
widget_ref = [widget_obj_num, 0]
|
|
45
|
+
original_widget_body = get_object_body_with_patch(widget_ref)
|
|
46
|
+
# Use +"" instead of dup to create a mutable copy without keeping reference to original
|
|
47
|
+
widget_body = original_widget_body.to_s
|
|
48
|
+
|
|
49
|
+
ap_dict = "<<\n /N <<\n /Yes #{yes_obj_num} 0 R\n /Off #{off_obj_num} 0 R\n >>\n>>"
|
|
50
|
+
|
|
51
|
+
widget_body = if widget_body.include?("/AP")
|
|
52
|
+
DictScan.replace_key_value(widget_body, "/AP", ap_dict)
|
|
53
|
+
else
|
|
54
|
+
DictScan.upsert_key_value(widget_body, "/AP", ap_dict)
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
value_str = @field_value.to_s
|
|
58
|
+
is_checked = value_str == "Yes" || value_str == "/Yes" || value_str == "true" || @field_value == true
|
|
59
|
+
normalized_value = is_checked ? "Yes" : "Off"
|
|
60
|
+
|
|
61
|
+
# Set /V to match /AS - both should be PDF names for checkboxes
|
|
62
|
+
v_value = DictScan.encode_pdf_name(normalized_value)
|
|
63
|
+
|
|
64
|
+
as_value = if normalized_value == "Yes"
|
|
65
|
+
"/Yes"
|
|
66
|
+
else
|
|
67
|
+
"/Off"
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
# Update /V to ensure it matches /AS format (both PDF names)
|
|
71
|
+
widget_body = if widget_body.include?("/V")
|
|
72
|
+
DictScan.replace_key_value(widget_body, "/V", v_value)
|
|
73
|
+
else
|
|
74
|
+
DictScan.upsert_key_value(widget_body, "/V", v_value)
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
# Update /AS to match the normalized value
|
|
78
|
+
widget_body = if widget_body.include?("/AS")
|
|
79
|
+
DictScan.replace_key_value(widget_body, "/AS", as_value)
|
|
80
|
+
else
|
|
81
|
+
DictScan.upsert_key_value(widget_body, "/AS", as_value)
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
apply_patch(widget_ref, widget_body, original_widget_body)
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
def create_checkbox_yes_appearance(width, height)
|
|
88
|
+
line_width = [width * 0.05, height * 0.05].min
|
|
89
|
+
border_width = [width * 0.08, height * 0.08].min
|
|
90
|
+
|
|
91
|
+
# Define checkmark in normalized coordinates (0-1 range) for consistent aspect ratio
|
|
92
|
+
# Checkmark shape: three points forming a checkmark
|
|
93
|
+
norm_x1 = 0.25
|
|
94
|
+
norm_y1 = 0.55
|
|
95
|
+
norm_x2 = 0.45
|
|
96
|
+
norm_y2 = 0.35
|
|
97
|
+
norm_x3 = 0.75
|
|
98
|
+
norm_y3 = 0.85
|
|
99
|
+
|
|
100
|
+
# Calculate scale to maximize size while maintaining aspect ratio
|
|
101
|
+
# Use the smaller dimension to ensure it fits
|
|
102
|
+
scale = [width, height].min * 0.85 # Use 85% of the smaller dimension
|
|
103
|
+
|
|
104
|
+
# Calculate checkmark dimensions
|
|
105
|
+
check_width = scale
|
|
106
|
+
check_height = scale
|
|
107
|
+
|
|
108
|
+
# Center the checkmark in the box
|
|
109
|
+
offset_x = (width - check_width) / 2
|
|
110
|
+
offset_y = (height - check_height) / 2
|
|
111
|
+
|
|
112
|
+
# Calculate actual coordinates
|
|
113
|
+
check_x1 = offset_x + norm_x1 * check_width
|
|
114
|
+
check_y1 = offset_y + norm_y1 * check_height
|
|
115
|
+
check_x2 = offset_x + norm_x2 * check_width
|
|
116
|
+
check_y2 = offset_y + norm_y2 * check_height
|
|
117
|
+
check_x3 = offset_x + norm_x3 * check_width
|
|
118
|
+
check_y3 = offset_y + norm_y3 * check_height
|
|
119
|
+
|
|
120
|
+
content_stream = "q\n"
|
|
121
|
+
# Draw square border around field bounds
|
|
122
|
+
content_stream += "0 0 0 RG\n" # Black stroke color
|
|
123
|
+
content_stream += "#{line_width} w\n" # Line width
|
|
124
|
+
# Draw rectangle from (0,0) to (width, height)
|
|
125
|
+
content_stream += "0 0 m\n"
|
|
126
|
+
content_stream += "#{width} 0 l\n"
|
|
127
|
+
content_stream += "#{width} #{height} l\n"
|
|
128
|
+
content_stream += "0 #{height} l\n"
|
|
129
|
+
content_stream += "0 0 l\n"
|
|
130
|
+
content_stream += "S\n" # Stroke the border
|
|
131
|
+
|
|
132
|
+
# Draw checkmark
|
|
133
|
+
content_stream += "0 0 0 rg\n" # Black fill color
|
|
134
|
+
content_stream += "#{border_width} w\n" # Line width for checkmark
|
|
135
|
+
content_stream += "#{check_x1} #{check_y1} m\n"
|
|
136
|
+
content_stream += "#{check_x2} #{check_y2} l\n"
|
|
137
|
+
content_stream += "#{check_x3} #{check_y3} l\n"
|
|
138
|
+
content_stream += "S\n" # Stroke the checkmark
|
|
139
|
+
content_stream += "Q\n"
|
|
140
|
+
|
|
141
|
+
build_form_xobject(content_stream, width, height)
|
|
142
|
+
end
|
|
143
|
+
|
|
144
|
+
def create_checkbox_off_appearance(width, height)
|
|
145
|
+
line_width = [width * 0.05, height * 0.05].min
|
|
146
|
+
|
|
147
|
+
content_stream = "q\n"
|
|
148
|
+
# Draw square border around field bounds
|
|
149
|
+
content_stream += "0 0 0 RG\n" # Black stroke color
|
|
150
|
+
content_stream += "#{line_width} w\n" # Line width
|
|
151
|
+
# Draw rectangle from (0,0) to (width, height)
|
|
152
|
+
content_stream += "0 0 m\n"
|
|
153
|
+
content_stream += "#{width} 0 l\n"
|
|
154
|
+
content_stream += "#{width} #{height} l\n"
|
|
155
|
+
content_stream += "0 #{height} l\n"
|
|
156
|
+
content_stream += "0 0 l\n"
|
|
157
|
+
content_stream += "S\n" # Stroke the border
|
|
158
|
+
content_stream += "Q\n"
|
|
159
|
+
|
|
160
|
+
build_form_xobject(content_stream, width, height)
|
|
161
|
+
end
|
|
162
|
+
end
|
|
163
|
+
end
|
|
164
|
+
end
|