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,148 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module PrawnX
|
|
4
|
+
module Graphics
|
|
5
|
+
module Color
|
|
6
|
+
COLOR_SPACES = %i[DeviceRGB DeviceCMYK Pattern].freeze
|
|
7
|
+
OPERATOR_BY_TYPE = {fill: "scn", stroke: "SCN"}.freeze
|
|
8
|
+
COLOR_SPACE_OPERATOR_BY_TYPE = {fill: "cs", stroke: "CS"}.freeze
|
|
9
|
+
# A document uses a handful of colours; computed ones (gradients drawn by hand) must not grow it forever.
|
|
10
|
+
HEX_COLORS_CACHED = 256
|
|
11
|
+
PDF_OPERANDS_BY_HEX_COLOR = {}
|
|
12
|
+
|
|
13
|
+
def fill_color(*color)
|
|
14
|
+
return current_fill_color if color.empty?
|
|
15
|
+
|
|
16
|
+
self.current_fill_color = process_color(*color)
|
|
17
|
+
set_fill_color
|
|
18
|
+
end
|
|
19
|
+
alias fill_color= fill_color
|
|
20
|
+
|
|
21
|
+
def stroke_color(*color)
|
|
22
|
+
return current_stroke_color if color.empty?
|
|
23
|
+
|
|
24
|
+
color = process_color(*color)
|
|
25
|
+
self.current_stroke_color = color
|
|
26
|
+
set_stroke_color(color)
|
|
27
|
+
end
|
|
28
|
+
alias stroke_color= stroke_color
|
|
29
|
+
|
|
30
|
+
module_function
|
|
31
|
+
|
|
32
|
+
def rgb2hex(rgb)
|
|
33
|
+
rgb.map { format("%02x", it) }.join
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
def hex2rgb(hex)
|
|
37
|
+
[hex[0..1], hex[2..3], hex[4..5]].map { it.to_i(16) }
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
private
|
|
41
|
+
|
|
42
|
+
def process_color(*color)
|
|
43
|
+
case color.size
|
|
44
|
+
when 1 then color[0]
|
|
45
|
+
when 4 then color
|
|
46
|
+
else raise ArgumentError, "wrong number of arguments supplied"
|
|
47
|
+
end
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
def color_type(color)
|
|
51
|
+
case color
|
|
52
|
+
when String
|
|
53
|
+
return :RGB if /\A\h{6}\z/.match?(color)
|
|
54
|
+
when Array
|
|
55
|
+
return :RGB if color.length == 3
|
|
56
|
+
return :CMYK if color.length == 4
|
|
57
|
+
end
|
|
58
|
+
raise ArgumentError, "Unknown type of color: #{color.inspect}"
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
def normalize_color(color)
|
|
62
|
+
case color_type(color)
|
|
63
|
+
when :RGB
|
|
64
|
+
r, g, b = hex2rgb(color)
|
|
65
|
+
[r / 255.0, g / 255.0, b / 255.0]
|
|
66
|
+
when :CMYK
|
|
67
|
+
color.map { it / 100.0 }
|
|
68
|
+
end
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
def color_to_s(color)
|
|
72
|
+
return PDF.real_params(normalize_color(color)) unless color.is_a?(String)
|
|
73
|
+
|
|
74
|
+
PDF_OPERANDS_BY_HEX_COLOR.fetch(color) do
|
|
75
|
+
operands = PDF.real_params(normalize_color(color))
|
|
76
|
+
PDF_OPERANDS_BY_HEX_COLOR[color.dup.freeze] = operands if PDF_OPERANDS_BY_HEX_COLOR.size < HEX_COLORS_CACHED
|
|
77
|
+
operands
|
|
78
|
+
end
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
def color_space(color)
|
|
82
|
+
color_type(color) == :RGB ? :DeviceRGB : :DeviceCMYK
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
def set_color_space(type, color_space)
|
|
86
|
+
return if current_color_space(type) == color_space && !page.in_stamp_stream?
|
|
87
|
+
|
|
88
|
+
set_current_color_space(color_space, type)
|
|
89
|
+
raise ArgumentError, "unknown color space: '#{color_space}'" unless COLOR_SPACES.include?(color_space)
|
|
90
|
+
|
|
91
|
+
operator = COLOR_SPACE_OPERATOR_BY_TYPE.fetch(type) { raise ArgumentError, "unknown type '#{type}'" }
|
|
92
|
+
add_content("/#{color_space} #{operator}")
|
|
93
|
+
end
|
|
94
|
+
|
|
95
|
+
def set_color(type, color, options = {})
|
|
96
|
+
operator = OPERATOR_BY_TYPE.fetch(type) { raise ArgumentError, "unknown type '#{type}'" }
|
|
97
|
+
if options[:pattern]
|
|
98
|
+
set_color_space(type, :Pattern)
|
|
99
|
+
add_content("/#{color} #{operator}")
|
|
100
|
+
else
|
|
101
|
+
set_color_space(type, color_space(color))
|
|
102
|
+
write_color(color_to_s(color), operator)
|
|
103
|
+
end
|
|
104
|
+
end
|
|
105
|
+
|
|
106
|
+
def set_fill_color(color = nil)
|
|
107
|
+
set_color(:fill, color || current_fill_color)
|
|
108
|
+
end
|
|
109
|
+
|
|
110
|
+
def set_stroke_color(color = nil)
|
|
111
|
+
set_color(:stroke, color || current_stroke_color)
|
|
112
|
+
end
|
|
113
|
+
|
|
114
|
+
def update_colors
|
|
115
|
+
set_fill_color
|
|
116
|
+
set_stroke_color
|
|
117
|
+
end
|
|
118
|
+
|
|
119
|
+
def current_color_space(type)
|
|
120
|
+
graphic_state.color_space[type]
|
|
121
|
+
end
|
|
122
|
+
|
|
123
|
+
def set_current_color_space(color_space, type)
|
|
124
|
+
graphic_state.color_space[type] = color_space
|
|
125
|
+
end
|
|
126
|
+
|
|
127
|
+
def current_fill_color
|
|
128
|
+
graphic_state.fill_color
|
|
129
|
+
end
|
|
130
|
+
|
|
131
|
+
def current_fill_color=(color)
|
|
132
|
+
graphic_state.fill_color = color
|
|
133
|
+
end
|
|
134
|
+
|
|
135
|
+
def current_stroke_color
|
|
136
|
+
graphic_state.stroke_color
|
|
137
|
+
end
|
|
138
|
+
|
|
139
|
+
def current_stroke_color=(color)
|
|
140
|
+
graphic_state.stroke_color = color
|
|
141
|
+
end
|
|
142
|
+
|
|
143
|
+
def write_color(color, operator)
|
|
144
|
+
add_content("#{color} #{operator}")
|
|
145
|
+
end
|
|
146
|
+
end
|
|
147
|
+
end
|
|
148
|
+
end
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "digest/sha1"
|
|
4
|
+
|
|
5
|
+
module PrawnX
|
|
6
|
+
module Graphics
|
|
7
|
+
# Axial and radial gradients, as shading patterns (ISO 32000-1 §8.7.4.5).
|
|
8
|
+
module Patterns
|
|
9
|
+
GradientStop = Struct.new(:position, :color)
|
|
10
|
+
Gradient = Struct.new(:type, :apply_transformations, :stops, :from, :to, :r1, :r2)
|
|
11
|
+
IDENTITY_MATRIX = [1, 0, 0, 1, 0, 0].freeze
|
|
12
|
+
|
|
13
|
+
def fill_gradient(*arguments, **options)
|
|
14
|
+
set_gradient(:fill, *arguments, **options)
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
def stroke_gradient(*arguments, **options)
|
|
18
|
+
set_gradient(:stroke, *arguments, **options)
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
private
|
|
22
|
+
|
|
23
|
+
def set_gradient(type, *arguments, **options)
|
|
24
|
+
gradient = parse_gradient_arguments(*arguments, **options)
|
|
25
|
+
patterns = page.resources[:Pattern] ||= {}
|
|
26
|
+
name = "SP#{gradient_registry_key(gradient)}"
|
|
27
|
+
patterns[name] ||= (@gradient_registry ||= {})[name] ||= create_gradient_pattern(gradient)
|
|
28
|
+
operator = Color::OPERATOR_BY_TYPE.fetch(type) { raise ArgumentError, "unknown type '#{type}'" }
|
|
29
|
+
set_color_space(type, :Pattern)
|
|
30
|
+
add_content("/#{name} #{operator}")
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
def parse_gradient_arguments(*arguments, from: nil, to: nil, r1: nil, r2: nil, stops: nil, apply_transformations: nil)
|
|
34
|
+
case arguments.length
|
|
35
|
+
when 0 then apply_transformations = true if apply_transformations.nil?
|
|
36
|
+
when 4 then from, to, *stops = arguments
|
|
37
|
+
when 6 then from, r1, to, r2, *stops = arguments
|
|
38
|
+
else raise ArgumentError, "Unknown type of gradient: #{arguments.inspect}"
|
|
39
|
+
end
|
|
40
|
+
raise ArgumentError, "At least two stops must be specified" if stops.length < 2
|
|
41
|
+
|
|
42
|
+
stops = stops.each_with_index.map do |stop, index|
|
|
43
|
+
if stop.is_a?(Array) || stop.is_a?(Hash)
|
|
44
|
+
position, color = stop
|
|
45
|
+
else
|
|
46
|
+
position = index / (Float(stops.length) - 1)
|
|
47
|
+
color = stop
|
|
48
|
+
end
|
|
49
|
+
raise ArgumentError, "position must be between 0 and 1" unless (0..1).cover?(position)
|
|
50
|
+
|
|
51
|
+
GradientStop.new(position, normalize_color(color))
|
|
52
|
+
end
|
|
53
|
+
raise ArgumentError, "The first stop must have a position of 0" if stops.first.position != 0
|
|
54
|
+
raise ArgumentError, "The last stop must have a position of 1" if stops.last.position != 1
|
|
55
|
+
raise ArgumentError, "All colors must be of the same color space" if stops.map { color_type(it.color) }.uniq.length > 1
|
|
56
|
+
|
|
57
|
+
Gradient.new(r1 ? :radial : :axial, apply_transformations, stops, from, to, r1, r2)
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
def gradient_registry_key(gradient)
|
|
61
|
+
_x1, _y1, x2, y2, transformation = gradient_coordinates(gradient)
|
|
62
|
+
key = [gradient.type.to_s, transformation, x2, y2, gradient.r1 || -1, gradient.r2 || -1, gradient.stops.length,
|
|
63
|
+
gradient.stops.map { [it.position, it.color] }].flatten
|
|
64
|
+
Digest::SHA1.hexdigest(key.join(","))
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
def create_gradient_pattern(gradient)
|
|
68
|
+
if gradient.apply_transformations.nil? && current_transformation_matrix_with_translation(0, 0) != IDENTITY_MATRIX
|
|
69
|
+
warn "Gradients in Prawn 2.x and lower are not correctly positioned when a transformation has been made to the " \
|
|
70
|
+
"document. Pass 'apply_transformations: true' to correctly transform the gradient, or see " \
|
|
71
|
+
"https://github.com/prawnpdf/prawn/wiki/Gradient-Transformations for more information."
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
functions = gradient.stops.each_cons(2).map do |first, second|
|
|
75
|
+
ref!(FunctionType: 2, Domain: [0.0, 1.0], C0: first.color, C1: second.color, N: 1.0)
|
|
76
|
+
end
|
|
77
|
+
function =
|
|
78
|
+
if functions.length == 1
|
|
79
|
+
functions.first
|
|
80
|
+
else
|
|
81
|
+
ref!(FunctionType: 3, Domain: [0.0, 1.0], Functions: functions, Bounds: gradient.stops[1..-2].map(&:position),
|
|
82
|
+
Encode: [0.0, 1.0] * functions.length)
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
x1, y1, x2, y2, transformation = gradient_coordinates(gradient)
|
|
86
|
+
coordinates = gradient.type == :axial ? [0, 0, x2 - x1, y2 - y1] : [0, 0, gradient.r1, x2 - x1, y2 - y1, gradient.r2]
|
|
87
|
+
shading = ref!(ShadingType: gradient.type == :axial ? 2 : 3, ColorSpace: color_space(gradient.stops.first.color),
|
|
88
|
+
Coords: coordinates, Function: function, Extend: [true, true])
|
|
89
|
+
ref!(PatternType: 2, Shading: shading, Matrix: transformation)
|
|
90
|
+
end
|
|
91
|
+
|
|
92
|
+
def gradient_coordinates(gradient)
|
|
93
|
+
x1, y1 = map_to_absolute(gradient.from)
|
|
94
|
+
x2, y2 = map_to_absolute(gradient.to)
|
|
95
|
+
transformation = gradient.apply_transformations ? current_transformation_matrix_with_translation(x1, y1) : [1, 0, 0, 1, x1, y1]
|
|
96
|
+
[x1, y1, x2, y2, transformation]
|
|
97
|
+
end
|
|
98
|
+
end
|
|
99
|
+
end
|
|
100
|
+
end
|
|
@@ -0,0 +1,171 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module PrawnX
|
|
4
|
+
module Graphics
|
|
5
|
+
module Dash
|
|
6
|
+
def dash(length = nil, options = {})
|
|
7
|
+
return current_dash_state if length.nil?
|
|
8
|
+
|
|
9
|
+
length = Array(length)
|
|
10
|
+
raise ArgumentError, "Zero length dashes are invalid. Call #undash to disable dashes." if length.all?(&:zero?)
|
|
11
|
+
raise ArgumentError, "Negative numbers are not allowed for dash lengths." if length.any?(&:negative?)
|
|
12
|
+
|
|
13
|
+
length = length.first if length.length == 1
|
|
14
|
+
self.current_dash_state = {dash: length, space: length.is_a?(Array) ? nil : options[:space] || length, phase: options[:phase] || 0}
|
|
15
|
+
write_stroke_dash
|
|
16
|
+
end
|
|
17
|
+
alias dash= dash
|
|
18
|
+
|
|
19
|
+
def undash
|
|
20
|
+
self.current_dash_state = GraphicState::UNDASHED.dup
|
|
21
|
+
write_stroke_dash
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
def dashed?
|
|
25
|
+
current_dash_state != GraphicState::UNDASHED
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
private
|
|
29
|
+
|
|
30
|
+
def write_stroke_dash
|
|
31
|
+
add_content(graphic_state.dash_setting)
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
def current_dash_state=(dash_options)
|
|
35
|
+
graphic_state.dash = dash_options
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
def current_dash_state
|
|
39
|
+
graphic_state.dash
|
|
40
|
+
end
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
module CapStyle
|
|
44
|
+
CAP_STYLES = {butt: 0, round: 1, projecting_square: 2}.freeze
|
|
45
|
+
|
|
46
|
+
def cap_style(style = nil)
|
|
47
|
+
return graphic_state.cap_style || :butt if style.nil?
|
|
48
|
+
|
|
49
|
+
graphic_state.cap_style = style
|
|
50
|
+
write_stroke_cap_style
|
|
51
|
+
end
|
|
52
|
+
alias cap_style= cap_style
|
|
53
|
+
|
|
54
|
+
private
|
|
55
|
+
|
|
56
|
+
def write_stroke_cap_style
|
|
57
|
+
add_content("#{CAP_STYLES[graphic_state.cap_style]} J")
|
|
58
|
+
end
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
module JoinStyle
|
|
62
|
+
JOIN_STYLES = {miter: 0, round: 1, bevel: 2}.freeze
|
|
63
|
+
|
|
64
|
+
def join_style(style = nil)
|
|
65
|
+
return graphic_state.join_style || :miter if style.nil?
|
|
66
|
+
|
|
67
|
+
graphic_state.join_style = style
|
|
68
|
+
unless JOIN_STYLES.key?(style)
|
|
69
|
+
raise Errors::InvalidJoinStyle, "#{style} is not a recognized join style. Valid styles are #{JOIN_STYLES.keys.join(", ")}"
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
write_stroke_join_style
|
|
73
|
+
end
|
|
74
|
+
alias join_style= join_style
|
|
75
|
+
|
|
76
|
+
private
|
|
77
|
+
|
|
78
|
+
def write_stroke_join_style
|
|
79
|
+
add_content("#{JOIN_STYLES[graphic_state.join_style]} j")
|
|
80
|
+
end
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
module Transparency
|
|
84
|
+
def transparent(opacity, stroke_opacity = opacity)
|
|
85
|
+
min_version(1.4)
|
|
86
|
+
opacity = opacity.clamp(0.0, 1.0)
|
|
87
|
+
stroke_opacity = stroke_opacity.clamp(0.0, 1.0)
|
|
88
|
+
save_graphics_state
|
|
89
|
+
add_content("/#{opacity_dictionary_name(opacity, stroke_opacity)} gs")
|
|
90
|
+
yield
|
|
91
|
+
restore_graphics_state
|
|
92
|
+
end
|
|
93
|
+
|
|
94
|
+
private
|
|
95
|
+
|
|
96
|
+
def opacity_dictionary_name(opacity, stroke_opacity)
|
|
97
|
+
@opacity_dictionary_registry ||= {}
|
|
98
|
+
entry = @opacity_dictionary_registry["#{opacity}_#{stroke_opacity}"] ||=
|
|
99
|
+
{name: "Tr#{@opacity_dictionary_registry.length + 1}", reference: ref!(Type: :ExtGState, CA: stroke_opacity, ca: opacity)}
|
|
100
|
+
page.ext_gstates[entry[:name]] = entry[:reference]
|
|
101
|
+
entry[:name]
|
|
102
|
+
end
|
|
103
|
+
end
|
|
104
|
+
|
|
105
|
+
module BlendMode
|
|
106
|
+
def blend_mode(blend_mode = :Normal)
|
|
107
|
+
min_version(1.4)
|
|
108
|
+
save_graphics_state if block_given?
|
|
109
|
+
add_content("/#{blend_mode_dictionary_name(blend_mode)} gs")
|
|
110
|
+
return unless block_given?
|
|
111
|
+
|
|
112
|
+
yield
|
|
113
|
+
restore_graphics_state
|
|
114
|
+
end
|
|
115
|
+
|
|
116
|
+
private
|
|
117
|
+
|
|
118
|
+
def blend_mode_dictionary_name(blend_mode)
|
|
119
|
+
@blend_mode_dictionary_registry ||= {}
|
|
120
|
+
name = "BM#{Array(blend_mode).join}"
|
|
121
|
+
page.ext_gstates[name] = @blend_mode_dictionary_registry[name] ||= ref!(Type: :ExtGState, BM: blend_mode)
|
|
122
|
+
name
|
|
123
|
+
end
|
|
124
|
+
end
|
|
125
|
+
|
|
126
|
+
module Transformation
|
|
127
|
+
def rotate(angle, options = {}, &block)
|
|
128
|
+
PrawnX.verify_options(:origin, options)
|
|
129
|
+
radians = degree_to_rad(angle)
|
|
130
|
+
cos = Math.cos(radians)
|
|
131
|
+
sin = Math.sin(radians)
|
|
132
|
+
return transformation_matrix(cos, sin, -sin, cos, 0, 0, &block) if options[:origin].nil?
|
|
133
|
+
raise Errors::BlockRequired unless block
|
|
134
|
+
|
|
135
|
+
x = options[:origin][0] + bounds.absolute_left
|
|
136
|
+
y = options[:origin][1] + bounds.absolute_bottom
|
|
137
|
+
translate(x - ((x * cos) - (y * sin)), y - ((x * sin) + (y * cos))) do
|
|
138
|
+
transformation_matrix(cos, sin, -sin, cos, 0, 0, &block)
|
|
139
|
+
end
|
|
140
|
+
end
|
|
141
|
+
|
|
142
|
+
def translate(x, y, &)
|
|
143
|
+
transformation_matrix(1, 0, 0, 1, x, y, &)
|
|
144
|
+
end
|
|
145
|
+
|
|
146
|
+
def scale(factor, options = {}, &block)
|
|
147
|
+
PrawnX.verify_options(:origin, options)
|
|
148
|
+
return transformation_matrix(factor, 0, 0, factor, 0, 0, &block) if options[:origin].nil?
|
|
149
|
+
raise Errors::BlockRequired unless block
|
|
150
|
+
|
|
151
|
+
x = options[:origin][0] + bounds.absolute_left
|
|
152
|
+
y = options[:origin][1] + bounds.absolute_bottom
|
|
153
|
+
translate(x - (factor * x), y - (factor * y)) do
|
|
154
|
+
transformation_matrix(factor, 0, 0, factor, 0, 0, &block)
|
|
155
|
+
end
|
|
156
|
+
end
|
|
157
|
+
|
|
158
|
+
def transformation_matrix(*matrix)
|
|
159
|
+
raise ArgumentError, "Transformation matrix must have exacty 6 elements" if matrix.length != 6
|
|
160
|
+
|
|
161
|
+
save_graphics_state if block_given?
|
|
162
|
+
add_to_transformation_stack(*matrix)
|
|
163
|
+
add_content("#{PDF.real_params(matrix)} cm")
|
|
164
|
+
return unless block_given?
|
|
165
|
+
|
|
166
|
+
yield
|
|
167
|
+
restore_graphics_state
|
|
168
|
+
end
|
|
169
|
+
end
|
|
170
|
+
end
|
|
171
|
+
end
|
|
@@ -0,0 +1,208 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module PrawnX
|
|
4
|
+
module Graphics
|
|
5
|
+
include BlendMode
|
|
6
|
+
include Color
|
|
7
|
+
include Dash
|
|
8
|
+
include CapStyle
|
|
9
|
+
include JoinStyle
|
|
10
|
+
include Transparency
|
|
11
|
+
include Transformation
|
|
12
|
+
include Patterns
|
|
13
|
+
|
|
14
|
+
# Distance of a cubic Bézier control point approximating a quarter circle of radius 1.
|
|
15
|
+
KAPPA = 4.0 * ((Math.sqrt(2) - 1.0) / 3.0)
|
|
16
|
+
|
|
17
|
+
def move_to(*point)
|
|
18
|
+
add_content("#{PDF.real_params(map_to_absolute(point))} m")
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
def line_to(*point)
|
|
22
|
+
add_content("#{PDF.real_params(map_to_absolute(point))} l")
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
def curve_to(destination, options = {})
|
|
26
|
+
options[:bounds] || raise(Errors::InvalidGraphicsPath, "Bounding points for bezier curve must be specified as :bounds => [[x1,y1],[x2,y2]]")
|
|
27
|
+
|
|
28
|
+
add_content("#{PDF.real_params((options[:bounds] << destination).flat_map { map_to_absolute(it) })} c")
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
def rectangle(point, width, height)
|
|
32
|
+
x, y = map_to_absolute(point)
|
|
33
|
+
add_content("#{PDF.real_params([x, y - height, width, height])} re")
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
def rounded_rectangle(point, width, height, radius)
|
|
37
|
+
x, y = point
|
|
38
|
+
rounded_polygon(radius, point, [x + width, y], [x + width, y - height], [x, y - height])
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
def line_width=(width)
|
|
42
|
+
graphic_state.line_width = width
|
|
43
|
+
write_line_width
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
def line_width(width = nil)
|
|
47
|
+
return graphic_state.line_width unless width
|
|
48
|
+
|
|
49
|
+
self.line_width = width
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
def line(*points)
|
|
53
|
+
x0, y0, x1, y1 = points.flatten
|
|
54
|
+
move_to(x0, y0)
|
|
55
|
+
line_to(x1, y1)
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
def horizontal_line(x1, x2, options = {})
|
|
59
|
+
y1 = options[:at] || (y - bounds.absolute_bottom)
|
|
60
|
+
line(x1, y1, x2, y1)
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
def horizontal_rule
|
|
64
|
+
horizontal_line(bounds.left, bounds.right)
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
def vertical_line(y1, y2, params)
|
|
68
|
+
line(params[:at], y1, params[:at], y2)
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
def curve(origin, destination, options = {})
|
|
72
|
+
move_to(*origin)
|
|
73
|
+
curve_to(destination, options)
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
def circle(center, radius)
|
|
77
|
+
ellipse(center, radius, radius)
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
def ellipse(point, radius1, radius2 = radius1)
|
|
81
|
+
x, y = point
|
|
82
|
+
l1 = radius1 * KAPPA
|
|
83
|
+
l2 = radius2 * KAPPA
|
|
84
|
+
move_to(x + radius1, y)
|
|
85
|
+
curve_to([x, y + radius2], bounds: [[x + radius1, y + l2], [x + l1, y + radius2]])
|
|
86
|
+
curve_to([x - radius1, y], bounds: [[x - l1, y + radius2], [x - radius1, y + l2]])
|
|
87
|
+
curve_to([x, y - radius2], bounds: [[x - radius1, y - l2], [x - l1, y - radius2]])
|
|
88
|
+
curve_to([x + radius1, y], bounds: [[x + l1, y - radius2], [x + radius1, y - l2]])
|
|
89
|
+
move_to(x, y)
|
|
90
|
+
end
|
|
91
|
+
|
|
92
|
+
def polygon(*points)
|
|
93
|
+
move_to(points[0])
|
|
94
|
+
(points[1..] << points[0]).each { line_to(*it) }
|
|
95
|
+
add_content("h")
|
|
96
|
+
end
|
|
97
|
+
|
|
98
|
+
def rounded_polygon(radius, *points)
|
|
99
|
+
move_to(point_on_line(radius, points[1], points[0]))
|
|
100
|
+
sides = points.size
|
|
101
|
+
points << points[0] << points[1]
|
|
102
|
+
sides.times { rounded_vertex(radius, points[it], points[it + 1], points[it + 2]) }
|
|
103
|
+
add_content("h")
|
|
104
|
+
end
|
|
105
|
+
|
|
106
|
+
def rounded_vertex(radius, *points)
|
|
107
|
+
radial_point1 = point_on_line(radius, points[0], points[1])
|
|
108
|
+
bezier_point1 = point_on_line(radius - (radius * KAPPA), points[0], points[1])
|
|
109
|
+
radial_point2 = point_on_line(radius, points[2], points[1])
|
|
110
|
+
bezier_point2 = point_on_line(radius - (radius * KAPPA), points[2], points[1])
|
|
111
|
+
line_to(radial_point1)
|
|
112
|
+
curve_to(radial_point2, bounds: [bezier_point1, bezier_point2])
|
|
113
|
+
end
|
|
114
|
+
|
|
115
|
+
def stroke
|
|
116
|
+
yield if block_given?
|
|
117
|
+
add_content("S")
|
|
118
|
+
end
|
|
119
|
+
|
|
120
|
+
def close_and_stroke
|
|
121
|
+
yield if block_given?
|
|
122
|
+
add_content("s")
|
|
123
|
+
end
|
|
124
|
+
|
|
125
|
+
def stroke_bounds
|
|
126
|
+
stroke_rectangle(bounds.top_left, bounds.width, bounds.height)
|
|
127
|
+
end
|
|
128
|
+
|
|
129
|
+
def stroke_axis(options = {})
|
|
130
|
+
origin = options[:at] || [0, 0]
|
|
131
|
+
options = {
|
|
132
|
+
at: [0, 0], height: bounds.height - origin[1], width: bounds.width - origin[0],
|
|
133
|
+
step_length: 100, negative_axes_length: 20, color: "000000",
|
|
134
|
+
}.merge(options)
|
|
135
|
+
PrawnX.verify_options(%i[at width height step_length negative_axes_length color], options)
|
|
136
|
+
x, y = options[:at]
|
|
137
|
+
|
|
138
|
+
save_graphics_state do
|
|
139
|
+
fill_color(options[:color])
|
|
140
|
+
stroke_color(options[:color])
|
|
141
|
+
dash(1, space: 4)
|
|
142
|
+
stroke_horizontal_line(x - options[:negative_axes_length], x + options[:width], at: y)
|
|
143
|
+
stroke_vertical_line(y - options[:negative_axes_length], y + options[:height], at: x)
|
|
144
|
+
undash
|
|
145
|
+
fill_circle(options[:at], 1)
|
|
146
|
+
(options[:step_length]..options[:width]).step(options[:step_length]) do |point|
|
|
147
|
+
fill_circle([x + point, y], 1)
|
|
148
|
+
draw_text(point, at: [x + point - 5, y - 10], size: 7)
|
|
149
|
+
end
|
|
150
|
+
(options[:step_length]..options[:height]).step(options[:step_length]) do |point|
|
|
151
|
+
fill_circle([x, y + point], 1)
|
|
152
|
+
draw_text(point, at: [x - 17, y + point - 2], size: 7)
|
|
153
|
+
end
|
|
154
|
+
end
|
|
155
|
+
end
|
|
156
|
+
|
|
157
|
+
def fill(options = {})
|
|
158
|
+
yield if block_given?
|
|
159
|
+
add_content(options[:fill_rule] == :even_odd ? "f*" : "f")
|
|
160
|
+
end
|
|
161
|
+
|
|
162
|
+
def fill_and_stroke(options = {})
|
|
163
|
+
yield if block_given?
|
|
164
|
+
add_content(options[:fill_rule] == :even_odd ? "b*" : "b")
|
|
165
|
+
end
|
|
166
|
+
|
|
167
|
+
def close_path
|
|
168
|
+
add_content("h")
|
|
169
|
+
end
|
|
170
|
+
|
|
171
|
+
SHAPES = %i[
|
|
172
|
+
line_to curve_to rectangle rounded_rectangle line horizontal_line horizontal_rule vertical_line curve circle_at circle
|
|
173
|
+
ellipse_at ellipse polygon rounded_polygon rounded_vertex
|
|
174
|
+
].freeze
|
|
175
|
+
|
|
176
|
+
%i[fill stroke fill_and_stroke].product(SHAPES).each do |operation, shape|
|
|
177
|
+
define_method(:"#{operation}_#{shape}") do |*arguments|
|
|
178
|
+
public_send(operation) { public_send(shape, *arguments) }
|
|
179
|
+
end
|
|
180
|
+
end
|
|
181
|
+
|
|
182
|
+
private
|
|
183
|
+
|
|
184
|
+
def write_line_width
|
|
185
|
+
add_content("#{graphic_state.line_width} w")
|
|
186
|
+
end
|
|
187
|
+
|
|
188
|
+
def map_to_absolute(*point)
|
|
189
|
+
x, y = point.flatten
|
|
190
|
+
[@bounding_box.absolute_left + x, @bounding_box.absolute_bottom + y]
|
|
191
|
+
end
|
|
192
|
+
|
|
193
|
+
def map_to_absolute!(point)
|
|
194
|
+
point.replace(map_to_absolute(point))
|
|
195
|
+
end
|
|
196
|
+
|
|
197
|
+
def degree_to_rad(angle)
|
|
198
|
+
angle * Math::PI / 180
|
|
199
|
+
end
|
|
200
|
+
|
|
201
|
+
def point_on_line(distance_from_end, *points)
|
|
202
|
+
x0, y0, x1, y1 = points.flatten
|
|
203
|
+
length = Math.sqrt(((x1 - x0)**2) + ((y1 - y0)**2))
|
|
204
|
+
ratio = (length - distance_from_end) / length
|
|
205
|
+
[x0 + (ratio * (x1 - x0)), y0 + (ratio * (y1 - y0))]
|
|
206
|
+
end
|
|
207
|
+
end
|
|
208
|
+
end
|