corp_pdf 1.0.5

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,220 @@
1
+ # frozen_string_literal: true
2
+
3
+ module CorpPdf
4
+ module Fields
5
+ # Handles radio button field creation
6
+ class Radio
7
+ include Base
8
+
9
+ attr_reader :field_obj_num
10
+
11
+ def call
12
+ @field_value = @field_value.to_s.gsub(" ", "")
13
+ group_id = @options[:group_id]
14
+ radio_groups = @document.instance_variable_get(:@radio_groups)
15
+ parent_ref = radio_groups[group_id]
16
+
17
+ if parent_ref
18
+ # Subsequent radio button: add widget to existing parent's Kids array
19
+ add_subsequent_widget(parent_ref)
20
+ else
21
+ # First radio button in group: create parent field and first widget
22
+ create_first_widget(group_id, radio_groups)
23
+ end
24
+
25
+ true
26
+ end
27
+
28
+ private
29
+
30
+ def add_subsequent_widget(parent_ref)
31
+ widget_obj_num = next_fresh_object_number
32
+ page_ref = find_page_ref(page_num)
33
+
34
+ widget_body = create_widget_annotation_with_parent(widget_obj_num, parent_ref, page_ref, x, y, width,
35
+ height, @field_type, @field_value, is_radio: true)
36
+
37
+ document.instance_variable_get(:@patches) << { ref: [widget_obj_num, 0], body: widget_body }
38
+
39
+ add_widget_to_parent_kids(parent_ref, widget_obj_num)
40
+ add_field_to_acroform_with_defaults(widget_obj_num)
41
+ add_widget_to_page(widget_obj_num, page_num)
42
+
43
+ add_radio_button_appearance(widget_obj_num, @field_value, x, y, width, height, parent_ref)
44
+
45
+ @field_obj_num = parent_ref[0]
46
+ end
47
+
48
+ def create_first_widget(group_id, radio_groups)
49
+ @field_obj_num = next_fresh_object_number
50
+ widget_obj_num = @field_obj_num + 1
51
+
52
+ field_body = create_field_dictionary(@field_value, @field_type)
53
+ page_ref = find_page_ref(page_num)
54
+
55
+ widget_body = create_widget_annotation_with_parent(widget_obj_num, [@field_obj_num, 0], page_ref, x, y, width,
56
+ height, @field_type, @field_value, is_radio: true)
57
+
58
+ document.instance_variable_get(:@patches) << { ref: [@field_obj_num, 0], body: field_body }
59
+ document.instance_variable_get(:@patches) << { ref: [widget_obj_num, 0], body: widget_body }
60
+
61
+ add_widget_to_parent_kids([@field_obj_num, 0], widget_obj_num)
62
+ add_field_to_acroform_with_defaults(@field_obj_num)
63
+ add_field_to_acroform_with_defaults(widget_obj_num)
64
+ add_widget_to_page(widget_obj_num, page_num)
65
+
66
+ add_radio_button_appearance(widget_obj_num, @field_value, x, y, width, height, [@field_obj_num, 0])
67
+
68
+ radio_groups[group_id] = [@field_obj_num, 0]
69
+ end
70
+
71
+ def add_radio_button_appearance(widget_obj_num, export_value, _x, _y, width, height, parent_ref = nil)
72
+ widget_ref = [widget_obj_num, 0]
73
+ original_widget_body = get_object_body_with_patch(widget_ref)
74
+ return unless original_widget_body
75
+
76
+ # Store original before modifying to avoid loading again
77
+ widget_body = original_widget_body.to_s
78
+
79
+ # Ensure we have a valid export value - if empty, generate a unique one
80
+ # Export value must be unique for each widget in the group for mutual exclusivity
81
+ if export_value.nil? || export_value.to_s.empty?
82
+ # Generate unique export value based on widget object number
83
+ export_value = "widget_#{widget_obj_num}"
84
+ end
85
+
86
+ # Encode export value as PDF name (escapes special characters like parentheses)
87
+ export_name = DictScan.encode_pdf_name(export_value)
88
+
89
+ unchecked_obj_num = next_fresh_object_number
90
+ unchecked_body = create_radio_unchecked_appearance(width, height)
91
+ document.instance_variable_get(:@patches) << { ref: [unchecked_obj_num, 0], body: unchecked_body }
92
+
93
+ checked_obj_num = next_fresh_object_number
94
+ checked_body = create_radio_checked_appearance(width, height)
95
+ document.instance_variable_get(:@patches) << { ref: [checked_obj_num, 0], body: checked_body }
96
+
97
+ widget_ap_dict = "<<\n /N <<\n /Off #{unchecked_obj_num} 0 R\n #{export_name} #{checked_obj_num} 0 R\n >>\n>>"
98
+
99
+ widget_body = if widget_body.include?("/AP")
100
+ DictScan.replace_key_value(widget_body, "/AP", widget_ap_dict)
101
+ else
102
+ DictScan.upsert_key_value(widget_body, "/AP", widget_ap_dict)
103
+ end
104
+
105
+ # Determine if this button should be selected by default
106
+ # Only set selected if the selected option is explicitly set to true
107
+ should_be_selected = [true, "true"].include?(@options[:selected])
108
+
109
+ as_value = should_be_selected ? export_name : "/Off"
110
+ widget_body = if widget_body.include?("/AS")
111
+ DictScan.replace_key_value(widget_body, "/AS", as_value)
112
+ else
113
+ DictScan.upsert_key_value(widget_body, "/AS", as_value)
114
+ end
115
+
116
+ # Use stored original_widget_body instead of loading again
117
+ apply_patch(widget_ref, widget_body, original_widget_body)
118
+
119
+ # Track original_parent_body outside blocks so we can reuse it
120
+ original_parent_body = nil
121
+
122
+ # Update parent field's /V if this button is selected by default
123
+ if parent_ref && should_be_selected
124
+ original_parent_body = get_object_body_with_patch(parent_ref)
125
+ if original_parent_body
126
+ # Store original before modifying
127
+ parent_body = original_parent_body.to_s
128
+ # Update parent's /V to match the selected button's export value
129
+ parent_body = if parent_body.include?("/V")
130
+ DictScan.replace_key_value(parent_body, "/V", export_name)
131
+ else
132
+ DictScan.upsert_key_value(parent_body, "/V", export_name)
133
+ end
134
+ # Use stored original_parent_body instead of loading again
135
+ apply_patch(parent_ref, parent_body, original_parent_body)
136
+ end
137
+ end
138
+
139
+ # Update parent field's /AP if parent_ref is provided
140
+ if parent_ref
141
+ # Reuse original_parent_body if we already loaded it, otherwise load it
142
+ original_parent_body_for_ap = original_parent_body || get_object_body_with_patch(parent_ref)
143
+ return unless original_parent_body_for_ap
144
+
145
+ # Use a working copy for modification
146
+ parent_body_for_ap = original_parent_body_for_ap.to_s
147
+ parent_ap_tok = DictScan.value_token_after("/AP", parent_body_for_ap)
148
+ if parent_ap_tok && parent_ap_tok.start_with?("<<")
149
+ n_tok = DictScan.value_token_after("/N", parent_ap_tok)
150
+ if n_tok && n_tok.start_with?("<<") && !n_tok.include?(export_name.to_s)
151
+ new_n_tok = n_tok.chomp(">>") + " #{export_name} #{checked_obj_num} 0 R\n>>"
152
+ new_ap_tok = parent_ap_tok.sub(n_tok) { |_| new_n_tok }
153
+ new_parent_body = parent_body_for_ap.sub(parent_ap_tok) { |_| new_ap_tok }
154
+ apply_patch(parent_ref, new_parent_body, original_parent_body_for_ap)
155
+ end
156
+ else
157
+ ap_dict = "<<\n /N <<\n #{export_name} #{checked_obj_num} 0 R\n /Off #{unchecked_obj_num} 0 R\n >>\n>>"
158
+ new_parent_body = DictScan.upsert_key_value(parent_body_for_ap, "/AP", ap_dict)
159
+ apply_patch(parent_ref, new_parent_body, original_parent_body_for_ap)
160
+ end
161
+ end
162
+ end
163
+
164
+ def create_radio_checked_appearance(width, height)
165
+ # Draw only the checkmark (no border)
166
+ border_width = [width * 0.08, height * 0.08].min
167
+
168
+ # Define checkmark in normalized coordinates (0-1 range) for consistent aspect ratio
169
+ # Checkmark shape: three points forming a checkmark
170
+ norm_x1 = 0.25
171
+ norm_y1 = 0.55
172
+ norm_x2 = 0.45
173
+ norm_y2 = 0.35
174
+ norm_x3 = 0.75
175
+ norm_y3 = 0.85
176
+
177
+ # Calculate scale to maximize size while maintaining aspect ratio
178
+ # Use the smaller dimension to ensure it fits
179
+ scale = [width, height].min * 0.85 # Use 85% of the smaller dimension
180
+
181
+ # Calculate checkmark dimensions
182
+ check_width = scale
183
+ check_height = scale
184
+
185
+ # Center the checkmark in the box
186
+ offset_x = (width - check_width) / 2
187
+ offset_y = (height - check_height) / 2
188
+
189
+ # Calculate actual coordinates
190
+ check_x1 = offset_x + (norm_x1 * check_width)
191
+ check_y1 = offset_y + (norm_y1 * check_height)
192
+ check_x2 = offset_x + (norm_x2 * check_width)
193
+ check_y2 = offset_y + (norm_y2 * check_height)
194
+ check_x3 = offset_x + (norm_x3 * check_width)
195
+ check_y3 = offset_y + (norm_y3 * check_height)
196
+
197
+ content_stream = "q\n"
198
+ # Draw checkmark only (no border)
199
+ content_stream += "0 0 0 rg\n" # Black fill color
200
+ content_stream += "#{border_width} w\n" # Line width for checkmark
201
+ content_stream += "#{check_x1} #{check_y1} m\n"
202
+ content_stream += "#{check_x2} #{check_y2} l\n"
203
+ content_stream += "#{check_x3} #{check_y3} l\n"
204
+ content_stream += "S\n" # Stroke the checkmark
205
+ content_stream += "Q\n"
206
+
207
+ build_form_xobject(content_stream, width, height)
208
+ end
209
+
210
+ def create_radio_unchecked_appearance(width, height)
211
+ # Empty appearance (no border, no checkmark)
212
+ content_stream = "q\n"
213
+ # Empty appearance for unchecked state
214
+ content_stream += "Q\n"
215
+
216
+ build_form_xobject(content_stream, width, height)
217
+ end
218
+ end
219
+ end
220
+ end
@@ -0,0 +1,393 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "chunky_png"
4
+
5
+ module CorpPdf
6
+ module Fields
7
+ # Handles signature field creation
8
+ class Signature
9
+ include Base
10
+
11
+ attr_reader :field_obj_num
12
+
13
+ # Class method to add appearance to an existing signature field
14
+ # Can be called from both Signature field creation and UpdateField
15
+ def self.add_appearance(document, field_ref, image_data, width: nil, height: nil)
16
+ new(document, "", {}).add_appearance_to_field(field_ref, image_data, width: width, height: height)
17
+ end
18
+
19
+ def call
20
+ @field_obj_num = next_fresh_object_number
21
+ widget_obj_num = @field_obj_num + 1
22
+
23
+ field_body = create_field_dictionary(@field_value, @field_type)
24
+ page_ref = find_page_ref(page_num)
25
+
26
+ widget_body = create_widget_annotation_with_parent(widget_obj_num, [@field_obj_num, 0], page_ref, x, y, width,
27
+ height, @field_type, @field_value)
28
+
29
+ document.instance_variable_get(:@patches) << { ref: [@field_obj_num, 0], body: field_body }
30
+ document.instance_variable_get(:@patches) << { ref: [widget_obj_num, 0], body: widget_body }
31
+
32
+ add_field_to_acroform_with_defaults(@field_obj_num)
33
+ add_widget_to_page(widget_obj_num, page_num)
34
+
35
+ # If this is a signature field with image data, add the signature appearance
36
+ if @field_value && !@field_value.empty?
37
+ image_data = @field_value
38
+ if image_data.is_a?(String) && (image_data.start_with?("data:image/") || (image_data.length > 50 && image_data.match?(%r{^[A-Za-z0-9+/]*={0,2}$})))
39
+ field_ref = [@field_obj_num, 0]
40
+ add_appearance_to_field(field_ref, image_data, width: width, height: height)
41
+ end
42
+ end
43
+
44
+ true
45
+ end
46
+
47
+ def add_appearance_to_field(field_ref, image_data, width: nil, height: nil)
48
+ return false unless field_ref && image_data && !image_data.empty?
49
+
50
+ # Decode image data if needed
51
+ decoded_image_data = image_data.is_a?(String) && image_data.start_with?("data:") ? decode_base64_data_uri(image_data) : decode_base64_if_needed(image_data)
52
+ return false unless decoded_image_data && !decoded_image_data.empty?
53
+
54
+ # Detect image format and dimensions
55
+ image_info = detect_image_format(decoded_image_data)
56
+ return false unless image_info
57
+
58
+ # Find widget annotation for this field
59
+ widget_ref = find_signature_widget(field_ref)
60
+ return false unless widget_ref
61
+
62
+ widget_body = get_object_body_with_patch(widget_ref)
63
+ return false unless widget_body
64
+
65
+ # Get widget rectangle for appearance size
66
+ rect = extract_rect(widget_body)
67
+ return false unless rect
68
+
69
+ # Ensure width and height are positive
70
+ rect_width = (rect[:x2] - rect[:x1]).abs
71
+ rect_height = (rect[:y2] - rect[:y1]).abs
72
+ return false if rect_width <= 0 || rect_height <= 0
73
+
74
+ # Get field dimensions (use provided width/height or field rect)
75
+ field_width = width || rect_width
76
+ field_height = height || rect_height
77
+
78
+ # Get image natural dimensions
79
+ image_width = image_info[:width].to_f
80
+ image_height = image_info[:height].to_f
81
+ return false if image_width <= 0 || image_height <= 0
82
+
83
+ # Calculate scaling factor to fit image within field while maintaining aspect ratio
84
+ scale_x = field_width / image_width
85
+ scale_y = field_height / image_height
86
+ scale_factor = [scale_x, scale_y].min
87
+
88
+ # Calculate scaled dimensions (maintains aspect ratio, fits within field)
89
+ scaled_width = image_width * scale_factor
90
+ scaled_height = image_height * scale_factor
91
+
92
+ # Create Image XObject(s) - use natural image dimensions (not scaled)
93
+ image_obj_num = next_fresh_object_number
94
+ image_result = create_image_xobject(image_obj_num, decoded_image_data, image_info, image_width, image_height)
95
+ image_body = image_result[:body]
96
+ mask_obj_num = image_result[:mask_obj_num]
97
+
98
+ # Create Form XObject (appearance stream) - use field dimensions for bounding box
99
+ form_obj_num = mask_obj_num ? mask_obj_num + 1 : image_obj_num + 1
100
+ form_body = create_form_xobject(form_obj_num, image_obj_num, field_width, field_height, scale_factor,
101
+ scaled_width, scaled_height)
102
+
103
+ # Queue new objects
104
+ document.instance_variable_get(:@patches) << { ref: [image_obj_num, 0], body: image_body }
105
+ if mask_obj_num
106
+ document.instance_variable_get(:@patches) << { ref: [mask_obj_num, 0],
107
+ body: image_result[:mask_body] }
108
+ end
109
+ document.instance_variable_get(:@patches) << { ref: [form_obj_num, 0], body: form_body }
110
+
111
+ # Update widget annotation with /AP dictionary
112
+ # Use already-loaded widget_body as original (we already have it from line 62)
113
+ # Only reload if we don't have it (shouldn't happen, but for safety)
114
+ original_widget_body = widget_body || resolver.object_body(widget_ref)
115
+ updated_widget = add_appearance_to_widget(widget_body, form_obj_num)
116
+ apply_patch(widget_ref, updated_widget, original_widget_body)
117
+
118
+ true
119
+ end
120
+
121
+ private
122
+
123
+ def decode_base64_data_uri(data_uri)
124
+ if data_uri =~ %r{^data:image/[^;]+;base64,(.+)$}
125
+ Base64.decode64(Regexp.last_match(1))
126
+ end
127
+ end
128
+
129
+ def decode_base64_if_needed(data)
130
+ if data.is_a?(String) && data.match?(%r{^[A-Za-z0-9+/]*={0,2}$})
131
+ begin
132
+ Base64.decode64(data)
133
+ rescue StandardError
134
+ data.b
135
+ end
136
+ else
137
+ data.is_a?(String) ? data.b : data
138
+ end
139
+ end
140
+
141
+ def detect_image_format(data)
142
+ # JPEG: starts with FF D8 FF
143
+ if data.bytesize >= 3 && data.getbyte(0) == 0xFF && data.getbyte(1) == 0xD8 && data.getbyte(2) == 0xFF
144
+ width, height = extract_jpeg_dimensions(data)
145
+ return { format: :jpeg, width: width, height: height, filter: "/DCTDecode" } if width && height
146
+ end
147
+
148
+ # PNG: starts with 89 50 4E 47 0D 0A 1A 0A
149
+ if data.bytesize >= 8 && data[0, 8] == "\x89PNG\r\n\x1A\n".b
150
+ width, height = extract_png_dimensions(data)
151
+ return { format: :png, width: width, height: height, filter: "/FlateDecode" } if width && height
152
+ end
153
+
154
+ nil
155
+ end
156
+
157
+ def extract_jpeg_dimensions(data)
158
+ i = 2
159
+ while i < data.bytesize - 9
160
+ if data.getbyte(i) == 0xFF && (data.getbyte(i + 1) & 0xF0) == 0xC0 && (i + 8 < data.bytesize)
161
+ height = (data.getbyte(i + 5) << 8) | data.getbyte(i + 6)
162
+ width = (data.getbyte(i + 7) << 8) | data.getbyte(i + 8)
163
+ return [width, height] if width.positive? && height.positive?
164
+ end
165
+ i += 1
166
+ end
167
+ [1, 1]
168
+ end
169
+
170
+ def extract_png_dimensions(data)
171
+ if data.bytesize >= 24
172
+ width = data[16, 4].unpack1("N")
173
+ height = data[20, 4].unpack1("N")
174
+ return [width, height]
175
+ end
176
+ nil
177
+ end
178
+
179
+ def find_signature_widget(field_ref)
180
+ # First check patches (for newly created widgets)
181
+ document.instance_variable_get(:@patches).each do |patch|
182
+ next unless patch[:body]
183
+ next unless DictScan.is_widget?(patch[:body])
184
+
185
+ if patch[:body] =~ %r{/Parent\s+(\d+)\s+(\d+)\s+R}
186
+ parent_ref = [Integer(Regexp.last_match(1)), Integer(Regexp.last_match(2))]
187
+ return patch[:ref] if parent_ref == field_ref
188
+ end
189
+
190
+ if patch[:body].include?("/FT") && DictScan.value_token_after("/FT",
191
+ patch[:body]) == "/Sig" && (patch[:ref] == field_ref)
192
+ return patch[:ref]
193
+ end
194
+ end
195
+
196
+ # Then check resolver (for existing widgets)
197
+ resolver.each_object do |ref, body|
198
+ next unless body && DictScan.is_widget?(body)
199
+
200
+ if body =~ %r{/Parent\s+(\d+)\s+(\d+)\s+R}
201
+ parent_ref = [Integer(Regexp.last_match(1)), Integer(Regexp.last_match(2))]
202
+ return ref if parent_ref == field_ref
203
+ end
204
+
205
+ if body.include?("/FT") && DictScan.value_token_after("/FT", body) == "/Sig" && (ref == field_ref)
206
+ return ref
207
+ end
208
+ end
209
+
210
+ # Fallback: if field_ref itself is a widget
211
+ body = get_object_body_with_patch(field_ref)
212
+ return field_ref if body && DictScan.is_widget?(body) && body.include?("/FT") && DictScan.value_token_after(
213
+ "/FT", body
214
+ ) == "/Sig"
215
+
216
+ nil
217
+ end
218
+
219
+ def extract_rect(widget_body)
220
+ rect_tok = DictScan.value_token_after("/Rect", widget_body)
221
+ return nil unless rect_tok && rect_tok.start_with?("[")
222
+
223
+ values = rect_tok.scan(/[-+]?\d*\.?\d+/).map(&:to_f)
224
+ return nil unless values.length == 4
225
+
226
+ { x1: values[0], y1: values[1], x2: values[2], y2: values[3] }
227
+ end
228
+
229
+ def create_image_xobject(obj_num, image_data, image_info, _width, _height)
230
+ stream_data = nil
231
+ filter = image_info[:filter]
232
+ mask_obj_num = nil
233
+ mask_body = nil
234
+
235
+ case image_info[:format]
236
+ when :jpeg
237
+ stream_data = image_data.b
238
+ when :png
239
+ begin
240
+ png = ChunkyPNG::Image.from_io(StringIO.new(image_data))
241
+
242
+ has_transparency = if png.palette
243
+ png.palette.include?(ChunkyPNG::Color::TRANSPARENT)
244
+ else
245
+ sample_size = [png.width * png.height, 1000].min
246
+ step = [png.width * png.height / sample_size, 1].max
247
+ has_alpha = false
248
+ (0...(png.width * png.height)).step(step) do |i|
249
+ x = i % png.width
250
+ y = i / png.width
251
+ alpha = ChunkyPNG::Color.a(png[x, y])
252
+ if alpha < 255
253
+ has_alpha = true
254
+ break
255
+ end
256
+ end
257
+ has_alpha
258
+ end
259
+
260
+ width = png.width
261
+ height = png.height
262
+
263
+ rgb_data = +""
264
+ alpha_data = +"" if has_transparency
265
+
266
+ height.times do |y|
267
+ width.times do |x|
268
+ color = png[x, y]
269
+ r = ChunkyPNG::Color.r(color)
270
+ g = ChunkyPNG::Color.g(color)
271
+ b = ChunkyPNG::Color.b(color)
272
+ rgb_data << [r, g, b].pack("C*")
273
+
274
+ if has_transparency
275
+ alpha = ChunkyPNG::Color.a(color)
276
+ alpha_data << [alpha].pack("C*")
277
+ end
278
+ end
279
+ end
280
+
281
+ stream_data = Zlib::Deflate.deflate(rgb_data)
282
+ filter = "/FlateDecode"
283
+
284
+ if has_transparency && alpha_data
285
+ mask_obj_num = obj_num + 1
286
+ compressed_alpha = Zlib::Deflate.deflate(alpha_data)
287
+ mask_length = compressed_alpha.bytesize
288
+
289
+ mask_body = "<<\n"
290
+ mask_body += " /Type /XObject\n"
291
+ mask_body += " /Subtype /Image\n"
292
+ mask_body += " /Width #{width}\n"
293
+ mask_body += " /Height #{height}\n"
294
+ mask_body += " /BitsPerComponent 8\n"
295
+ mask_body += " /ColorSpace /DeviceGray\n"
296
+ mask_body += " /Filter /FlateDecode\n"
297
+ mask_body += " /Length #{mask_length}\n"
298
+ mask_body += ">>\n"
299
+ mask_body += "stream\n"
300
+ mask_body += compressed_alpha
301
+ mask_body += "\nendstream"
302
+ end
303
+ rescue StandardError
304
+ stream_data = image_data.b
305
+ end
306
+ else
307
+ stream_data = image_data.b
308
+ end
309
+
310
+ stream_length = stream_data.bytesize
311
+
312
+ dict = "<<\n"
313
+ dict += " /Type /XObject\n"
314
+ dict += " /Subtype /Image\n"
315
+ dict += " /Width #{image_info[:width]}\n"
316
+ dict += " /Height #{image_info[:height]}\n"
317
+ dict += " /BitsPerComponent 8\n"
318
+ dict += " /ColorSpace /DeviceRGB\n"
319
+ dict += " /Filter #{filter}\n"
320
+ dict += " /Length #{stream_length}\n"
321
+ dict += " /SMask #{mask_obj_num} 0 R\n" if mask_obj_num
322
+ dict += ">>\n"
323
+ dict += "stream\n"
324
+ dict += stream_data
325
+ dict += "\nendstream"
326
+
327
+ { body: dict, mask_obj_num: mask_obj_num, mask_body: mask_body }
328
+ end
329
+
330
+ def create_form_xobject(_obj_num, image_obj_num, field_width, field_height, _scale_factor, scaled_width,
331
+ scaled_height)
332
+ offset_x = 0.0
333
+ offset_y = (field_height - scaled_height) / 2.0
334
+
335
+ content_stream = "q\n"
336
+ content_stream += "1 0 0 1 #{offset_x} #{offset_y} cm\n" if offset_x != 0 || offset_y != 0
337
+ content_stream += "#{scaled_width} 0 0 #{scaled_height} 0 0 cm\n"
338
+ content_stream += "/Im1 Do\n"
339
+ content_stream += "Q"
340
+
341
+ dict = "<<\n"
342
+ dict += " /Type /XObject\n"
343
+ dict += " /Subtype /Form\n"
344
+ dict += " /BBox [0 0 #{field_width} #{field_height}]\n"
345
+ dict += " /Resources << /XObject << /Im1 #{image_obj_num} 0 R >> >>\n"
346
+ dict += " /Length #{content_stream.bytesize}\n"
347
+ dict += ">>\n"
348
+ dict += "stream\n"
349
+ dict += content_stream
350
+ dict += "\nendstream"
351
+
352
+ dict
353
+ end
354
+
355
+ def add_appearance_to_widget(widget_body, form_obj_num)
356
+ new_ap_value = "<< /N #{form_obj_num} 0 R >>"
357
+
358
+ if widget_body.include?("/AP")
359
+ ap_key_match = widget_body.match(%r{/AP(?=[\s(<\[/])})
360
+ return widget_body unless ap_key_match
361
+
362
+ value_start = ap_key_match.end(0)
363
+ value_start += 1 while value_start < widget_body.length && widget_body[value_start] =~ /\s/
364
+
365
+ depth = 0
366
+ value_end = value_start
367
+ while value_end < widget_body.length
368
+ if widget_body[value_end, 2] == "<<"
369
+ depth += 1
370
+ value_end += 2
371
+ elsif widget_body[value_end, 2] == ">>"
372
+ depth -= 1
373
+ value_end += 2
374
+ break if depth.zero?
375
+ else
376
+ value_end += 1
377
+ end
378
+ end
379
+
380
+ before = widget_body[0...value_start]
381
+ after = widget_body[value_end..]
382
+ return "#{before}#{new_ap_value}#{after}"
383
+ end
384
+
385
+ if widget_body.include?(">>")
386
+ widget_body.sub(/(\s*)>>\s*$/, "\\1/AP #{new_ap_value}\n\\1>>")
387
+ else
388
+ widget_body + " /AP #{new_ap_value}"
389
+ end
390
+ end
391
+ end
392
+ end
393
+ end
@@ -0,0 +1,31 @@
1
+ # frozen_string_literal: true
2
+
3
+ module CorpPdf
4
+ module Fields
5
+ # Handles text field creation
6
+ class Text
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
+ true
28
+ end
29
+ end
30
+ end
31
+ end