sevgi-graphics 0.94.0 → 0.95.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 +4 -4
- data/CHANGELOG.md +150 -2
- data/LICENSE +672 -3
- data/README.md +1 -1
- data/lib/sevgi/graphics/attribute.rb +132 -38
- data/lib/sevgi/graphics/auxiliary/canvas.rb +38 -16
- data/lib/sevgi/graphics/auxiliary/content.rb +152 -29
- data/lib/sevgi/graphics/auxiliary/margin.rb +14 -2
- data/lib/sevgi/graphics/auxiliary/paper.rb +91 -84
- data/lib/sevgi/graphics/auxiliary/scalar.rb +40 -0
- data/lib/sevgi/graphics/auxiliary/sizes.rb +62 -0
- data/lib/sevgi/graphics/auxiliary.rb +2 -0
- data/lib/sevgi/graphics/document.rb +186 -74
- data/lib/sevgi/graphics/element.rb +41 -14
- data/lib/sevgi/graphics/mixtures/core.rb +89 -18
- data/lib/sevgi/graphics/mixtures/duplicate.rb +33 -13
- data/lib/sevgi/graphics/mixtures/export.rb +6 -0
- data/lib/sevgi/graphics/mixtures/include.rb +17 -0
- data/lib/sevgi/graphics/mixtures/inkscape.rb +15 -0
- data/lib/sevgi/graphics/mixtures/rdf.rb +20 -0
- data/lib/sevgi/graphics/mixtures/render.rb +56 -11
- data/lib/sevgi/graphics/mixtures/save.rb +9 -0
- data/lib/sevgi/graphics/mixtures/symbols.rb +3 -0
- data/lib/sevgi/graphics/mixtures/tile.rb +6 -0
- data/lib/sevgi/graphics/mixtures/underscore.rb +2 -2
- data/lib/sevgi/graphics/mixtures/validate.rb +12 -0
- data/lib/sevgi/graphics/mixtures/wrappers.rb +4 -1
- data/lib/sevgi/graphics/mixtures.rb +4 -0
- data/lib/sevgi/graphics/version.rb +1 -1
- data/lib/sevgi/graphics/xml.rb +127 -0
- data/lib/sevgi/graphics.rb +19 -18
- metadata +7 -4
|
@@ -15,12 +15,14 @@ module Sevgi
|
|
|
15
15
|
# @!attribute [r] left
|
|
16
16
|
# @return [Float] left margin
|
|
17
17
|
|
|
18
|
-
# Creates a margin from one to four shorthand values.
|
|
18
|
+
# Creates a margin from one to four shorthand values. Values must be finite real numbers greater than or equal to
|
|
19
|
+
# zero.
|
|
19
20
|
# @param top [Numeric, nil] top value or all-sides shorthand
|
|
20
21
|
# @param right [Numeric, nil] right value or horizontal shorthand
|
|
21
22
|
# @param bottom [Numeric, nil] bottom value
|
|
22
23
|
# @param left [Numeric, nil] left value
|
|
23
24
|
# @return [void]
|
|
25
|
+
# @raise [Sevgi::ArgumentError] when a supplied margin is not a finite, non-negative real number
|
|
24
26
|
def initialize(top: nil, right: nil, bottom: nil, left: nil)
|
|
25
27
|
super(**normalize(top, right, bottom, left))
|
|
26
28
|
end
|
|
@@ -54,6 +56,9 @@ module Sevgi
|
|
|
54
56
|
def vertical = top + bottom
|
|
55
57
|
|
|
56
58
|
alias_method :==, :eql?
|
|
59
|
+
|
|
60
|
+
# Returns margin values in top, right, bottom, left order.
|
|
61
|
+
# @return [Array<Float>]
|
|
57
62
|
alias_method :to_a, :deconstruct
|
|
58
63
|
|
|
59
64
|
# Builds a margin from an array-like shorthand.
|
|
@@ -74,7 +79,14 @@ module Sevgi
|
|
|
74
79
|
def normalize(top, right, bottom, left)
|
|
75
80
|
values = [top, right, bottom, left]
|
|
76
81
|
|
|
77
|
-
Margin
|
|
82
|
+
Margin
|
|
83
|
+
.members
|
|
84
|
+
.zip(
|
|
85
|
+
values_for(values.compact.size, values).map do |value|
|
|
86
|
+
Scalar.finite(value, context: "margin", field: :value, nonnegative: true)
|
|
87
|
+
end
|
|
88
|
+
)
|
|
89
|
+
.to_h
|
|
78
90
|
end
|
|
79
91
|
|
|
80
92
|
def values_for(size, values)
|
|
@@ -15,26 +15,30 @@ module Sevgi
|
|
|
15
15
|
# @!attribute [r] name
|
|
16
16
|
# @return [Symbol] profile name
|
|
17
17
|
|
|
18
|
-
# Creates a paper profile.
|
|
18
|
+
# Creates a paper profile. Dimensions must be finite real numbers greater than zero.
|
|
19
19
|
# @param width [Numeric] paper width
|
|
20
20
|
# @param height [Numeric] paper height
|
|
21
21
|
# @param unit [Symbol, String] SVG unit
|
|
22
22
|
# @param name [Symbol, String] profile name
|
|
23
|
+
# @param options [Hash] unsupported extra options
|
|
23
24
|
# @return [void]
|
|
24
|
-
# @raise [Sevgi::ArgumentError] when dimensions, unit, or
|
|
25
|
-
def initialize(width:, height:, unit: "mm", name: :custom)
|
|
25
|
+
# @raise [Sevgi::ArgumentError] when dimensions, unit, name, or options are invalid
|
|
26
|
+
def initialize(width:, height:, unit: "mm", name: :custom, **options)
|
|
27
|
+
self.class.send(:options!, options)
|
|
26
28
|
super(
|
|
27
29
|
width: self.class.send(:dimension!, :width, width),
|
|
28
30
|
height: self.class.send(:dimension!, :height, height),
|
|
29
|
-
unit: self.class.send(:
|
|
30
|
-
name: self.class.send(:
|
|
31
|
+
unit: self.class.send(:normalize!, :unit, unit),
|
|
32
|
+
name: self.class.send(:normalize!, :name, name)
|
|
31
33
|
)
|
|
32
34
|
end
|
|
33
35
|
|
|
34
36
|
# Compares papers by width, height, unit, then name.
|
|
35
37
|
# @param other [Sevgi::Graphics::Paper] paper to compare
|
|
36
|
-
# @return [Integer, nil]
|
|
37
|
-
def <=>(other)
|
|
38
|
+
# @return [Integer, nil] comparison result, or nil for a non-Paper operand
|
|
39
|
+
def <=>(other)
|
|
40
|
+
deconstruct <=> other.deconstruct if other.is_a?(self.class)
|
|
41
|
+
end
|
|
38
42
|
|
|
39
43
|
# Reports strict paper equality.
|
|
40
44
|
# @param other [Object] object to compare
|
|
@@ -56,108 +60,111 @@ module Sevgi
|
|
|
56
60
|
alias_method :==, :eql?
|
|
57
61
|
|
|
58
62
|
@profiles = {}
|
|
63
|
+
@accessors = {}
|
|
64
|
+
@mutex = ::Mutex.new
|
|
59
65
|
|
|
60
|
-
# Reports whether a named paper profile exists.
|
|
66
|
+
# Reports whether a normalizable named paper profile exists. Invalid converters return false.
|
|
61
67
|
# @param name [Object] profile name
|
|
62
68
|
# @return [Boolean]
|
|
63
|
-
def self.exist?(name)
|
|
69
|
+
def self.exist?(name)
|
|
70
|
+
name = normalize(name)
|
|
71
|
+
name ? @mutex.synchronize { @profiles.key?(name) } : false
|
|
72
|
+
end
|
|
64
73
|
|
|
65
|
-
# Defines
|
|
74
|
+
# Defines a named paper profile after complete validation. Registration is process-global and thread-atomic.
|
|
75
|
+
# Existing profiles are replaced by default; with `overwrite: false`, identical definitions return the canonical
|
|
76
|
+
# profile and conflicting definitions raise. Names that are not Ruby call syntax remain accessible through
|
|
77
|
+
# `public_send`.
|
|
66
78
|
# @param name [Symbol, String] profile name
|
|
79
|
+
# @param overwrite [Boolean] true to replace an existing profile
|
|
67
80
|
# @param spec [Hash] paper dimensions and unit
|
|
68
81
|
# @option spec [Numeric] :width paper width
|
|
69
82
|
# @option spec [Numeric] :height paper height
|
|
70
83
|
# @option spec [Symbol, String] :unit SVG unit
|
|
71
84
|
# @return [Sevgi::Graphics::Paper]
|
|
72
|
-
# @raise [Sevgi::ArgumentError] when the
|
|
73
|
-
|
|
74
|
-
|
|
85
|
+
# @raise [Sevgi::ArgumentError] when the name, dimensions, unit, overwrite flag, or options are reserved or invalid,
|
|
86
|
+
# or a non-bang definition conflicts with the registered profile
|
|
87
|
+
def self.define(name, overwrite: true, **spec)
|
|
88
|
+
name = normalize!(:name, name)
|
|
89
|
+
ArgumentError.("Paper name is reserved: #{name}") if reserved?(name)
|
|
90
|
+
overwrite = overwrite!(overwrite)
|
|
91
|
+
profile = new(name:, **spec)
|
|
92
|
+
|
|
93
|
+
register(name, profile, overwrite:)
|
|
94
|
+
end
|
|
75
95
|
|
|
76
|
-
|
|
96
|
+
def self.install(name)
|
|
97
|
+
return if @accessors.key?(name)
|
|
77
98
|
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
profiles[name] = instance_variable_set("@#{name}", new(name:, **spec))
|
|
99
|
+
define_singleton_method(name) { @mutex.synchronize { @profiles.fetch(name) } }
|
|
100
|
+
@accessors[name] = true
|
|
81
101
|
end
|
|
82
102
|
|
|
83
|
-
def self.
|
|
103
|
+
def self.register(name, profile, overwrite:)
|
|
104
|
+
@mutex.synchronize do
|
|
105
|
+
if !overwrite && (current = @profiles[name])
|
|
106
|
+
ArgumentError.("Paper already defined differently: #{name}") unless current == profile
|
|
84
107
|
|
|
85
|
-
|
|
108
|
+
next current
|
|
109
|
+
end
|
|
110
|
+
|
|
111
|
+
install(name)
|
|
112
|
+
@profiles[name] = profile
|
|
113
|
+
end
|
|
114
|
+
end
|
|
115
|
+
|
|
116
|
+
def self.reserved?(name) = @reserved.include?(name)
|
|
86
117
|
|
|
87
118
|
def self.dimension!(field, value)
|
|
88
|
-
|
|
89
|
-
rescue ::ArgumentError, ::TypeError
|
|
90
|
-
ArgumentError.("Invalid paper #{field}: #{value.inspect}")
|
|
119
|
+
Scalar.finite(value, context: "paper", field:, positive: true)
|
|
91
120
|
end
|
|
92
121
|
|
|
93
|
-
def self.
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
ArgumentError.("
|
|
122
|
+
def self.options!(options)
|
|
123
|
+
return if options.empty?
|
|
124
|
+
|
|
125
|
+
ArgumentError.("Unknown paper options: #{options.keys.join(", ")}")
|
|
126
|
+
end
|
|
127
|
+
|
|
128
|
+
def self.overwrite!(value)
|
|
129
|
+
return value if [true, false].include?(value)
|
|
130
|
+
|
|
131
|
+
ArgumentError.("Paper overwrite must be true or false")
|
|
132
|
+
end
|
|
133
|
+
|
|
134
|
+
def self.normalize(value)
|
|
135
|
+
normalized = value.to_sym if value.respond_to?(:to_sym)
|
|
136
|
+
normalized if normalized.is_a?(::Symbol)
|
|
137
|
+
rescue ::StandardError
|
|
138
|
+
nil
|
|
139
|
+
end
|
|
140
|
+
|
|
141
|
+
def self.normalize!(field, value)
|
|
142
|
+
normalize(value) || ArgumentError.("Invalid paper #{field}")
|
|
97
143
|
end
|
|
98
144
|
|
|
99
145
|
@reserved = methods.map(&:to_sym).freeze
|
|
100
146
|
|
|
101
|
-
private_class_method
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
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:) }
|
|
155
|
-
|
|
156
|
-
class << self
|
|
157
|
-
alias_method :default, :a4
|
|
147
|
+
private_class_method(
|
|
148
|
+
:dimension!,
|
|
149
|
+
:install,
|
|
150
|
+
:normalize,
|
|
151
|
+
:normalize!,
|
|
152
|
+
:options!,
|
|
153
|
+
:overwrite!,
|
|
154
|
+
:register,
|
|
155
|
+
:reserved?
|
|
156
|
+
)
|
|
157
|
+
|
|
158
|
+
PAPER_SIZES.each { |name, (width, height, unit)| define(name, width:, height:, unit:) }
|
|
159
|
+
|
|
160
|
+
# Returns the default paper profile.
|
|
161
|
+
# @return [Sevgi::Graphics::Paper]
|
|
162
|
+
def self.default
|
|
163
|
+
@mutex.synchronize { @profiles.fetch(:default) }
|
|
158
164
|
end
|
|
159
165
|
|
|
160
|
-
|
|
166
|
+
@accessors[:default] = true
|
|
167
|
+
@profiles[:default] = @profiles.fetch(:a4)
|
|
161
168
|
end
|
|
162
169
|
end
|
|
163
170
|
end
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Sevgi
|
|
4
|
+
module Graphics
|
|
5
|
+
# Validates finite real numeric values used by graphics auxiliaries.
|
|
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] field name
|
|
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
|
+
def self.real?(value) = value.is_a?(::Numeric) && !value.is_a?(::Complex)
|
|
28
|
+
|
|
29
|
+
def self.valid?(number, positive:, nonnegative:)
|
|
30
|
+
number.finite? && (!positive || number.positive?) && (!nonnegative || number >= 0)
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
def self.invalid(context, field, value) = ArgumentError.("Invalid #{context} #{field}: #{value.inspect}")
|
|
34
|
+
|
|
35
|
+
private_class_method :invalid, :real?, :valid?
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
private_constant :Scalar
|
|
39
|
+
end
|
|
40
|
+
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
|