pdfrb 0.6.0 → 0.7.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 +51 -0
- data/RELEASE.md +46 -33
- data/data/pdfrb/afm/Courier-Bold.afm +342 -342
- data/data/pdfrb/afm/Courier-BoldOblique.afm +342 -342
- data/data/pdfrb/afm/Courier-Oblique.afm +342 -342
- data/data/pdfrb/afm/Courier.afm +342 -342
- data/data/pdfrb/afm/Helvetica-Bold.afm +2827 -2827
- data/data/pdfrb/afm/Helvetica-BoldOblique.afm +2827 -2827
- data/data/pdfrb/afm/Helvetica-Oblique.afm +3051 -3051
- data/data/pdfrb/afm/Helvetica.afm +3051 -3051
- data/data/pdfrb/afm/Symbol.afm +213 -213
- data/data/pdfrb/afm/Times-Bold.afm +2588 -2588
- data/data/pdfrb/afm/Times-BoldItalic.afm +2384 -2384
- data/data/pdfrb/afm/Times-Italic.afm +2667 -2667
- data/data/pdfrb/afm/Times-Roman.afm +2419 -2419
- data/data/pdfrb/afm/ZapfDingbats.afm +225 -225
- data/lib/pdfrb/content/canvas.rb +137 -0
- data/lib/pdfrb/content/operators/inline_image.rb +53 -0
- data/lib/pdfrb/content/operators.rb +1 -0
- data/lib/pdfrb/image_loader/png.rb +76 -19
- data/lib/pdfrb/model/type/page_piece_info.rb +58 -0
- data/lib/pdfrb/model/type.rb +3 -0
- data/lib/pdfrb/task/regenerate_appearances.rb +98 -0
- data/lib/pdfrb/task.rb +1 -0
- data/lib/pdfrb/version.rb +1 -1
- metadata +9 -3
|
@@ -10,46 +10,103 @@ module Pdfrb
|
|
|
10
10
|
info = parse_header(data)
|
|
11
11
|
return nil if info.empty?
|
|
12
12
|
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
13
|
+
dict = {
|
|
14
|
+
Type: :XObject,
|
|
15
|
+
Subtype: :Image,
|
|
16
|
+
Width: info[:width],
|
|
17
|
+
Height: info[:height],
|
|
18
|
+
ColorSpace: info[:color_space],
|
|
19
|
+
BitsPerComponent: info[:bits],
|
|
20
|
+
Filter: :FlateDecode,
|
|
21
|
+
DecodeParms: { Predictor: 15, Columns: info[:width] },
|
|
22
|
+
Length: 0,
|
|
23
|
+
}
|
|
24
|
+
mask = color_key_mask(info)
|
|
25
|
+
dict[:Mask] = mask if mask
|
|
26
|
+
|
|
27
|
+
image = document.add(dict, type: Pdfrb::Model::Type::XObjectImage)
|
|
27
28
|
image.stream = ""
|
|
28
29
|
image
|
|
29
30
|
end
|
|
30
31
|
|
|
31
32
|
def parse_header(data)
|
|
32
|
-
return {} unless data.is_a?(String) && data.bytesize >= 24
|
|
33
|
+
return {} unless data.is_a?(::String) && data.bytesize >= 24
|
|
33
34
|
return {} unless data.start_with?("\x89PNG\r\n\x1A\n".b)
|
|
34
35
|
return {} unless data.bytes[12, 4].pack("C*") == "IHDR"
|
|
35
36
|
|
|
36
37
|
off = 16
|
|
37
|
-
|
|
38
|
+
color_type = data.getbyte(off + 9)
|
|
39
|
+
result = {
|
|
38
40
|
width: read_u32(data, off),
|
|
39
41
|
height: read_u32(data, off + 4),
|
|
40
42
|
bits: data.getbyte(off + 8),
|
|
41
|
-
color_type:
|
|
42
|
-
color_space: case
|
|
43
|
+
color_type: color_type,
|
|
44
|
+
color_space: case color_type
|
|
43
45
|
when 0, 4 then :DeviceGray
|
|
44
46
|
when 2, 6 then :DeviceRGB
|
|
45
47
|
when 3 then :Indexed
|
|
46
48
|
else :DeviceRGB
|
|
47
49
|
end,
|
|
48
50
|
}
|
|
51
|
+
result[:trns] = read_trns_chunk(data, color_type)
|
|
52
|
+
result
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
# Walk the PNG chunks looking for tRNS. Returns the raw tRNS
|
|
56
|
+
# payload bytes, or nil if absent. Stops at IDAT to avoid
|
|
57
|
+
# scanning the whole file.
|
|
58
|
+
def read_trns_chunk(data, color_type)
|
|
59
|
+
offset = 8 # skip PNG signature
|
|
60
|
+
while offset + 8 <= data.bytesize
|
|
61
|
+
length = read_u32(data, offset)
|
|
62
|
+
chunk_type = data.byteslice(offset + 4, 4)
|
|
63
|
+
offset += 8
|
|
64
|
+
case chunk_type
|
|
65
|
+
when "IHDR" # already parsed; skip
|
|
66
|
+
offset += length + 4
|
|
67
|
+
when "tRNS"
|
|
68
|
+
return data.byteslice(offset, length)
|
|
69
|
+
when "IDAT", "IEND"
|
|
70
|
+
return nil
|
|
71
|
+
else
|
|
72
|
+
offset += length + 4
|
|
73
|
+
end
|
|
74
|
+
end
|
|
75
|
+
nil
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
# Build a PDF /Mask color-key array from the tRNS chunk. Each
|
|
79
|
+
# entry is [min, max] per channel. Returns nil if tRNS is
|
|
80
|
+
# absent or for indexed color (which uses /SMask instead).
|
|
81
|
+
def color_key_mask(info)
|
|
82
|
+
trns = info[:trns]
|
|
83
|
+
return nil unless trns
|
|
84
|
+
return nil if info[:color_type] == 3 # palette uses /SMask
|
|
85
|
+
|
|
86
|
+
case info[:color_type]
|
|
87
|
+
when 0 # grayscale: 1 two-byte value
|
|
88
|
+
v = read_u16(trns, 0)
|
|
89
|
+
[v, v]
|
|
90
|
+
when 2, 6 # RGB: 3 two-byte values
|
|
91
|
+
r = read_u16(trns, 0)
|
|
92
|
+
g = read_u16(trns, 2)
|
|
93
|
+
b = read_u16(trns, 4)
|
|
94
|
+
[r, r, g, g, b, b]
|
|
95
|
+
when 4 # grayscale+alpha: same as grayscale
|
|
96
|
+
v = read_u16(trns, 0)
|
|
97
|
+
[v, v]
|
|
98
|
+
end
|
|
99
|
+
end
|
|
100
|
+
|
|
101
|
+
def read_u16(data, off)
|
|
102
|
+
return 0 unless data && data.bytesize >= off + 2
|
|
103
|
+
|
|
104
|
+
(data.getbyte(off) << 8) | data.getbyte(off + 1)
|
|
49
105
|
end
|
|
50
106
|
|
|
51
107
|
def read_u32(data, off)
|
|
52
|
-
(data.getbyte(off) << 24) | (data.getbyte(off + 1) << 16) |
|
|
108
|
+
(data.getbyte(off) << 24) | (data.getbyte(off + 1) << 16) |
|
|
109
|
+
(data.getbyte(off + 2) << 8) | data.getbyte(off + 3)
|
|
53
110
|
end
|
|
54
111
|
end
|
|
55
112
|
end
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Pdfrb
|
|
4
|
+
module Model
|
|
5
|
+
module Type
|
|
6
|
+
# Page-piece dictionary (s14.5). Per-page metadata recording
|
|
7
|
+
# application-specific private data. The dictionary hangs off
|
|
8
|
+
# a page's /PieceInfo key and is keyed by application name;
|
|
9
|
+
# each value carries /LastModified, /Private, and (optionally)
|
|
10
|
+
# an /ASFStream reference for a stream-encoded private payload.
|
|
11
|
+
#
|
|
12
|
+
# Conforming readers MUST NOT interpret application-private
|
|
13
|
+
# data; it survives round-trips and incremental updates intact.
|
|
14
|
+
class PagePieceInfo < Pdfrb::Model::Cos::Dictionary
|
|
15
|
+
# The application-name key (e.g. :Adobe, :Pdfrb). Multiple
|
|
16
|
+
# applications may each have their own entry in the same
|
|
17
|
+
# /PieceInfo dict.
|
|
18
|
+
def application_name
|
|
19
|
+
value.key?(:Application) ? value[:Application] : nil
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
# /LastModified — PDF date string, parsed to a Time.
|
|
23
|
+
def last_modified
|
|
24
|
+
v = value[:LastModified]
|
|
25
|
+
return nil unless v.is_a?(::String)
|
|
26
|
+
|
|
27
|
+
Pdfrb::Model::Date.parse(v)
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
# /Private — application-private dict. Untyped on purpose:
|
|
31
|
+
# the contents are app-defined.
|
|
32
|
+
def private_data
|
|
33
|
+
value[:Private]
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
# Set the application's /LastModified to +time+. +time+ is
|
|
37
|
+
# converted to a PDF date string.
|
|
38
|
+
def last_modified=(time)
|
|
39
|
+
value[:LastModified] = Pdfrb::Model::Date.format(time)
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
# Merge +hash+ into /Private. If /Private is absent, it is
|
|
43
|
+
# created from the hash.
|
|
44
|
+
def merge_private!(hash)
|
|
45
|
+
existing = value[:Private]
|
|
46
|
+
if existing.is_a?(Pdfrb::Model::Cos::Dictionary)
|
|
47
|
+
existing.value.merge!(hash)
|
|
48
|
+
elsif existing.is_a?(::Hash)
|
|
49
|
+
existing.merge!(hash)
|
|
50
|
+
else
|
|
51
|
+
value[:Private] = hash
|
|
52
|
+
end
|
|
53
|
+
self
|
|
54
|
+
end
|
|
55
|
+
end
|
|
56
|
+
end
|
|
57
|
+
end
|
|
58
|
+
end
|
data/lib/pdfrb/model/type.rb
CHANGED
|
@@ -353,6 +353,9 @@ module Pdfrb
|
|
|
353
353
|
# Structure types (s14.7).
|
|
354
354
|
autoload :StructureAttributes, "pdfrb/model/type/structure_attributes"
|
|
355
355
|
autoload :StructureElementKid, "pdfrb/model/type/structure_element_kid"
|
|
356
|
+
|
|
357
|
+
# Page-piece dictionary (s14.5).
|
|
358
|
+
autoload :PagePieceInfo, "pdfrb/model/type/page_piece_info"
|
|
356
359
|
end
|
|
357
360
|
end
|
|
358
361
|
end
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Pdfrb
|
|
4
|
+
module Task
|
|
5
|
+
# Walks every widget annotation in the document and regenerates
|
|
6
|
+
# the /AP /N appearance stream via Appearance::Generator. Use
|
|
7
|
+
# after mutating field values directly (e.g. via Form#set_value)
|
|
8
|
+
# or to repair documents with /NeedAppearances set so viewers
|
|
9
|
+
# don't have to synthesise appearances on the fly.
|
|
10
|
+
#
|
|
11
|
+
# Per ISO 32000-2 §12.5.5, an annotation's /AP /N is the normal
|
|
12
|
+
# appearance; PDF/A requires it for every widget. This task
|
|
13
|
+
# also clears /NeedAppearances on /AcroForm since by definition
|
|
14
|
+
# all fields now have a computed appearance.
|
|
15
|
+
module RegenerateAppearances
|
|
16
|
+
module_function
|
|
17
|
+
|
|
18
|
+
# @param document [Pdfrb::Document]
|
|
19
|
+
# @param only: [Array<Symbol>, nil] limit to the given FT values
|
|
20
|
+
# (e.g. [:Tx, :Btn]). Default: all field types.
|
|
21
|
+
# @return [Integer] count of appearances regenerated.
|
|
22
|
+
def call(document, only: nil)
|
|
23
|
+
generator = Pdfrb::Appearance::Generator.new(document)
|
|
24
|
+
widgets = each_widget(document).to_a
|
|
25
|
+
count = 0
|
|
26
|
+
widgets.each do |widget|
|
|
27
|
+
next unless matching?(widget, only)
|
|
28
|
+
|
|
29
|
+
regenerated = regenerate(generator, widget)
|
|
30
|
+
count += 1 if regenerated
|
|
31
|
+
end
|
|
32
|
+
clear_need_appearances(document)
|
|
33
|
+
count
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
def each_widget(document)
|
|
37
|
+
return enum_for(:each_widget, document) unless block_given?
|
|
38
|
+
|
|
39
|
+
document.each_indirect_object do |obj|
|
|
40
|
+
next unless obj.value.is_a?(::Hash)
|
|
41
|
+
|
|
42
|
+
subtype = obj.value[:Subtype]
|
|
43
|
+
yield obj if subtype == :Widget
|
|
44
|
+
end
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
def matching?(widget, only)
|
|
48
|
+
return true if only.nil?
|
|
49
|
+
|
|
50
|
+
ft = widget.value[:FT]
|
|
51
|
+
ft && only.include?(ft.to_sym)
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
def regenerate(generator, widget)
|
|
55
|
+
ft = widget.value[:FT]&.to_sym
|
|
56
|
+
value = widget.value[:V]
|
|
57
|
+
case ft
|
|
58
|
+
when :Tx
|
|
59
|
+
generator.text_field(widget, value: value.to_s)
|
|
60
|
+
true
|
|
61
|
+
when :Btn
|
|
62
|
+
if value.is_a?(::Hash)
|
|
63
|
+
# Multi-state radio; regenerate each state's appearance.
|
|
64
|
+
value.each_key do |state|
|
|
65
|
+
generator.checkbox(widget, checked: state != :Off)
|
|
66
|
+
end
|
|
67
|
+
else
|
|
68
|
+
generator.checkbox(widget, checked: [:Yes, true].include?(value))
|
|
69
|
+
end
|
|
70
|
+
true
|
|
71
|
+
when :Ch
|
|
72
|
+
generator.combo(widget, value: value.to_s)
|
|
73
|
+
true
|
|
74
|
+
else
|
|
75
|
+
false
|
|
76
|
+
end
|
|
77
|
+
rescue StandardError
|
|
78
|
+
# Appearance generation is best-effort; if a single field
|
|
79
|
+
# raises, skip it rather than aborting the whole sweep.
|
|
80
|
+
false
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
def clear_need_appearances(document)
|
|
84
|
+
catalog = document.catalog
|
|
85
|
+
return unless catalog
|
|
86
|
+
|
|
87
|
+
acroform = catalog.value[:AcroForm]
|
|
88
|
+
return unless acroform
|
|
89
|
+
|
|
90
|
+
if acroform.is_a?(Pdfrb::Model::Cos::Dictionary)
|
|
91
|
+
acroform.value.delete(:NeedAppearances)
|
|
92
|
+
elsif acroform.is_a?(::Hash)
|
|
93
|
+
acroform.delete(:NeedAppearances)
|
|
94
|
+
end
|
|
95
|
+
end
|
|
96
|
+
end
|
|
97
|
+
end
|
|
98
|
+
end
|
data/lib/pdfrb/task.rb
CHANGED
data/lib/pdfrb/version.rb
CHANGED
metadata
CHANGED
|
@@ -1,13 +1,14 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: pdfrb
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.
|
|
4
|
+
version: 0.7.1
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Ribose Inc.
|
|
8
|
+
autorequire:
|
|
8
9
|
bindir: exe
|
|
9
10
|
cert_chain: []
|
|
10
|
-
date:
|
|
11
|
+
date: 2026-08-08 00:00:00.000000000 Z
|
|
11
12
|
dependencies:
|
|
12
13
|
- !ruby/object:Gem::Dependency
|
|
13
14
|
name: thor
|
|
@@ -774,6 +775,7 @@ files:
|
|
|
774
775
|
- lib/pdfrb/content/operators/color.rb
|
|
775
776
|
- lib/pdfrb/content/operators/general.rb
|
|
776
777
|
- lib/pdfrb/content/operators/graphics_state_params.rb
|
|
778
|
+
- lib/pdfrb/content/operators/inline_image.rb
|
|
777
779
|
- lib/pdfrb/content/operators/marked_content.rb
|
|
778
780
|
- lib/pdfrb/content/operators/painting.rb
|
|
779
781
|
- lib/pdfrb/content/operators/path.rb
|
|
@@ -1057,6 +1059,7 @@ files:
|
|
|
1057
1059
|
- lib/pdfrb/model/type/output_intent.rb
|
|
1058
1060
|
- lib/pdfrb/model/type/page.rb
|
|
1059
1061
|
- lib/pdfrb/model/type/page_label.rb
|
|
1062
|
+
- lib/pdfrb/model/type/page_piece_info.rb
|
|
1060
1063
|
- lib/pdfrb/model/type/page_tree_node.rb
|
|
1061
1064
|
- lib/pdfrb/model/type/page_tree_node_root.rb
|
|
1062
1065
|
- lib/pdfrb/model/type/pattern.rb
|
|
@@ -1160,6 +1163,7 @@ files:
|
|
|
1160
1163
|
- lib/pdfrb/task/memory_profile.rb
|
|
1161
1164
|
- lib/pdfrb/task/merge.rb
|
|
1162
1165
|
- lib/pdfrb/task/optimize.rb
|
|
1166
|
+
- lib/pdfrb/task/regenerate_appearances.rb
|
|
1163
1167
|
- lib/pdfrb/test_utils.rb
|
|
1164
1168
|
- lib/pdfrb/validator.rb
|
|
1165
1169
|
- lib/pdfrb/version.rb
|
|
@@ -1181,6 +1185,7 @@ metadata:
|
|
|
1181
1185
|
changelog_uri: https://github.com/claricle/pdfrb/blob/main/CHANGELOG.md
|
|
1182
1186
|
bug_tracker_uri: https://github.com/claricle/pdfrb/issues
|
|
1183
1187
|
rubygems_mfa_required: 'true'
|
|
1188
|
+
post_install_message:
|
|
1184
1189
|
rdoc_options: []
|
|
1185
1190
|
require_paths:
|
|
1186
1191
|
- lib
|
|
@@ -1195,7 +1200,8 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
|
1195
1200
|
- !ruby/object:Gem::Version
|
|
1196
1201
|
version: '0'
|
|
1197
1202
|
requirements: []
|
|
1198
|
-
rubygems_version:
|
|
1203
|
+
rubygems_version: 3.5.22
|
|
1204
|
+
signing_key:
|
|
1199
1205
|
specification_version: 4
|
|
1200
1206
|
summary: Pure-Ruby PDF parser, Arlington-model-driven domain model, and serializer
|
|
1201
1207
|
test_files: []
|