sevgi-graphics 0.94.0 → 0.98.2
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 +4 -4
- data/CHANGELOG.md +275 -2
- data/LICENSE +672 -3
- data/README.md +8 -8
- data/lib/sevgi/graphics/attribute.rb +271 -54
- data/lib/sevgi/graphics/auxiliary/canvas.rb +137 -58
- data/lib/sevgi/graphics/auxiliary/content.rb +200 -57
- data/lib/sevgi/graphics/auxiliary/margin.rb +33 -14
- data/lib/sevgi/graphics/auxiliary/paper.rb +118 -86
- data/lib/sevgi/graphics/auxiliary/path.rb +44 -0
- data/lib/sevgi/graphics/auxiliary/scalar.rb +69 -0
- data/lib/sevgi/graphics/auxiliary/sizes.rb +62 -0
- data/lib/sevgi/graphics/auxiliary.rb +3 -0
- data/lib/sevgi/graphics/document/base.rb +6 -2
- data/lib/sevgi/graphics/document/default.rb +1 -1
- data/lib/sevgi/graphics/document.rb +370 -124
- data/lib/sevgi/graphics/element.rb +143 -33
- data/lib/sevgi/graphics/mixtures/call.rb +249 -88
- data/lib/sevgi/graphics/mixtures/core.rb +143 -33
- data/lib/sevgi/graphics/mixtures/duplicate.rb +76 -22
- data/lib/sevgi/graphics/mixtures/export.rb +58 -12
- data/lib/sevgi/graphics/mixtures/hatch.rb +49 -7
- data/lib/sevgi/graphics/mixtures/identify.rb +30 -17
- data/lib/sevgi/graphics/mixtures/include.rb +40 -5
- data/lib/sevgi/graphics/mixtures/inkscape.rb +215 -46
- data/lib/sevgi/graphics/mixtures/rdf.rb +79 -7
- data/lib/sevgi/graphics/mixtures/render.rb +88 -19
- data/lib/sevgi/graphics/mixtures/save.rb +79 -26
- data/lib/sevgi/graphics/mixtures/symbols.rb +81 -9
- data/lib/sevgi/graphics/mixtures/tile.rb +88 -64
- data/lib/sevgi/graphics/mixtures/transform.rb +68 -28
- data/lib/sevgi/graphics/mixtures/underscore.rb +16 -7
- data/lib/sevgi/graphics/mixtures/validate.rb +14 -2
- data/lib/sevgi/graphics/mixtures/wrappers.rb +66 -24
- data/lib/sevgi/graphics/mixtures.rb +19 -13
- data/lib/sevgi/graphics/version.rb +1 -1
- data/lib/sevgi/graphics/xml.rb +127 -0
- data/lib/sevgi/graphics.rb +75 -28
- metadata +10 -6
|
@@ -3,6 +3,19 @@
|
|
|
3
3
|
module Sevgi
|
|
4
4
|
module Graphics
|
|
5
5
|
# Four-sided margin with CSS-like shorthand normalization.
|
|
6
|
+
# @!parse
|
|
7
|
+
# class Margin
|
|
8
|
+
# # Creates a margin from one to four CSS-like shorthand values.
|
|
9
|
+
# # @param top [Numeric, nil] top value or all-sides shorthand
|
|
10
|
+
# # @param right [Numeric, nil] right value or horizontal shorthand
|
|
11
|
+
# # @param bottom [Numeric, nil] bottom value
|
|
12
|
+
# # @param left [Numeric, nil] left value
|
|
13
|
+
# # @return [Sevgi::Graphics::Margin]
|
|
14
|
+
# # @raise [Sevgi::ArgumentError] when a supplied value is not a finite, non-negative real number
|
|
15
|
+
# # @example Create a vertical and horizontal margin
|
|
16
|
+
# # Sevgi::Graphics::Margin[5, 10]
|
|
17
|
+
# def self.[](top = nil, right = nil, bottom = nil, left = nil); end
|
|
18
|
+
# end
|
|
6
19
|
Margin = Data.define(:top, :right, :bottom, :left) do
|
|
7
20
|
include Comparable
|
|
8
21
|
|
|
@@ -15,25 +28,30 @@ module Sevgi
|
|
|
15
28
|
# @!attribute [r] left
|
|
16
29
|
# @return [Float] left margin
|
|
17
30
|
|
|
18
|
-
# Creates a margin from one to four shorthand values.
|
|
31
|
+
# Creates a margin from one to four shorthand values. Values must be finite real numbers greater than or equal to
|
|
32
|
+
# zero.
|
|
19
33
|
# @param top [Numeric, nil] top value or all-sides shorthand
|
|
20
34
|
# @param right [Numeric, nil] right value or horizontal shorthand
|
|
21
35
|
# @param bottom [Numeric, nil] bottom value
|
|
22
36
|
# @param left [Numeric, nil] left value
|
|
23
37
|
# @return [void]
|
|
38
|
+
# @raise [Sevgi::ArgumentError] when a supplied margin is not a finite, non-negative real number
|
|
24
39
|
def initialize(top: nil, right: nil, bottom: nil, left: nil)
|
|
25
40
|
super(**normalize(top, right, bottom, left))
|
|
26
41
|
end
|
|
27
42
|
|
|
28
43
|
# Compares margins by top, right, bottom, then left.
|
|
29
|
-
# @param other [
|
|
30
|
-
# @return [Integer, nil]
|
|
31
|
-
def <=>(other)
|
|
44
|
+
# @param other [Object] margin to compare
|
|
45
|
+
# @return [Integer, nil] comparison result, or nil for a non-Margin operand
|
|
46
|
+
def <=>(other)
|
|
47
|
+
deconstruct <=> other.deconstruct if other.is_a?(self.class)
|
|
48
|
+
end
|
|
32
49
|
|
|
33
50
|
# Returns a margin inflated horizontally and vertically.
|
|
34
51
|
# @param h [Numeric] horizontal addition
|
|
35
52
|
# @param v [Numeric] vertical addition
|
|
36
53
|
# @return [Sevgi::Graphics::Margin]
|
|
54
|
+
# @raise [Sevgi::ArgumentError] when the adjusted margin is negative or not finite
|
|
37
55
|
def adjust(h, v) = self.class[top + v, right + h, bottom + v, left + h]
|
|
38
56
|
|
|
39
57
|
# Reports strict margin equality.
|
|
@@ -54,16 +72,10 @@ module Sevgi
|
|
|
54
72
|
def vertical = top + bottom
|
|
55
73
|
|
|
56
74
|
alias_method :==, :eql?
|
|
57
|
-
alias_method :to_a, :deconstruct
|
|
58
75
|
|
|
59
|
-
#
|
|
60
|
-
# @
|
|
61
|
-
|
|
62
|
-
def self.margin(array)
|
|
63
|
-
self[
|
|
64
|
-
*(array = Array(array)[0...(size = Margin.members.size)]).fill(nil, array.size, size - array.size)
|
|
65
|
-
]
|
|
66
|
-
end
|
|
76
|
+
# Returns margin values in top, right, bottom, left order.
|
|
77
|
+
# @return [Array<Float>]
|
|
78
|
+
alias_method :to_a, :deconstruct
|
|
67
79
|
|
|
68
80
|
# Returns a zero margin.
|
|
69
81
|
# @return [Sevgi::Graphics::Margin]
|
|
@@ -74,7 +86,14 @@ module Sevgi
|
|
|
74
86
|
def normalize(top, right, bottom, left)
|
|
75
87
|
values = [top, right, bottom, left]
|
|
76
88
|
|
|
77
|
-
Margin
|
|
89
|
+
Margin
|
|
90
|
+
.members
|
|
91
|
+
.zip(
|
|
92
|
+
values_for(values.compact.size, values).map do |value|
|
|
93
|
+
Scalar.finite(value, context: "margin", field: :value, nonnegative: true)
|
|
94
|
+
end
|
|
95
|
+
)
|
|
96
|
+
.to_h
|
|
78
97
|
end
|
|
79
98
|
|
|
80
99
|
def values_for(size, values)
|
|
@@ -3,6 +3,19 @@
|
|
|
3
3
|
module Sevgi
|
|
4
4
|
module Graphics
|
|
5
5
|
# Paper size and unit profile.
|
|
6
|
+
# @!parse
|
|
7
|
+
# class Paper
|
|
8
|
+
# # Creates a paper profile from dimensions and optional metadata.
|
|
9
|
+
# # @param width [Numeric] paper width
|
|
10
|
+
# # @param height [Numeric] paper height
|
|
11
|
+
# # @param unit [Symbol, String] SVG unit
|
|
12
|
+
# # @param name [Symbol, String] profile name
|
|
13
|
+
# # @return [Sevgi::Graphics::Paper]
|
|
14
|
+
# # @raise [Sevgi::ArgumentError] when dimensions, unit, or name are invalid
|
|
15
|
+
# # @example Create a custom paper profile with value notation
|
|
16
|
+
# # Sevgi::Graphics::Paper[90, 50, :mm, :card]
|
|
17
|
+
# def self.[](width, height, unit = "mm", name = :custom); end
|
|
18
|
+
# end
|
|
6
19
|
Paper = Data.define(:width, :height, :unit, :name) do
|
|
7
20
|
include Comparable
|
|
8
21
|
|
|
@@ -15,26 +28,30 @@ module Sevgi
|
|
|
15
28
|
# @!attribute [r] name
|
|
16
29
|
# @return [Symbol] profile name
|
|
17
30
|
|
|
18
|
-
# Creates a paper profile.
|
|
31
|
+
# Creates a paper profile. Dimensions must be finite real numbers greater than zero.
|
|
19
32
|
# @param width [Numeric] paper width
|
|
20
33
|
# @param height [Numeric] paper height
|
|
21
34
|
# @param unit [Symbol, String] SVG unit
|
|
22
35
|
# @param name [Symbol, String] profile name
|
|
36
|
+
# @param options [Hash] unsupported extra options
|
|
23
37
|
# @return [void]
|
|
24
|
-
# @raise [Sevgi::ArgumentError] when dimensions, unit, or
|
|
25
|
-
def initialize(width:, height:, unit: "mm", name: :custom)
|
|
38
|
+
# @raise [Sevgi::ArgumentError] when dimensions, unit, name, or options are invalid
|
|
39
|
+
def initialize(width:, height:, unit: "mm", name: :custom, **options)
|
|
40
|
+
self.class.send(:options!, options)
|
|
26
41
|
super(
|
|
27
42
|
width: self.class.send(:dimension!, :width, width),
|
|
28
43
|
height: self.class.send(:dimension!, :height, height),
|
|
29
|
-
unit: self.class.send(:
|
|
30
|
-
name: self.class.send(:
|
|
44
|
+
unit: self.class.send(:normalize!, :unit, unit),
|
|
45
|
+
name: self.class.send(:normalize!, :name, name)
|
|
31
46
|
)
|
|
32
47
|
end
|
|
33
48
|
|
|
34
49
|
# Compares papers by width, height, unit, then name.
|
|
35
50
|
# @param other [Sevgi::Graphics::Paper] paper to compare
|
|
36
|
-
# @return [Integer, nil]
|
|
37
|
-
def <=>(other)
|
|
51
|
+
# @return [Integer, nil] comparison result, or nil for a non-Paper operand
|
|
52
|
+
def <=>(other)
|
|
53
|
+
deconstruct <=> other.deconstruct if other.is_a?(self.class)
|
|
54
|
+
end
|
|
38
55
|
|
|
39
56
|
# Reports strict paper equality.
|
|
40
57
|
# @param other [Object] object to compare
|
|
@@ -56,108 +73,123 @@ module Sevgi
|
|
|
56
73
|
alias_method :==, :eql?
|
|
57
74
|
|
|
58
75
|
@profiles = {}
|
|
76
|
+
@accessors = {}
|
|
77
|
+
@mutex = ::Mutex.new
|
|
59
78
|
|
|
60
|
-
# Reports whether a named paper profile exists.
|
|
79
|
+
# Reports whether a normalizable named paper profile exists. Invalid converters return false.
|
|
61
80
|
# @param name [Object] profile name
|
|
62
81
|
# @return [Boolean]
|
|
63
|
-
def self.exist?(name)
|
|
82
|
+
def self.exist?(name)
|
|
83
|
+
name = normalize(name)
|
|
84
|
+
name ? @mutex.synchronize { @profiles.key?(name) } : false
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
# Returns a registered paper profile by name.
|
|
88
|
+
# @param name [Symbol, String] profile name, including names that are not Ruby identifiers
|
|
89
|
+
# @return [Sevgi::Graphics::Paper] registered profile
|
|
90
|
+
# @raise [Sevgi::ArgumentError] when name is invalid or no profile is registered
|
|
91
|
+
# @example Look up a non-identifier profile name
|
|
92
|
+
# Sevgi::Graphics::Paper.define("business-card", width: 90, height: 50)
|
|
93
|
+
# Sevgi::Graphics::Paper.fetch("business-card")
|
|
94
|
+
def self.fetch(name)
|
|
95
|
+
name = normalize!(:name, name)
|
|
96
|
+
@mutex.synchronize { @profiles.fetch(name) { ArgumentError.("Unknown paper profile: #{name}") } }
|
|
97
|
+
end
|
|
98
|
+
|
|
99
|
+
# Returns registered profile names.
|
|
100
|
+
# @return [Array<Symbol>] frozen name snapshot
|
|
101
|
+
def self.keys = @mutex.synchronize { @profiles.keys.freeze }
|
|
64
102
|
|
|
65
|
-
# Defines
|
|
103
|
+
# Defines a named paper profile after complete validation. Registration is process-global and thread-atomic.
|
|
104
|
+
# Identical definitions return the canonical profile and conflicting definitions raise unless replacement is
|
|
105
|
+
# explicitly requested. Names that are not Ruby call syntax remain accessible through {.fetch}.
|
|
66
106
|
# @param name [Symbol, String] profile name
|
|
107
|
+
# @param overwrite [Boolean] true to replace an existing profile
|
|
67
108
|
# @param spec [Hash] paper dimensions and unit
|
|
68
109
|
# @option spec [Numeric] :width paper width
|
|
69
110
|
# @option spec [Numeric] :height paper height
|
|
70
111
|
# @option spec [Symbol, String] :unit SVG unit
|
|
71
112
|
# @return [Sevgi::Graphics::Paper]
|
|
72
|
-
# @raise [Sevgi::ArgumentError] when the
|
|
73
|
-
|
|
74
|
-
|
|
113
|
+
# @raise [Sevgi::ArgumentError] when the name, dimensions, unit, overwrite flag, or options are reserved or invalid,
|
|
114
|
+
# or a non-bang definition conflicts with the registered profile
|
|
115
|
+
# @example Define or reuse a matching profile
|
|
116
|
+
# Sevgi::Graphics::Paper.define(:card, width: 90, height: 50)
|
|
117
|
+
# @example Replace a profile explicitly
|
|
118
|
+
# Sevgi::Graphics::Paper.define(:card, width: 100, height: 60, overwrite: true)
|
|
119
|
+
def self.define(name, overwrite: false, **spec)
|
|
120
|
+
name = normalize!(:name, name)
|
|
121
|
+
ArgumentError.("Paper name is reserved: #{name}") if reserved?(name)
|
|
122
|
+
overwrite = overwrite!(overwrite)
|
|
123
|
+
profile = new(name:, **spec)
|
|
124
|
+
|
|
125
|
+
register(name, profile, overwrite:)
|
|
126
|
+
end
|
|
75
127
|
|
|
76
|
-
|
|
128
|
+
class << self
|
|
129
|
+
private
|
|
77
130
|
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
profiles[name] = instance_variable_set("@#{name}", new(name:, **spec))
|
|
81
|
-
end
|
|
131
|
+
def install(name)
|
|
132
|
+
return if @accessors.key?(name)
|
|
82
133
|
|
|
83
|
-
|
|
134
|
+
define_singleton_method(name) { @mutex.synchronize { @profiles.fetch(name) } }
|
|
135
|
+
@accessors[name] = true
|
|
136
|
+
end
|
|
84
137
|
|
|
85
|
-
|
|
138
|
+
def register(name, profile, overwrite:)
|
|
139
|
+
@mutex.synchronize do
|
|
140
|
+
if !overwrite && (current = @profiles[name])
|
|
141
|
+
ArgumentError.("Paper already defined differently: #{name}") unless current == profile
|
|
86
142
|
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
143
|
+
next current
|
|
144
|
+
end
|
|
145
|
+
|
|
146
|
+
install(name)
|
|
147
|
+
@profiles[name] = profile
|
|
148
|
+
end
|
|
149
|
+
end
|
|
92
150
|
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
151
|
+
def reserved?(name) = @reserved.include?(name)
|
|
152
|
+
|
|
153
|
+
def dimension!(field, value)
|
|
154
|
+
Scalar.finite(value, context: "paper", field:, positive: true)
|
|
155
|
+
end
|
|
156
|
+
|
|
157
|
+
def options!(options)
|
|
158
|
+
return if options.empty?
|
|
159
|
+
|
|
160
|
+
ArgumentError.("Unknown paper options: #{options.keys.join(", ")}")
|
|
161
|
+
end
|
|
162
|
+
|
|
163
|
+
def overwrite!(value)
|
|
164
|
+
return value if [true, false].include?(value)
|
|
165
|
+
|
|
166
|
+
ArgumentError.("Paper overwrite must be true or false")
|
|
167
|
+
end
|
|
168
|
+
|
|
169
|
+
def normalize(value)
|
|
170
|
+
normalized = value.to_sym if value.respond_to?(:to_sym)
|
|
171
|
+
normalized if normalized.is_a?(::Symbol)
|
|
172
|
+
rescue ::StandardError
|
|
173
|
+
nil
|
|
174
|
+
end
|
|
175
|
+
|
|
176
|
+
def normalize!(field, value)
|
|
177
|
+
normalize(value) || ArgumentError.("Invalid paper #{field}")
|
|
178
|
+
end
|
|
97
179
|
end
|
|
98
180
|
|
|
99
181
|
@reserved = methods.map(&:to_sym).freeze
|
|
100
182
|
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
{
|
|
104
|
-
a0: [841, 1189, "mm"],
|
|
105
|
-
a1: [594, 841, "mm"],
|
|
106
|
-
a2: [420, 594, "mm"],
|
|
107
|
-
a3: [297, 420, "mm"],
|
|
108
|
-
a4: [210, 297, "mm"],
|
|
109
|
-
a5: [148, 210, "mm"],
|
|
110
|
-
a6: [105, 148, "mm"],
|
|
111
|
-
a7: [74, 105, "mm"],
|
|
112
|
-
a8: [52, 74, "mm"],
|
|
113
|
-
a9: [37, 52, "mm"],
|
|
114
|
-
a10: [26, 37, "mm"],
|
|
115
|
-
|
|
116
|
-
b0: [1000, 1414, "mm"],
|
|
117
|
-
b1: [707, 1000, "mm"],
|
|
118
|
-
b2: [500, 707, "mm"],
|
|
119
|
-
b3: [353, 500, "mm"],
|
|
120
|
-
b4: [250, 353, "mm"],
|
|
121
|
-
b5: [176, 250, "mm"],
|
|
122
|
-
b6: [125, 176, "mm"],
|
|
123
|
-
b7: [88, 125, "mm"],
|
|
124
|
-
b8: [62, 88, "mm"],
|
|
125
|
-
b9: [44, 62, "mm"],
|
|
126
|
-
b10: [31, 44, "mm"],
|
|
127
|
-
|
|
128
|
-
c0: [917, 1297, "mm"],
|
|
129
|
-
c1: [648, 917, "mm"],
|
|
130
|
-
c2: [458, 648, "mm"],
|
|
131
|
-
c3: [324, 458, "mm"],
|
|
132
|
-
c4: [229, 324, "mm"],
|
|
133
|
-
c5: [162, 229, "mm"],
|
|
134
|
-
c6: [114, 162, "mm"],
|
|
135
|
-
c7: [81, 114, "mm"],
|
|
136
|
-
c8: [57, 81, "mm"],
|
|
137
|
-
c9: [40, 57, "mm"],
|
|
138
|
-
c10: [28, 40, "mm"],
|
|
139
|
-
|
|
140
|
-
business: [85, 55, "mm"],
|
|
141
|
-
large: [130, 210, "mm"],
|
|
142
|
-
passport: [88, 125, "mm"],
|
|
143
|
-
pocket: [90, 140, "mm"],
|
|
144
|
-
travelers: [110, 210, "mm"],
|
|
145
|
-
us: [216, 279, "mm"],
|
|
146
|
-
xlarge: [190, 250, "mm"],
|
|
147
|
-
|
|
148
|
-
icon16: [16, 16, "px"],
|
|
149
|
-
icon32: [32, 32, "px"],
|
|
150
|
-
icon64: [64, 64, "px"],
|
|
151
|
-
icon128: [128, 128, "px"],
|
|
152
|
-
icon256: [256, 256, "px"],
|
|
153
|
-
icon512: [512, 512, "px"]
|
|
154
|
-
}.each { |name, (width, height, unit)| define(name, width:, height:, unit:) }
|
|
183
|
+
PAPER_SIZES.each { |name, (width, height, unit)| define(name, width:, height:, unit:) }
|
|
155
184
|
|
|
156
|
-
|
|
157
|
-
|
|
185
|
+
# Returns the default paper profile.
|
|
186
|
+
# @return [Sevgi::Graphics::Paper]
|
|
187
|
+
def self.default
|
|
188
|
+
@mutex.synchronize { @profiles.fetch(:default) }
|
|
158
189
|
end
|
|
159
190
|
|
|
160
|
-
|
|
191
|
+
@accessors[:default] = true
|
|
192
|
+
@profiles[:default] = @profiles.fetch(:a4)
|
|
161
193
|
end
|
|
162
194
|
end
|
|
163
195
|
end
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Sevgi
|
|
4
|
+
module Graphics
|
|
5
|
+
# Output path normalization shared by Graphics writers and exporters.
|
|
6
|
+
# @api private
|
|
7
|
+
module Path
|
|
8
|
+
# Converts a non-blank path to an expanded String.
|
|
9
|
+
# @param value [String, #to_path] raw path value
|
|
10
|
+
# @param context [String] public operation named in errors
|
|
11
|
+
# @return [String] expanded path
|
|
12
|
+
# @raise [Sevgi::ArgumentError] when value is blank, has an invalid type, or path conversion fails
|
|
13
|
+
def self.call(value, context:)
|
|
14
|
+
path = value.respond_to?(:to_path) ? value.to_path : value
|
|
15
|
+
ArgumentError.("#{context} must be a String or path-like object") unless path.is_a?(::String)
|
|
16
|
+
ArgumentError.("#{context} must be provided") if path.strip.empty?
|
|
17
|
+
|
|
18
|
+
::File.expand_path(path)
|
|
19
|
+
rescue ::Sevgi::ArgumentError
|
|
20
|
+
raise
|
|
21
|
+
rescue ::StandardError => e
|
|
22
|
+
ArgumentError.("#{context} must be a String or path-like object: #{e.message}")
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
# Resolves an optional path with the writer/exporter directory convention.
|
|
26
|
+
# @param value [String, #to_path, nil] explicit path or directory
|
|
27
|
+
# @param default [String, #to_path] default output path
|
|
28
|
+
# @param context [String] public operation named in errors
|
|
29
|
+
# @return [String] expanded output file path
|
|
30
|
+
# @raise [Sevgi::ArgumentError] when a selected path/default is blank, invalid, or cannot be converted
|
|
31
|
+
def self.resolve(value, default:, context:)
|
|
32
|
+
return call(default, context: "#{context} default") if value.nil?
|
|
33
|
+
|
|
34
|
+
path = call(value, context: "#{context} path")
|
|
35
|
+
return path unless ::File.directory?(path)
|
|
36
|
+
|
|
37
|
+
default = call(default, context: "#{context} default")
|
|
38
|
+
::File.join(path, ::File.basename(default))
|
|
39
|
+
end
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
private_constant :Path
|
|
43
|
+
end
|
|
44
|
+
end
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Sevgi
|
|
4
|
+
module Graphics
|
|
5
|
+
# Validates finite real numeric values used by Graphics APIs.
|
|
6
|
+
# @api private
|
|
7
|
+
module Scalar
|
|
8
|
+
# Converts one finite real value.
|
|
9
|
+
# @param value [Numeric] value to validate
|
|
10
|
+
# @param context [String] error context
|
|
11
|
+
# @param field [Symbol, Integer] field name or position
|
|
12
|
+
# @param positive [Boolean] require a strictly positive value
|
|
13
|
+
# @param nonnegative [Boolean] require a non-negative value
|
|
14
|
+
# @return [Float] validated value
|
|
15
|
+
# @raise [Sevgi::ArgumentError] when value is not a finite real number in the requested range
|
|
16
|
+
def self.finite(value, context:, field:, positive: false, nonnegative: false)
|
|
17
|
+
invalid(context, field, value) unless real?(value)
|
|
18
|
+
|
|
19
|
+
number = Float(value)
|
|
20
|
+
invalid(context, field, value) unless valid?(number, positive:, nonnegative:)
|
|
21
|
+
|
|
22
|
+
number
|
|
23
|
+
rescue ::ArgumentError, ::RangeError, ::TypeError
|
|
24
|
+
invalid(context, field, value)
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
# Converts one finite real value to an SVG number.
|
|
28
|
+
# @param value [Numeric] value to validate
|
|
29
|
+
# @param context [String] error context
|
|
30
|
+
# @param field [Symbol, Integer] field name or position
|
|
31
|
+
# @param positive [Boolean] require a strictly positive value
|
|
32
|
+
# @param nonnegative [Boolean] require a non-negative value
|
|
33
|
+
# @return [Integer, Float] normalized number with integral values represented as Integer
|
|
34
|
+
# @raise [Sevgi::ArgumentError] when value is not a finite real number
|
|
35
|
+
def self.number(value, context:, field:, positive: false, nonnegative: false)
|
|
36
|
+
if value.is_a?(::Integer)
|
|
37
|
+
invalid(context, field, value) unless valid?(value, positive:, nonnegative:)
|
|
38
|
+
return value
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
value = finite(value, context:, field:, positive:, nonnegative:)
|
|
42
|
+
value == value.to_i ? value.to_i : value
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
# Converts indexed finite real values to SVG numbers.
|
|
46
|
+
# @param values [Array<Numeric>] values to normalize
|
|
47
|
+
# @param context [String] error context
|
|
48
|
+
# @return [Array<(Integer, Float)>] normalized SVG numbers
|
|
49
|
+
# @raise [Sevgi::ArgumentError] when a value is not a finite real number
|
|
50
|
+
def self.numbers(values, context:)
|
|
51
|
+
values.each_with_index.map { |value, index| number(value, context:, field: index) }
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
class << self
|
|
55
|
+
private
|
|
56
|
+
|
|
57
|
+
def real?(value) = value.is_a?(::Numeric) && !value.is_a?(::Complex)
|
|
58
|
+
|
|
59
|
+
def valid?(number, positive:, nonnegative:)
|
|
60
|
+
number.finite? && (!positive || number.positive?) && (!nonnegative || number >= 0)
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
def invalid(context, field, value) = ArgumentError.("Invalid #{context} #{field}: #{value.inspect}")
|
|
64
|
+
end
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
private_constant :Scalar
|
|
68
|
+
end
|
|
69
|
+
end
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Sevgi
|
|
4
|
+
module Graphics
|
|
5
|
+
# Built-in paper dimensions and units.
|
|
6
|
+
# @api private
|
|
7
|
+
PAPER_SIZES = {
|
|
8
|
+
a0: [841, 1189, "mm"],
|
|
9
|
+
a1: [594, 841, "mm"],
|
|
10
|
+
a2: [420, 594, "mm"],
|
|
11
|
+
a3: [297, 420, "mm"],
|
|
12
|
+
a4: [210, 297, "mm"],
|
|
13
|
+
a5: [148, 210, "mm"],
|
|
14
|
+
a6: [105, 148, "mm"],
|
|
15
|
+
a7: [74, 105, "mm"],
|
|
16
|
+
a8: [52, 74, "mm"],
|
|
17
|
+
a9: [37, 52, "mm"],
|
|
18
|
+
a10: [26, 37, "mm"],
|
|
19
|
+
|
|
20
|
+
b0: [1000, 1414, "mm"],
|
|
21
|
+
b1: [707, 1000, "mm"],
|
|
22
|
+
b2: [500, 707, "mm"],
|
|
23
|
+
b3: [353, 500, "mm"],
|
|
24
|
+
b4: [250, 353, "mm"],
|
|
25
|
+
b5: [176, 250, "mm"],
|
|
26
|
+
b6: [125, 176, "mm"],
|
|
27
|
+
b7: [88, 125, "mm"],
|
|
28
|
+
b8: [62, 88, "mm"],
|
|
29
|
+
b9: [44, 62, "mm"],
|
|
30
|
+
b10: [31, 44, "mm"],
|
|
31
|
+
|
|
32
|
+
c0: [917, 1297, "mm"],
|
|
33
|
+
c1: [648, 917, "mm"],
|
|
34
|
+
c2: [458, 648, "mm"],
|
|
35
|
+
c3: [324, 458, "mm"],
|
|
36
|
+
c4: [229, 324, "mm"],
|
|
37
|
+
c5: [162, 229, "mm"],
|
|
38
|
+
c6: [114, 162, "mm"],
|
|
39
|
+
c7: [81, 114, "mm"],
|
|
40
|
+
c8: [57, 81, "mm"],
|
|
41
|
+
c9: [40, 57, "mm"],
|
|
42
|
+
c10: [28, 40, "mm"],
|
|
43
|
+
|
|
44
|
+
business: [85, 55, "mm"],
|
|
45
|
+
large: [130, 210, "mm"],
|
|
46
|
+
passport: [88, 125, "mm"],
|
|
47
|
+
pocket: [90, 140, "mm"],
|
|
48
|
+
travelers: [110, 210, "mm"],
|
|
49
|
+
us: [216, 279, "mm"],
|
|
50
|
+
xlarge: [190, 250, "mm"],
|
|
51
|
+
|
|
52
|
+
icon16: [16, 16, "px"],
|
|
53
|
+
icon32: [32, 32, "px"],
|
|
54
|
+
icon64: [64, 64, "px"],
|
|
55
|
+
icon128: [128, 128, "px"],
|
|
56
|
+
icon256: [256, 256, "px"],
|
|
57
|
+
icon512: [512, 512, "px"]
|
|
58
|
+
}.transform_values(&:freeze).freeze
|
|
59
|
+
|
|
60
|
+
private_constant :PAPER_SIZES
|
|
61
|
+
end
|
|
62
|
+
end
|
|
@@ -3,9 +3,11 @@
|
|
|
3
3
|
module Sevgi
|
|
4
4
|
module Graphics
|
|
5
5
|
module Document
|
|
6
|
-
#
|
|
6
|
+
# Abstract common document layer with the profile-independent DSL mixture set. It is not registered as a
|
|
7
|
+
# selectable profile. Advanced extensions may target this class through {Sevgi::Graphics::Mixtures.mixin}; doing
|
|
8
|
+
# so changes every descendant profile process-wide. Subclass it first when an extension should remain scoped.
|
|
7
9
|
class Base < Proto
|
|
8
|
-
document :
|
|
10
|
+
document nil, register: false
|
|
9
11
|
|
|
10
12
|
mixture :Call
|
|
11
13
|
mixture :Duplicate
|
|
@@ -21,6 +23,8 @@ module Sevgi
|
|
|
21
23
|
|
|
22
24
|
# Runs pre-render validation and lint checks.
|
|
23
25
|
# @param options [Hash] pre-render options
|
|
26
|
+
# @option options [Boolean] :validate run SVG standard validation
|
|
27
|
+
# @option options [Boolean] :lint run document lint checks
|
|
24
28
|
# @return [void]
|
|
25
29
|
# @raise [Sevgi::ValidationError] when validation fails
|
|
26
30
|
# @raise [Sevgi::Graphics::LintError] when linting fails
|