svg_drawer 1.0.0

Sign up to get free protection for your applications and to get access to all the features.
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