asciidoctor-pdf 1.5.0.beta.8 → 1.5.0.rc.1
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/CHANGELOG.adoc +49 -0
- data/LICENSE.adoc +1 -1
- data/NOTICE.adoc +1 -1
- data/README.adoc +43 -47
- data/asciidoctor-pdf.gemspec +5 -1
- data/bin/asciidoctor-pdf-optimize +1 -1
- data/data/themes/base-theme.yml +4 -3
- data/data/themes/default-theme.yml +10 -5
- data/docs/theming-guide.adoc +286 -22
- data/lib/asciidoctor-pdf.rb +1 -0
- data/lib/asciidoctor-pdf/converter.rb +1 -0
- data/lib/asciidoctor-pdf/version.rb +1 -0
- data/lib/asciidoctor/pdf.rb +13 -2
- data/lib/asciidoctor/pdf/converter.rb +3962 -3955
- data/lib/asciidoctor/pdf/ext.rb +9 -0
- data/lib/asciidoctor/pdf/ext/asciidoctor.rb +1 -0
- data/lib/asciidoctor/pdf/ext/asciidoctor/abstract_block.rb +1 -0
- data/lib/asciidoctor/pdf/ext/asciidoctor/abstract_node.rb +1 -0
- data/lib/asciidoctor/pdf/ext/asciidoctor/document.rb +1 -0
- data/lib/asciidoctor/pdf/ext/asciidoctor/image.rb +18 -16
- data/lib/asciidoctor/pdf/ext/asciidoctor/list.rb +3 -2
- data/lib/asciidoctor/pdf/ext/asciidoctor/list_item.rb +2 -1
- data/lib/asciidoctor/pdf/ext/asciidoctor/logging_shim.rb +3 -4
- data/lib/asciidoctor/pdf/ext/asciidoctor/section.rb +8 -6
- data/lib/asciidoctor/pdf/ext/core.rb +2 -0
- data/lib/asciidoctor/pdf/ext/core/array.rb +1 -0
- data/lib/asciidoctor/pdf/ext/core/hash.rb +1 -0
- data/lib/asciidoctor/pdf/ext/core/numeric.rb +4 -3
- data/lib/asciidoctor/pdf/ext/core/object.rb +1 -0
- data/lib/asciidoctor/pdf/ext/core/quantifiable_stdout.rb +8 -1
- data/lib/asciidoctor/pdf/ext/core/regexp.rb +1 -0
- data/lib/asciidoctor/pdf/ext/core/string.rb +6 -7
- data/lib/asciidoctor/pdf/ext/pdf-core.rb +1 -0
- data/lib/asciidoctor/pdf/ext/pdf-core/page.rb +3 -4
- data/lib/asciidoctor/pdf/ext/pdf-core/pdf_object.rb +2 -1
- data/lib/asciidoctor/pdf/ext/prawn-svg.rb +1 -0
- data/lib/asciidoctor/pdf/ext/prawn-svg/interface.rb +11 -8
- data/lib/asciidoctor/pdf/ext/prawn-table.rb +2 -1
- data/lib/asciidoctor/pdf/ext/prawn-table/cell.rb +9 -10
- data/lib/asciidoctor/pdf/ext/prawn-table/cell/asciidoc.rb +62 -57
- data/lib/asciidoctor/pdf/ext/prawn-table/cell/text.rb +5 -3
- data/lib/asciidoctor/pdf/ext/prawn-templates.rb +1 -0
- data/lib/asciidoctor/pdf/ext/prawn.rb +1 -0
- data/lib/asciidoctor/pdf/ext/prawn/coderay_encoder.rb +73 -72
- data/lib/asciidoctor/pdf/ext/prawn/extensions.rb +814 -818
- data/lib/asciidoctor/pdf/ext/prawn/font/afm.rb +4 -3
- data/lib/asciidoctor/pdf/ext/prawn/formatted_text/box.rb +2 -1
- data/lib/asciidoctor/pdf/ext/prawn/formatted_text/fragment.rb +7 -2
- data/lib/asciidoctor/pdf/ext/prawn/images.rb +45 -44
- data/lib/asciidoctor/pdf/ext/pygments.rb +34 -0
- data/lib/asciidoctor/pdf/ext/rouge.rb +1 -1
- data/lib/asciidoctor/pdf/ext/rouge/formatters/prawn.rb +181 -149
- data/lib/asciidoctor/pdf/ext/rouge/themes/asciidoctor_pdf_default.rb +1 -0
- data/lib/asciidoctor/pdf/formatted_text.rb +2 -0
- data/lib/asciidoctor/pdf/formatted_text/formatter.rb +35 -34
- data/lib/asciidoctor/pdf/formatted_text/fragment_position_renderer.rb +8 -7
- data/lib/asciidoctor/pdf/formatted_text/inline_destination_marker.rb +13 -14
- data/lib/asciidoctor/pdf/formatted_text/inline_image_arranger.rb +112 -133
- data/lib/asciidoctor/pdf/formatted_text/inline_image_renderer.rb +43 -41
- data/lib/asciidoctor/pdf/formatted_text/inline_text_aligner.rb +15 -14
- data/lib/asciidoctor/pdf/formatted_text/source_wrap.rb +43 -0
- data/lib/asciidoctor/pdf/formatted_text/text_background_and_border_renderer.rb +46 -37
- data/lib/asciidoctor/pdf/formatted_text/transform.rb +371 -352
- data/lib/asciidoctor/pdf/index_catalog.rb +99 -95
- data/lib/asciidoctor/pdf/measurements.rb +51 -48
- data/lib/asciidoctor/pdf/optimizer.rb +34 -31
- data/lib/asciidoctor/pdf/pdfmark.rb +34 -33
- data/lib/asciidoctor/pdf/roman_numeral.rb +80 -79
- data/lib/asciidoctor/pdf/sanitizer.rb +38 -37
- data/lib/asciidoctor/pdf/temporary_path.rb +10 -9
- data/lib/asciidoctor/pdf/text_transformer.rb +101 -100
- data/lib/asciidoctor/pdf/theme_loader.rb +258 -256
- data/lib/asciidoctor/pdf/version.rb +5 -4
- metadata +55 -6
- data/lib/asciidoctor/pdf/ext/rouge/themes/bw.rb +0 -39
- data/lib/asciidoctor/pdf/ext/ttfunk.rb +0 -9
@@ -1,13 +1,15 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
|
+
|
2
3
|
class Prawn::Table::Cell::Text
|
3
4
|
# Override draw_content method to drop cursor advancement
|
4
5
|
remove_method :draw_content
|
5
6
|
def draw_content
|
6
7
|
with_font do
|
7
8
|
with_text_color do
|
8
|
-
(text_box
|
9
|
-
|
10
|
-
|
9
|
+
(text_box \
|
10
|
+
width: spanned_content_width + FPTolerance,
|
11
|
+
height: spanned_content_height + FPTolerance,
|
12
|
+
at: [0, @pdf.cursor]).render
|
11
13
|
end
|
12
14
|
end
|
13
15
|
end
|
@@ -1,4 +1,5 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
|
+
|
2
3
|
######################################################################
|
3
4
|
#
|
4
5
|
# This file was copied from Prawn (manual/syntax_highlight.rb) and
|
@@ -32,85 +33,85 @@ require 'coderay'
|
|
32
33
|
# CodeRay.scan(string, :ruby).to_prawn
|
33
34
|
#
|
34
35
|
module Asciidoctor
|
35
|
-
module Prawn
|
36
|
-
class CodeRayEncoder < ::CodeRay::Encoders::Encoder
|
37
|
-
|
36
|
+
module Prawn
|
37
|
+
class CodeRayEncoder < ::CodeRay::Encoders::Encoder
|
38
|
+
register_for :to_prawn
|
38
39
|
|
39
|
-
|
40
|
-
|
41
|
-
|
42
|
-
|
43
|
-
|
44
|
-
|
45
|
-
|
46
|
-
|
47
|
-
|
48
|
-
|
49
|
-
|
50
|
-
|
51
|
-
|
52
|
-
|
53
|
-
|
54
|
-
|
55
|
-
|
56
|
-
|
57
|
-
|
58
|
-
|
59
|
-
|
60
|
-
|
61
|
-
|
62
|
-
|
63
|
-
|
64
|
-
|
65
|
-
|
66
|
-
|
67
|
-
|
68
|
-
|
69
|
-
|
70
|
-
|
40
|
+
# Manni theme from Pygments
|
41
|
+
COLORS = {
|
42
|
+
default: '333333',
|
43
|
+
annotation: '9999FF',
|
44
|
+
attribute_name: '4F9FCF',
|
45
|
+
attribute_value: 'D44950',
|
46
|
+
class: '00AA88',
|
47
|
+
class_variable: '003333',
|
48
|
+
color: 'FF6600',
|
49
|
+
comment: '999999',
|
50
|
+
constant: '336600',
|
51
|
+
directive: '006699',
|
52
|
+
doctype: '009999',
|
53
|
+
entity: '999999',
|
54
|
+
float: 'FF6600',
|
55
|
+
function: 'CC00FF',
|
56
|
+
important: '9999FF',
|
57
|
+
inline_delimiter: 'EF804F',
|
58
|
+
instance_variable: '003333',
|
59
|
+
integer: 'FF6600',
|
60
|
+
key: '006699',
|
61
|
+
keyword: '006699',
|
62
|
+
method: 'CC00FF',
|
63
|
+
namespace: '00CCFF',
|
64
|
+
predefined_type: '007788',
|
65
|
+
regexp: '33AAAA',
|
66
|
+
string: 'CC3300',
|
67
|
+
symbol: 'FFCC33',
|
68
|
+
tag: '2F6F9F',
|
69
|
+
type: '007788',
|
70
|
+
value: '336600',
|
71
|
+
}
|
71
72
|
|
72
|
-
|
73
|
-
|
74
|
-
|
75
|
-
|
76
|
-
|
73
|
+
LF = ?\n
|
74
|
+
NoBreakSpace = ?\u00a0
|
75
|
+
InnerIndent = LF + ' '
|
76
|
+
GuardedIndent = ?\u00a0
|
77
|
+
GuardedInnerIndent = LF + GuardedIndent
|
77
78
|
|
78
|
-
|
79
|
-
|
80
|
-
|
81
|
-
|
82
|
-
|
83
|
-
|
84
|
-
|
79
|
+
def setup options
|
80
|
+
super
|
81
|
+
@out = []
|
82
|
+
@open = []
|
83
|
+
# NOTE tracks whether text token begins at the start of a line
|
84
|
+
@start_of_line = true
|
85
|
+
end
|
85
86
|
|
86
|
-
|
87
|
-
|
88
|
-
|
89
|
-
|
90
|
-
|
91
|
-
|
92
|
-
|
93
|
-
|
94
|
-
|
87
|
+
def text_token text, kind
|
88
|
+
if text == LF
|
89
|
+
@out << { text: text }
|
90
|
+
@start_of_line = true
|
91
|
+
# NOTE text is nil and kind is :error when CodeRay ends parsing on an error
|
92
|
+
elsif text
|
93
|
+
# NOTE add guard character to prevent Prawn from trimming indentation
|
94
|
+
text[0] = GuardedIndent if @start_of_line && (text.start_with? ' ')
|
95
|
+
text.gsub! InnerIndent, GuardedInnerIndent if text.include? InnerIndent
|
95
96
|
|
96
|
-
|
97
|
-
|
98
|
-
|
99
|
-
|
100
|
-
|
101
|
-
|
97
|
+
# NOTE this optimization assumes we don't support/use background colors
|
98
|
+
if text.rstrip.empty?
|
99
|
+
@out << { text: text }
|
100
|
+
else
|
101
|
+
# QUESTION should we default to no color?
|
102
|
+
@out << { text: text, color: (COLORS[kind] || COLORS[@open[-1]] || COLORS[:default]) }
|
103
|
+
end
|
104
|
+
@start_of_line = text.end_with? LF
|
105
|
+
end
|
102
106
|
end
|
103
|
-
@start_of_line = text.end_with? LF
|
104
|
-
end
|
105
|
-
end
|
106
107
|
|
107
|
-
|
108
|
-
|
109
|
-
|
108
|
+
def begin_group kind
|
109
|
+
@open << kind
|
110
|
+
end
|
110
111
|
|
111
|
-
|
112
|
-
|
112
|
+
def end_group _kind
|
113
|
+
@open.pop
|
114
|
+
end
|
115
|
+
end
|
113
116
|
end
|
114
117
|
end
|
115
|
-
end
|
116
|
-
end
|
@@ -1,4 +1,5 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
|
+
|
2
3
|
Prawn::Font::AFM.instance_variable_set :@hide_m17n_warning, true
|
3
4
|
|
4
5
|
require 'prawn/icon'
|
@@ -6,912 +7,907 @@ require 'prawn/icon'
|
|
6
7
|
Prawn::Icon::Compatibility.send :prepend, (::Module.new { def warning *args; end })
|
7
8
|
|
8
9
|
module Asciidoctor
|
9
|
-
module Prawn
|
10
|
-
module Extensions
|
11
|
-
|
12
|
-
|
13
|
-
|
14
|
-
|
15
|
-
|
16
|
-
|
17
|
-
|
18
|
-
|
19
|
-
|
20
|
-
|
21
|
-
|
22
|
-
|
23
|
-
|
24
|
-
|
25
|
-
|
26
|
-
|
27
|
-
|
28
|
-
|
29
|
-
|
30
|
-
|
31
|
-
|
32
|
-
|
33
|
-
|
34
|
-
|
35
|
-
|
36
|
-
|
37
|
-
|
38
|
-
|
10
|
+
module Prawn
|
11
|
+
module Extensions
|
12
|
+
include ::Asciidoctor::PDF::Measurements
|
13
|
+
include ::Asciidoctor::PDF::Sanitizer
|
14
|
+
include ::Asciidoctor::PDF::TextTransformer
|
15
|
+
|
16
|
+
FontAwesomeIconSets = %w(fab far fas)
|
17
|
+
IconSets = %w(fab far fas fi pf).to_set
|
18
|
+
IconSetPrefixes = IconSets.map {|it| it + '-' }
|
19
|
+
InitialPageContent = %(q\n)
|
20
|
+
(FontStyleToSet = {
|
21
|
+
bold: [:bold].to_set,
|
22
|
+
italic: [:italic].to_set,
|
23
|
+
bold_italic: [:bold, :italic].to_set,
|
24
|
+
}).default = ::Set.new
|
25
|
+
|
26
|
+
# - :height is the height of a line
|
27
|
+
# - :leading is spacing between adjacent lines
|
28
|
+
# - :padding_top is half line spacing, plus any line_gap in the font
|
29
|
+
# - :padding_bottom is half line spacing
|
30
|
+
# - :final_gap determines whether a gap is added below the last line
|
31
|
+
LineMetrics = ::Struct.new :height, :leading, :padding_top, :padding_bottom, :final_gap
|
32
|
+
|
33
|
+
# Core
|
34
|
+
|
35
|
+
# Retrieves the catalog reference data for the PDF.
|
36
|
+
#
|
37
|
+
def catalog
|
38
|
+
state.store.root
|
39
|
+
end
|
39
40
|
|
40
|
-
|
41
|
-
|
42
|
-
|
43
|
-
|
44
|
-
|
41
|
+
# Retrieves the compatiblity version of the PDF.
|
42
|
+
#
|
43
|
+
def min_version
|
44
|
+
state.version
|
45
|
+
end
|
45
46
|
|
46
|
-
|
47
|
+
# Measurements
|
47
48
|
|
48
|
-
|
49
|
-
|
50
|
-
|
51
|
-
|
52
|
-
|
49
|
+
# Returns the width of the current page from edge-to-edge
|
50
|
+
#
|
51
|
+
def page_width
|
52
|
+
page.dimensions[2]
|
53
|
+
end
|
53
54
|
|
54
|
-
|
55
|
-
|
56
|
-
|
57
|
-
|
58
|
-
|
59
|
-
|
60
|
-
|
55
|
+
# Returns the effective (writable) width of the page
|
56
|
+
#
|
57
|
+
# If inside a bounding box, returns width of box.
|
58
|
+
#
|
59
|
+
def effective_page_width
|
60
|
+
reference_bounds.width
|
61
|
+
end
|
61
62
|
|
62
|
-
|
63
|
-
|
64
|
-
|
65
|
-
|
66
|
-
|
63
|
+
# Returns the height of the current page from edge-to-edge
|
64
|
+
#
|
65
|
+
def page_height
|
66
|
+
page.dimensions[3]
|
67
|
+
end
|
67
68
|
|
68
|
-
|
69
|
-
|
70
|
-
|
71
|
-
|
72
|
-
|
73
|
-
|
74
|
-
|
69
|
+
# Returns the effective (writable) height of the page
|
70
|
+
#
|
71
|
+
# If inside a fixed-height bounding box, returns width of box.
|
72
|
+
#
|
73
|
+
def effective_page_height
|
74
|
+
reference_bounds.height
|
75
|
+
end
|
75
76
|
|
76
|
-
|
77
|
-
|
78
|
-
|
79
|
-
|
80
|
-
|
81
|
-
|
82
|
-
|
77
|
+
# Set the margins for the current page.
|
78
|
+
#
|
79
|
+
def set_page_margin margin
|
80
|
+
# FIXME: is there a cleaner way to set margins? does it make sense to override create_new_page?
|
81
|
+
apply_margin_options margin: margin
|
82
|
+
generate_margin_box
|
83
|
+
end
|
83
84
|
|
84
|
-
|
85
|
-
|
86
|
-
|
87
|
-
|
88
|
-
|
85
|
+
# Returns the margins for the current page as a 4 element array (top, right, bottom, left)
|
86
|
+
#
|
87
|
+
def page_margin
|
88
|
+
[page.margins[:top], page.margins[:right], page.margins[:bottom], page.margins[:left]]
|
89
|
+
end
|
89
90
|
|
90
|
-
|
91
|
-
|
92
|
-
|
93
|
-
|
94
|
-
|
95
|
-
|
96
|
-
|
91
|
+
# Returns the width of the left margin for the current page
|
92
|
+
#
|
93
|
+
def page_margin_left
|
94
|
+
page.margins[:left]
|
95
|
+
end
|
96
|
+
# deprecated
|
97
|
+
alias left_margin page_margin_left
|
97
98
|
|
98
|
-
|
99
|
-
|
100
|
-
|
101
|
-
|
102
|
-
|
103
|
-
|
104
|
-
|
99
|
+
# Returns the width of the right margin for the current page
|
100
|
+
#
|
101
|
+
def page_margin_right
|
102
|
+
page.margins[:right]
|
103
|
+
end
|
104
|
+
# deprecated
|
105
|
+
alias right_margin page_margin_right
|
105
106
|
|
106
|
-
|
107
|
-
|
108
|
-
|
109
|
-
|
110
|
-
|
107
|
+
# Returns the width of the top margin for the current page
|
108
|
+
#
|
109
|
+
def page_margin_top
|
110
|
+
page.margins[:top]
|
111
|
+
end
|
111
112
|
|
112
|
-
|
113
|
-
|
114
|
-
|
115
|
-
|
116
|
-
|
113
|
+
# Returns the width of the bottom margin for the current page
|
114
|
+
#
|
115
|
+
def page_margin_bottom
|
116
|
+
page.margins[:bottom]
|
117
|
+
end
|
117
118
|
|
118
|
-
|
119
|
-
|
120
|
-
|
121
|
-
|
122
|
-
|
119
|
+
# Returns the total left margin (to the page edge) for the current bounds.
|
120
|
+
#
|
121
|
+
def bounds_margin_left
|
122
|
+
bounds.absolute_left
|
123
|
+
end
|
123
124
|
|
124
|
-
|
125
|
-
|
126
|
-
|
127
|
-
|
128
|
-
|
125
|
+
# Returns the total right margin (to the page edge) for the current bounds.
|
126
|
+
#
|
127
|
+
def bounds_margin_right
|
128
|
+
page.dimensions[2] - bounds.absolute_right
|
129
|
+
end
|
129
130
|
|
130
|
-
|
131
|
-
|
132
|
-
|
133
|
-
|
134
|
-
|
135
|
-
|
136
|
-
|
137
|
-
|
138
|
-
|
131
|
+
# Returns the side the current page is facing, :recto or :verso.
|
132
|
+
#
|
133
|
+
def page_side pgnum = nil, invert = nil
|
134
|
+
if invert
|
135
|
+
(recto_page? pgnum) ? :verso : :recto
|
136
|
+
else
|
137
|
+
(recto_page? pgnum) ? :recto : :verso
|
138
|
+
end
|
139
|
+
end
|
139
140
|
|
140
|
-
|
141
|
-
|
142
|
-
|
143
|
-
|
144
|
-
|
141
|
+
# Returns whether the page is a recto page.
|
142
|
+
#
|
143
|
+
def recto_page? pgnum = nil
|
144
|
+
(pgnum || page_number).odd?
|
145
|
+
end
|
145
146
|
|
146
|
-
|
147
|
-
|
148
|
-
|
149
|
-
|
150
|
-
|
147
|
+
# Returns whether the page is a verso page.
|
148
|
+
#
|
149
|
+
def verso_page? pgnum = nil
|
150
|
+
(pgnum || page_number).even?
|
151
|
+
end
|
151
152
|
|
152
|
-
|
153
|
-
|
154
|
-
|
155
|
-
|
156
|
-
|
153
|
+
# Returns whether the cursor is at the top of the page (i.e., margin box).
|
154
|
+
#
|
155
|
+
def at_page_top?
|
156
|
+
@y == @margin_box.absolute_top
|
157
|
+
end
|
157
158
|
|
158
|
-
|
159
|
-
|
160
|
-
|
161
|
-
|
162
|
-
|
159
|
+
# Returns whether the current page is the last page in the document.
|
160
|
+
#
|
161
|
+
def last_page?
|
162
|
+
page_number == page_count
|
163
|
+
end
|
163
164
|
|
164
|
-
|
165
|
+
# Destinations
|
165
166
|
|
166
|
-
|
167
|
-
|
168
|
-
|
169
|
-
|
170
|
-
|
171
|
-
|
172
|
-
|
173
|
-
|
167
|
+
# Generates a destination object that resolves to the top of the page
|
168
|
+
# specified by the page_num parameter or the current page if no page number
|
169
|
+
# is provided. The destination preserves the user's zoom level unlike
|
170
|
+
# the destinations generated by the outline builder.
|
171
|
+
#
|
172
|
+
def dest_top page_num = nil
|
173
|
+
dest_xyz 0, page_height, nil, (page_num ? state.pages[page_num - 1] : page)
|
174
|
+
end
|
174
175
|
|
175
|
-
|
176
|
-
|
177
|
-
|
178
|
-
|
179
|
-
|
180
|
-
|
181
|
-
|
182
|
-
|
183
|
-
|
184
|
-
|
185
|
-
|
186
|
-
|
187
|
-
|
188
|
-
|
189
|
-
|
190
|
-
|
191
|
-
|
176
|
+
# Fonts
|
177
|
+
|
178
|
+
# Registers a new custom font described in the data parameter
|
179
|
+
# after converting the font name to a String.
|
180
|
+
#
|
181
|
+
# Example:
|
182
|
+
#
|
183
|
+
# register_font Roboto: {
|
184
|
+
# normal: 'fonts/roboto-normal.ttf',
|
185
|
+
# italic: 'fonts/roboto-italic.ttf',
|
186
|
+
# bold: 'fonts/roboto-bold.ttf',
|
187
|
+
# bold_italic: 'fonts/roboto-bold_italic.ttf'
|
188
|
+
# }
|
189
|
+
#
|
190
|
+
def register_font data
|
191
|
+
font_families.update data.each_with_object({}) {|(key, val), accum| accum[key.to_s] = val }
|
192
|
+
end
|
192
193
|
|
193
|
-
|
194
|
-
|
195
|
-
|
196
|
-
|
197
|
-
|
198
|
-
|
199
|
-
|
200
|
-
|
201
|
-
|
202
|
-
|
194
|
+
# Enhances the built-in font method to allow the font
|
195
|
+
# size to be specified as the second option and to
|
196
|
+
# lazily load font-based icons.
|
197
|
+
#
|
198
|
+
def font name = nil, options = {}
|
199
|
+
if name
|
200
|
+
options = { size: options } if ::Numeric === options
|
201
|
+
if IconSets.include? name
|
202
|
+
::Prawn::Icon::FontData.load self, name
|
203
|
+
options = options.reject {|k| k == :style } if options.key? :style
|
204
|
+
end
|
205
|
+
end
|
206
|
+
super name, options
|
203
207
|
end
|
204
|
-
end
|
205
|
-
super name, options
|
206
|
-
end
|
207
208
|
|
208
|
-
|
209
|
-
|
210
|
-
|
211
|
-
|
212
|
-
|
209
|
+
# Retrieves the current font name (i.e., family).
|
210
|
+
#
|
211
|
+
def font_family
|
212
|
+
font.options[:family]
|
213
|
+
end
|
213
214
|
|
214
|
-
|
215
|
+
alias font_name font_family
|
215
216
|
|
216
|
-
|
217
|
-
|
218
|
-
|
219
|
-
|
220
|
-
|
217
|
+
# Retrieves the current font info (family, style, size) as a Hash
|
218
|
+
#
|
219
|
+
def font_info
|
220
|
+
{ family: font.options[:family], style: (font.options[:style] || :normal), size: @font_size }
|
221
|
+
end
|
221
222
|
|
222
|
-
|
223
|
-
|
224
|
-
|
225
|
-
|
226
|
-
|
227
|
-
|
228
|
-
|
229
|
-
|
223
|
+
# Sets the font style for the scope of the block to which this method
|
224
|
+
# yields. If the style is nil and no block is given, return the current
|
225
|
+
# font style.
|
226
|
+
#
|
227
|
+
def font_style style = nil
|
228
|
+
if block_given?
|
229
|
+
font font.options[:family], style: style do
|
230
|
+
yield
|
231
|
+
end
|
232
|
+
elsif style
|
233
|
+
font font.options[:family], style: style
|
234
|
+
else
|
235
|
+
font.options[:style] || :normal
|
236
|
+
end
|
230
237
|
end
|
231
|
-
elsif style
|
232
|
-
font font.options[:family], style: style
|
233
|
-
else
|
234
|
-
font.options[:style] || :normal
|
235
|
-
end
|
236
|
-
end
|
237
238
|
|
238
|
-
|
239
|
-
|
240
|
-
|
241
|
-
|
242
|
-
|
243
|
-
|
244
|
-
|
245
|
-
|
246
|
-
|
247
|
-
|
248
|
-
|
249
|
-
|
250
|
-
|
251
|
-
|
252
|
-
|
253
|
-
|
254
|
-
|
255
|
-
|
256
|
-
|
257
|
-
|
258
|
-
|
259
|
-
|
260
|
-
|
261
|
-
|
262
|
-
|
263
|
-
|
264
|
-
|
239
|
+
# Applies points as a scale factor of the current font if the value provided
|
240
|
+
# is less than or equal to 1 or it's a string (e.g., 1.1em), then delegates to the super
|
241
|
+
# implementation to carry out the built-in functionality.
|
242
|
+
#
|
243
|
+
#--
|
244
|
+
# QUESTION should we round the result?
|
245
|
+
def font_size points = nil
|
246
|
+
return @font_size unless points
|
247
|
+
if points == 1
|
248
|
+
super @font_size
|
249
|
+
elsif String === points
|
250
|
+
if points.end_with? 'rem'
|
251
|
+
super @root_font_size * points.to_f
|
252
|
+
elsif points.end_with? 'em'
|
253
|
+
super @font_size * points.to_f
|
254
|
+
elsif points.end_with? '%'
|
255
|
+
super @font_size * (points.to_f / 100)
|
256
|
+
else
|
257
|
+
super points.to_f
|
258
|
+
end
|
259
|
+
# FIXME: HACK assume em value
|
260
|
+
elsif points < 1
|
261
|
+
super @font_size * points
|
262
|
+
else
|
263
|
+
super points
|
264
|
+
end
|
265
|
+
end
|
265
266
|
|
266
|
-
|
267
|
-
|
268
|
-
|
269
|
-
|
270
|
-
|
271
|
-
|
272
|
-
|
273
|
-
|
274
|
-
|
267
|
+
def resolve_font_style styles
|
268
|
+
if styles.include? :bold
|
269
|
+
(styles.include? :italic) ? :bold_italic : :bold
|
270
|
+
elsif styles.include? :italic
|
271
|
+
:italic
|
272
|
+
else
|
273
|
+
:normal
|
274
|
+
end
|
275
|
+
end
|
275
276
|
|
276
|
-
|
277
|
-
|
278
|
-
|
279
|
-
|
280
|
-
|
281
|
-
|
277
|
+
# Retreives the collection of font styles from the given font style key,
|
278
|
+
# which defaults to the current font style.
|
279
|
+
#
|
280
|
+
def font_styles style = font_style
|
281
|
+
FontStyleToSet[style].dup
|
282
|
+
end
|
282
283
|
|
283
|
-
|
284
|
-
|
285
|
-
|
286
|
-
|
287
|
-
|
288
|
-
|
289
|
-
|
290
|
-
|
291
|
-
|
292
|
-
|
293
|
-
|
294
|
-
|
295
|
-
|
296
|
-
|
284
|
+
# Apply the font settings (family, size, styles and character spacing) from
|
285
|
+
# the fragment to the document, then yield to the block.
|
286
|
+
#
|
287
|
+
# The original font settings are restored before this method returns.
|
288
|
+
#
|
289
|
+
def fragment_font fragment
|
290
|
+
f_info = font_info
|
291
|
+
f_family = fragment[:font] || f_info[:family]
|
292
|
+
f_size = fragment[:size] || f_info[:size]
|
293
|
+
if (f_styles = fragment[:styles])
|
294
|
+
f_style = resolve_font_style f_styles
|
295
|
+
else
|
296
|
+
f_style = :normal
|
297
|
+
end
|
297
298
|
|
298
|
-
|
299
|
-
|
300
|
-
|
301
|
-
|
299
|
+
if (c_spacing = fragment[:character_spacing])
|
300
|
+
character_spacing c_spacing do
|
301
|
+
font f_family, size: f_size, style: f_style do
|
302
|
+
yield
|
303
|
+
end
|
304
|
+
end
|
305
|
+
else
|
306
|
+
font f_family, size: f_size, style: f_style do
|
307
|
+
yield
|
308
|
+
end
|
302
309
|
end
|
303
310
|
end
|
304
|
-
else
|
305
|
-
font f_family, size: f_size, style: f_style do
|
306
|
-
yield
|
307
|
-
end
|
308
|
-
end
|
309
|
-
end
|
310
311
|
|
311
|
-
|
312
|
-
|
313
|
-
|
312
|
+
def icon_font_data family
|
313
|
+
::Prawn::Icon::FontData.load self, family
|
314
|
+
end
|
314
315
|
|
315
|
-
|
316
|
-
|
317
|
-
|
316
|
+
def resolve_legacy_icon_name name
|
317
|
+
::Prawn::Icon::Compatibility::SHIMS[%(fa-#{name})]
|
318
|
+
end
|
318
319
|
|
319
|
-
|
320
|
-
|
321
|
-
|
322
|
-
|
323
|
-
|
324
|
-
|
325
|
-
|
326
|
-
|
320
|
+
def calc_line_metrics line_height = 1, font = self.font, font_size = self.font_size
|
321
|
+
line_height_length = line_height * font_size
|
322
|
+
leading = line_height_length - font_size
|
323
|
+
half_leading = leading / 2
|
324
|
+
padding_top = half_leading + font.line_gap
|
325
|
+
padding_bottom = half_leading
|
326
|
+
LineMetrics.new line_height_length, leading, padding_top, padding_bottom, false
|
327
|
+
end
|
327
328
|
|
328
329
|
=begin
|
329
|
-
|
330
|
-
|
331
|
-
|
332
|
-
|
333
|
-
|
334
|
-
|
335
|
-
|
336
|
-
|
337
|
-
|
338
|
-
|
339
|
-
|
330
|
+
# these line metrics attempted to figure out a correction based on the reported height and the font_size
|
331
|
+
# however, it only works for some fonts, and breaks down for fonts like Noto Serif
|
332
|
+
def calc_line_metrics line_height = 1, font = self.font, font_size = self.font_size
|
333
|
+
line_height_length = font_size * line_height
|
334
|
+
line_gap = line_height_length - font_size
|
335
|
+
correction = font.height - font_size
|
336
|
+
leading = line_gap - correction
|
337
|
+
shift = (font.line_gap + correction + line_gap) / 2
|
338
|
+
final_gap = font.line_gap != 0
|
339
|
+
LineMetrics.new line_height_length, leading, shift, shift, final_gap
|
340
|
+
end
|
340
341
|
=end
|
341
342
|
|
342
|
-
|
343
|
-
|
344
|
-
|
343
|
+
# Parse the text into an array of fragments using the text formatter.
|
344
|
+
def parse_text string, options = {}
|
345
|
+
return [] if string.nil?
|
345
346
|
|
346
|
-
|
347
|
-
|
348
|
-
|
349
|
-
|
350
|
-
|
351
|
-
|
352
|
-
|
347
|
+
options = options.dup
|
348
|
+
if (format_option = options.delete :inline_format)
|
349
|
+
format_option = [] unless ::Array === format_option
|
350
|
+
fragments = text_formatter.format string, *format_option
|
351
|
+
else
|
352
|
+
fragments = [text: string]
|
353
|
+
end
|
353
354
|
|
354
|
-
|
355
|
-
|
356
|
-
|
355
|
+
if (color = options.delete :color)
|
356
|
+
fragments.map do |fragment|
|
357
|
+
fragment[:color] ? fragment : fragment.merge(color: color)
|
358
|
+
end
|
359
|
+
else
|
360
|
+
fragments
|
361
|
+
end
|
357
362
|
end
|
358
|
-
else
|
359
|
-
fragments
|
360
|
-
end
|
361
|
-
end
|
362
363
|
|
363
|
-
|
364
|
-
|
365
|
-
|
366
|
-
|
367
|
-
|
368
|
-
|
369
|
-
|
370
|
-
|
371
|
-
end
|
372
|
-
|
373
|
-
# Performs the same work as Prawn::Text.text except that the first_line_opts are applied to the first line of text
|
374
|
-
# renderered. It's necessary to use low-level APIs in this method so we only style the first line and not the
|
375
|
-
# remaining lines (which is the default behavior in Prawn).
|
376
|
-
def text_with_formatted_first_line string, first_line_opts, opts
|
377
|
-
color = opts.delete :color
|
378
|
-
fragments = parse_text string, opts
|
379
|
-
# NOTE the low-level APIs we're using don't recognize the :styles option, so we must resolve
|
380
|
-
if (styles = opts.delete :styles)
|
381
|
-
opts[:style] = resolve_font_style styles
|
382
|
-
end
|
383
|
-
if (first_line_styles = first_line_opts.delete :styles)
|
384
|
-
first_line_opts[:style] = resolve_font_style first_line_styles
|
385
|
-
end
|
386
|
-
first_line_color = (first_line_opts.delete :color) || color
|
387
|
-
opts = opts.merge document: self
|
388
|
-
# QUESTION should we merge more carefully here? (hand-select keys?)
|
389
|
-
first_line_opts = opts.merge(first_line_opts).merge single_line: true
|
390
|
-
box = ::Prawn::Text::Formatted::Box.new fragments, first_line_opts
|
391
|
-
# NOTE get remaining_fragments before we add color to fragments on first line
|
392
|
-
if (text_indent = opts.delete :indent_paragraphs)
|
393
|
-
remaining_fragments = indent text_indent do
|
394
|
-
box.render dry_run: true
|
395
|
-
end
|
396
|
-
else
|
397
|
-
remaining_fragments = box.render dry_run: true
|
398
|
-
end
|
399
|
-
# NOTE color must be applied per-fragment
|
400
|
-
if first_line_color
|
401
|
-
fragments.each {|fragment| fragment[:color] ||= first_line_color}
|
402
|
-
end
|
403
|
-
if text_indent
|
404
|
-
indent text_indent do
|
405
|
-
fill_formatted_text_box fragments, first_line_opts
|
364
|
+
# NOTE override built-in draw_indented_formatted_line to insert leading before second line
|
365
|
+
def draw_indented_formatted_line string, opts
|
366
|
+
result = super
|
367
|
+
unless @no_text_printed || @all_text_printed
|
368
|
+
# as of Prawn 1.2.1, we have to handle the line gap after the first line manually
|
369
|
+
move_down opts[:leading]
|
370
|
+
end
|
371
|
+
result
|
406
372
|
end
|
407
|
-
else
|
408
|
-
fill_formatted_text_box fragments, first_line_opts
|
409
|
-
end
|
410
|
-
unless remaining_fragments.empty?
|
411
|
-
# NOTE color must be applied per-fragment
|
412
|
-
remaining_fragments.each {|fragment| fragment[:color] ||= color } if color
|
413
|
-
# as of Prawn 1.2.1, we have to handle the line gap after the first line manually
|
414
|
-
move_down opts[:leading]
|
415
|
-
remaining_fragments = fill_formatted_text_box remaining_fragments, opts
|
416
|
-
draw_remaining_formatted_text_on_new_pages remaining_fragments, opts
|
417
|
-
end
|
418
|
-
end
|
419
373
|
|
420
|
-
|
421
|
-
|
422
|
-
|
423
|
-
|
424
|
-
|
425
|
-
|
426
|
-
|
427
|
-
|
428
|
-
|
429
|
-
|
430
|
-
|
431
|
-
|
432
|
-
|
433
|
-
|
434
|
-
|
435
|
-
|
436
|
-
|
437
|
-
|
438
|
-
|
439
|
-
|
440
|
-
|
441
|
-
|
442
|
-
|
443
|
-
|
374
|
+
# Performs the same work as Prawn::Text.text except that the first_line_opts are applied to the first line of text
|
375
|
+
# renderered. It's necessary to use low-level APIs in this method so we only style the first line and not the
|
376
|
+
# remaining lines (which is the default behavior in Prawn).
|
377
|
+
def text_with_formatted_first_line string, first_line_opts, opts
|
378
|
+
color = opts.delete :color
|
379
|
+
fragments = parse_text string, opts
|
380
|
+
# NOTE the low-level APIs we're using don't recognize the :styles option, so we must resolve
|
381
|
+
if (styles = opts.delete :styles)
|
382
|
+
opts[:style] = resolve_font_style styles
|
383
|
+
end
|
384
|
+
if (first_line_styles = first_line_opts.delete :styles)
|
385
|
+
first_line_opts[:style] = resolve_font_style first_line_styles
|
386
|
+
end
|
387
|
+
first_line_color = (first_line_opts.delete :color) || color
|
388
|
+
opts = opts.merge document: self
|
389
|
+
# QUESTION should we merge more carefully here? (hand-select keys?)
|
390
|
+
first_line_opts = opts.merge(first_line_opts).merge single_line: true
|
391
|
+
box = ::Prawn::Text::Formatted::Box.new fragments, first_line_opts
|
392
|
+
# NOTE get remaining_fragments before we add color to fragments on first line
|
393
|
+
if (text_indent = opts.delete :indent_paragraphs)
|
394
|
+
remaining_fragments = indent text_indent do
|
395
|
+
box.render dry_run: true
|
396
|
+
end
|
397
|
+
else
|
398
|
+
remaining_fragments = box.render dry_run: true
|
399
|
+
end
|
400
|
+
# NOTE color must be applied per-fragment
|
401
|
+
fragments.each {|fragment| fragment[:color] ||= first_line_color } if first_line_color
|
402
|
+
if text_indent
|
403
|
+
indent text_indent do
|
404
|
+
fill_formatted_text_box fragments, first_line_opts
|
405
|
+
end
|
406
|
+
else
|
407
|
+
fill_formatted_text_box fragments, first_line_opts
|
408
|
+
end
|
409
|
+
unless remaining_fragments.empty?
|
410
|
+
# NOTE color must be applied per-fragment
|
411
|
+
remaining_fragments.each {|fragment| fragment[:color] ||= color } if color
|
412
|
+
# as of Prawn 1.2.1, we have to handle the line gap after the first line manually
|
413
|
+
move_down opts[:leading]
|
414
|
+
remaining_fragments = fill_formatted_text_box remaining_fragments, opts
|
415
|
+
draw_remaining_formatted_text_on_new_pages remaining_fragments, opts
|
416
|
+
end
|
417
|
+
end
|
444
418
|
|
445
|
-
|
419
|
+
# Apply the text transform to the specified text.
|
420
|
+
#
|
421
|
+
# Supported transform values are "uppercase", "lowercase", or "none" (passed
|
422
|
+
# as either a String or a Symbol). When the uppercase transform is applied to
|
423
|
+
# the text, it correctly uppercases visible text while leaving markup and
|
424
|
+
# named character entities unchanged. The none transform returns the text
|
425
|
+
# unmodified.
|
426
|
+
#
|
427
|
+
def transform_text text, transform
|
428
|
+
case transform
|
429
|
+
when :uppercase, 'uppercase'
|
430
|
+
uppercase_pcdata text
|
431
|
+
when :lowercase, 'lowercase'
|
432
|
+
lowercase_pcdata text
|
433
|
+
when :capitalize, 'capitalize'
|
434
|
+
capitalize_words_pcdata text
|
435
|
+
else
|
436
|
+
text
|
437
|
+
end
|
438
|
+
end
|
446
439
|
|
447
|
-
|
448
|
-
|
449
|
-
|
450
|
-
def move_up n
|
451
|
-
super unless n == 0
|
452
|
-
end
|
440
|
+
def hyphenate_text text, hyphenator
|
441
|
+
hyphenate_words_pcdata text, hyphenator
|
442
|
+
end
|
453
443
|
|
454
|
-
|
455
|
-
# to next page if image doesn't fit before rendering image.
|
456
|
-
#--
|
457
|
-
# NOTE could use :at option when calling image/embed_image instead
|
458
|
-
def move_text_position h
|
459
|
-
end
|
444
|
+
# Cursor
|
460
445
|
|
461
|
-
|
462
|
-
|
463
|
-
|
464
|
-
|
465
|
-
|
466
|
-
|
446
|
+
# Short-circuits the call to the built-in move_up operation
|
447
|
+
# when n is 0.
|
448
|
+
#
|
449
|
+
def move_up n
|
450
|
+
super unless n == 0
|
451
|
+
end
|
467
452
|
|
468
|
-
|
469
|
-
|
470
|
-
|
471
|
-
|
472
|
-
|
473
|
-
|
474
|
-
|
475
|
-
|
476
|
-
|
477
|
-
|
478
|
-
|
479
|
-
|
480
|
-
yield
|
481
|
-
move_down(bottom || top)
|
482
|
-
end
|
453
|
+
# Override built-in move_text_position method to prevent Prawn from advancing
|
454
|
+
# to next page if image doesn't fit before rendering image.
|
455
|
+
#--
|
456
|
+
# NOTE could use :at option when calling image/embed_image instead
|
457
|
+
def move_text_position h; end
|
458
|
+
|
459
|
+
# Short-circuits the call to the built-in move_down operation
|
460
|
+
# when n is 0.
|
461
|
+
#
|
462
|
+
def move_down n
|
463
|
+
super unless n == 0
|
464
|
+
end
|
483
465
|
|
484
|
-
|
485
|
-
|
486
|
-
|
487
|
-
|
488
|
-
|
489
|
-
|
490
|
-
|
491
|
-
|
492
|
-
|
493
|
-
|
494
|
-
|
495
|
-
|
496
|
-
#
|
497
|
-
# pad_box [10, 10, 10, 20] do
|
498
|
-
# text 'An indented paragraph inside a box with equal padding on all sides.'
|
499
|
-
# end
|
500
|
-
#
|
501
|
-
def pad_box padding
|
502
|
-
if padding
|
503
|
-
# TODO implement shorthand combinations like in CSS
|
504
|
-
p_top, p_right, p_bottom, p_left = ::Array === padding ? padding : (::Array.new 4, padding)
|
505
|
-
begin
|
506
|
-
# logic is intentionally inlined
|
507
|
-
move_down p_top
|
508
|
-
bounds.add_left_padding p_left
|
509
|
-
bounds.add_right_padding p_right
|
466
|
+
# Bounds
|
467
|
+
|
468
|
+
# Overrides the built-in pad operation to allow for asymmetric paddings.
|
469
|
+
#
|
470
|
+
# Example:
|
471
|
+
#
|
472
|
+
# pad 20, 10 do
|
473
|
+
# text 'A paragraph with twice as much top padding as bottom padding.'
|
474
|
+
# end
|
475
|
+
#
|
476
|
+
def pad top, bottom = nil
|
477
|
+
move_down top
|
510
478
|
yield
|
511
|
-
|
512
|
-
|
513
|
-
|
514
|
-
|
479
|
+
move_down(bottom || top)
|
480
|
+
end
|
481
|
+
|
482
|
+
# Combines the built-in pad and indent operations into a single method.
|
483
|
+
#
|
484
|
+
# Padding may be specified as an array of four values, or as a single value.
|
485
|
+
# The single value is used as the padding around all four sides of the box.
|
486
|
+
#
|
487
|
+
# If padding is nil, this method simply yields to the block and returns.
|
488
|
+
#
|
489
|
+
# Example:
|
490
|
+
#
|
491
|
+
# pad_box 20 do
|
492
|
+
# text 'A paragraph inside a blox with even padding on all sides.'
|
493
|
+
# end
|
494
|
+
#
|
495
|
+
# pad_box [10, 10, 10, 20] do
|
496
|
+
# text 'An indented paragraph inside a box with equal padding on all sides.'
|
497
|
+
# end
|
498
|
+
#
|
499
|
+
def pad_box padding
|
500
|
+
if padding
|
501
|
+
# TODO: implement shorthand combinations like in CSS
|
502
|
+
p_top, p_right, p_bottom, p_left = ::Array === padding ? padding : (::Array.new 4, padding)
|
503
|
+
begin
|
504
|
+
# logic is intentionally inlined
|
505
|
+
move_down p_top
|
506
|
+
bounds.add_left_padding p_left
|
507
|
+
bounds.add_right_padding p_right
|
508
|
+
yield
|
509
|
+
# NOTE support negative bottom padding for use with quote block
|
510
|
+
if p_bottom < 0
|
511
|
+
# QUESTION should we return to previous page if top of page is reached?
|
512
|
+
p_bottom < cursor - reference_bounds.top ? (move_cursor_to reference_bounds.top) : (move_down p_bottom)
|
513
|
+
else
|
514
|
+
p_bottom < cursor ? (move_down p_bottom) : reference_bounds.move_past_bottom
|
515
|
+
end
|
516
|
+
ensure
|
517
|
+
bounds.subtract_left_padding p_left
|
518
|
+
bounds.subtract_right_padding p_right
|
519
|
+
end
|
515
520
|
else
|
516
|
-
|
521
|
+
yield
|
517
522
|
end
|
518
|
-
ensure
|
519
|
-
bounds.subtract_left_padding p_left
|
520
|
-
bounds.subtract_right_padding p_right
|
521
|
-
end
|
522
|
-
else
|
523
|
-
yield
|
524
|
-
end
|
525
523
|
|
526
|
-
|
527
|
-
|
528
|
-
|
529
|
-
|
530
|
-
|
531
|
-
|
532
|
-
|
524
|
+
# alternate, delegated logic
|
525
|
+
#pad padding[0], padding[2] do
|
526
|
+
# indent padding[1], padding[3] do
|
527
|
+
# yield
|
528
|
+
# end
|
529
|
+
#end
|
530
|
+
end
|
533
531
|
|
534
|
-
|
535
|
-
|
536
|
-
|
532
|
+
def inflate_indent value
|
533
|
+
(::Array === value ? (value.slice 0, 2) : (::Array.new 2, value)).map(&:to_f)
|
534
|
+
end
|
537
535
|
|
538
|
-
|
539
|
-
|
540
|
-
|
541
|
-
|
542
|
-
|
543
|
-
|
544
|
-
|
545
|
-
|
546
|
-
|
547
|
-
|
548
|
-
|
549
|
-
|
550
|
-
|
551
|
-
|
536
|
+
# TODO: memoize the result
|
537
|
+
def inflate_padding padding
|
538
|
+
padding = [*(padding || 0)].slice 0, 4
|
539
|
+
case padding.size
|
540
|
+
when 1
|
541
|
+
[padding[0], padding[0], padding[0], padding[0]]
|
542
|
+
when 2
|
543
|
+
[padding[0], padding[1], padding[0], padding[1]]
|
544
|
+
when 3
|
545
|
+
[padding[0], padding[1], padding[2], padding[1]]
|
546
|
+
else
|
547
|
+
padding
|
548
|
+
end
|
549
|
+
end
|
552
550
|
|
553
|
-
|
554
|
-
|
555
|
-
|
556
|
-
|
557
|
-
|
558
|
-
|
559
|
-
|
560
|
-
|
551
|
+
# Stretch the current bounds to the left and right edges of the current page
|
552
|
+
# while yielding the specified block if the verdict argument is true.
|
553
|
+
# Otherwise, simply yield the specified block.
|
554
|
+
#
|
555
|
+
def span_page_width_if verdict
|
556
|
+
if verdict
|
557
|
+
indent(-bounds_margin_left, -bounds_margin_right) do
|
558
|
+
yield
|
559
|
+
end
|
560
|
+
else
|
561
|
+
yield
|
562
|
+
end
|
561
563
|
end
|
562
|
-
else
|
563
|
-
yield
|
564
|
-
end
|
565
|
-
end
|
566
564
|
|
567
|
-
|
568
|
-
|
569
|
-
|
570
|
-
|
571
|
-
|
572
|
-
|
573
|
-
|
574
|
-
|
575
|
-
|
576
|
-
|
577
|
-
|
578
|
-
|
565
|
+
# A flowing version of the bounding_box. If the content runs to another page, the cursor starts
|
566
|
+
# at the top of the page instead of the original cursor position. Similar to span, except
|
567
|
+
# you can specify an absolute left position and pass additional options through to bounding_box.
|
568
|
+
#
|
569
|
+
def flow_bounding_box left = 0, opts = {}
|
570
|
+
original_y = y
|
571
|
+
# QUESTION should preserving original_x be an option?
|
572
|
+
original_x = bounds.absolute_left - margin_box.absolute_left
|
573
|
+
canvas do
|
574
|
+
bounding_box [margin_box.absolute_left + original_x + left, margin_box.absolute_top], opts do
|
575
|
+
self.y = original_y
|
576
|
+
yield
|
577
|
+
end
|
578
|
+
end
|
579
579
|
end
|
580
|
-
end
|
581
|
-
end
|
582
580
|
|
583
|
-
|
584
|
-
|
585
|
-
|
586
|
-
|
587
|
-
|
588
|
-
|
589
|
-
|
590
|
-
|
591
|
-
|
592
|
-
|
593
|
-
|
594
|
-
|
595
|
-
|
581
|
+
# Graphics
|
582
|
+
|
583
|
+
# Fills the current bounding box with the specified fill color. Before
|
584
|
+
# returning from this method, the original fill color on the document is
|
585
|
+
# restored.
|
586
|
+
def fill_bounds f_color = fill_color
|
587
|
+
if f_color && f_color != 'transparent'
|
588
|
+
prev_fill_color = fill_color
|
589
|
+
fill_color f_color
|
590
|
+
fill_rectangle bounds.top_left, bounds.width, bounds.height
|
591
|
+
fill_color prev_fill_color
|
592
|
+
end
|
593
|
+
end
|
596
594
|
|
597
|
-
|
598
|
-
|
599
|
-
|
600
|
-
|
601
|
-
|
602
|
-
|
595
|
+
# Fills the absolute bounding box with the specified fill color. Before
|
596
|
+
# returning from this method, the original fill color on the document is
|
597
|
+
# restored.
|
598
|
+
def fill_absolute_bounds f_color = fill_color
|
599
|
+
canvas { fill_bounds f_color }
|
600
|
+
end
|
603
601
|
|
604
|
-
|
605
|
-
|
606
|
-
|
607
|
-
|
608
|
-
|
609
|
-
|
610
|
-
|
611
|
-
|
612
|
-
|
613
|
-
|
614
|
-
|
615
|
-
|
616
|
-
|
617
|
-
|
618
|
-
|
619
|
-
|
620
|
-
|
621
|
-
|
622
|
-
|
623
|
-
|
624
|
-
|
625
|
-
|
626
|
-
|
627
|
-
|
628
|
-
|
629
|
-
|
630
|
-
|
631
|
-
|
602
|
+
# Fills the current bounds using the specified fill color and strokes the
|
603
|
+
# bounds using the specified stroke color. Sets the line with if specified
|
604
|
+
# in the options. Before returning from this method, the original fill
|
605
|
+
# color, stroke color and line width on the document are restored.
|
606
|
+
#
|
607
|
+
def fill_and_stroke_bounds f_color = fill_color, s_color = stroke_color, options = {}
|
608
|
+
no_fill = !f_color || f_color == 'transparent'
|
609
|
+
no_stroke = !s_color || s_color == 'transparent' || options[:line_width] == 0
|
610
|
+
return if no_fill && no_stroke
|
611
|
+
save_graphics_state do
|
612
|
+
radius = options[:radius] || 0
|
613
|
+
|
614
|
+
# fill
|
615
|
+
unless no_fill
|
616
|
+
fill_color f_color
|
617
|
+
fill_rounded_rectangle bounds.top_left, bounds.width, bounds.height, radius
|
618
|
+
end
|
619
|
+
|
620
|
+
# stroke
|
621
|
+
unless no_stroke
|
622
|
+
stroke_color s_color
|
623
|
+
line_width(options[:line_width] || 0.5)
|
624
|
+
# FIXME: think about best way to indicate dashed borders
|
625
|
+
#if options.has_key? :dash_width
|
626
|
+
# dash options[:dash_width], space: options[:dash_space] || 1
|
627
|
+
#end
|
628
|
+
stroke_rounded_rectangle bounds.top_left, bounds.width, bounds.height, radius
|
629
|
+
#undash if options.has_key? :dash_width
|
630
|
+
end
|
631
|
+
end
|
632
632
|
end
|
633
|
-
end
|
634
|
-
end
|
635
633
|
|
636
|
-
|
637
|
-
|
638
|
-
|
639
|
-
|
640
|
-
|
641
|
-
|
642
|
-
|
643
|
-
|
644
|
-
|
645
|
-
|
646
|
-
|
647
|
-
|
648
|
-
|
649
|
-
|
650
|
-
|
651
|
-
|
652
|
-
|
634
|
+
# Fills and, optionally, strokes the current bounds using the fill and
|
635
|
+
# stroke color specified, then yields to the block. The only_if option can
|
636
|
+
# be used to conditionally disable this behavior.
|
637
|
+
#
|
638
|
+
def shade_box color, line_color = nil, options = {}
|
639
|
+
if (!options.key? :only_if) || options[:only_if]
|
640
|
+
# FIXME: could use save_graphics_state here
|
641
|
+
previous_fill_color = current_fill_color
|
642
|
+
fill_color color
|
643
|
+
fill_rectangle [bounds.left, bounds.top], bounds.right, bounds.top - bounds.bottom
|
644
|
+
fill_color previous_fill_color
|
645
|
+
if line_color
|
646
|
+
line_width 0.5
|
647
|
+
previous_stroke_color = current_stroke_color
|
648
|
+
stroke_color line_color
|
649
|
+
stroke_bounds
|
650
|
+
stroke_color previous_stroke_color
|
651
|
+
end
|
652
|
+
end
|
653
|
+
yield
|
653
654
|
end
|
654
|
-
end
|
655
|
-
yield
|
656
|
-
end
|
657
655
|
|
658
|
-
|
659
|
-
|
660
|
-
|
661
|
-
|
662
|
-
|
663
|
-
|
664
|
-
|
665
|
-
|
666
|
-
|
667
|
-
|
668
|
-
|
669
|
-
|
670
|
-
|
671
|
-
|
672
|
-
|
673
|
-
|
674
|
-
|
675
|
-
|
676
|
-
|
677
|
-
|
678
|
-
|
679
|
-
|
680
|
-
|
681
|
-
|
682
|
-
|
683
|
-
|
656
|
+
# Strokes a horizontal line using the current bounds. The width of the line
|
657
|
+
# can be specified using the line_width option. The offset from the cursor
|
658
|
+
# can be set using the at option.
|
659
|
+
#
|
660
|
+
def stroke_horizontal_rule rule_color = stroke_color, options = {}
|
661
|
+
rule_y = cursor - (options[:at] || 0)
|
662
|
+
rule_style = options[:line_style]
|
663
|
+
rule_width = options[:line_width] || 0.5
|
664
|
+
rule_x_start = bounds.left
|
665
|
+
rule_x_end = bounds.right
|
666
|
+
rule_inked = false
|
667
|
+
save_graphics_state do
|
668
|
+
line_width rule_width
|
669
|
+
stroke_color rule_color
|
670
|
+
case rule_style
|
671
|
+
when :dashed
|
672
|
+
dash rule_width * 4
|
673
|
+
when :dotted
|
674
|
+
dash rule_width
|
675
|
+
when :double
|
676
|
+
stroke_horizontal_line rule_x_start, rule_x_end, at: (rule_y + rule_width)
|
677
|
+
stroke_horizontal_line rule_x_start, rule_x_end, at: (rule_y - rule_width)
|
678
|
+
rule_inked = true
|
679
|
+
end if rule_style
|
680
|
+
stroke_horizontal_line rule_x_start, rule_x_end, at: rule_y unless rule_inked
|
681
|
+
end
|
682
|
+
end
|
684
683
|
|
685
|
-
|
686
|
-
|
687
|
-
|
688
|
-
|
689
|
-
|
690
|
-
|
691
|
-
|
692
|
-
|
693
|
-
|
694
|
-
|
695
|
-
|
696
|
-
|
697
|
-
|
698
|
-
|
699
|
-
|
700
|
-
|
701
|
-
|
702
|
-
|
703
|
-
|
704
|
-
|
705
|
-
|
706
|
-
|
707
|
-
|
708
|
-
|
709
|
-
|
710
|
-
|
711
|
-
end
|
712
|
-
end
|
684
|
+
# A compliment to the stroke_horizontal_rule method, strokes a
|
685
|
+
# vertical line using the current bounds. The width of the line
|
686
|
+
# can be specified using the line_width option. The horizontal (x)
|
687
|
+
# position can be specified using the at option.
|
688
|
+
#
|
689
|
+
def stroke_vertical_rule rule_color = stroke_color, options = {}
|
690
|
+
rule_x = options[:at] || 0
|
691
|
+
rule_y_from = bounds.top
|
692
|
+
rule_y_to = bounds.bottom
|
693
|
+
rule_style = options[:line_style]
|
694
|
+
rule_width = options[:line_width] || 0.5
|
695
|
+
save_graphics_state do
|
696
|
+
line_width rule_width
|
697
|
+
stroke_color rule_color
|
698
|
+
case rule_style
|
699
|
+
when :dashed
|
700
|
+
dash rule_width * 4
|
701
|
+
when :dotted
|
702
|
+
dash rule_width
|
703
|
+
when :double
|
704
|
+
stroke_vertical_line rule_y_from, rule_y_to, at: (rule_x - rule_width)
|
705
|
+
rule_x += rule_width
|
706
|
+
end if rule_style
|
707
|
+
stroke_vertical_line rule_y_from, rule_y_to, at: rule_x
|
708
|
+
end
|
709
|
+
end
|
713
710
|
|
714
|
-
|
715
|
-
|
716
|
-
|
717
|
-
|
718
|
-
|
719
|
-
|
720
|
-
|
721
|
-
|
722
|
-
|
723
|
-
|
724
|
-
|
725
|
-
|
726
|
-
|
727
|
-
|
728
|
-
|
729
|
-
|
730
|
-
|
731
|
-
|
732
|
-
|
733
|
-
|
734
|
-
|
735
|
-
|
736
|
-
|
737
|
-
|
738
|
-
|
711
|
+
# Pages
|
712
|
+
|
713
|
+
# Deletes the current page and move the cursor
|
714
|
+
# to the previous page.
|
715
|
+
def delete_page
|
716
|
+
pg = page_number
|
717
|
+
pdf_store = state.store
|
718
|
+
pdf_objs = pdf_store.instance_variable_get :@objects
|
719
|
+
pdf_ids = pdf_store.instance_variable_get :@identifiers
|
720
|
+
page_id = pdf_store.object_id_for_page pg
|
721
|
+
content_id = page.content.identifier
|
722
|
+
[page_id, content_id].each do |key|
|
723
|
+
pdf_objs.delete key
|
724
|
+
pdf_ids.delete key
|
725
|
+
end
|
726
|
+
pdf_store.pages.data[:Kids].pop
|
727
|
+
pdf_store.pages.data[:Count] -= 1
|
728
|
+
state.pages.pop
|
729
|
+
if pg > 1
|
730
|
+
go_to_page pg - 1
|
731
|
+
else
|
732
|
+
@page_number = 0
|
733
|
+
state.page = nil
|
734
|
+
end
|
735
|
+
end
|
739
736
|
|
740
|
-
|
741
|
-
|
742
|
-
|
743
|
-
|
744
|
-
|
745
|
-
|
746
|
-
|
747
|
-
|
748
|
-
|
749
|
-
|
750
|
-
|
751
|
-
|
752
|
-
|
753
|
-
|
754
|
-
|
755
|
-
|
756
|
-
|
757
|
-
|
758
|
-
|
759
|
-
|
760
|
-
|
761
|
-
|
762
|
-
|
763
|
-
|
764
|
-
|
737
|
+
# Import the specified page into the current document.
|
738
|
+
#
|
739
|
+
# By default, advance to the next page afterwards, creating it if necessary.
|
740
|
+
# This behavior can be disabled by passing the option `advance: false`.
|
741
|
+
# However, due to how page creation works in Prawn, understand that advancing
|
742
|
+
# to the next page is necessary to prevent the size & layout of the imported
|
743
|
+
# page from affecting a newly created page.
|
744
|
+
def import_page file, opts = {}
|
745
|
+
prev_page_layout = page.layout
|
746
|
+
prev_page_size = page.size
|
747
|
+
state.compress = false if state.compress # can't use compression if using template
|
748
|
+
prev_text_rendering_mode = (defined? @text_rendering_mode) ? @text_rendering_mode : nil
|
749
|
+
delete_page if opts[:replace]
|
750
|
+
# NOTE use functionality provided by prawn-templates
|
751
|
+
start_new_page_discretely template: file, template_page: opts[:page]
|
752
|
+
# prawn-templates sets text_rendering_mode to :unknown, which breaks running content; revert
|
753
|
+
@text_rendering_mode = prev_text_rendering_mode
|
754
|
+
if opts.fetch :advance, true
|
755
|
+
# NOTE set page size & layout explicitly in case imported page differs
|
756
|
+
# I'm not sure it's right to start a new page here, but unfortunately there's no other
|
757
|
+
# way atm to prevent the size & layout of the imported page from affecting subsequent pages
|
758
|
+
advance_page size: prev_page_size, layout: prev_page_layout
|
759
|
+
end
|
760
|
+
nil
|
761
|
+
end
|
765
762
|
|
766
|
-
|
767
|
-
|
768
|
-
|
769
|
-
|
770
|
-
|
771
|
-
|
772
|
-
|
773
|
-
|
774
|
-
|
775
|
-
|
776
|
-
|
777
|
-
|
778
|
-
|
779
|
-
|
763
|
+
# Create a new page for the specified image. If the canvas option is true,
|
764
|
+
# the image is positioned relative to the boundaries of the page.
|
765
|
+
def image_page file, options = {}
|
766
|
+
start_new_page_discretely
|
767
|
+
image_page_number = page_number
|
768
|
+
if options.delete :canvas
|
769
|
+
canvas { image file, ({ position: :center, vposition: :center }.merge options) }
|
770
|
+
else
|
771
|
+
image file, (options.merge position: :center, vposition: :center, fit: [bounds.width, bounds.height])
|
772
|
+
end
|
773
|
+
# NOTE advance to newly created page just in case the image function threw off the cursor
|
774
|
+
go_to_page image_page_number
|
775
|
+
nil
|
776
|
+
end
|
780
777
|
|
781
|
-
|
782
|
-
|
783
|
-
|
784
|
-
|
785
|
-
|
786
|
-
|
787
|
-
|
788
|
-
|
789
|
-
|
790
|
-
|
791
|
-
|
792
|
-
|
793
|
-
|
778
|
+
# Perform an operation (such as creating a new page) without triggering the on_page_create callback
|
779
|
+
#
|
780
|
+
def perform_discretely
|
781
|
+
if (saved_callback = state.on_page_create_callback)
|
782
|
+
# equivalent to calling `on_page_create`
|
783
|
+
state.on_page_create_callback = nil
|
784
|
+
yield
|
785
|
+
# equivalent to calling `on_page_create &saved_callback`
|
786
|
+
state.on_page_create_callback = saved_callback
|
787
|
+
else
|
788
|
+
yield
|
789
|
+
end
|
790
|
+
end
|
794
791
|
|
795
|
-
|
796
|
-
|
797
|
-
|
798
|
-
|
799
|
-
|
800
|
-
|
792
|
+
# This method is a smarter version of start_new_page. It calls start_new_page
|
793
|
+
# if the current page is the last page of the document. Otherwise, it simply
|
794
|
+
# advances to the next existing page.
|
795
|
+
def advance_page opts = {}
|
796
|
+
last_page? ? (start_new_page opts) : (go_to_page page_number + 1)
|
797
|
+
end
|
801
798
|
|
802
|
-
|
803
|
-
|
804
|
-
|
805
|
-
|
806
|
-
|
799
|
+
# Start a new page without triggering the on_page_create callback
|
800
|
+
#
|
801
|
+
def start_new_page_discretely options = {}
|
802
|
+
perform_discretely { start_new_page options }
|
803
|
+
end
|
807
804
|
|
808
|
-
|
805
|
+
# Grouping
|
809
806
|
|
810
|
-
|
811
|
-
|
812
|
-
|
813
|
-
|
814
|
-
|
815
|
-
|
816
|
-
|
817
|
-
|
818
|
-
|
819
|
-
|
807
|
+
# Conditional group operation
|
808
|
+
#
|
809
|
+
def group_if verdict
|
810
|
+
if verdict
|
811
|
+
state.optimize_objects = false # optimize_objects breaks group
|
812
|
+
group { yield }
|
813
|
+
else
|
814
|
+
yield
|
815
|
+
end
|
816
|
+
end
|
820
817
|
|
821
|
-
|
822
|
-
|
823
|
-
|
824
|
-
|
825
|
-
|
826
|
-
|
827
|
-
|
828
|
-
|
829
|
-
|
830
|
-
|
831
|
-
|
832
|
-
|
833
|
-
|
834
|
-
|
835
|
-
|
836
|
-
|
818
|
+
def get_scratch_document
|
819
|
+
# marshal if not using transaction feature
|
820
|
+
#Marshal.load Marshal.dump @prototype
|
821
|
+
|
822
|
+
# use cached instance, tests show it's faster
|
823
|
+
#@prototype ||= ::Prawn::Document.new
|
824
|
+
@scratch ||= if defined? @prototype # rubocop:disable Naming/MemoizedInstanceVariableName
|
825
|
+
scratch = Marshal.load Marshal.dump @prototype
|
826
|
+
scratch.instance_variable_set :@prototype, @prototype
|
827
|
+
# TODO: set scratch number on scratch document
|
828
|
+
scratch
|
829
|
+
else
|
830
|
+
logger.warn 'no scratch prototype available; instantiating fresh scratch document'
|
831
|
+
::Prawn::Document.new
|
832
|
+
end
|
833
|
+
end
|
837
834
|
|
838
|
-
|
839
|
-
|
840
|
-
|
841
|
-
|
842
|
-
|
843
|
-
|
844
|
-
|
845
|
-
|
846
|
-
|
847
|
-
|
848
|
-
|
849
|
-
|
850
|
-
|
851
|
-
|
852
|
-
|
853
|
-
|
854
|
-
|
855
|
-
|
856
|
-
|
857
|
-
|
858
|
-
|
859
|
-
|
860
|
-
|
861
|
-
|
862
|
-
|
863
|
-
|
864
|
-
|
865
|
-
|
866
|
-
|
867
|
-
|
868
|
-
end
|
835
|
+
def scratch?
|
836
|
+
(@_label ||= (state.store.info.data[:Scratch] ? :scratch : :primary)) == :scratch
|
837
|
+
rescue
|
838
|
+
false # NOTE this method may get called before the state is initialized
|
839
|
+
end
|
840
|
+
alias is_scratch? scratch?
|
841
|
+
|
842
|
+
def dry_run &block
|
843
|
+
scratch = get_scratch_document
|
844
|
+
# QUESTION should we use scratch.advance_page instead?
|
845
|
+
scratch.start_new_page
|
846
|
+
start_page_number = scratch.page_number
|
847
|
+
start_y = scratch.y
|
848
|
+
if (left_padding = bounds.total_left_padding) > 0
|
849
|
+
scratch.bounds.add_left_padding left_padding
|
850
|
+
end
|
851
|
+
if (right_padding = bounds.total_right_padding) > 0
|
852
|
+
scratch.bounds.add_right_padding right_padding
|
853
|
+
end
|
854
|
+
scratch.font font_family, style: font_style, size: font_size do
|
855
|
+
scratch.instance_exec(&block)
|
856
|
+
end
|
857
|
+
# NOTE don't count excess if cursor exceeds writable area (due to padding)
|
858
|
+
full_page_height = scratch.effective_page_height
|
859
|
+
partial_page_height = [full_page_height, start_y - scratch.y].min
|
860
|
+
scratch.bounds.subtract_left_padding left_padding if left_padding > 0
|
861
|
+
scratch.bounds.subtract_right_padding right_padding if right_padding > 0
|
862
|
+
whole_pages = scratch.page_number - start_page_number
|
863
|
+
[(whole_pages * full_page_height + partial_page_height), whole_pages, partial_page_height]
|
864
|
+
end
|
869
865
|
|
870
|
-
|
871
|
-
|
872
|
-
|
873
|
-
|
874
|
-
|
875
|
-
|
876
|
-
|
877
|
-
|
878
|
-
|
879
|
-
|
880
|
-
|
881
|
-
|
882
|
-
|
883
|
-
|
866
|
+
# Attempt to keep the objects generated in the block on the same page
|
867
|
+
#
|
868
|
+
# TODO: short-circuit nested usage
|
869
|
+
def keep_together &block
|
870
|
+
available_space = cursor
|
871
|
+
total_height, = dry_run(&block)
|
872
|
+
# NOTE technically, if we're at the page top, we don't even need to do the
|
873
|
+
# dry run, except several uses of this method rely on the calculated height
|
874
|
+
if total_height > available_space && !at_page_top? && total_height <= effective_page_height
|
875
|
+
advance_page
|
876
|
+
started_new_page = true
|
877
|
+
else
|
878
|
+
started_new_page = false
|
879
|
+
end
|
884
880
|
|
885
|
-
|
886
|
-
|
887
|
-
|
888
|
-
|
881
|
+
# HACK: yield doesn't work here on JRuby (at least not when called from AsciidoctorJ)
|
882
|
+
#yield remainder, started_new_page
|
883
|
+
instance_exec(total_height, started_new_page, &block)
|
884
|
+
end
|
889
885
|
|
890
|
-
|
891
|
-
|
892
|
-
|
893
|
-
|
894
|
-
|
895
|
-
|
896
|
-
|
897
|
-
|
898
|
-
|
899
|
-
|
886
|
+
# Attempt to keep the objects generated in the block on the same page
|
887
|
+
# if the verdict parameter is true.
|
888
|
+
#
|
889
|
+
def keep_together_if verdict, &block
|
890
|
+
if verdict
|
891
|
+
keep_together(&block)
|
892
|
+
else
|
893
|
+
yield
|
894
|
+
end
|
895
|
+
end
|
900
896
|
|
901
897
|
=begin
|
902
|
-
|
903
|
-
|
904
|
-
|
905
|
-
|
906
|
-
|
907
|
-
|
908
|
-
|
898
|
+
def run_with_trial &block
|
899
|
+
available_space = cursor
|
900
|
+
total_height, whole_pages, remainder = dry_run(&block)
|
901
|
+
if whole_pages > 0 || remainder > available_space
|
902
|
+
started_new_page = true
|
903
|
+
else
|
904
|
+
started_new_page = false
|
905
|
+
end
|
906
|
+
# HACK yield doesn't work here on JRuby (at least not when called from AsciidoctorJ)
|
907
|
+
#yield remainder, started_new_page
|
908
|
+
instance_exec(remainder, started_new_page, &block)
|
909
|
+
end
|
910
|
+
=end
|
909
911
|
end
|
910
|
-
# HACK yield doesn't work here on JRuby (at least not when called from AsciidoctorJ)
|
911
|
-
#yield remainder, started_new_page
|
912
|
-
instance_exec(remainder, started_new_page, &block)
|
913
912
|
end
|
914
|
-
=end
|
915
|
-
end
|
916
|
-
end
|
917
913
|
end
|