acro_that 1.0.0 → 1.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.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: c25e9ad58be5a660ea6abb5a56896fef953d67578425b01a9de86f1cc1ae8a1d
4
- data.tar.gz: 45133e292e45343903ee53b44f709149bea0353d9aa7e2a2a9f3c6fa676da1dd
3
+ metadata.gz: e18f96c8c68c8c55a6081ebd91d6e5e45b49901f74224b1a192f0ddae567b097
4
+ data.tar.gz: 30b26260b600358b0be4bc2f212b175a3ec32c928d38f413a6aa92a7fe267707
5
5
  SHA512:
6
- metadata.gz: 7762f8c137e7b29c6cdbc15c8ce3fd9387b96dfc4a22a64ee171fc0c9c8d7f67951b42fb491cb283e160aced6a37d0e6eaa30599fa63d0876a77a9be8506d493
7
- data.tar.gz: 7a92be45bc9810a3e085c67fe0ee7b0a245925179ec345bbf0f3cb56cc4b2195bc6faca01f27a64c86646c7ef5c91dd1c6f732140905a6d208c4e8c476ff0458
6
+ metadata.gz: fb17fbe00e0f1ada261e12bc8025242a7a6884ff30a3aa9bbb12d86255bb3af8a1705e6e6621d2b2a2147cb432ed6b35b4228d1ffbf5795ab49e751ccb23f228
7
+ data.tar.gz: 1005a449cf42d39404c04667939dbba672325ee84437836a2bea5d40db02598ee8063acb9bd5a0fc902f3167cdde636a784436d98ed07c11a9aed67a08cc17e7
data/.gitignore CHANGED
@@ -2,6 +2,8 @@
2
2
  .rspec_status
3
3
 
4
4
  *.pdf
5
+ *.png
6
+ *.jpg
5
7
  *.gem
6
8
 
7
9
  research/
data/Gemfile.lock CHANGED
@@ -1,7 +1,7 @@
1
1
  PATH
2
2
  remote: .
3
3
  specs:
4
- acro_that (0.1.8)
4
+ acro_that (1.0.1)
5
5
  chunky_png (~> 1.4)
6
6
 
7
7
  GEM
@@ -54,12 +54,14 @@ module AcroThat
54
54
  def create_field_handler(type_input)
55
55
  is_radio = [:radio, "radio"].include?(type_input)
56
56
  group_id = @options[:group_id]
57
+ is_button = [:button, "button", "/Btn", "/btn"].include?(type_input)
57
58
 
58
59
  if is_radio && group_id
59
60
  AcroThat::Fields::Radio.new(@document, @name, @options.merge(metadata: @metadata))
60
61
  elsif [:signature, "signature", "/Sig"].include?(type_input)
61
62
  AcroThat::Fields::Signature.new(@document, @name, @options.merge(metadata: @metadata))
62
- elsif [:checkbox, "checkbox"].include?(type_input)
63
+ elsif [:checkbox, "checkbox"].include?(type_input) || is_button
64
+ # :button type maps to /Btn which are checkboxes by default (unless radio flag is set)
63
65
  AcroThat::Fields::Checkbox.new(@document, @name, @options.merge(metadata: @metadata))
64
66
  else
65
67
  # Default to text field
@@ -178,8 +178,34 @@ module AcroThat
178
178
  ft_pattern = %r{/FT\s+/Btn}
179
179
  is_button_field = ft_pattern.match(dict_body)
180
180
 
181
- normalized_value = if is_button_field
182
- # For checkboxes/radio buttons, normalize to "Yes" or "Off"
181
+ # Check if it's a radio button by checking field flags
182
+ # For widgets, check the parent field's flags since widgets don't have /Ff directly
183
+ is_radio = false
184
+ if is_button_field
185
+ field_flags_match = dict_body.match(%r{/Ff\s+(\d+)})
186
+ if field_flags_match
187
+ field_flags = field_flags_match[1].to_i
188
+ # Radio button flag is bit 15 = 32768
189
+ is_radio = field_flags.anybits?(32_768)
190
+ elsif dict_body.include?("/Parent")
191
+ # This is a widget - check parent field's flags
192
+ parent_tok = DictScan.value_token_after("/Parent", dict_body)
193
+ if parent_tok && parent_tok =~ /\A(\d+)\s+(\d+)\s+R/
194
+ parent_ref = [Integer(::Regexp.last_match(1)), Integer(::Regexp.last_match(2))]
195
+ parent_body = get_object_body_with_patch(parent_ref)
196
+ if parent_body
197
+ parent_flags_match = parent_body.match(%r{/Ff\s+(\d+)})
198
+ if parent_flags_match
199
+ parent_flags = parent_flags_match[1].to_i
200
+ is_radio = parent_flags.anybits?(32_768)
201
+ end
202
+ end
203
+ end
204
+ end
205
+ end
206
+
207
+ normalized_value = if is_button_field && !is_radio
208
+ # For checkboxes, normalize to "Yes" or "Off"
183
209
  # Accept "Yes", "/Yes" (PDF name format), true (boolean), or "true" (string)
184
210
  value_str = new_value.to_s
185
211
  is_checked = ["Yes", "/Yes", "true"].include?(value_str) || new_value == true
@@ -189,7 +215,13 @@ module AcroThat
189
215
  end
190
216
 
191
217
  # Encode the normalized value
192
- v_token = DictScan.encode_pdf_string(normalized_value)
218
+ # For checkboxes, use PDF name format to match /AS appearance state format
219
+ # For radio buttons and other fields, use PDF string format
220
+ v_token = if is_button_field && !is_radio
221
+ DictScan.encode_pdf_name(normalized_value)
222
+ else
223
+ DictScan.encode_pdf_string(normalized_value)
224
+ end
193
225
 
194
226
  # Find /V using pattern matching to ensure we get the complete key
195
227
  v_key_pattern = %r{/V(?=[\s(<\[/])}
@@ -545,34 +577,57 @@ module AcroThat
545
577
  end
546
578
 
547
579
  def create_checkbox_yes_appearance(width, height)
548
- # Create a form XObject that draws a checked checkbox
549
- # Box outline + checkmark
550
- # Scale to match width and height
551
- # Simple appearance: draw a box and a checkmark
552
- # For simplicity, use PDF drawing operators
553
- # Box: rectangle from (0,0) to (width, height)
554
- # Checkmark: simple path drawing
555
-
556
- # PDF content stream for checked checkbox
557
- # Draw just the checkmark (no box border)
580
+ line_width = [width * 0.05, height * 0.05].min
558
581
  border_width = [width * 0.08, height * 0.08].min
559
582
 
560
- # Calculate checkmark path
561
- check_x1 = width * 0.25
562
- check_y1 = height * 0.45
563
- check_x2 = width * 0.45
564
- check_y2 = height * 0.25
565
- check_x3 = width * 0.75
566
- check_y3 = height * 0.75
583
+ # Define checkmark in normalized coordinates (0-1 range) for consistent aspect ratio
584
+ # Checkmark shape: three points forming a checkmark
585
+ norm_x1 = 0.25
586
+ norm_y1 = 0.55
587
+ norm_x2 = 0.45
588
+ norm_y2 = 0.35
589
+ norm_x3 = 0.75
590
+ norm_y3 = 0.85
591
+
592
+ # Calculate scale to maximize size while maintaining aspect ratio
593
+ # Use the smaller dimension to ensure it fits
594
+ scale = [width, height].min * 0.85 # Use 85% of the smaller dimension
595
+
596
+ # Calculate checkmark dimensions
597
+ check_width = scale
598
+ check_height = scale
599
+
600
+ # Center the checkmark in the box
601
+ offset_x = (width - check_width) / 2
602
+ offset_y = (height - check_height) / 2
603
+
604
+ # Calculate actual coordinates
605
+ check_x1 = offset_x + norm_x1 * check_width
606
+ check_y1 = offset_y + norm_y1 * check_height
607
+ check_x2 = offset_x + norm_x2 * check_width
608
+ check_y2 = offset_y + norm_y2 * check_height
609
+ check_x3 = offset_x + norm_x3 * check_width
610
+ check_y3 = offset_y + norm_y3 * check_height
567
611
 
568
612
  content_stream = "q\n"
569
- content_stream += "0 0 0 rg\n" # Black color (darker)
570
- content_stream += "#{border_width} w\n" # Line width
571
- # Draw checkmark only (no box border)
613
+ # Draw square border around field bounds
614
+ content_stream += "0 0 0 RG\n" # Black stroke color
615
+ content_stream += "#{line_width} w\n" # Line width
616
+ # Draw rectangle from (0,0) to (width, height)
617
+ content_stream += "0 0 m\n"
618
+ content_stream += "#{width} 0 l\n"
619
+ content_stream += "#{width} #{height} l\n"
620
+ content_stream += "0 #{height} l\n"
621
+ content_stream += "0 0 l\n"
622
+ content_stream += "S\n" # Stroke the border
623
+
624
+ # Draw checkmark
625
+ content_stream += "0 0 0 rg\n" # Black fill color
626
+ content_stream += "#{border_width} w\n" # Line width for checkmark
572
627
  content_stream += "#{check_x1} #{check_y1} m\n"
573
628
  content_stream += "#{check_x2} #{check_y2} l\n"
574
629
  content_stream += "#{check_x3} #{check_y3} l\n"
575
- content_stream += "S\n" # Stroke
630
+ content_stream += "S\n" # Stroke the checkmark
576
631
  content_stream += "Q\n"
577
632
 
578
633
  build_form_xobject(content_stream, width, height)
@@ -107,6 +107,7 @@ module AcroThat
107
107
  end
108
108
 
109
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
110
111
  # For other fields, encode as PDF string
111
112
  if should_set_value && normalized_field_value && !normalized_field_value.to_s.empty?
112
113
  # For radio buttons, only set /V if selected option is explicitly set to true
@@ -115,6 +116,10 @@ module AcroThat
115
116
  if [true, "true"].include?(@options[:selected]) && normalized_field_value.to_s.start_with?("/")
116
117
  dict += " /V #{normalized_field_value}\n"
117
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"
118
123
  else
119
124
  dict += " /V #{DictScan.encode_pdf_string(normalized_field_value)}\n"
120
125
  end
@@ -156,10 +161,11 @@ module AcroThat
156
161
  end
157
162
 
158
163
  if type == "/Btn" && should_set_value
164
+ # For checkboxes, encode value as PDF name to match /AS appearance state format
159
165
  value_str = value.to_s
160
166
  is_checked = ["Yes", "/Yes", "true"].include?(value_str) || value == true
161
167
  checkbox_value = is_checked ? "Yes" : "Off"
162
- widget += " /V #{DictScan.encode_pdf_string(checkbox_value)}\n"
168
+ widget += " /V #{DictScan.encode_pdf_name(checkbox_value)}\n"
163
169
  elsif should_set_value && value && !value.empty?
164
170
  widget += " /V #{DictScan.encode_pdf_string(value)}\n"
165
171
  end
@@ -193,7 +199,7 @@ module AcroThat
193
199
 
194
200
  af_body = get_object_body_with_patch(af_ref)
195
201
  # Use +"" instead of dup to create a mutable copy without keeping reference to original
196
- patched = af_body + ""
202
+ patched = af_body.to_s
197
203
 
198
204
  # Step 1: Add field to /Fields array
199
205
  fields_array_ref = DictScan.value_token_after("/Fields", patched)
@@ -44,7 +44,7 @@ module AcroThat
44
44
  widget_ref = [widget_obj_num, 0]
45
45
  original_widget_body = get_object_body_with_patch(widget_ref)
46
46
  # Use +"" instead of dup to create a mutable copy without keeping reference to original
47
- widget_body = original_widget_body + ""
47
+ widget_body = original_widget_body.to_s
48
48
 
49
49
  ap_dict = "<<\n /N <<\n /Yes #{yes_obj_num} 0 R\n /Off #{off_obj_num} 0 R\n >>\n>>"
50
50
 
@@ -58,12 +58,23 @@ module AcroThat
58
58
  is_checked = value_str == "Yes" || value_str == "/Yes" || value_str == "true" || @field_value == true
59
59
  normalized_value = is_checked ? "Yes" : "Off"
60
60
 
61
+ # Set /V to match /AS - both should be PDF names for checkboxes
62
+ v_value = DictScan.encode_pdf_name(normalized_value)
63
+
61
64
  as_value = if normalized_value == "Yes"
62
65
  "/Yes"
63
66
  else
64
67
  "/Off"
65
68
  end
66
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
67
78
  widget_body = if widget_body.include?("/AS")
68
79
  DictScan.replace_key_value(widget_body, "/AS", as_value)
69
80
  else
@@ -74,15 +85,37 @@ module AcroThat
74
85
  end
75
86
 
76
87
  def create_checkbox_yes_appearance(width, height)
77
- border_width = [width * 0.08, height * 0.08].min
78
88
  line_width = [width * 0.05, height * 0.05].min
89
+ border_width = [width * 0.08, height * 0.08].min
79
90
 
80
- check_x1 = width * 0.25
81
- check_y1 = height * 0.45
82
- check_x2 = width * 0.45
83
- check_y2 = height * 0.25
84
- check_x3 = width * 0.75
85
- check_y3 = height * 0.75
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
86
119
 
87
120
  content_stream = "q\n"
88
121
  # Draw square border around field bounds
@@ -74,7 +74,7 @@ module AcroThat
74
74
  return unless original_widget_body
75
75
 
76
76
  # Store original before modifying to avoid loading again
77
- widget_body = original_widget_body + ""
77
+ widget_body = original_widget_body.to_s
78
78
 
79
79
  # Ensure we have a valid export value - if empty, generate a unique one
80
80
  # Export value must be unique for each widget in the group for mutual exclusivity
@@ -124,7 +124,7 @@ module AcroThat
124
124
  original_parent_body = get_object_body_with_patch(parent_ref)
125
125
  if original_parent_body
126
126
  # Store original before modifying
127
- parent_body = original_parent_body + ""
127
+ parent_body = original_parent_body.to_s
128
128
  # Update parent's /V to match the selected button's export value
129
129
  parent_body = if parent_body.include?("/V")
130
130
  DictScan.replace_key_value(parent_body, "/V", export_name)
@@ -143,7 +143,7 @@ module AcroThat
143
143
  return unless original_parent_body_for_ap
144
144
 
145
145
  # Use a working copy for modification
146
- parent_body_for_ap = original_parent_body_for_ap + ""
146
+ parent_body_for_ap = original_parent_body_for_ap.to_s
147
147
  parent_ap_tok = DictScan.value_token_after("/AP", parent_body_for_ap)
148
148
  if parent_ap_tok && parent_ap_tok.start_with?("<<")
149
149
  n_tok = DictScan.value_token_after("/N", parent_ap_tok)
@@ -165,12 +165,34 @@ module AcroThat
165
165
  # Draw only the checkmark (no border)
166
166
  border_width = [width * 0.08, height * 0.08].min
167
167
 
168
- check_x1 = width * 0.25
169
- check_y1 = height * 0.45
170
- check_x2 = width * 0.45
171
- check_y2 = height * 0.25
172
- check_x3 = width * 0.75
173
- check_y3 = height * 0.75
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
174
196
 
175
197
  content_stream = "q\n"
176
198
  # Draw checkmark only (no border)
@@ -47,29 +47,64 @@ module AcroThat
47
47
  # Sort offsets and group consecutive objects into subsections
48
48
  sorted = @offsets.sort_by { |num, gen, _offset| [num, gen] }
49
49
 
50
- i = 0
51
- while i < sorted.length
52
- first_num = sorted[i][0]
53
-
54
- # Find consecutive run
55
- run_length = 1
56
- while (i + run_length) < sorted.length &&
57
- sorted[i + run_length][0] == first_num + run_length &&
58
- sorted[i + run_length][1] == sorted[i][1]
59
- run_length += 1
60
- end
61
-
62
- # Write subsection header
63
- xref << "#{first_num} #{run_length}\n".b
50
+ # Find max object number to determine Size
51
+ max_obj_num = sorted.map { |num, _gen, _offset| num }.max || 0
64
52
 
65
- # Write entries in this subsection
66
- run_length.times do |j|
67
- offset = sorted[i + j][2]
68
- gen = sorted[i + j][1]
69
- xref << format("%010d %05d n \n", offset, gen).b
53
+ # Build xref entries covering all objects from 0 to max_obj_num
54
+ # Missing objects are marked as free (type 'f')
55
+ i = 0
56
+ current_obj = 0
57
+
58
+ while current_obj <= max_obj_num
59
+ # Find next existing object
60
+ next_existing = sorted.find { |num, _gen, _offset| num >= current_obj }
61
+
62
+ if next_existing && next_existing[0] == current_obj
63
+ # Object exists - find consecutive run of existing objects
64
+ first_num = current_obj
65
+ run_length = 1
66
+
67
+ while (i + run_length) < sorted.length &&
68
+ sorted[i + run_length][0] == first_num + run_length &&
69
+ sorted[i + run_length][1] == sorted[i][1]
70
+ run_length += 1
71
+ end
72
+
73
+ # Write subsection header
74
+ xref << "#{first_num} #{run_length}\n".b
75
+
76
+ # Write entries in this subsection
77
+ run_length.times do |j|
78
+ offset = sorted[i + j][2]
79
+ gen = sorted[i + j][1]
80
+ xref << format("%010d %05d n \n", offset, gen).b
81
+ end
82
+
83
+ i += run_length
84
+ current_obj = first_num + run_length
85
+ else
86
+ # Object doesn't exist - find consecutive run of missing objects
87
+ first_missing = current_obj
88
+ missing_count = 1
89
+
90
+ while current_obj + missing_count <= max_obj_num
91
+ check_obj = current_obj + missing_count
92
+ if sorted.any? { |num, _gen, _offset| num == check_obj }
93
+ break
94
+ end
95
+ missing_count += 1
96
+ end
97
+
98
+ # Write subsection header for missing objects
99
+ xref << "#{first_missing} #{missing_count}\n".b
100
+
101
+ # Write free entries
102
+ missing_count.times do
103
+ xref << "0000000000 65535 f \n".b
104
+ end
105
+
106
+ current_obj = first_missing + missing_count
70
107
  end
71
-
72
- i += run_length
73
108
  end
74
109
 
75
110
  @buffer << xref
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module AcroThat
4
- VERSION = "1.0.0"
4
+ VERSION = "1.0.2"
5
5
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: acro_that
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.0.0
4
+ version: 1.0.2
5
5
  platform: ruby
6
6
  authors:
7
7
  - Michael Wynkoop