prawnx 0.1.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 +7 -0
- data/COPYING +2 -0
- data/GPLv2 +339 -0
- data/GPLv3 +674 -0
- data/LICENSE +65 -0
- data/LICENSE-prawn-svg +21 -0
- data/README.md +107 -0
- data/data/fonts/Courier-Bold.afm +342 -0
- data/data/fonts/Courier-BoldOblique.afm +342 -0
- data/data/fonts/Courier-Oblique.afm +342 -0
- data/data/fonts/Courier.afm +342 -0
- data/data/fonts/Helvetica-Bold.afm +2827 -0
- data/data/fonts/Helvetica-BoldOblique.afm +2827 -0
- data/data/fonts/Helvetica-Oblique.afm +3051 -0
- data/data/fonts/Helvetica.afm +3051 -0
- data/data/fonts/MustRead.html +19 -0
- data/data/fonts/Symbol.afm +213 -0
- data/data/fonts/Times-Bold.afm +2588 -0
- data/data/fonts/Times-BoldItalic.afm +2384 -0
- data/data/fonts/Times-Italic.afm +2667 -0
- data/data/fonts/Times-Roman.afm +2419 -0
- data/data/fonts/ZapfDingbats.afm +225 -0
- data/lib/pdf/core.rb +4 -0
- data/lib/prawn/measurement_extensions.rb +4 -0
- data/lib/prawn/svg/attributes/clip_path.rb +46 -0
- data/lib/prawn/svg/attributes/mask.rb +16 -0
- data/lib/prawn/svg/attributes/opacity.rb +30 -0
- data/lib/prawn/svg/attributes/space.rb +10 -0
- data/lib/prawn/svg/attributes/stroke.rb +50 -0
- data/lib/prawn/svg/attributes/transform.rb +10 -0
- data/lib/prawn/svg/attributes.rb +6 -0
- data/lib/prawn/svg/calculators/arc_to_bezier_curve.rb +57 -0
- data/lib/prawn/svg/calculators/aspect_ratio.rb +60 -0
- data/lib/prawn/svg/calculators/document_sizing.rb +103 -0
- data/lib/prawn/svg/calculators/path_length.rb +168 -0
- data/lib/prawn/svg/calculators/pixels.rb +57 -0
- data/lib/prawn/svg/color.rb +240 -0
- data/lib/prawn/svg/css/font_face_parser.rb +72 -0
- data/lib/prawn/svg/css/font_family_parser.rb +39 -0
- data/lib/prawn/svg/css/font_parser.rb +46 -0
- data/lib/prawn/svg/css/selector_parser.rb +178 -0
- data/lib/prawn/svg/css/stylesheets.rb +233 -0
- data/lib/prawn/svg/css/values_parser.rb +68 -0
- data/lib/prawn/svg/document.rb +170 -0
- data/lib/prawn/svg/elements/anchor.rb +10 -0
- data/lib/prawn/svg/elements/base.rb +312 -0
- data/lib/prawn/svg/elements/bbox_scaling.rb +78 -0
- data/lib/prawn/svg/elements/call_duplicator.rb +37 -0
- data/lib/prawn/svg/elements/circle.rb +25 -0
- data/lib/prawn/svg/elements/clip_path.rb +26 -0
- data/lib/prawn/svg/elements/container.rb +10 -0
- data/lib/prawn/svg/elements/direct_render_base.rb +27 -0
- data/lib/prawn/svg/elements/ellipse.rb +22 -0
- data/lib/prawn/svg/elements/gradient.rb +184 -0
- data/lib/prawn/svg/elements/ignored.rb +5 -0
- data/lib/prawn/svg/elements/image.rb +177 -0
- data/lib/prawn/svg/elements/line.rb +27 -0
- data/lib/prawn/svg/elements/marker.rb +72 -0
- data/lib/prawn/svg/elements/mask.rb +65 -0
- data/lib/prawn/svg/elements/path.rb +294 -0
- data/lib/prawn/svg/elements/pattern.rb +150 -0
- data/lib/prawn/svg/elements/polygon.rb +25 -0
- data/lib/prawn/svg/elements/polyline.rb +23 -0
- data/lib/prawn/svg/elements/rect.rb +64 -0
- data/lib/prawn/svg/elements/root.rb +22 -0
- data/lib/prawn/svg/elements/switch.rb +92 -0
- data/lib/prawn/svg/elements/text.rb +99 -0
- data/lib/prawn/svg/elements/text_component.rb +346 -0
- data/lib/prawn/svg/elements/text_node.rb +308 -0
- data/lib/prawn/svg/elements/text_path.rb +153 -0
- data/lib/prawn/svg/elements/use.rb +142 -0
- data/lib/prawn/svg/elements/viewport.rb +29 -0
- data/lib/prawn/svg/elements.rb +46 -0
- data/lib/prawn/svg/extension.rb +25 -0
- data/lib/prawn/svg/font.rb +20 -0
- data/lib/prawn/svg/font_metrics.rb +144 -0
- data/lib/prawn/svg/font_registry.rb +315 -0
- data/lib/prawn/svg/funciri.rb +14 -0
- data/lib/prawn/svg/gradient_renderer.rb +332 -0
- data/lib/prawn/svg/gradients.rb +53 -0
- data/lib/prawn/svg/interface.rb +72 -0
- data/lib/prawn/svg/length.rb +43 -0
- data/lib/prawn/svg/link_renderer.rb +40 -0
- data/lib/prawn/svg/loaders/data.rb +17 -0
- data/lib/prawn/svg/loaders/file.rb +111 -0
- data/lib/prawn/svg/loaders/web.rb +24 -0
- data/lib/prawn/svg/paint.rb +67 -0
- data/lib/prawn/svg/pathable.rb +153 -0
- data/lib/prawn/svg/pattern_renderer.rb +112 -0
- data/lib/prawn/svg/pdfmatrix.rb +37 -0
- data/lib/prawn/svg/percentage.rb +24 -0
- data/lib/prawn/svg/properties.rb +329 -0
- data/lib/prawn/svg/renderer.rb +249 -0
- data/lib/prawn/svg/state.rb +24 -0
- data/lib/prawn/svg/transform_parser.rb +78 -0
- data/lib/prawn/svg/ttc.rb +35 -0
- data/lib/prawn/svg/ttf.rb +95 -0
- data/lib/prawn/svg/url_loader.rb +46 -0
- data/lib/prawn/svg/version.rb +5 -0
- data/lib/prawn/table.rb +4 -0
- data/lib/prawn-svg.rb +53 -0
- data/lib/prawn.rb +10 -0
- data/lib/prawnx/annotations.rb +21 -0
- data/lib/prawnx/document/bounding_box.rb +193 -0
- data/lib/prawnx/document/column_box.rb +91 -0
- data/lib/prawnx/document/internals.rb +144 -0
- data/lib/prawnx/document/span.rb +25 -0
- data/lib/prawnx/document.rb +253 -0
- data/lib/prawnx/encoding.rb +29 -0
- data/lib/prawnx/errors.rb +37 -0
- data/lib/prawnx/font.rb +214 -0
- data/lib/prawnx/fonts/afm.rb +171 -0
- data/lib/prawnx/fonts/otf.rb +50 -0
- data/lib/prawnx/fonts/ttf.rb +410 -0
- data/lib/prawnx/graphic_state.rb +41 -0
- data/lib/prawnx/graphics/color.rb +148 -0
- data/lib/prawnx/graphics/patterns.rb +100 -0
- data/lib/prawnx/graphics/style.rb +171 -0
- data/lib/prawnx/graphics.rb +208 -0
- data/lib/prawnx/grid.rb +216 -0
- data/lib/prawnx/images/image.rb +244 -0
- data/lib/prawnx/images/png_decoder.rb +65 -0
- data/lib/prawnx/images.rb +115 -0
- data/lib/prawnx/measurement_extensions.rb +40 -0
- data/lib/prawnx/measurements.rb +62 -0
- data/lib/prawnx/outline.rb +146 -0
- data/lib/prawnx/page.rb +125 -0
- data/lib/prawnx/page_geometry.rb +27 -0
- data/lib/prawnx/pdf/outline_node.rb +53 -0
- data/lib/prawnx/pdf/reference.rb +31 -0
- data/lib/prawnx/pdf/writer.rb +105 -0
- data/lib/prawnx/pdf.rb +66 -0
- data/lib/prawnx/pdf_a/srgb_profile.rb +61 -0
- data/lib/prawnx/pdf_a.rb +130 -0
- data/lib/prawnx/pdf_core.rb +25 -0
- data/lib/prawnx/repeater.rb +79 -0
- data/lib/prawnx/security.rb +108 -0
- data/lib/prawnx/stamp.rb +74 -0
- data/lib/prawnx/table/cell/kinds.rb +246 -0
- data/lib/prawnx/table/cell.rb +331 -0
- data/lib/prawnx/table/cells.rb +208 -0
- data/lib/prawnx/table.rb +296 -0
- data/lib/prawnx/text/box.rb +16 -0
- data/lib/prawnx/text/formatted/arranger.rb +217 -0
- data/lib/prawnx/text/formatted/box.rb +312 -0
- data/lib/prawnx/text/formatted/fragment.rb +205 -0
- data/lib/prawnx/text/formatted/line_wrap.rb +204 -0
- data/lib/prawnx/text/formatted/parser.rb +149 -0
- data/lib/prawnx/text/formatted/wrap.rb +103 -0
- data/lib/prawnx/text.rb +281 -0
- data/lib/prawnx/transformation_stack.rb +43 -0
- data/lib/prawnx/true_type/cff_subset.rb +157 -0
- data/lib/prawnx/true_type/file.rb +371 -0
- data/lib/prawnx/true_type/open_type_layout.rb +189 -0
- data/lib/prawnx/true_type/simple_subsets.rb +94 -0
- data/lib/prawnx/true_type/subset.rb +187 -0
- data/lib/prawnx/version.rb +7 -0
- data/lib/prawnx/view.rb +28 -0
- data/lib/prawnx.rb +102 -0
- metadata +251 -0
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module PrawnX
|
|
4
|
+
# Conversions between metric and imperial units and PDF points (1/72 inch).
|
|
5
|
+
module Measurements
|
|
6
|
+
def cm2mm(cm)
|
|
7
|
+
cm * 10
|
|
8
|
+
end
|
|
9
|
+
|
|
10
|
+
def dm2mm(dm)
|
|
11
|
+
dm * 100
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
def m2mm(m)
|
|
15
|
+
m * 1000
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
def ft2in(ft)
|
|
19
|
+
ft * 12
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
def yd2in(yd)
|
|
23
|
+
yd * 36
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
def pt2pt(pt)
|
|
27
|
+
pt
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
def in2pt(inch)
|
|
31
|
+
inch * 72
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
def ft2pt(ft)
|
|
35
|
+
in2pt(ft2in(ft))
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
def yd2pt(yd)
|
|
39
|
+
in2pt(yd2in(yd))
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
def mm2pt(mm)
|
|
43
|
+
mm * (72 / 25.4)
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
def cm2pt(cm)
|
|
47
|
+
mm2pt(cm2mm(cm))
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
def dm2pt(dm)
|
|
51
|
+
mm2pt(dm2mm(dm))
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
def m2pt(m)
|
|
55
|
+
mm2pt(m2mm(m))
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
def pt2mm(pt)
|
|
59
|
+
pt * 1 / mm2pt(1)
|
|
60
|
+
end
|
|
61
|
+
end
|
|
62
|
+
end
|
|
@@ -0,0 +1,146 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module PrawnX
|
|
4
|
+
class Document
|
|
5
|
+
def outline
|
|
6
|
+
@outline ||= Outline.new(self)
|
|
7
|
+
end
|
|
8
|
+
end
|
|
9
|
+
|
|
10
|
+
# The document outline (bookmarks): sections holding pages and subsections, each pointing at a page.
|
|
11
|
+
class Outline
|
|
12
|
+
attr_accessor :parent, :prev, :document, :items
|
|
13
|
+
|
|
14
|
+
def initialize(document)
|
|
15
|
+
@document = document
|
|
16
|
+
@parent = root
|
|
17
|
+
@prev = nil
|
|
18
|
+
@items = {}
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
def page_number
|
|
22
|
+
@document.page_number
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
def define(&block)
|
|
26
|
+
instance_eval(&block) if block
|
|
27
|
+
end
|
|
28
|
+
alias update define
|
|
29
|
+
|
|
30
|
+
def add_subsection_to(title, position = :last, &)
|
|
31
|
+
@parent = items[title] || raise(Errors::UnknownOutlineTitle, "\n No outline item with title: '#{title}' exists in the outline tree")
|
|
32
|
+
@prev = position == :first ? nil : @parent.data.last
|
|
33
|
+
insert_section(position == :first ? @parent.data.first : nil, &)
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
def insert_section_after(title, &)
|
|
37
|
+
@prev = items[title] || raise(Errors::UnknownOutlineTitle, "\n No outline item with title: '#{title}' exists in the outline tree")
|
|
38
|
+
@parent = @prev.data.parent
|
|
39
|
+
insert_section(@prev.data.next, &)
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
def section(title, options = {}, &)
|
|
43
|
+
add_outline_item(title, options, &)
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
def page(options = {})
|
|
47
|
+
raise Errors::RequiredOption, "\nTitle is a required option for page" unless options[:title]
|
|
48
|
+
|
|
49
|
+
add_outline_item(options[:title], options)
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
private
|
|
53
|
+
|
|
54
|
+
def root
|
|
55
|
+
document.outlines_reference
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
def add_outline_item(title, options, &block)
|
|
59
|
+
outline_item = create_outline_item(title, options)
|
|
60
|
+
prev.data.next = outline_item if prev
|
|
61
|
+
parent.data.first = outline_item unless prev
|
|
62
|
+
parent.data.last = outline_item
|
|
63
|
+
increase_count
|
|
64
|
+
self.prev = block ? nil : outline_item
|
|
65
|
+
self.parent = outline_item if block
|
|
66
|
+
yield if block
|
|
67
|
+
return unless parent == outline_item
|
|
68
|
+
|
|
69
|
+
self.prev = outline_item
|
|
70
|
+
self.parent = outline_item.data.parent
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
def create_outline_item(title, options)
|
|
74
|
+
outline_item = PDF::OutlineItem.new(title, parent, options)
|
|
75
|
+
case options[:destination]
|
|
76
|
+
when Integer then outline_item.dest = [document.pages[options[:destination] - 1].dictionary, :Fit]
|
|
77
|
+
when Array then outline_item.dest = options[:destination]
|
|
78
|
+
end
|
|
79
|
+
outline_item.prev = prev if @prev
|
|
80
|
+
items[title] = document.ref!(outline_item)
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
def increase_count
|
|
84
|
+
counting_parent = parent
|
|
85
|
+
while counting_parent
|
|
86
|
+
counting_parent.data.count += 1
|
|
87
|
+
counting_parent = counting_parent == root ? nil : counting_parent.data.parent
|
|
88
|
+
end
|
|
89
|
+
end
|
|
90
|
+
|
|
91
|
+
def insert_section(next_item)
|
|
92
|
+
last = @parent.data.last
|
|
93
|
+
yield if block_given?
|
|
94
|
+
if next_item
|
|
95
|
+
next_item.data.prev = @prev
|
|
96
|
+
@prev.data.next = next_item
|
|
97
|
+
@parent.data.last = last
|
|
98
|
+
end
|
|
99
|
+
@parent = root
|
|
100
|
+
@prev = root.data.last
|
|
101
|
+
end
|
|
102
|
+
end
|
|
103
|
+
|
|
104
|
+
module Destinations
|
|
105
|
+
def dests
|
|
106
|
+
names.data[:Dests] ||= ref!(PDF::NameTree.new)
|
|
107
|
+
end
|
|
108
|
+
|
|
109
|
+
def add_dest(name, reference)
|
|
110
|
+
reference = ref!(reference) unless reference.is_a?(PDF::Reference)
|
|
111
|
+
dests.data.add(name, reference)
|
|
112
|
+
end
|
|
113
|
+
|
|
114
|
+
def dest_xyz(left, top, zoom = nil, dest_page = page)
|
|
115
|
+
[dest_page.dictionary, :XYZ, left, top, zoom]
|
|
116
|
+
end
|
|
117
|
+
|
|
118
|
+
def dest_fit(dest_page = page)
|
|
119
|
+
[dest_page.dictionary, :Fit]
|
|
120
|
+
end
|
|
121
|
+
|
|
122
|
+
def dest_fit_horizontally(top, dest_page = page)
|
|
123
|
+
[dest_page.dictionary, :FitH, top]
|
|
124
|
+
end
|
|
125
|
+
|
|
126
|
+
def dest_fit_vertically(left, dest_page = page)
|
|
127
|
+
[dest_page.dictionary, :FitV, left]
|
|
128
|
+
end
|
|
129
|
+
|
|
130
|
+
def dest_fit_rect(left, bottom, right, top, dest_page = page)
|
|
131
|
+
[dest_page.dictionary, :FitR, left, bottom, right, top]
|
|
132
|
+
end
|
|
133
|
+
|
|
134
|
+
def dest_fit_bounds(dest_page = page)
|
|
135
|
+
[dest_page.dictionary, :FitB]
|
|
136
|
+
end
|
|
137
|
+
|
|
138
|
+
def dest_fit_bounds_horizontally(top, dest_page = page)
|
|
139
|
+
[dest_page.dictionary, :FitBH, top]
|
|
140
|
+
end
|
|
141
|
+
|
|
142
|
+
def dest_fit_bounds_vertically(left, dest_page = page)
|
|
143
|
+
[dest_page.dictionary, :FitBV, left]
|
|
144
|
+
end
|
|
145
|
+
end
|
|
146
|
+
end
|
data/lib/prawnx/page.rb
ADDED
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module PrawnX
|
|
4
|
+
# One page: its geometry, the content stream being written, its resources and the stack of graphic states.
|
|
5
|
+
class Page
|
|
6
|
+
DEFAULT_MARGINS = {left: 36, right: 36, top: 36, bottom: 36}.freeze
|
|
7
|
+
NO_INDENTS = {left: 0, bottom: 0, right: 0, top: 0}.freeze
|
|
8
|
+
|
|
9
|
+
attr_accessor :margins, :crops, :bleeds, :trims, :art_indents, :stack
|
|
10
|
+
attr_reader :document, :content
|
|
11
|
+
|
|
12
|
+
def initialize(document, options = {})
|
|
13
|
+
@document = document
|
|
14
|
+
@margins = options[:margins] || DEFAULT_MARGINS.dup
|
|
15
|
+
@crops = options[:crops] || NO_INDENTS
|
|
16
|
+
@bleeds = options[:bleeds] || NO_INDENTS
|
|
17
|
+
@trims = options[:trims] || NO_INDENTS
|
|
18
|
+
@art_indents = options[:art_indents] || NO_INDENTS
|
|
19
|
+
@stack = [GraphicState.new(options[:graphic_state])]
|
|
20
|
+
@size = options[:size] || "LETTER"
|
|
21
|
+
@layout = options[:layout] || :portrait
|
|
22
|
+
@content = document.ref!({}, (+"q\n").b)
|
|
23
|
+
@dictionary = document.ref!(Type: :Page, Parent: document.pages_reference, MediaBox: dimensions, Contents: @content)
|
|
24
|
+
@stamp_stream = nil
|
|
25
|
+
@stamp_dictionary = nil
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
def graphic_state
|
|
29
|
+
stack.last
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
def size
|
|
33
|
+
@size || dimensions[2, 2]
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
def layout
|
|
37
|
+
@layout || (dimensions[3] > dimensions[2] ? :portrait : :landscape)
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
def content_stream
|
|
41
|
+
@stamp_stream || content.stream
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
def dictionary
|
|
45
|
+
@stamp_dictionary || @dictionary
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
# Writes what the block draws into a form XObject instead of the page, starting from a copy of the current
|
|
49
|
+
# graphic settings.
|
|
50
|
+
def stamp_stream(reference)
|
|
51
|
+
@stamp_dictionary = reference
|
|
52
|
+
@stamp_stream = reference.stream
|
|
53
|
+
depth = stack.size
|
|
54
|
+
document.save_graphics_state
|
|
55
|
+
document.__send__(:freeze_stamp_graphics)
|
|
56
|
+
yield if block_given?
|
|
57
|
+
document.restore_graphics_state until stack.size == depth
|
|
58
|
+
@stamp_stream = nil
|
|
59
|
+
@stamp_dictionary = nil
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
def in_stamp_stream?
|
|
63
|
+
!@stamp_stream.nil?
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
def resources
|
|
67
|
+
dictionary.data[:Resources] ||= {}
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
def fonts
|
|
71
|
+
resources[:Font] ||= {}
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
def xobjects
|
|
75
|
+
resources[:XObject] ||= {}
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
def ext_gstates
|
|
79
|
+
resources[:ExtGState] ||= {}
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
def dimensions
|
|
83
|
+
sides = PageGeometry::SIZES[@size] || @size
|
|
84
|
+
sides =
|
|
85
|
+
case @layout
|
|
86
|
+
when :portrait then sides
|
|
87
|
+
when :landscape then sides.reverse
|
|
88
|
+
else raise Errors::InvalidPageLayout, "Layout must be either :portrait or :landscape"
|
|
89
|
+
end
|
|
90
|
+
[0, 0, *sides]
|
|
91
|
+
end
|
|
92
|
+
|
|
93
|
+
def crop_box
|
|
94
|
+
boxed(crops)
|
|
95
|
+
end
|
|
96
|
+
|
|
97
|
+
def bleed_box
|
|
98
|
+
boxed(bleeds)
|
|
99
|
+
end
|
|
100
|
+
|
|
101
|
+
def trim_box
|
|
102
|
+
boxed(trims)
|
|
103
|
+
end
|
|
104
|
+
|
|
105
|
+
def art_box
|
|
106
|
+
boxed(art_indents)
|
|
107
|
+
end
|
|
108
|
+
|
|
109
|
+
# Resources are required even when empty; every box defaults to the media box, so only those that differ are
|
|
110
|
+
# written.
|
|
111
|
+
def finalize
|
|
112
|
+
resources
|
|
113
|
+
{CropBox: crop_box, BleedBox: bleed_box, TrimBox: trim_box, ArtBox: art_box}.each do |name, box|
|
|
114
|
+
dictionary.data[name] = box unless box == dimensions
|
|
115
|
+
end
|
|
116
|
+
end
|
|
117
|
+
|
|
118
|
+
private
|
|
119
|
+
|
|
120
|
+
def boxed(indents)
|
|
121
|
+
left, bottom, right, top = dimensions
|
|
122
|
+
[left + indents[:left], bottom + indents[:bottom], right - indents[:right], top - indents[:top]]
|
|
123
|
+
end
|
|
124
|
+
end
|
|
125
|
+
end
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module PrawnX
|
|
4
|
+
module PageGeometry
|
|
5
|
+
POINTS_PER_INCH = 72.0
|
|
6
|
+
MILLIMETERS_PER_INCH = 25.4
|
|
7
|
+
|
|
8
|
+
# ISO 216 (A, B), ISO 217 (RA, SRA) and ISO 269 (C) sizes in millimeters, then North American sizes in inches.
|
|
9
|
+
MILLIMETERS_BY_SIZE = {
|
|
10
|
+
"4A0" => [1682, 2378], "2A0" => [1189, 1682],
|
|
11
|
+
"A0" => [841, 1189], "A1" => [594, 841], "A2" => [420, 594], "A3" => [297, 420], "A4" => [210, 297],
|
|
12
|
+
"A5" => [148, 210], "A6" => [105, 148], "A7" => [74, 105], "A8" => [52, 74], "A9" => [37, 52], "A10" => [26, 37],
|
|
13
|
+
"B0" => [1000, 1414], "B1" => [707, 1000], "B2" => [500, 707], "B3" => [353, 500], "B4" => [250, 353],
|
|
14
|
+
"B5" => [176, 250], "B6" => [125, 176], "B7" => [88, 125], "B8" => [62, 88], "B9" => [44, 62], "B10" => [31, 44],
|
|
15
|
+
"C0" => [917, 1297], "C1" => [648, 917], "C2" => [458, 648], "C3" => [324, 458], "C4" => [229, 324],
|
|
16
|
+
"C5" => [162, 229], "C6" => [114, 162], "C7" => [81, 114], "C8" => [57, 81], "C9" => [40, 57], "C10" => [28, 40],
|
|
17
|
+
"RA0" => [860, 1220], "RA1" => [610, 860], "RA2" => [430, 610], "RA3" => [305, 430], "RA4" => [215, 305],
|
|
18
|
+
"SRA0" => [900, 1280], "SRA1" => [640, 900], "SRA2" => [450, 640], "SRA3" => [320, 450], "SRA4" => [225, 320],
|
|
19
|
+
"EXECUTIVE" => [184.1, 266.7],
|
|
20
|
+
}.freeze
|
|
21
|
+
INCHES_BY_SIZE = {"FOLIO" => [8.5, 13], "LEGAL" => [8.5, 14], "LETTER" => [8.5, 11], "TABLOID" => [11, 17]}.freeze
|
|
22
|
+
|
|
23
|
+
SIZES = MILLIMETERS_BY_SIZE.transform_values { |sides| sides.map { (it * POINTS_PER_INCH / MILLIMETERS_PER_INCH).round(2) } }
|
|
24
|
+
.merge(INCHES_BY_SIZE.transform_values { |sides| sides.map { it * POINTS_PER_INCH } })
|
|
25
|
+
.freeze
|
|
26
|
+
end
|
|
27
|
+
end
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module PrawnX
|
|
4
|
+
module PDF
|
|
5
|
+
class OutlineRoot
|
|
6
|
+
include Serializable
|
|
7
|
+
|
|
8
|
+
attr_accessor :count, :first, :last
|
|
9
|
+
|
|
10
|
+
def initialize
|
|
11
|
+
@count = 0
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
def to_hash
|
|
15
|
+
{Type: :Outlines, Count: count, First: first, Last: last}
|
|
16
|
+
end
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
class OutlineItem
|
|
20
|
+
include Serializable
|
|
21
|
+
|
|
22
|
+
attr_accessor :count, :first, :last, :next, :prev, :parent, :title, :dest, :closed
|
|
23
|
+
|
|
24
|
+
def initialize(title, parent, options)
|
|
25
|
+
@closed = options[:closed]
|
|
26
|
+
@title = title
|
|
27
|
+
@parent = parent
|
|
28
|
+
@count = 0
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
def to_hash
|
|
32
|
+
{Title: title, Parent: parent, Count: closed ? -count : count, First: first, Last: last, Next: @next, Prev: prev, Dest: dest}.compact
|
|
33
|
+
end
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
# A name tree kept as a single sorted leaf: valid at any size (ISO 32000-1 §7.9.6).
|
|
37
|
+
class NameTree
|
|
38
|
+
include Serializable
|
|
39
|
+
|
|
40
|
+
def initialize
|
|
41
|
+
@value_by_name = {}
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
def add(name, value)
|
|
45
|
+
@value_by_name[name] = value
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
def to_hash
|
|
49
|
+
{Names: @value_by_name.sort_by { |name, _| name.to_s }.flat_map { |name, value| [LiteralString.new(name.to_s), value] }}
|
|
50
|
+
end
|
|
51
|
+
end
|
|
52
|
+
end
|
|
53
|
+
end
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module PrawnX
|
|
4
|
+
module PDF
|
|
5
|
+
# An indirect object: a number, its value and, for a stream, its bytes.
|
|
6
|
+
class Reference
|
|
7
|
+
attr_reader :identifier
|
|
8
|
+
attr_accessor :data, :uncompressed
|
|
9
|
+
attr_writer :stream
|
|
10
|
+
|
|
11
|
+
def initialize(identifier, data, stream = nil)
|
|
12
|
+
@identifier = identifier
|
|
13
|
+
@data = data
|
|
14
|
+
@stream = stream
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
def to_s
|
|
18
|
+
"#{identifier} 0 R"
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
# A reference becomes a stream once bytes are written to it: prawn-svg appends to ref!({}).stream.
|
|
22
|
+
def stream
|
|
23
|
+
@stream ||= +"".b
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
def stream?
|
|
27
|
+
!@stream.nil? && !@stream.empty?
|
|
28
|
+
end
|
|
29
|
+
end
|
|
30
|
+
end
|
|
31
|
+
end
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "digest/md5"
|
|
4
|
+
require "zlib"
|
|
5
|
+
|
|
6
|
+
module PrawnX
|
|
7
|
+
module PDF
|
|
8
|
+
# Writes a PDF 1.5 file: streams compressed, every other object packed into one compressed object stream, and
|
|
9
|
+
# a compressed cross-reference stream instead of the xref table (ISO 32000-1 §7.5.7, §7.5.8).
|
|
10
|
+
class Writer
|
|
11
|
+
MINIMUM_VERSION = 1.5
|
|
12
|
+
BINARY_MARKER = "%\xE2\xE3\xCF\xD3\n".b
|
|
13
|
+
|
|
14
|
+
def self.call(...)
|
|
15
|
+
new(...).call
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
def initialize(objects:, root:, info:, version:, trailer: {}, encryption: nil, identified: false)
|
|
19
|
+
@objects = objects
|
|
20
|
+
@root = root
|
|
21
|
+
@info = info
|
|
22
|
+
@version = [version, MINIMUM_VERSION].max
|
|
23
|
+
@trailer = trailer
|
|
24
|
+
return unless encryption || identified
|
|
25
|
+
|
|
26
|
+
file_identifier = PDF::ByteString.new(document_digest)
|
|
27
|
+
@trailer = trailer.merge(ID: [file_identifier, file_identifier])
|
|
28
|
+
return unless encryption
|
|
29
|
+
|
|
30
|
+
@encryption_key = encryption.key(file_identifier)
|
|
31
|
+
@trailer[:Encrypt] = encryption.dictionary(file_identifier)
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
def call
|
|
35
|
+
output = (+"%PDF-#{version}\n").b << BINARY_MARKER
|
|
36
|
+
location_by_identifier = {}
|
|
37
|
+
|
|
38
|
+
stream_objects, packed_objects = objects.partition(&:stream?)
|
|
39
|
+
stream_objects.each do |reference|
|
|
40
|
+
location_by_identifier[reference.identifier] = [1, output.bytesize]
|
|
41
|
+
output << indirect_stream(reference.identifier, reference.data, reference.stream, compressed: !reference.uncompressed)
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
object_stream_identifier = objects.size + 1
|
|
45
|
+
unless packed_objects.empty?
|
|
46
|
+
location_by_identifier[object_stream_identifier] = [1, output.bytesize]
|
|
47
|
+
output << object_stream(object_stream_identifier, packed_objects, location_by_identifier)
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
cross_reference_identifier = object_stream_identifier + 1
|
|
51
|
+
location_by_identifier[cross_reference_identifier] = [1, output.bytesize]
|
|
52
|
+
output << cross_reference_stream(cross_reference_identifier, location_by_identifier)
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
private
|
|
56
|
+
|
|
57
|
+
attr_reader :objects, :root, :info, :version, :trailer
|
|
58
|
+
|
|
59
|
+
# A file identifier derived from the document's content, so the same document always gets the same one.
|
|
60
|
+
def document_digest
|
|
61
|
+
digest = Digest::MD5.new
|
|
62
|
+
objects.each do |reference|
|
|
63
|
+
digest << PDF.serialize(reference.data)
|
|
64
|
+
digest << reference.stream if reference.stream?
|
|
65
|
+
end
|
|
66
|
+
digest.digest
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
# Streams are encrypted once compressed; the cross-reference stream never is (ISO 32000-1 §7.5.8.4).
|
|
70
|
+
def indirect_stream(identifier, dictionary, bytes, encrypted: true, compressed: true)
|
|
71
|
+
compressed &&= dictionary[:Filter].nil?
|
|
72
|
+
bytes = Zlib::Deflate.deflate(bytes) if compressed
|
|
73
|
+
bytes = Document::Arcfour.new(Document::Security.object_key(@encryption_key, identifier, 0)).encrypt(bytes) if @encryption_key && encrypted
|
|
74
|
+
dictionary = dictionary.merge(Length: bytes.bytesize)
|
|
75
|
+
dictionary[:Filter] = :FlateDecode if compressed
|
|
76
|
+
"#{identifier} 0 obj\n#{PDF.serialize(dictionary)}\nstream\n".b << bytes.b << "\nendstream\nendobj\n"
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
def object_stream(identifier, references, location_by_identifier)
|
|
80
|
+
offsets = +""
|
|
81
|
+
body = +""
|
|
82
|
+
references.each_with_index do |reference, index|
|
|
83
|
+
location_by_identifier[reference.identifier] = [2, identifier, index]
|
|
84
|
+
offsets << "#{reference.identifier} #{body.bytesize} "
|
|
85
|
+
body << PDF.serialize(reference.data) << "\n"
|
|
86
|
+
end
|
|
87
|
+
dictionary = {Type: :ObjStm, N: references.size, First: offsets.bytesize}
|
|
88
|
+
indirect_stream(identifier, dictionary, offsets << body)
|
|
89
|
+
end
|
|
90
|
+
|
|
91
|
+
# Each row is one byte of type, then the offset or the object stream, then the generation or the index.
|
|
92
|
+
def cross_reference_stream(identifier, location_by_identifier)
|
|
93
|
+
size = identifier + 1
|
|
94
|
+
offset_width = [(location_by_identifier.values.map { it[1] }.max.bit_length + 7) / 8, 1].max
|
|
95
|
+
rows = (+"\x00".b) << ("\x00" * offset_width).b << "\xFF\xFF".b
|
|
96
|
+
(1...size).each do |object_identifier|
|
|
97
|
+
type, field, generation_or_index = location_by_identifier.fetch(object_identifier)
|
|
98
|
+
rows << type.chr << [field].pack("Q>").byteslice(8 - offset_width, offset_width) << [generation_or_index.to_i].pack("n")
|
|
99
|
+
end
|
|
100
|
+
dictionary = {Type: :XRef, Size: size, W: [1, offset_width, 2], Root: root, Info: info}.merge(trailer)
|
|
101
|
+
indirect_stream(identifier, dictionary, rows, encrypted: false) << "startxref\n#{location_by_identifier.fetch(identifier)[1]}\n%%EOF\n"
|
|
102
|
+
end
|
|
103
|
+
end
|
|
104
|
+
end
|
|
105
|
+
end
|
data/lib/prawnx/pdf.rb
ADDED
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module PrawnX
|
|
4
|
+
# PDF syntax: numbers, names, strings, arrays and dictionaries, as ISO 32000-1 §7.3 writes them.
|
|
5
|
+
module PDF
|
|
6
|
+
Name = Data.define(:value)
|
|
7
|
+
|
|
8
|
+
# Objects that build their dictionary when the document is written, once every reference they hold is known.
|
|
9
|
+
module Serializable
|
|
10
|
+
end
|
|
11
|
+
LiteralString = Class.new(String)
|
|
12
|
+
ByteString = Class.new(String)
|
|
13
|
+
|
|
14
|
+
DELIMITER_OR_IRREGULAR_BYTES = ((0..32).to_a + [35, 37, 40, 41, 47, 60, 62, 91, 93, 123, 125] + (127..255).to_a).freeze
|
|
15
|
+
LITERAL_STRING_ESCAPES = {"(" => "\\(", ")" => "\\)", "\\" => "\\\\", "\r" => "\\r"}.freeze
|
|
16
|
+
NAME_BY_SYMBOL = Hash.new do |cache, symbol|
|
|
17
|
+
cache[symbol] = (+"/").concat(*symbol.to_s.b.each_byte.map { DELIMITER_OR_IRREGULAR_BYTES.include?(it) ? format("#%02X", it) : it.chr }).freeze
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
module_function
|
|
21
|
+
|
|
22
|
+
# Five decimals, trailing zeros dropped but one: 1 is "1.0", 0.5 is "0.5", 1/3 is "0.33333".
|
|
23
|
+
def real(number)
|
|
24
|
+
digits = format("%.5f", number)
|
|
25
|
+
last = digits.bytesize - 1
|
|
26
|
+
last -= 1 while digits.getbyte(last) == 48 && digits.getbyte(last - 1) != 46
|
|
27
|
+
digits.byteslice(0, last + 1)
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
def real_params(numbers)
|
|
31
|
+
numbers.map { real(it) }.join(" ")
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
# Integers as integers, other numbers to five decimals without a trailing ".0".
|
|
35
|
+
def number(value)
|
|
36
|
+
return value.to_s if value.is_a?(Integer)
|
|
37
|
+
|
|
38
|
+
digits = real(value)
|
|
39
|
+
digits.end_with?(".0") ? digits.delete_suffix(".0") : digits
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
def serialize(value, in_content_stream = false)
|
|
43
|
+
case value
|
|
44
|
+
when nil then "null"
|
|
45
|
+
when true then "true"
|
|
46
|
+
when false then "false"
|
|
47
|
+
when Numeric then number(value)
|
|
48
|
+
when Symbol then NAME_BY_SYMBOL[value]
|
|
49
|
+
when Name then NAME_BY_SYMBOL[value.value.to_sym]
|
|
50
|
+
when LiteralString then "(#{value.gsub(/[\\\r()]/, LITERAL_STRING_ESCAPES)})"
|
|
51
|
+
when ByteString then "<#{value.unpack1("H*")}>"
|
|
52
|
+
when Time then "(#{value.strftime("D:%Y%m%d%H%M%S%z").chop.chop}'00')"
|
|
53
|
+
when String then "<#{(in_content_stream ? value : utf16be(value)).unpack1("H*")}>"
|
|
54
|
+
when Array then "[#{value.map { serialize(it, in_content_stream) }.join(" ")}]"
|
|
55
|
+
when Hash then "<<#{value.map { |key, entry| "#{serialize(key.to_sym)} #{serialize(entry, in_content_stream)}" }.join("\n")}>>"
|
|
56
|
+
when Reference then value.to_s
|
|
57
|
+
when Serializable then serialize(value.to_hash, in_content_stream)
|
|
58
|
+
else raise Errors::FailedObjectConversion, "This object cannot be serialized to PDF (#{value.inspect})"
|
|
59
|
+
end
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
def utf16be(string)
|
|
63
|
+
"\xFE\xFF".b + string.encode(::Encoding::UTF_16BE).b
|
|
64
|
+
end
|
|
65
|
+
end
|
|
66
|
+
end
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module PrawnX
|
|
4
|
+
module PdfA
|
|
5
|
+
# An ICC version 2 display profile for sRGB (IEC 61966-2.1), built from the standard's primaries adapted to D50
|
|
6
|
+
# (ICC.1:2001-04): PDF/A needs one as output intent, and building it avoids shipping a third-party file.
|
|
7
|
+
module SrgbProfile
|
|
8
|
+
DESCRIPTION = "sRGB IEC61966-2.1"
|
|
9
|
+
D50_WHITE = [0.9642, 1.0, 0.8249].freeze
|
|
10
|
+
# sRGB primaries, Bradford-adapted from D65 to D50.
|
|
11
|
+
RED = [0.4361, 0.2225, 0.0139].freeze
|
|
12
|
+
GREEN = [0.3851, 0.7169, 0.0971].freeze
|
|
13
|
+
BLUE = [0.1431, 0.0606, 0.7141].freeze
|
|
14
|
+
# The sRGB tone curve, approximated by a 2.2 gamma, as u8Fixed8Number.
|
|
15
|
+
GAMMA = 0x0233
|
|
16
|
+
|
|
17
|
+
module_function
|
|
18
|
+
|
|
19
|
+
def bytes
|
|
20
|
+
@bytes ||= begin
|
|
21
|
+
curve = "curv".b + "\x00" * 4 + [1, GAMMA].pack("Nn") + "\x00\x00".b
|
|
22
|
+
tags = [
|
|
23
|
+
["desc", text_description(DESCRIPTION)],
|
|
24
|
+
["cprt", "text".b + ("\x00" * 4) + "No copyright, use freely\x00".b],
|
|
25
|
+
["wtpt", xyz(D50_WHITE)],
|
|
26
|
+
["rXYZ", xyz(RED)], ["gXYZ", xyz(GREEN)], ["bXYZ", xyz(BLUE)],
|
|
27
|
+
["rTRC", curve], ["gTRC", curve], ["bTRC", curve],
|
|
28
|
+
]
|
|
29
|
+
table_size = 4 + (tags.size * 12)
|
|
30
|
+
offset = 128 + table_size
|
|
31
|
+
directory = [tags.size].pack("N")
|
|
32
|
+
data = +"".b
|
|
33
|
+
tags.each do |signature, tag|
|
|
34
|
+
directory << [signature, offset + data.bytesize, tag.bytesize].pack("a4N2")
|
|
35
|
+
data << tag << ("\x00" * ((4 - (tag.bytesize % 4)) % 4))
|
|
36
|
+
end
|
|
37
|
+
body = directory + data
|
|
38
|
+
header(128 + body.bytesize) + body
|
|
39
|
+
end
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
def header(size)
|
|
43
|
+
[size, "", 0x02100000, "mntr", "RGB ", "XYZ ", 2026, 1, 1, 0, 0, 0, "acsp", "APPL", 0, "", "", 0, 0,
|
|
44
|
+
*D50_WHITE.map { s15_fixed16(it) }, ""].pack("N a4 N a4 a4 a4 n6 a4 a4 N a4 a4 Q> N N3 a4") + ("\x00" * 44)
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
def xyz(values)
|
|
48
|
+
"XYZ ".b + ("\x00" * 4) + values.map { s15_fixed16(it) }.pack("N3")
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
def s15_fixed16(value)
|
|
52
|
+
(value * 65_536).round & 0xFFFFFFFF
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
def text_description(text)
|
|
56
|
+
ascii = "#{text}\x00".b
|
|
57
|
+
"desc".b + ("\x00" * 4) + [ascii.bytesize].pack("N") + ascii + [0, 0, 0].pack("N2n") + ("\x00" * 67)
|
|
58
|
+
end
|
|
59
|
+
end
|
|
60
|
+
end
|
|
61
|
+
end
|