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.
Files changed (160) hide show
  1. checksums.yaml +7 -0
  2. data/COPYING +2 -0
  3. data/GPLv2 +339 -0
  4. data/GPLv3 +674 -0
  5. data/LICENSE +65 -0
  6. data/LICENSE-prawn-svg +21 -0
  7. data/README.md +107 -0
  8. data/data/fonts/Courier-Bold.afm +342 -0
  9. data/data/fonts/Courier-BoldOblique.afm +342 -0
  10. data/data/fonts/Courier-Oblique.afm +342 -0
  11. data/data/fonts/Courier.afm +342 -0
  12. data/data/fonts/Helvetica-Bold.afm +2827 -0
  13. data/data/fonts/Helvetica-BoldOblique.afm +2827 -0
  14. data/data/fonts/Helvetica-Oblique.afm +3051 -0
  15. data/data/fonts/Helvetica.afm +3051 -0
  16. data/data/fonts/MustRead.html +19 -0
  17. data/data/fonts/Symbol.afm +213 -0
  18. data/data/fonts/Times-Bold.afm +2588 -0
  19. data/data/fonts/Times-BoldItalic.afm +2384 -0
  20. data/data/fonts/Times-Italic.afm +2667 -0
  21. data/data/fonts/Times-Roman.afm +2419 -0
  22. data/data/fonts/ZapfDingbats.afm +225 -0
  23. data/lib/pdf/core.rb +4 -0
  24. data/lib/prawn/measurement_extensions.rb +4 -0
  25. data/lib/prawn/svg/attributes/clip_path.rb +46 -0
  26. data/lib/prawn/svg/attributes/mask.rb +16 -0
  27. data/lib/prawn/svg/attributes/opacity.rb +30 -0
  28. data/lib/prawn/svg/attributes/space.rb +10 -0
  29. data/lib/prawn/svg/attributes/stroke.rb +50 -0
  30. data/lib/prawn/svg/attributes/transform.rb +10 -0
  31. data/lib/prawn/svg/attributes.rb +6 -0
  32. data/lib/prawn/svg/calculators/arc_to_bezier_curve.rb +57 -0
  33. data/lib/prawn/svg/calculators/aspect_ratio.rb +60 -0
  34. data/lib/prawn/svg/calculators/document_sizing.rb +103 -0
  35. data/lib/prawn/svg/calculators/path_length.rb +168 -0
  36. data/lib/prawn/svg/calculators/pixels.rb +57 -0
  37. data/lib/prawn/svg/color.rb +240 -0
  38. data/lib/prawn/svg/css/font_face_parser.rb +72 -0
  39. data/lib/prawn/svg/css/font_family_parser.rb +39 -0
  40. data/lib/prawn/svg/css/font_parser.rb +46 -0
  41. data/lib/prawn/svg/css/selector_parser.rb +178 -0
  42. data/lib/prawn/svg/css/stylesheets.rb +233 -0
  43. data/lib/prawn/svg/css/values_parser.rb +68 -0
  44. data/lib/prawn/svg/document.rb +170 -0
  45. data/lib/prawn/svg/elements/anchor.rb +10 -0
  46. data/lib/prawn/svg/elements/base.rb +312 -0
  47. data/lib/prawn/svg/elements/bbox_scaling.rb +78 -0
  48. data/lib/prawn/svg/elements/call_duplicator.rb +37 -0
  49. data/lib/prawn/svg/elements/circle.rb +25 -0
  50. data/lib/prawn/svg/elements/clip_path.rb +26 -0
  51. data/lib/prawn/svg/elements/container.rb +10 -0
  52. data/lib/prawn/svg/elements/direct_render_base.rb +27 -0
  53. data/lib/prawn/svg/elements/ellipse.rb +22 -0
  54. data/lib/prawn/svg/elements/gradient.rb +184 -0
  55. data/lib/prawn/svg/elements/ignored.rb +5 -0
  56. data/lib/prawn/svg/elements/image.rb +177 -0
  57. data/lib/prawn/svg/elements/line.rb +27 -0
  58. data/lib/prawn/svg/elements/marker.rb +72 -0
  59. data/lib/prawn/svg/elements/mask.rb +65 -0
  60. data/lib/prawn/svg/elements/path.rb +294 -0
  61. data/lib/prawn/svg/elements/pattern.rb +150 -0
  62. data/lib/prawn/svg/elements/polygon.rb +25 -0
  63. data/lib/prawn/svg/elements/polyline.rb +23 -0
  64. data/lib/prawn/svg/elements/rect.rb +64 -0
  65. data/lib/prawn/svg/elements/root.rb +22 -0
  66. data/lib/prawn/svg/elements/switch.rb +92 -0
  67. data/lib/prawn/svg/elements/text.rb +99 -0
  68. data/lib/prawn/svg/elements/text_component.rb +346 -0
  69. data/lib/prawn/svg/elements/text_node.rb +308 -0
  70. data/lib/prawn/svg/elements/text_path.rb +153 -0
  71. data/lib/prawn/svg/elements/use.rb +142 -0
  72. data/lib/prawn/svg/elements/viewport.rb +29 -0
  73. data/lib/prawn/svg/elements.rb +46 -0
  74. data/lib/prawn/svg/extension.rb +25 -0
  75. data/lib/prawn/svg/font.rb +20 -0
  76. data/lib/prawn/svg/font_metrics.rb +144 -0
  77. data/lib/prawn/svg/font_registry.rb +315 -0
  78. data/lib/prawn/svg/funciri.rb +14 -0
  79. data/lib/prawn/svg/gradient_renderer.rb +332 -0
  80. data/lib/prawn/svg/gradients.rb +53 -0
  81. data/lib/prawn/svg/interface.rb +72 -0
  82. data/lib/prawn/svg/length.rb +43 -0
  83. data/lib/prawn/svg/link_renderer.rb +40 -0
  84. data/lib/prawn/svg/loaders/data.rb +17 -0
  85. data/lib/prawn/svg/loaders/file.rb +111 -0
  86. data/lib/prawn/svg/loaders/web.rb +24 -0
  87. data/lib/prawn/svg/paint.rb +67 -0
  88. data/lib/prawn/svg/pathable.rb +153 -0
  89. data/lib/prawn/svg/pattern_renderer.rb +112 -0
  90. data/lib/prawn/svg/pdfmatrix.rb +37 -0
  91. data/lib/prawn/svg/percentage.rb +24 -0
  92. data/lib/prawn/svg/properties.rb +329 -0
  93. data/lib/prawn/svg/renderer.rb +249 -0
  94. data/lib/prawn/svg/state.rb +24 -0
  95. data/lib/prawn/svg/transform_parser.rb +78 -0
  96. data/lib/prawn/svg/ttc.rb +35 -0
  97. data/lib/prawn/svg/ttf.rb +95 -0
  98. data/lib/prawn/svg/url_loader.rb +46 -0
  99. data/lib/prawn/svg/version.rb +5 -0
  100. data/lib/prawn/table.rb +4 -0
  101. data/lib/prawn-svg.rb +53 -0
  102. data/lib/prawn.rb +10 -0
  103. data/lib/prawnx/annotations.rb +21 -0
  104. data/lib/prawnx/document/bounding_box.rb +193 -0
  105. data/lib/prawnx/document/column_box.rb +91 -0
  106. data/lib/prawnx/document/internals.rb +144 -0
  107. data/lib/prawnx/document/span.rb +25 -0
  108. data/lib/prawnx/document.rb +253 -0
  109. data/lib/prawnx/encoding.rb +29 -0
  110. data/lib/prawnx/errors.rb +37 -0
  111. data/lib/prawnx/font.rb +214 -0
  112. data/lib/prawnx/fonts/afm.rb +171 -0
  113. data/lib/prawnx/fonts/otf.rb +50 -0
  114. data/lib/prawnx/fonts/ttf.rb +410 -0
  115. data/lib/prawnx/graphic_state.rb +41 -0
  116. data/lib/prawnx/graphics/color.rb +148 -0
  117. data/lib/prawnx/graphics/patterns.rb +100 -0
  118. data/lib/prawnx/graphics/style.rb +171 -0
  119. data/lib/prawnx/graphics.rb +208 -0
  120. data/lib/prawnx/grid.rb +216 -0
  121. data/lib/prawnx/images/image.rb +244 -0
  122. data/lib/prawnx/images/png_decoder.rb +65 -0
  123. data/lib/prawnx/images.rb +115 -0
  124. data/lib/prawnx/measurement_extensions.rb +40 -0
  125. data/lib/prawnx/measurements.rb +62 -0
  126. data/lib/prawnx/outline.rb +146 -0
  127. data/lib/prawnx/page.rb +125 -0
  128. data/lib/prawnx/page_geometry.rb +27 -0
  129. data/lib/prawnx/pdf/outline_node.rb +53 -0
  130. data/lib/prawnx/pdf/reference.rb +31 -0
  131. data/lib/prawnx/pdf/writer.rb +105 -0
  132. data/lib/prawnx/pdf.rb +66 -0
  133. data/lib/prawnx/pdf_a/srgb_profile.rb +61 -0
  134. data/lib/prawnx/pdf_a.rb +130 -0
  135. data/lib/prawnx/pdf_core.rb +25 -0
  136. data/lib/prawnx/repeater.rb +79 -0
  137. data/lib/prawnx/security.rb +108 -0
  138. data/lib/prawnx/stamp.rb +74 -0
  139. data/lib/prawnx/table/cell/kinds.rb +246 -0
  140. data/lib/prawnx/table/cell.rb +331 -0
  141. data/lib/prawnx/table/cells.rb +208 -0
  142. data/lib/prawnx/table.rb +296 -0
  143. data/lib/prawnx/text/box.rb +16 -0
  144. data/lib/prawnx/text/formatted/arranger.rb +217 -0
  145. data/lib/prawnx/text/formatted/box.rb +312 -0
  146. data/lib/prawnx/text/formatted/fragment.rb +205 -0
  147. data/lib/prawnx/text/formatted/line_wrap.rb +204 -0
  148. data/lib/prawnx/text/formatted/parser.rb +149 -0
  149. data/lib/prawnx/text/formatted/wrap.rb +103 -0
  150. data/lib/prawnx/text.rb +281 -0
  151. data/lib/prawnx/transformation_stack.rb +43 -0
  152. data/lib/prawnx/true_type/cff_subset.rb +157 -0
  153. data/lib/prawnx/true_type/file.rb +371 -0
  154. data/lib/prawnx/true_type/open_type_layout.rb +189 -0
  155. data/lib/prawnx/true_type/simple_subsets.rb +94 -0
  156. data/lib/prawnx/true_type/subset.rb +187 -0
  157. data/lib/prawnx/version.rb +7 -0
  158. data/lib/prawnx/view.rb +28 -0
  159. data/lib/prawnx.rb +102 -0
  160. 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