svg_drawer 1.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.
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA1:
3
+ metadata.gz: 79996165279d088f2e412c952599a1f4425e6022
4
+ data.tar.gz: 72e4bce3f625e41bf91d581ab1b17f529f7f8619
5
+ SHA512:
6
+ metadata.gz: 64e21f2a34481ceb931564a69b13c910fcda76de467e9f1507e8d472e9e3e51c0b20a953e4d10b1c38bf8ea485fe9c10bb1de7daf1ca7c07fb6c9d576777d63c
7
+ data.tar.gz: 545bf250522002f54b193fdc1ce20776f1287c30bacf5d52c488e5627f06b1f82076d6c37d5bbfaa136c48c4dd4c259433b8c2d3b8db7908674c8409ccb68485
data/LICENSE.txt ADDED
@@ -0,0 +1,21 @@
1
+ The MIT License (MIT)
2
+
3
+ Copyright (c) 2017 Simeon Manolov
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in
13
+ all copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
21
+ THE SOFTWARE.
data/lib/fonts.yml ADDED
@@ -0,0 +1,43 @@
1
+ #
2
+ # Heights and widths calculated experimentally with chrome inspector.
3
+ #
4
+ # y_offset is also calculated experimentally with chrome inspector:
5
+ # As opposed to HTML, in SVG when a text is drawed, all text is
6
+ # *transposed* down by (1.2 * [font_height]) px.
7
+ # This causes problems when drawing a text box with a border, as
8
+ # chars like 'g' or 'p' will intersect with the bottom border (and
9
+ # and '_' char will even be drawed entirely below the border)
10
+ # This is compensated with the y_offset, which seems to be ~0.22
11
+ # (how that value is used can be seen in #calculate_y_offset_px)
12
+ #
13
+
14
+ default:
15
+ width: 0.63
16
+ height: 1.2
17
+ y_offset: 0.23
18
+ wrap_policies:
19
+ weak: 1
20
+ normal: 1
21
+ aggressive: 1
22
+ max: 1
23
+
24
+ 'NK57 Monospace':
25
+ width: 0.63
26
+
27
+ 'Ubuntu Mono':
28
+ width: 0.5
29
+
30
+ 'Roboto Mono':
31
+ width: 0.596
32
+
33
+ 'Courier New':
34
+ width: 0.8
35
+
36
+ 'Arial':
37
+ width: 0.63
38
+ height: 1.3
39
+ wrap_policies:
40
+ weak: 0.9 # english text with very occasional capitals
41
+ normal: 1 # randomly mixed small/capital chars
42
+ aggressive: 1.17 # capital chars
43
+ max: 1.85 # capital "W" (widest english char)
@@ -0,0 +1,215 @@
1
+ module SvgDrawer
2
+ class Base
3
+ class ElementIncomplete < StandardError
4
+ def initialize(element)
5
+ super("Element incomplete: #{element.inspect}")
6
+ end
7
+ end
8
+
9
+ class << self
10
+ attr_reader :required_params, :special_params, :default_params
11
+
12
+ #
13
+ # The required_params contain param keys vital to the element.
14
+ # For example, a table can't be initialized without a :columns param.
15
+ # Their values can be provided via inherited_params.
16
+ #
17
+ def requires(*names)
18
+ required_params.concat(names)
19
+ end
20
+
21
+ #
22
+ # The special_params contain param keys special to the element.
23
+ # They will not be stored in @child_params
24
+ # See note in #initialize
25
+ #
26
+ def special(*names)
27
+ # Ensure special param are also a valid param (see ParameterMerger#param)
28
+ names_hash = names.map { |n| [n, nil] }.to_h
29
+
30
+ # this just Hash#reverse_merge! as implemented in activesupport-4.2
31
+ default_params.merge!(names_hash) { |_key, left, _right| left }
32
+ special_params.concat(names)
33
+ end
34
+
35
+ #
36
+ # The default_values is a hash of param values that will be
37
+ # used as a last-resort fallback
38
+ #
39
+ # With and height are applicable to all elements, so they
40
+ # are always included here to avoid "No such param" errors
41
+ # (see ParamMerger#param for details)
42
+ #
43
+ def defaults(hash)
44
+ default_params.update(hash)
45
+ end
46
+
47
+ def required_params
48
+ @required_params ||= []
49
+ end
50
+
51
+ # These should never be passed down to children
52
+ def special_params
53
+ @special_params ||= %i[id class inherited border borders]
54
+ end
55
+
56
+ # Mark these as valid for *all* elements (no default value though)
57
+ def default_params
58
+ @default_params ||= {
59
+ id: nil,
60
+ class: name.gsub(/.*::/, '').downcase,
61
+ width: nil,
62
+ height: nil,
63
+ border: nil,
64
+ borders: nil,
65
+ border_style: nil
66
+ }
67
+ end
68
+ end
69
+
70
+ attr_writer :width, :height
71
+ attr_reader :params, :inherited_params, :child_params
72
+
73
+ #
74
+ # All elements can are initialized with a params hash *only*.
75
+ # This hash contains stuff regular keys like :width, :height, etc.
76
+ # If this hash does *not* contain any of the keys found
77
+ # in self.class.required_params, an error is risen.
78
+ # It may contain an :inherited. Its value is a hash
79
+ # which will serve as a fall-back to any keys not found in the top-level
80
+ # hash.
81
+ # For example:
82
+ # { width: 100, inherited: { width: 200, height: 500 } }
83
+ #
84
+ # Calling #param(:width) on the element will return 100
85
+ # Calling #param(:height) on the element will return 500
86
+ # Calling #param(:foo) will not be found in any of the hashes, so
87
+ # it will be looked up in self.class.default_params:
88
+ # - if found, will be returned
89
+ # - if not found, an error will be risen (see ParameterMerger#param)
90
+ #
91
+ # A special @child_params hash is automatically constructed every
92
+ # time #update_params! is called (incl. #initialize)
93
+ # It is a hash that will be passed on as the value of
94
+ # the :inherited key to any child elements constructed.
95
+ # Note that it will not contain any keys found in self.class.special_params
96
+ #
97
+ # Inheritance example:
98
+ #
99
+ # Table.new(font: 'A') do |table|
100
+ # table.row do |row|
101
+ # row.cell do |cell|
102
+ # cell.content = TextBox.new('foo') # text 1
103
+ # end
104
+ #
105
+ # table.row(font: 'B') do |row|
106
+ # row.cell do |cell|
107
+ # cell.content = TextBox.new('foo') # text 2
108
+ # end
109
+ #
110
+ # row.cell(font: 'C') do |cell|
111
+ # cell.content = TextBox.new('foo') # text 3
112
+ # end
113
+ #
114
+ # row.cell(font: 'C') do |cell|
115
+ # cell.content(TextBox.new('foo', font: 'D')) # text 4
116
+ # end
117
+ # end
118
+ # end
119
+ #
120
+ # The texts 1, 2, 3 and 4 will have font 'A', 'B', 'C' and 'D'.
121
+ #
122
+ def initialize(params = {})
123
+ @params = {}
124
+ @inherited_params = {}
125
+ @child_params = {}
126
+ @pmerger = Utils::ParameterMerger.new(@params,
127
+ @inherited_params,
128
+ self.class.default_params)
129
+
130
+ update_params!(params)
131
+ end
132
+
133
+ # The way to update an element's params
134
+ def update_params!(params)
135
+ @params.update(_deep_dup(params))
136
+ @inherited_params.update(@params.delete(:inherited) || {})
137
+
138
+ # Note: self.class.default_params is NOT to be merged in @child_params
139
+ @child_params = @inherited_params.merge(@params)
140
+ self.class.special_params.each { |name| @child_params.delete(name) }
141
+
142
+ self.class.required_params.each do |rp|
143
+ raise "Required param is missing: #{rp}" unless @pmerger.param?(rp)
144
+ end
145
+ end
146
+
147
+ def param(name, default = nil)
148
+ @pmerger.param(name) || default
149
+ end
150
+
151
+ def param!(name)
152
+ param(name) or raise "No default value for: #{name}"
153
+ end
154
+
155
+ def draw(*args, debug: false)
156
+ ensure_complete!
157
+ @debug = debug
158
+ _draw(*args)
159
+ end
160
+
161
+ def ensure_complete!
162
+ pending_element = incomplete
163
+ raise ElementIncomplete, pending_element if pending_element
164
+ end
165
+
166
+ #
167
+ # To be defined in subclasses
168
+ #
169
+
170
+ def width
171
+ raise NotImplementedError
172
+ end
173
+
174
+ def height
175
+ raise NotImplementedError
176
+ end
177
+
178
+ def incomplete
179
+ raise NotImplementedError
180
+ end
181
+
182
+ private
183
+
184
+ def _draw(*)
185
+ raise NotImplementedError
186
+ end
187
+
188
+ def _complete?
189
+ raise NotImplementedError
190
+ end
191
+
192
+ def draw_border(svg, width_override: width, height_override: height)
193
+ borders = param(:borders)
194
+ borders ||= %i[left right top bottom] if param(:border)
195
+ Border.draw(
196
+ svg,
197
+ width_override,
198
+ height_override,
199
+ borders,
200
+ param(:border_style),
201
+ param(:class),
202
+ @debug
203
+ )
204
+ end
205
+
206
+ def _deep_dup(value)
207
+ case value
208
+ when Array then value.map { |v| _deep_dup(v) }
209
+ when Hash then value.map { |k, v| [_deep_dup(k), _deep_dup(v)] }.to_h
210
+ when Fixnum, Float, Symbol, TrueClass, FalseClass then value
211
+ else value.dup
212
+ end
213
+ end
214
+ end
215
+ end
@@ -0,0 +1,139 @@
1
+ module SvgDrawer
2
+ class Circle < Base
3
+ defaults fill: 'none',
4
+ stroke: 'black',
5
+ size: 1,
6
+ x_reposition: 'none', # none/left/center/right
7
+ y_reposition: 'none', # none/top/middle/bottom
8
+ expand: false,
9
+ shrink: false,
10
+ overflow: false,
11
+ scale: 1,
12
+ scale_size: true
13
+
14
+ def initialize(center, radius, params = {})
15
+ @center = center
16
+ @radius = radius
17
+ super(params)
18
+ end
19
+
20
+ def width
21
+ param(:overflow) ?
22
+ param(:width, calc_width) :
23
+ [param(:width, 0), calc_width].max
24
+ end
25
+
26
+ def height
27
+ param(:overflow) ?
28
+ param(:height, calc_height) :
29
+ [param(:height, 0), calc_height].max
30
+ end
31
+
32
+ def incomplete
33
+ false
34
+ end
35
+
36
+ def min_x
37
+ @min_x ||= @center.first - @radius
38
+ end
39
+
40
+ def max_x
41
+ @max_x ||= @center.first + @radius
42
+ end
43
+
44
+ def min_y
45
+ @min_y ||= @center.last - @radius
46
+ end
47
+
48
+ def max_y
49
+ @max_y ||= @center.last + @radius
50
+ end
51
+
52
+ private
53
+
54
+ def _draw(parent)
55
+ size = param(:scale_size) ? param(:size) : param(:size) / scale
56
+ style = {}
57
+
58
+ # need symbol keys due to a bug in Rasem::SVGTag#write_styles
59
+ style[:fill] = param(:fill)
60
+ style[:stroke] = param(:stroke)
61
+ style[:'stroke-width'] = size
62
+
63
+ Utils::RasemWrapper.group(parent, class: 'circle') do |circle_group|
64
+ poly = circle_group.circle(@center.first, @center.last, @radius, style: style.dup)
65
+ poly.translate(translate_x, translate_y).scale(scale, scale)
66
+ end
67
+ end
68
+
69
+ def calc_width
70
+ calc_width_unscaled * scale
71
+ end
72
+
73
+ def calc_height
74
+ calc_height_unscaled * scale
75
+ end
76
+
77
+ def calc_width_unscaled
78
+ max_x - min_x
79
+ end
80
+
81
+ def calc_height_unscaled
82
+ max_y - min_y
83
+ end
84
+
85
+ def width_unscaled
86
+ param(:overflow) ?
87
+ param(:width, calc_width_unscaled) :
88
+ [param(:width, 0), calc_width_unscaled].max
89
+ end
90
+
91
+ def height_unscaled
92
+ param(:overflow) ?
93
+ param(:height, calc_height_unscaled) :
94
+ [param(:height, 0), calc_height_unscaled].max
95
+ end
96
+
97
+ def scale
98
+ [scale_x, scale_y].min * param(:scale)
99
+ end
100
+
101
+ def scale_x
102
+ return 1 unless param(:width) && (param(:expand) || param(:shrink))
103
+ scale = param(:width).to_d / calc_width_unscaled
104
+ return 1 if (scale > 1 && !param(:expand)) || (scale < 1 && !param(:shrink))
105
+ scale
106
+ end
107
+
108
+ def scale_y
109
+ return 1 unless param(:height) && (param(:expand) || param(:shrink))
110
+ scale = param(:height).to_d / calc_height_unscaled
111
+ return 1 if (scale > 1 && !param(:expand)) || (scale < 1 && !param(:shrink))
112
+ scale
113
+ end
114
+
115
+ def translate_x
116
+ width_diff = (width - calc_width)
117
+
118
+ case param(:x_reposition)
119
+ when 'left' then -min_x * scale
120
+ when 'center' then -min_x * scale + width_diff / 2
121
+ when 'right' then -min_x * scale + width_diff
122
+ when 'none' then 0
123
+ else raise "Bad x_reposition: #{param(:x_reposition)}. Valid are: [left, right, center, none]"
124
+ end
125
+ end
126
+
127
+ def translate_y
128
+ height_diff = height - calc_height
129
+
130
+ case param(:y_reposition)
131
+ when 'top' then -min_y * scale
132
+ when 'middle' then -min_y * scale + height_diff / 2
133
+ when 'bottom' then -min_y * scale + height_diff
134
+ when 'none' then 0
135
+ else raise "Bad y_reposition: #{param(:y_reposition)}. Valid are: [top, bottom, middle, none]"
136
+ end
137
+ end
138
+ end
139
+ end
@@ -0,0 +1,10 @@
1
+ module SvgDrawer
2
+ class Line < Polyline
3
+ # Retranslate ensures the parent element can correctly draw borders
4
+ defaults Polyline.default_params
5
+
6
+ def incomplete
7
+ @points.size != 4 && self
8
+ end
9
+ end
10
+ end
@@ -0,0 +1,144 @@
1
+ module SvgDrawer
2
+ class Multipolyline < Base
3
+ special :x_reposition, :y_reposition, :resize, :overflow,
4
+ :height, :width, :scale, :scale_size
5
+
6
+ # See notes in Polyline
7
+ defaults fill: 'none',
8
+ stroke: 'black',
9
+ linecap: 'butt',
10
+ linejoin: 'miter',
11
+ size: 1,
12
+ x_reposition: 'none', # none/left/center/right
13
+ y_reposition: 'none', # none/top/middle/bottom
14
+ expand: false,
15
+ shrink: false,
16
+ overflow: false,
17
+ scale: 1,
18
+ scale_size: true
19
+
20
+ def initialize(strokes, params = {})
21
+ super(params)
22
+ @polylines = strokes.map { |stroke| Polyline.new(stroke, child_params) }
23
+ end
24
+
25
+ def width
26
+ param(:overflow) ?
27
+ param(:width, calc_width) :
28
+ [param(:width, 0), calc_width].max
29
+ end
30
+
31
+ def height
32
+ param(:overflow) ?
33
+ param(:height, calc_height) :
34
+ [param(:height, 0), calc_height].max
35
+ end
36
+
37
+ def incomplete
38
+ @polylines.none? ? self : @polylines.find(&:incomplete)
39
+ end
40
+
41
+ private
42
+
43
+ def _draw(parent)
44
+ unless param(:scale_size)
45
+ @polylines.each { |p| p.update_params!(size: param(:size) / scale) }
46
+ end
47
+
48
+ Utils::RasemWrapper.group(parent, class: 'multi_polyline') do |mpoly_group|
49
+ # Need a sub-group to prevent parents from overwriting translate()
50
+ grouped = Utils::RasemWrapper.group(mpoly_group) do |g|
51
+ @polylines.each { |p| p.draw(g, debug: @debug) }
52
+ end
53
+
54
+ grouped.translate(translate_x, translate_y).scale(scale, scale)
55
+ end
56
+ end
57
+
58
+ def min_x
59
+ @min_x ||= @polylines.min_by(&:min_x).min_x
60
+ end
61
+
62
+ def max_x
63
+ @max_x ||= @polylines.max_by(&:max_x).max_x
64
+ end
65
+
66
+ def min_y
67
+ @min_y ||= @polylines.min_by(&:min_y).min_y
68
+ end
69
+
70
+ def max_y
71
+ @max_y ||= @polylines.max_by(&:max_y).max_y
72
+ end
73
+
74
+ def calc_width
75
+ calc_width_unscaled * scale
76
+ end
77
+
78
+ def calc_height
79
+ calc_height_unscaled * scale
80
+ end
81
+
82
+ def calc_width_unscaled
83
+ max_x - min_x
84
+ end
85
+
86
+ def calc_height_unscaled
87
+ max_y - min_y
88
+ end
89
+
90
+ def width_unscaled
91
+ param(:overflow) ?
92
+ param(:width, calc_width_unscaled) :
93
+ [param(:width, 0), calc_width_unscaled].max
94
+ end
95
+
96
+ def height_unscaled
97
+ param(:overflow) ?
98
+ param(:height, calc_height_unscaled) :
99
+ [param(:height, 0), calc_height_unscaled].max
100
+ end
101
+
102
+ def scale
103
+ [scale_x, scale_y].min * param(:scale)
104
+ end
105
+
106
+ def scale_x
107
+ return 1 unless param(:width) && (param(:expand) || param(:shrink))
108
+ scale = param(:width).to_d / calc_width_unscaled
109
+ return 1 if (scale > 1 && !param(:expand)) || (scale < 1 && !param(:shrink))
110
+ scale
111
+ end
112
+
113
+ def scale_y
114
+ return 1 unless param(:height) && (param(:expand) || param(:shrink))
115
+ scale = param(:height).to_d / calc_height_unscaled
116
+ return 1 if (scale > 1 && !param(:expand)) || (scale < 1 && !param(:shrink))
117
+ scale
118
+ end
119
+
120
+ def translate_x
121
+ width_diff = (width - calc_width)
122
+
123
+ case param(:x_reposition)
124
+ when 'left' then -min_x * scale
125
+ when 'center' then -min_x * scale + width_diff / 2
126
+ when 'right' then -min_x * scale + width_diff
127
+ when 'none' then 0
128
+ else raise "Bad x_reposition: #{param(:x_reposition)}. Valid are: [left, right, center, none]"
129
+ end
130
+ end
131
+
132
+ def translate_y
133
+ height_diff = height - calc_height
134
+
135
+ case param(:y_reposition)
136
+ when 'top' then -min_y * scale
137
+ when 'middle' then -min_y * scale + height_diff / 2
138
+ when 'bottom' then -min_y * scale + height_diff
139
+ when 'none' then 0
140
+ else raise "Bad y_reposition: #{param(:y_reposition)}. Valid are: [top, bottom, middle, none]"
141
+ end
142
+ end
143
+ end
144
+ end
@@ -0,0 +1,43 @@
1
+ module SvgDrawer
2
+ class Path < Base
3
+ # Required since there is no known way to calculate them
4
+ requires :width
5
+ requires :height
6
+
7
+ # Retranslate ensures the parent element can correctly draw borders
8
+ defaults scale: [1, 1],
9
+ overflow: true, # false not supported
10
+ retranslate: false # true not supported
11
+
12
+ def initialize(path_components, defaults = {})
13
+ super(defaults)
14
+ @components = path_components
15
+ end
16
+
17
+ # No idea how to compute dimensions for paths
18
+ def width
19
+ raise NotImplementedError unless param(:overflow)
20
+ param(:width)
21
+ end
22
+
23
+ def height
24
+ raise NotImplementedError unless param(:overflow)
25
+ param(:height)
26
+ end
27
+
28
+ def incomplete
29
+ false
30
+ end
31
+
32
+ private
33
+
34
+ def _draw(parent)
35
+ # No idea how to find boundary coordinates
36
+ raise NotImplementedError if param(:retranslate)
37
+
38
+ Utils::RasemWrapper.group(parent, class: 'path') do |path_group|
39
+ @components.each { |path| path_group.path(d: path) }
40
+ end.scale(*param(:scale))
41
+ end
42
+ end
43
+ end