gamba 2.0.0

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.
Files changed (163) hide show
  1. checksums.yaml +7 -0
  2. data/CHANGELOG.md +80 -0
  3. data/COPYING +2 -0
  4. data/GPLv2 +339 -0
  5. data/GPLv3 +674 -0
  6. data/LICENSE +65 -0
  7. data/LICENSE-prawn-svg +21 -0
  8. data/README.md +148 -0
  9. data/data/fonts/Courier-Bold.afm +342 -0
  10. data/data/fonts/Courier-BoldOblique.afm +342 -0
  11. data/data/fonts/Courier-Oblique.afm +342 -0
  12. data/data/fonts/Courier.afm +342 -0
  13. data/data/fonts/Helvetica-Bold.afm +2827 -0
  14. data/data/fonts/Helvetica-BoldOblique.afm +2827 -0
  15. data/data/fonts/Helvetica-Oblique.afm +3051 -0
  16. data/data/fonts/Helvetica.afm +3051 -0
  17. data/data/fonts/MustRead.html +19 -0
  18. data/data/fonts/Symbol.afm +213 -0
  19. data/data/fonts/Times-Bold.afm +2588 -0
  20. data/data/fonts/Times-BoldItalic.afm +2384 -0
  21. data/data/fonts/Times-Italic.afm +2667 -0
  22. data/data/fonts/Times-Roman.afm +2419 -0
  23. data/data/fonts/ZapfDingbats.afm +225 -0
  24. data/lib/gamba/annotations.rb +21 -0
  25. data/lib/gamba/combine.rb +307 -0
  26. data/lib/gamba/document/bounding_box.rb +193 -0
  27. data/lib/gamba/document/column_box.rb +91 -0
  28. data/lib/gamba/document/internals.rb +264 -0
  29. data/lib/gamba/document/span.rb +25 -0
  30. data/lib/gamba/document.rb +268 -0
  31. data/lib/gamba/encoding.rb +13 -0
  32. data/lib/gamba/errors.rb +39 -0
  33. data/lib/gamba/font.rb +228 -0
  34. data/lib/gamba/fonts/afm.rb +171 -0
  35. data/lib/gamba/fonts/otf.rb +50 -0
  36. data/lib/gamba/fonts/ttf.rb +413 -0
  37. data/lib/gamba/graphic_state.rb +84 -0
  38. data/lib/gamba/graphics/color.rb +174 -0
  39. data/lib/gamba/graphics/patterns.rb +100 -0
  40. data/lib/gamba/graphics/style.rb +159 -0
  41. data/lib/gamba/graphics.rb +212 -0
  42. data/lib/gamba/grid.rb +216 -0
  43. data/lib/gamba/images/image.rb +271 -0
  44. data/lib/gamba/images/png_decoder.rb +65 -0
  45. data/lib/gamba/images.rb +115 -0
  46. data/lib/gamba/measurement_extensions.rb +40 -0
  47. data/lib/gamba/measurements.rb +62 -0
  48. data/lib/gamba/outline.rb +148 -0
  49. data/lib/gamba/page.rb +125 -0
  50. data/lib/gamba/page_geometry.rb +27 -0
  51. data/lib/gamba/pdf/decryption.rb +165 -0
  52. data/lib/gamba/pdf/outline_node.rb +53 -0
  53. data/lib/gamba/pdf/reader.rb +616 -0
  54. data/lib/gamba/pdf/reference.rb +31 -0
  55. data/lib/gamba/pdf/writer.rb +105 -0
  56. data/lib/gamba/pdf.rb +87 -0
  57. data/lib/gamba/pdf_a/srgb_profile.rb +61 -0
  58. data/lib/gamba/pdf_a.rb +132 -0
  59. data/lib/gamba/repeater.rb +79 -0
  60. data/lib/gamba/security.rb +111 -0
  61. data/lib/gamba/stamp.rb +76 -0
  62. data/lib/gamba/table/cell/kinds.rb +246 -0
  63. data/lib/gamba/table/cell.rb +331 -0
  64. data/lib/gamba/table/cells.rb +208 -0
  65. data/lib/gamba/table.rb +298 -0
  66. data/lib/gamba/text/box.rb +16 -0
  67. data/lib/gamba/text/formatted/arranger.rb +217 -0
  68. data/lib/gamba/text/formatted/box.rb +312 -0
  69. data/lib/gamba/text/formatted/fragment.rb +205 -0
  70. data/lib/gamba/text/formatted/line_wrap.rb +205 -0
  71. data/lib/gamba/text/formatted/parser.rb +150 -0
  72. data/lib/gamba/text/formatted/wrap.rb +103 -0
  73. data/lib/gamba/text.rb +310 -0
  74. data/lib/gamba/transformation_stack.rb +43 -0
  75. data/lib/gamba/true_type/cff_subset.rb +157 -0
  76. data/lib/gamba/true_type/file.rb +372 -0
  77. data/lib/gamba/true_type/open_type_layout.rb +189 -0
  78. data/lib/gamba/true_type/simple_subsets.rb +108 -0
  79. data/lib/gamba/true_type/subset.rb +206 -0
  80. data/lib/gamba/version.rb +10 -0
  81. data/lib/gamba/view.rb +28 -0
  82. data/lib/gamba.rb +124 -0
  83. data/lib/pdf/core.rb +4 -0
  84. data/lib/prawn/measurement_extensions.rb +4 -0
  85. data/lib/prawn/svg/attributes/clip_path.rb +46 -0
  86. data/lib/prawn/svg/attributes/mask.rb +16 -0
  87. data/lib/prawn/svg/attributes/opacity.rb +30 -0
  88. data/lib/prawn/svg/attributes/space.rb +10 -0
  89. data/lib/prawn/svg/attributes/stroke.rb +50 -0
  90. data/lib/prawn/svg/attributes/transform.rb +10 -0
  91. data/lib/prawn/svg/attributes.rb +6 -0
  92. data/lib/prawn/svg/calculators/arc_to_bezier_curve.rb +57 -0
  93. data/lib/prawn/svg/calculators/aspect_ratio.rb +60 -0
  94. data/lib/prawn/svg/calculators/document_sizing.rb +103 -0
  95. data/lib/prawn/svg/calculators/path_length.rb +168 -0
  96. data/lib/prawn/svg/calculators/pixels.rb +57 -0
  97. data/lib/prawn/svg/color.rb +240 -0
  98. data/lib/prawn/svg/css/font_face_parser.rb +72 -0
  99. data/lib/prawn/svg/css/font_family_parser.rb +39 -0
  100. data/lib/prawn/svg/css/font_parser.rb +46 -0
  101. data/lib/prawn/svg/css/selector_parser.rb +180 -0
  102. data/lib/prawn/svg/css/stylesheets.rb +233 -0
  103. data/lib/prawn/svg/css/values_parser.rb +68 -0
  104. data/lib/prawn/svg/document.rb +169 -0
  105. data/lib/prawn/svg/elements/anchor.rb +10 -0
  106. data/lib/prawn/svg/elements/base.rb +315 -0
  107. data/lib/prawn/svg/elements/bbox_scaling.rb +78 -0
  108. data/lib/prawn/svg/elements/call_duplicator.rb +37 -0
  109. data/lib/prawn/svg/elements/circle.rb +19 -0
  110. data/lib/prawn/svg/elements/clip_path.rb +26 -0
  111. data/lib/prawn/svg/elements/container.rb +10 -0
  112. data/lib/prawn/svg/elements/direct_render_base.rb +28 -0
  113. data/lib/prawn/svg/elements/ellipse.rb +20 -0
  114. data/lib/prawn/svg/elements/gradient.rb +183 -0
  115. data/lib/prawn/svg/elements/ignored.rb +5 -0
  116. data/lib/prawn/svg/elements/image.rb +178 -0
  117. data/lib/prawn/svg/elements/line.rb +27 -0
  118. data/lib/prawn/svg/elements/marker.rb +72 -0
  119. data/lib/prawn/svg/elements/mask.rb +65 -0
  120. data/lib/prawn/svg/elements/path.rb +294 -0
  121. data/lib/prawn/svg/elements/pattern.rb +150 -0
  122. data/lib/prawn/svg/elements/polygon.rb +25 -0
  123. data/lib/prawn/svg/elements/polyline.rb +23 -0
  124. data/lib/prawn/svg/elements/rect.rb +64 -0
  125. data/lib/prawn/svg/elements/root.rb +22 -0
  126. data/lib/prawn/svg/elements/switch.rb +92 -0
  127. data/lib/prawn/svg/elements/text.rb +99 -0
  128. data/lib/prawn/svg/elements/text_component.rb +352 -0
  129. data/lib/prawn/svg/elements/text_node.rb +308 -0
  130. data/lib/prawn/svg/elements/text_path.rb +153 -0
  131. data/lib/prawn/svg/elements/use.rb +142 -0
  132. data/lib/prawn/svg/elements/viewport.rb +29 -0
  133. data/lib/prawn/svg/elements.rb +46 -0
  134. data/lib/prawn/svg/extension.rb +25 -0
  135. data/lib/prawn/svg/font.rb +20 -0
  136. data/lib/prawn/svg/font_metrics.rb +144 -0
  137. data/lib/prawn/svg/font_registry.rb +315 -0
  138. data/lib/prawn/svg/funciri.rb +14 -0
  139. data/lib/prawn/svg/gradient_renderer.rb +332 -0
  140. data/lib/prawn/svg/gradients.rb +53 -0
  141. data/lib/prawn/svg/interface.rb +72 -0
  142. data/lib/prawn/svg/length.rb +43 -0
  143. data/lib/prawn/svg/link_renderer.rb +40 -0
  144. data/lib/prawn/svg/loaders/data.rb +17 -0
  145. data/lib/prawn/svg/loaders/file.rb +111 -0
  146. data/lib/prawn/svg/loaders/web.rb +24 -0
  147. data/lib/prawn/svg/paint.rb +67 -0
  148. data/lib/prawn/svg/pathable.rb +153 -0
  149. data/lib/prawn/svg/pattern_renderer.rb +112 -0
  150. data/lib/prawn/svg/pdfmatrix.rb +39 -0
  151. data/lib/prawn/svg/percentage.rb +24 -0
  152. data/lib/prawn/svg/properties.rb +329 -0
  153. data/lib/prawn/svg/renderer.rb +249 -0
  154. data/lib/prawn/svg/state.rb +24 -0
  155. data/lib/prawn/svg/transform_parser.rb +78 -0
  156. data/lib/prawn/svg/ttc.rb +35 -0
  157. data/lib/prawn/svg/ttf.rb +95 -0
  158. data/lib/prawn/svg/url_loader.rb +46 -0
  159. data/lib/prawn/svg/version.rb +5 -0
  160. data/lib/prawn/table.rb +4 -0
  161. data/lib/prawn-svg.rb +52 -0
  162. data/lib/prawn.rb +3 -0
  163. metadata +263 -0
@@ -0,0 +1,84 @@
1
+ # frozen_string_literal: true
2
+
3
+ module PDF::Core
4
+ # What a PDF `q` saves and `Q` restores, as far as Gamba writes it.
5
+ class GraphicState
6
+ UNDASHED = {dash: nil, space: nil, phase: 0}.freeze
7
+ # The text state parameters Prawn sets, with their defaults, in the order a new page writes them back; each
8
+ # state records them as last written in it.
9
+ DEFAULT_TEXT_STATE = {text_rendering_mode: :fill, character_spacing: 0, word_spacing: 0, horizontal_text_scaling: 100, rise: 0}.freeze
10
+ TEXT_STATE_WRITERS = DEFAULT_TEXT_STATE.to_h { |setting, _default| [setting, :"#{setting}="] }.freeze
11
+
12
+ attr_accessor :color_space, :dash, :cap_style, :join_style, :line_width, :fill_color, :stroke_color,
13
+ *DEFAULT_TEXT_STATE.keys
14
+ # What Prawn's state leaves out: the pattern painting in place of each colour since a gradient was set, and the
15
+ # blend mode's ExtGState, {name:, reference:} each, nil for none and for Normal.
16
+ attr_accessor :fill_pattern, :stroke_pattern, :blend_mode
17
+ # The save that opened this state on its page, or that it was reopened for on a new page; a copy has none.
18
+ attr_accessor :save
19
+
20
+ def initialize(previous_state = nil)
21
+ if previous_state
22
+ @color_space = previous_state.color_space.dup
23
+ @fill_color = previous_state.fill_color.dup
24
+ @stroke_color = previous_state.stroke_color.dup
25
+ @dash = previous_state.dash.dup
26
+ @cap_style = previous_state.cap_style
27
+ @join_style = previous_state.join_style
28
+ @line_width = previous_state.line_width
29
+ @fill_pattern = previous_state.fill_pattern
30
+ @stroke_pattern = previous_state.stroke_pattern
31
+ @blend_mode = previous_state.blend_mode
32
+ copy_text_state(previous_state)
33
+ else
34
+ @color_space = {}
35
+ @fill_color = "000000"
36
+ @stroke_color = "000000"
37
+ @dash = UNDASHED.dup
38
+ @cap_style = :butt
39
+ @join_style = :miter
40
+ @line_width = 1
41
+ reset_text_state
42
+ end
43
+ end
44
+
45
+ # The text state recorded in another state, the default one for nil.
46
+ def copy_text_state(other)
47
+ return reset_text_state unless other
48
+
49
+ @text_rendering_mode = other.text_rendering_mode
50
+ @character_spacing = other.character_spacing
51
+ @word_spacing = other.word_spacing
52
+ @horizontal_text_scaling = other.horizontal_text_scaling
53
+ @rise = other.rise
54
+ end
55
+
56
+ def reset_text_state
57
+ @text_rendering_mode = :fill
58
+ @character_spacing = 0
59
+ @word_spacing = 0
60
+ @horizontal_text_scaling = 100
61
+ @rise = 0
62
+ end
63
+
64
+ # What fills or strokes: the pattern's name, else the colour.
65
+ def fill_paint
66
+ fill_pattern ? fill_pattern[:name] : fill_color
67
+ end
68
+
69
+ def stroke_paint
70
+ stroke_pattern ? stroke_pattern[:name] : stroke_color
71
+ end
72
+
73
+ def dup
74
+ GraphicState.new(self)
75
+ end
76
+
77
+ def dash_setting
78
+ return "[] 0 d" unless dash[:dash]
79
+
80
+ lengths = dash[:dash].is_a?(Array) ? dash[:dash] : [dash[:dash], dash[:space]]
81
+ "[#{PDF::Core.real_params(lengths)}] #{PDF::Core.real(dash[:phase])} d"
82
+ end
83
+ end
84
+ end
@@ -0,0 +1,174 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Prawn
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_method :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_method :stroke_color=, :stroke_color
29
+
30
+ module_function
31
+
32
+ def rgb2hex(rgb)
33
+ rgb.map { format("%02x", _1) }.join
34
+ end
35
+
36
+ def hex2rgb(hex)
37
+ [hex[0..1], hex[2..3], hex[4..5]].map { _1.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 { _1 / 100.0 }
68
+ end
69
+ end
70
+
71
+ def color_to_s(color)
72
+ return PDF::Core.real_params(normalize_color(color)) unless color.is_a?(String)
73
+
74
+ PDF_OPERANDS_BY_HEX_COLOR.fetch(color) do
75
+ operands = PDF::Core.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
+ # A pattern paints until a colour of the same type replaces it. One the page's resources hold is recorded, to
96
+ # paint with on the next page.
97
+ def set_color(type, color, options = {})
98
+ operator = OPERATOR_BY_TYPE.fetch(type) { raise ArgumentError, "unknown type '#{type}'" }
99
+ if options[:pattern]
100
+ reference = page.dictionary.data[:Resources]&.dig(:Pattern, color)
101
+ set_color_space(type, :Pattern)
102
+ add_content("/#{color} #{operator}")
103
+ record_pattern(type, reference && {name: color, reference:})
104
+ else
105
+ set_color_space(type, color_space(color))
106
+ write_color(color_to_s(color), operator)
107
+ state = graphic_state
108
+ (type == :fill) ? state.fill_pattern &&= nil : state.stroke_pattern &&= nil
109
+ end
110
+ end
111
+
112
+ # A pattern, {name:, reference:}, on this page's resources too.
113
+ def write_pattern(type, pattern)
114
+ (page.resources[:Pattern] ||= {})[pattern[:name]] ||= pattern[:reference]
115
+ set_color_space(type, :Pattern)
116
+ add_content("/#{pattern[:name]} #{OPERATOR_BY_TYPE.fetch(type)}")
117
+ record_pattern(type, pattern)
118
+ end
119
+
120
+ def record_pattern(type, pattern)
121
+ (type == :fill) ? graphic_state.fill_pattern = pattern : graphic_state.stroke_pattern = pattern
122
+ end
123
+
124
+ def write_fill_paint
125
+ (pattern = graphic_state.fill_pattern) ? write_pattern(:fill, pattern) : set_fill_color
126
+ end
127
+
128
+ def write_stroke_paint
129
+ (pattern = graphic_state.stroke_pattern) ? write_pattern(:stroke, pattern) : set_stroke_color
130
+ end
131
+
132
+ def set_fill_color(color = nil)
133
+ set_color(:fill, color || current_fill_color)
134
+ end
135
+
136
+ def set_stroke_color(color = nil)
137
+ set_color(:stroke, color || current_stroke_color)
138
+ end
139
+
140
+ def update_colors
141
+ set_fill_color
142
+ set_stroke_color
143
+ end
144
+
145
+ def current_color_space(type)
146
+ graphic_state.color_space[type]
147
+ end
148
+
149
+ def set_current_color_space(color_space, type)
150
+ graphic_state.color_space[type] = color_space
151
+ end
152
+
153
+ def current_fill_color
154
+ graphic_state.fill_color
155
+ end
156
+
157
+ def current_fill_color=(color)
158
+ graphic_state.fill_color = color
159
+ end
160
+
161
+ def current_stroke_color
162
+ graphic_state.stroke_color
163
+ end
164
+
165
+ def current_stroke_color=(color)
166
+ graphic_state.stroke_color = color
167
+ end
168
+
169
+ def write_color(color, operator)
170
+ add_content("#{color} #{operator}")
171
+ end
172
+ end
173
+ end
174
+ end
@@ -0,0 +1,100 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "digest/sha1"
4
+
5
+ module Prawn
6
+ module Graphics
7
+ # Axial and radial gradients, as shading patterns (ISO 32000-1 §8.7.4.5).
8
+ module Patterns
9
+ GradientStop = Data.define(:position, :color)
10
+ Gradient = Data.define(:type, :apply_transformations, :stops, :from, :to, :r1, :r2)
11
+ IDENTITY_MATRIX = [1, 0, 0, 1, 0, 0].freeze
12
+
13
+ def fill_gradient(*, **)
14
+ set_gradient(:fill, *, **)
15
+ end
16
+
17
+ def stroke_gradient(*, **)
18
+ set_gradient(:stroke, *, **)
19
+ end
20
+
21
+ private
22
+
23
+ def set_gradient(type, *, **)
24
+ gradient = parse_gradient_arguments(*, **)
25
+ name = "SP#{gradient_registry_key(gradient)}"
26
+ reference = (@gradient_registry ||= {})[name] ||= create_gradient_pattern(gradient)
27
+ Color::OPERATOR_BY_TYPE.fetch(type) { raise ArgumentError, "unknown type '#{type}'" }
28
+ write_pattern(type, {name:, reference:})
29
+ end
30
+
31
+ def parse_gradient_arguments(*arguments, from: nil, to: nil, r1: nil, r2: nil, stops: nil, apply_transformations: nil)
32
+ case arguments.length
33
+ when 0 then apply_transformations = true if apply_transformations.nil?
34
+ when 4 then from, to, *stops = arguments
35
+ when 6 then from, r1, to, r2, *stops = arguments
36
+ else raise ArgumentError, "Unknown type of gradient: #{arguments.inspect}"
37
+ end
38
+ raise ArgumentError, "At least two stops must be specified" if stops.length < 2
39
+
40
+ stops = stops.each_with_index.map do |stop, index|
41
+ if stop.is_a?(Array) || stop.is_a?(Hash)
42
+ position, color = stop
43
+ else
44
+ position = index / (Float(stops.length) - 1)
45
+ color = stop
46
+ end
47
+ raise ArgumentError, "position must be between 0 and 1" unless (0..1).cover?(position)
48
+
49
+ GradientStop.new(position, normalize_color(color))
50
+ end
51
+ raise ArgumentError, "The first stop must have a position of 0" if stops.first.position != 0
52
+ raise ArgumentError, "The last stop must have a position of 1" if stops.last.position != 1
53
+ color_types = stops.map { color_type(_1.color) }
54
+ color_types.uniq!
55
+ raise ArgumentError, "All colors must be of the same color space" if color_types.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 { |stop| [stop.position, stop.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,159 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Prawn
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_method :dash=, :dash
18
+
19
+ def undash
20
+ self.current_dash_state = PDF::Core::GraphicState::UNDASHED.dup
21
+ write_stroke_dash
22
+ end
23
+
24
+ def dashed?
25
+ current_dash_state != PDF::Core::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_method :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_method :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
+ @opacity_dictionary_registry ||= {}
89
+ ext_gstate = @opacity_dictionary_registry["#{opacity}_#{stroke_opacity}"] ||=
90
+ {name: "Tr#{@opacity_dictionary_registry.length + 1}", reference: ref!(Type: :ExtGState, CA: stroke_opacity, ca: opacity)}
91
+ ext_gstate_block(ext_gstate) { yield }
92
+ end
93
+ end
94
+
95
+ module BlendMode
96
+ def blend_mode(blend_mode = :Normal)
97
+ min_version(1.4)
98
+ ext_gstate = blend_mode_ext_gstate(blend_mode)
99
+ return ext_gstate_block(ext_gstate) { yield } if block_given?
100
+
101
+ write_ext_gstate(ext_gstate)
102
+ nil
103
+ end
104
+
105
+ private
106
+
107
+ def blend_mode_ext_gstate(blend_mode)
108
+ @blend_mode_dictionary_registry ||= {}
109
+ name = "BM#{Array(blend_mode).join}"
110
+ @blend_mode_dictionary_registry[name] ||= {name:, reference: ref!(Type: :ExtGState, BM: blend_mode)}
111
+ end
112
+ end
113
+
114
+ module Transformation
115
+ def rotate(angle, options = {}, &block)
116
+ Prawn.verify_options(:origin, options)
117
+ radians = degree_to_rad(angle)
118
+ cos = Math.cos(radians)
119
+ sin = Math.sin(radians)
120
+ return transformation_matrix(cos, sin, -sin, cos, 0, 0, &block) if options[:origin].nil?
121
+ raise Errors::BlockRequired unless block
122
+
123
+ x = options[:origin][0] + bounds.absolute_left
124
+ y = options[:origin][1] + bounds.absolute_bottom
125
+ translate(x - ((x * cos) - (y * sin)), y - ((x * sin) + (y * cos))) do
126
+ transformation_matrix(cos, sin, -sin, cos, 0, 0, &block)
127
+ end
128
+ end
129
+
130
+ def translate(x, y, &)
131
+ transformation_matrix(1, 0, 0, 1, x, y, &)
132
+ end
133
+
134
+ def scale(factor, options = {}, &block)
135
+ Prawn.verify_options(:origin, options)
136
+ return transformation_matrix(factor, 0, 0, factor, 0, 0, &block) if options[:origin].nil?
137
+ raise Errors::BlockRequired unless block
138
+
139
+ x = options[:origin][0] + bounds.absolute_left
140
+ y = options[:origin][1] + bounds.absolute_bottom
141
+ translate(x - (factor * x), y - (factor * y)) do
142
+ transformation_matrix(factor, 0, 0, factor, 0, 0, &block)
143
+ end
144
+ end
145
+
146
+ def transformation_matrix(*matrix)
147
+ raise ArgumentError, "Transformation matrix must have exacty 6 elements" if matrix.length != 6
148
+
149
+ save_graphics_state if block_given?
150
+ add_to_transformation_stack(*matrix)
151
+ add_content("#{PDF::Core.real_params(matrix)} cm")
152
+ return unless block_given?
153
+
154
+ yield
155
+ restore_graphics_state
156
+ end
157
+ end
158
+ end
159
+ end