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.
Files changed (39) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +275 -2
  3. data/LICENSE +672 -3
  4. data/README.md +8 -8
  5. data/lib/sevgi/graphics/attribute.rb +271 -54
  6. data/lib/sevgi/graphics/auxiliary/canvas.rb +137 -58
  7. data/lib/sevgi/graphics/auxiliary/content.rb +200 -57
  8. data/lib/sevgi/graphics/auxiliary/margin.rb +33 -14
  9. data/lib/sevgi/graphics/auxiliary/paper.rb +118 -86
  10. data/lib/sevgi/graphics/auxiliary/path.rb +44 -0
  11. data/lib/sevgi/graphics/auxiliary/scalar.rb +69 -0
  12. data/lib/sevgi/graphics/auxiliary/sizes.rb +62 -0
  13. data/lib/sevgi/graphics/auxiliary.rb +3 -0
  14. data/lib/sevgi/graphics/document/base.rb +6 -2
  15. data/lib/sevgi/graphics/document/default.rb +1 -1
  16. data/lib/sevgi/graphics/document.rb +370 -124
  17. data/lib/sevgi/graphics/element.rb +143 -33
  18. data/lib/sevgi/graphics/mixtures/call.rb +249 -88
  19. data/lib/sevgi/graphics/mixtures/core.rb +143 -33
  20. data/lib/sevgi/graphics/mixtures/duplicate.rb +76 -22
  21. data/lib/sevgi/graphics/mixtures/export.rb +58 -12
  22. data/lib/sevgi/graphics/mixtures/hatch.rb +49 -7
  23. data/lib/sevgi/graphics/mixtures/identify.rb +30 -17
  24. data/lib/sevgi/graphics/mixtures/include.rb +40 -5
  25. data/lib/sevgi/graphics/mixtures/inkscape.rb +215 -46
  26. data/lib/sevgi/graphics/mixtures/rdf.rb +79 -7
  27. data/lib/sevgi/graphics/mixtures/render.rb +88 -19
  28. data/lib/sevgi/graphics/mixtures/save.rb +79 -26
  29. data/lib/sevgi/graphics/mixtures/symbols.rb +81 -9
  30. data/lib/sevgi/graphics/mixtures/tile.rb +88 -64
  31. data/lib/sevgi/graphics/mixtures/transform.rb +68 -28
  32. data/lib/sevgi/graphics/mixtures/underscore.rb +16 -7
  33. data/lib/sevgi/graphics/mixtures/validate.rb +14 -2
  34. data/lib/sevgi/graphics/mixtures/wrappers.rb +66 -24
  35. data/lib/sevgi/graphics/mixtures.rb +19 -13
  36. data/lib/sevgi/graphics/version.rb +1 -1
  37. data/lib/sevgi/graphics/xml.rb +127 -0
  38. data/lib/sevgi/graphics.rb +75 -28
  39. 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 [Sevgi::Graphics::Margin] margin to compare
30
- # @return [Integer, nil]
31
- def <=>(other) = deconstruct <=> other.deconstruct
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
- # Builds a margin from an array-like shorthand.
60
- # @param array [Object] value converted with Array()
61
- # @return [Sevgi::Graphics::Margin]
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.members.zip(values_for(values.compact.size, values).map { Float(it) }).to_h
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 profile name are invalid
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(:symbol!, :unit, unit),
30
- name: self.class.send(:symbol!, :name, name)
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) = deconstruct <=> other.deconstruct
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) = name.respond_to?(:to_sym) && profiles.key?(name.to_sym)
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 or replaces a named paper profile.
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 profile name is reserved or invalid
73
- def self.define(name, **spec)
74
- name = symbol!(:name, name)
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
- ArgumentError.("Paper name is reserved: #{name}") if reserved?(name) && !exist?(name)
128
+ class << self
129
+ private
77
130
 
78
- singleton_class.remove_method(name) if singleton_class.method_defined?(name, false)
79
- singleton_class.attr_reader(name)
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
- def self.reserved?(name) = @reserved.include?(name.to_sym)
134
+ define_singleton_method(name) { @mutex.synchronize { @profiles.fetch(name) } }
135
+ @accessors[name] = true
136
+ end
84
137
 
85
- def self.profiles = @profiles
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
- def self.dimension!(field, value)
88
- Float(value)
89
- rescue ::ArgumentError, ::TypeError
90
- ArgumentError.("Invalid paper #{field}: #{value.inspect}")
91
- end
143
+ next current
144
+ end
145
+
146
+ install(name)
147
+ @profiles[name] = profile
148
+ end
149
+ end
92
150
 
93
- def self.symbol!(field, value)
94
- value.to_sym
95
- rescue ::NoMethodError, ::TypeError
96
- ArgumentError.("Invalid paper #{field}: #{value.inspect}")
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
- private_class_method :dimension!, :profiles, :reserved?, :symbol!
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
- class << self
157
- alias_method :default, :a4
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
- profiles[:default] = profiles.fetch(:a4)
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
@@ -1,6 +1,9 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require_relative "auxiliary/scalar"
4
+ require_relative "auxiliary/path"
3
5
  require_relative "auxiliary/margin"
6
+ require_relative "auxiliary/sizes"
4
7
  require_relative "auxiliary/paper"
5
8
 
6
9
  require_relative "auxiliary/canvas"
@@ -3,9 +3,11 @@
3
3
  module Sevgi
4
4
  module Graphics
5
5
  module Document
6
- # Standard document profile with the full common DSL mixture set.
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 :base
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
@@ -4,7 +4,7 @@ module Sevgi
4
4
  module Graphics
5
5
  module Document
6
6
  # Default SVG document profile with XML preamble and SVG namespace.
7
- class Default < Minimal
7
+ class Default < Base
8
8
  document(
9
9
  :default,
10
10
  attributes: {