sevgi-graphics 1.0.0 → 1.1.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 +20 -0
- data/README.md +51 -0
- data/lib/sevgi/graphics/attribute.rb +7 -0
- data/lib/sevgi/graphics/auxiliary/paper.rb +52 -1
- data/lib/sevgi/graphics/auxiliary.rb +1 -2
- data/lib/sevgi/graphics/document/base.rb +1 -0
- data/lib/sevgi/graphics/mixtures/core.rb +9 -14
- data/lib/sevgi/graphics/mixtures/export.rb +6 -2
- data/lib/sevgi/graphics/mixtures/save.rb +2 -2
- data/lib/sevgi/graphics/mixtures/style/declaration.rb +117 -0
- data/lib/sevgi/graphics/mixtures/style.rb +36 -0
- data/lib/sevgi/graphics/mixtures.rb +1 -0
- data/lib/sevgi/graphics/version.rb +1 -1
- data/lib/sevgi/graphics.rb +1 -2
- metadata +20 -6
- data/lib/sevgi/graphics/auxiliary/path.rb +0 -44
- data/lib/sevgi/graphics/auxiliary/sizes.rb +0 -62
- /data/lib/sevgi/graphics/{xml.rb → auxiliary/xml.rb} +0 -0
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 9e205e418f5e60262a7e2cea0c4d32879942f7b238ea5fbf07bbbc01be5a9bbf
|
|
4
|
+
data.tar.gz: 889bb3fc5e72a27efba882624c3af23681a2811866d6c165f344edbb79173db7
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 67bcec199b5f5a1e607a316da7dd618792e07df4b9db5bdd86104373ac1772324eb9f1b268e3ae1ce28db413bcefb68db4d531e82098ffa112c9da998de9e9dd
|
|
7
|
+
data.tar.gz: ad2787c76d899b961dc73da906dcfb57ed38f94fc2f317c8ce0c0692ebbdd181260b1e97ee02efa871350ba9239ba53341c755919a02dd1860b8d6431357c6fb
|
data/CHANGELOG.md
CHANGED
|
@@ -3,6 +3,26 @@
|
|
|
3
3
|
This project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html) and follows
|
|
4
4
|
[Keep a Changelog](https://keepachangelog.com/en/1.1.0/).
|
|
5
5
|
|
|
6
|
+
## Unreleased
|
|
7
|
+
|
|
8
|
+
## 1.1.0 - 2026-10-29
|
|
9
|
+
|
|
10
|
+
### Added
|
|
11
|
+
|
|
12
|
+
- `Classes` returns an element's distinct CSS class names without changing the element; `Classify` shares its attribute normalization.
|
|
13
|
+
- Generic document-local Style declarations with `Target`, `Param`, `Group`, and `Profiles`, and optional prepared PDF export through the native Mainz CLI.
|
|
14
|
+
- Backend-independent `fallback: false` for export failures and `F.executable` for executable path lookup.
|
|
15
|
+
- `F.path` for path validation and expansion, and `F.output_path` for default output names and directory targets.
|
|
16
|
+
|
|
17
|
+
### Changed
|
|
18
|
+
|
|
19
|
+
- Moved Graphics XML validation into auxiliary helpers and kept built-in paper dimensions in `Paper`.
|
|
20
|
+
- Shared path validation between Graphics writers and Sundries exports while preserving output paths and error messages.
|
|
21
|
+
|
|
22
|
+
### Fixed
|
|
23
|
+
|
|
24
|
+
- Stamp code placeholders in nested and shared PDF Forms, with cycle-safe resource traversal.
|
|
25
|
+
|
|
6
26
|
## 1.0.0 - 2026-09-16
|
|
7
27
|
|
|
8
28
|
### Added
|
data/README.md
CHANGED
|
@@ -24,6 +24,57 @@ puts doc.Render
|
|
|
24
24
|
This focused gem exposes `Sevgi::Graphics.SVG` and lowercase component constructors. Install the umbrella `sevgi` gem
|
|
25
25
|
when you need the global `SVG` facade or the `.sevgi` script runner.
|
|
26
26
|
|
|
27
|
+
## Editable PDF styles
|
|
28
|
+
|
|
29
|
+
`Style` attaches an immutable producer declaration to an SVG document.
|
|
30
|
+
It does not change the SVG or load a PDF backend.
|
|
31
|
+
The lowercase `style` method still creates an SVG CSS element.
|
|
32
|
+
|
|
33
|
+
```ruby
|
|
34
|
+
doc = Sevgi::Graphics.SVG width: 20, height: 20 do
|
|
35
|
+
g id: "outline" do
|
|
36
|
+
rect width: 10, height: 10, fill: "none", stroke: "black"
|
|
37
|
+
end
|
|
38
|
+
Style do
|
|
39
|
+
Target "ink", selectors: ["#outline"]
|
|
40
|
+
Param "display", targets: ["ink"], property: "display",
|
|
41
|
+
schema: {type: "string", enum: %w[inline none]}
|
|
42
|
+
Param "opacity", targets: ["ink"], property: "stroke-opacity",
|
|
43
|
+
schema: {type: "number", minimum: 0, maximum: 1}
|
|
44
|
+
Group "Ink", parameters: ["opacity"]
|
|
45
|
+
Profiles "print", name: "Printer", parameters: ["opacity"], profiles: [
|
|
46
|
+
{id: "original", name: "Original", overrides: {}},
|
|
47
|
+
{id: "light", name: "Light", overrides: {opacity: 0.4}}
|
|
48
|
+
]
|
|
49
|
+
end
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
doc.PDF "drawing.pdf"
|
|
53
|
+
doc.PDF "prepared.pdf", fallback: false
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
`PDF` uses Mainz automatically when the document has a Style declaration.
|
|
57
|
+
If Mainz is unavailable or preparation fails, the default mode warns and exports an ordinary PDF.
|
|
58
|
+
`fallback: false` raises `Sevgi::Sundries::Mainz::Error` instead.
|
|
59
|
+
An unstyled PDF uses ordinary export without a Mainz warning.
|
|
60
|
+
PNG and SVG output retain their ordinary behavior.
|
|
61
|
+
|
|
62
|
+
`Target` selects whole SVG groups or direct `use` placements with `#id` or `#id > use.class` selectors.
|
|
63
|
+
Its optional `fill: true` allows eligible closed paths to acquire a fill.
|
|
64
|
+
`Param` binds one supported paint property to targets and supplies its value schema.
|
|
65
|
+
`Group` orders parameter references. `Profiles` records named sets of overrides.
|
|
66
|
+
Every target requires a `display` parameter.
|
|
67
|
+
`Param` accepts an optional `value:` to assign an override after Mainz captures original paint.
|
|
68
|
+
For an authored hidden target, `Target` accepts `display: "none"`.
|
|
69
|
+
Mainz owns selector resolution, supported properties, binding admission, and PDF baselines.
|
|
70
|
+
The [declaration contract](https://github.com/roktas/mainz/blob/v1.0.0/lib/doc/SPEC.md#71-svg-declarations) defines those fields.
|
|
71
|
+
|
|
72
|
+
`doc.Style` returns the declaration as deeply frozen JSON data with String keys.
|
|
73
|
+
`doc.Style declaration_hash` accepts the same fields without a builder block.
|
|
74
|
+
Applications supply their own parameter names, schemas, profiles, and publication requirements.
|
|
75
|
+
Separate documents have separate declarations. Document copies safely share an immutable declaration.
|
|
76
|
+
Invalid Ruby declarations raise `Sevgi::ArgumentError` before export.
|
|
77
|
+
|
|
27
78
|
## Ruby compatibility
|
|
28
79
|
|
|
29
80
|
Requires Ruby 3.4.0 or newer. CI verifies the current Ruby 3.4 release and the development Ruby from `.ruby-version`.
|
|
@@ -166,6 +166,13 @@ module Sevgi
|
|
|
166
166
|
XML.text(text, context: "XML attribute value")
|
|
167
167
|
end
|
|
168
168
|
|
|
169
|
+
# Returns distinct class names in rendered order from nested attribute values.
|
|
170
|
+
# @param values [Array<Object>] class values to combine
|
|
171
|
+
# @return [Array<String>] owned class-name snapshot
|
|
172
|
+
# @raise [Sevgi::ArgumentError] when a value is invalid, cyclic, or cannot be stringified as XML
|
|
173
|
+
# @api private
|
|
174
|
+
def self.classes(*values) = xml_text(values).split.uniq
|
|
175
|
+
|
|
169
176
|
# Mutable backing store for SVG attributes.
|
|
170
177
|
# @api private
|
|
171
178
|
class Store
|
|
@@ -180,7 +180,58 @@ module Sevgi
|
|
|
180
180
|
|
|
181
181
|
@reserved = methods.map(&:to_sym).freeze
|
|
182
182
|
|
|
183
|
-
|
|
183
|
+
{
|
|
184
|
+
a0: [841, 1189, "mm"],
|
|
185
|
+
a1: [594, 841, "mm"],
|
|
186
|
+
a2: [420, 594, "mm"],
|
|
187
|
+
a3: [297, 420, "mm"],
|
|
188
|
+
a4: [210, 297, "mm"],
|
|
189
|
+
a5: [148, 210, "mm"],
|
|
190
|
+
a6: [105, 148, "mm"],
|
|
191
|
+
a7: [74, 105, "mm"],
|
|
192
|
+
a8: [52, 74, "mm"],
|
|
193
|
+
a9: [37, 52, "mm"],
|
|
194
|
+
a10: [26, 37, "mm"],
|
|
195
|
+
|
|
196
|
+
b0: [1000, 1414, "mm"],
|
|
197
|
+
b1: [707, 1000, "mm"],
|
|
198
|
+
b2: [500, 707, "mm"],
|
|
199
|
+
b3: [353, 500, "mm"],
|
|
200
|
+
b4: [250, 353, "mm"],
|
|
201
|
+
b5: [176, 250, "mm"],
|
|
202
|
+
b6: [125, 176, "mm"],
|
|
203
|
+
b7: [88, 125, "mm"],
|
|
204
|
+
b8: [62, 88, "mm"],
|
|
205
|
+
b9: [44, 62, "mm"],
|
|
206
|
+
b10: [31, 44, "mm"],
|
|
207
|
+
|
|
208
|
+
c0: [917, 1297, "mm"],
|
|
209
|
+
c1: [648, 917, "mm"],
|
|
210
|
+
c2: [458, 648, "mm"],
|
|
211
|
+
c3: [324, 458, "mm"],
|
|
212
|
+
c4: [229, 324, "mm"],
|
|
213
|
+
c5: [162, 229, "mm"],
|
|
214
|
+
c6: [114, 162, "mm"],
|
|
215
|
+
c7: [81, 114, "mm"],
|
|
216
|
+
c8: [57, 81, "mm"],
|
|
217
|
+
c9: [40, 57, "mm"],
|
|
218
|
+
c10: [28, 40, "mm"],
|
|
219
|
+
|
|
220
|
+
business: [85, 55, "mm"],
|
|
221
|
+
large: [130, 210, "mm"],
|
|
222
|
+
passport: [88, 125, "mm"],
|
|
223
|
+
pocket: [90, 140, "mm"],
|
|
224
|
+
travelers: [110, 210, "mm"],
|
|
225
|
+
us: [216, 279, "mm"],
|
|
226
|
+
xlarge: [190, 250, "mm"],
|
|
227
|
+
|
|
228
|
+
icon16: [16, 16, "px"],
|
|
229
|
+
icon32: [32, 32, "px"],
|
|
230
|
+
icon64: [64, 64, "px"],
|
|
231
|
+
icon128: [128, 128, "px"],
|
|
232
|
+
icon256: [256, 256, "px"],
|
|
233
|
+
icon512: [512, 512, "px"]
|
|
234
|
+
}.each { |name, (width, height, unit)| define(name, width:, height:, unit:) }
|
|
184
235
|
|
|
185
236
|
# Returns the default paper profile.
|
|
186
237
|
# @return [Sevgi::Graphics::Paper]
|
|
@@ -1,9 +1,8 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
+
require_relative "auxiliary/xml"
|
|
3
4
|
require_relative "auxiliary/scalar"
|
|
4
|
-
require_relative "auxiliary/path"
|
|
5
5
|
require_relative "auxiliary/margin"
|
|
6
|
-
require_relative "auxiliary/sizes"
|
|
7
6
|
require_relative "auxiliary/paper"
|
|
8
7
|
|
|
9
8
|
require_relative "auxiliary/canvas"
|
|
@@ -93,24 +93,19 @@ module Sevgi
|
|
|
93
93
|
tap { Adoption.batch(elements, self, front: false) }
|
|
94
94
|
end
|
|
95
95
|
|
|
96
|
+
# Returns distinct CSS class names without changing this element.
|
|
97
|
+
# An absent class attribute returns an empty Array. Strings are split on whitespace and nested Arrays are flattened.
|
|
98
|
+
# @return [Array<String>] owned class-name snapshot in rendered order
|
|
99
|
+
# @see #Classify
|
|
100
|
+
def Classes = Attribute.classes(self[:class])
|
|
101
|
+
|
|
96
102
|
# Adds CSS classes without duplicating existing values.
|
|
97
103
|
# @param classes [Array<String, Symbol, Array>] class tokens. Strings are split on whitespace
|
|
98
104
|
# @return [Sevgi::Graphics::Element] self
|
|
105
|
+
# @raise [Sevgi::ArgumentError] when a class value is invalid, cyclic, or cannot be stringified as XML
|
|
106
|
+
# @see #Classes
|
|
99
107
|
def Classify(*classes)
|
|
100
|
-
tap
|
|
101
|
-
tokens = proc do |value|
|
|
102
|
-
case value
|
|
103
|
-
when nil
|
|
104
|
-
[]
|
|
105
|
-
when ::Array
|
|
106
|
-
value.flat_map { tokens.call(it) }
|
|
107
|
-
else
|
|
108
|
-
value.to_s.split
|
|
109
|
-
end
|
|
110
|
-
end
|
|
111
|
-
|
|
112
|
-
self[:class] = [*tokens.call(self[:class]), *tokens.call(classes)].uniq
|
|
113
|
-
end
|
|
108
|
+
tap { self[:class] = Attribute.classes(self[:class], classes) }
|
|
114
109
|
end
|
|
115
110
|
|
|
116
111
|
# Assigns default attributes only when they are absent.
|
|
@@ -10,7 +10,7 @@ module Sevgi
|
|
|
10
10
|
# Validates the closed export option channel before optional components load or rendering starts.
|
|
11
11
|
# @api private
|
|
12
12
|
module Options
|
|
13
|
-
KEYS = %i[css default dpi height width].freeze
|
|
13
|
+
KEYS = %i[css default dpi fallback height style width].freeze
|
|
14
14
|
private_constant :KEYS
|
|
15
15
|
|
|
16
16
|
# Returns validated export options.
|
|
@@ -36,6 +36,8 @@ module Sevgi
|
|
|
36
36
|
# @option kwargs [Numeric, nil] :height finite positive target height in CSS pixels
|
|
37
37
|
# @option kwargs [Numeric] :dpi (96.0) finite positive CSS pixel density
|
|
38
38
|
# @option kwargs [String, nil] :css CSS inserted before rendering
|
|
39
|
+
# @option kwargs [Hash, nil] :style (document Style) producer declaration
|
|
40
|
+
# @option kwargs [Boolean] :fallback (true) warn and use ordinary PDF after preparation failure
|
|
39
41
|
# @yield [svg] transforms SVG source before rendering
|
|
40
42
|
# @yieldparam svg [String] rendered SVG source
|
|
41
43
|
# @yieldreturn [String] transformed SVG source
|
|
@@ -46,6 +48,7 @@ module Sevgi
|
|
|
46
48
|
# @raise [Sevgi::MissingComponentError] when sevgi/sundries is unavailable
|
|
47
49
|
# @raise [Sevgi::MissingComponentError] when native export gems are unavailable
|
|
48
50
|
# @raise [Sevgi::Sundries::Export::ExportError] when native export fails
|
|
51
|
+
# @raise [Sevgi::Sundries::Mainz::Error] when required preparation cannot complete
|
|
49
52
|
# @raise [SystemCallError] when the output directory or file cannot be created or written
|
|
50
53
|
def PDF(path = nil, **kwargs, &block)
|
|
51
54
|
kwargs = Options.(kwargs)
|
|
@@ -58,6 +61,7 @@ module Sevgi
|
|
|
58
61
|
MissingComponentError.("sevgi/sundries")
|
|
59
62
|
end
|
|
60
63
|
|
|
64
|
+
kwargs[:style] = self.Style unless kwargs.key?(:style) || self.Style.nil?
|
|
61
65
|
Export(path, **kwargs, format: :pdf, &block)
|
|
62
66
|
end
|
|
63
67
|
|
|
@@ -104,7 +108,7 @@ module Sevgi
|
|
|
104
108
|
default = F.subext(extension, caller_locations(2..2).first.path)
|
|
105
109
|
end
|
|
106
110
|
|
|
107
|
-
path =
|
|
111
|
+
path = F.output_path(path, default:, context: "Export")
|
|
108
112
|
|
|
109
113
|
Sundries::Export.(call, path, **kwargs, &block)
|
|
110
114
|
end
|
|
@@ -74,7 +74,7 @@ module Sevgi
|
|
|
74
74
|
# @see Sevgi::Graphics::Document::Proto#call
|
|
75
75
|
def Save(path = nil, default: nil, backup_suffix: nil, **kwargs, &filter)
|
|
76
76
|
default = F.subext(EXT, caller_locations(1..1).first.path) if default.nil?
|
|
77
|
-
path =
|
|
77
|
+
path = F.output_path(path, default:, context: "Save")
|
|
78
78
|
|
|
79
79
|
Writer.(path, self.(**kwargs), backup_suffix:, &filter)
|
|
80
80
|
end
|
|
@@ -94,7 +94,7 @@ module Sevgi
|
|
|
94
94
|
# @raise [SystemCallError] when the destination cannot be read or written
|
|
95
95
|
# @see Sevgi::Graphics::Document::Proto#call
|
|
96
96
|
def Write(path, **kwargs, &filter)
|
|
97
|
-
path =
|
|
97
|
+
path = F.path(path, context: "Write path")
|
|
98
98
|
ArgumentError.("Write path must name a file") if ::File.directory?(path)
|
|
99
99
|
|
|
100
100
|
Writer.(path, self.(**kwargs), &filter)
|
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "json"
|
|
4
|
+
|
|
5
|
+
module Sevgi
|
|
6
|
+
module Graphics
|
|
7
|
+
module Mixtures
|
|
8
|
+
module Style
|
|
9
|
+
# Immutable authoring declaration for editable PDF paint properties.
|
|
10
|
+
# Source selectors stay here; the producer resolves them to PDF targets and baselines.
|
|
11
|
+
# @api private
|
|
12
|
+
class Declaration
|
|
13
|
+
# Captures a declaration without changing the SVG or loading a PDF backend.
|
|
14
|
+
# @param definition [Hash] targets, parameters, optional groups and profileSets
|
|
15
|
+
# @yield evaluates authoring operations in a private builder
|
|
16
|
+
# @yieldreturn [Object] ignored block result
|
|
17
|
+
# @return [void]
|
|
18
|
+
# @raise [Sevgi::ArgumentError] when the declaration is not finite, acyclic JSON data
|
|
19
|
+
def initialize(definition = {}, &block)
|
|
20
|
+
ArgumentError.("Style declaration must be a Hash") unless definition.is_a?(::Hash)
|
|
21
|
+
ArgumentError.("Style accepts a declaration or a block") if block && !definition.empty?
|
|
22
|
+
|
|
23
|
+
definition = Builder.new.call(&block) if block
|
|
24
|
+
@definition = Snapshot.capture(definition)
|
|
25
|
+
validate
|
|
26
|
+
freeze
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
# Returns the owned, deeply frozen declaration with JSON String keys.
|
|
30
|
+
# @return [Hash{String => Object}] producer authoring declaration
|
|
31
|
+
def to_h = @definition
|
|
32
|
+
|
|
33
|
+
private
|
|
34
|
+
|
|
35
|
+
def validate
|
|
36
|
+
unknown = @definition.keys - %w[targets parameters groups profileSets]
|
|
37
|
+
ArgumentError.("Unknown Style field: #{unknown.first}") unless unknown.empty?
|
|
38
|
+
return if %w[targets parameters].all? { @definition[it].is_a?(::Hash) }
|
|
39
|
+
ArgumentError.("Style requires targets and parameters")
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
# Authoring operations kept outside the SVG method namespace.
|
|
43
|
+
# @api private
|
|
44
|
+
class Builder
|
|
45
|
+
def initialize
|
|
46
|
+
@definition = {targets: {}, parameters: {}, groups: [], profileSets: []}
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
def call(&block)
|
|
50
|
+
instance_exec(&block)
|
|
51
|
+
@definition
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
def Target(id, selectors:, fill: false, display: Undefined)
|
|
55
|
+
record = {selectors:, fill:}
|
|
56
|
+
record[:display] = display unless display.equal?(Undefined)
|
|
57
|
+
add(:targets, id, record)
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
def Param(id, targets:, property:, schema:, value: Undefined)
|
|
61
|
+
record = {schema:, binding: {targets:, property:}}
|
|
62
|
+
record[:value] = value unless value.equal?(Undefined)
|
|
63
|
+
add(:parameters, id, record)
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
def Group(title, parameters:, description: nil)
|
|
67
|
+
record = {title:, parameters:}
|
|
68
|
+
record[:description] = description unless description.nil?
|
|
69
|
+
@definition[:groups] << record
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
def Profiles(id, name:, parameters:, profiles:, description: nil)
|
|
73
|
+
record = {id:, name:, parameters:, profiles:}
|
|
74
|
+
record[:description] = description unless description.nil?
|
|
75
|
+
@definition[:profileSets] << record
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
private
|
|
79
|
+
|
|
80
|
+
def add(field, id, value)
|
|
81
|
+
unless (id.is_a?(::String) || id.is_a?(::Symbol)) && !id.to_s.empty?
|
|
82
|
+
ArgumentError.("Style id must be nonempty text")
|
|
83
|
+
end
|
|
84
|
+
id = id.to_s
|
|
85
|
+
ArgumentError.("Duplicate Style id: #{id}") if @definition[field].key?(id)
|
|
86
|
+
@definition[field][id] = value
|
|
87
|
+
end
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
# JSON ownership rules differ from XML profile metadata.
|
|
91
|
+
# @api private
|
|
92
|
+
module Snapshot
|
|
93
|
+
def self.capture(value)
|
|
94
|
+
json = JSON.generate(value, strict: true, max_nesting: 32)
|
|
95
|
+
freeze_tree(JSON.parse(json, max_nesting: 32))
|
|
96
|
+
rescue JSON::JSONError => e
|
|
97
|
+
ArgumentError.("Invalid Style JSON: #{e.message}")
|
|
98
|
+
end
|
|
99
|
+
|
|
100
|
+
def self.freeze_tree(value)
|
|
101
|
+
case value
|
|
102
|
+
when ::Hash then value.each_value { freeze_tree(it) }
|
|
103
|
+
when ::Array then value.each { freeze_tree(it) }
|
|
104
|
+
end
|
|
105
|
+
value.freeze
|
|
106
|
+
end
|
|
107
|
+
|
|
108
|
+
private_class_method :freeze_tree
|
|
109
|
+
end
|
|
110
|
+
|
|
111
|
+
private_constant :Builder, :Snapshot
|
|
112
|
+
end
|
|
113
|
+
private_constant :Declaration
|
|
114
|
+
end
|
|
115
|
+
end
|
|
116
|
+
end
|
|
117
|
+
end
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "style/declaration"
|
|
4
|
+
|
|
5
|
+
module Sevgi
|
|
6
|
+
module Graphics
|
|
7
|
+
module Mixtures
|
|
8
|
+
# Document-local declarations for editable PDF export.
|
|
9
|
+
module Style
|
|
10
|
+
# Reads or replaces this document's Style declaration.
|
|
11
|
+
# A declaration changes export metadata, not rendered SVG appearance.
|
|
12
|
+
# Ordinary document copies safely share the immutable declaration.
|
|
13
|
+
# @param definition [Hash, Sevgi::Undefined] declaration, or omitted for a read
|
|
14
|
+
# @yield defines Target, Param, Group, and Profiles records
|
|
15
|
+
# @yieldreturn [Object] ignored block result
|
|
16
|
+
# @return [Hash, nil, Sevgi::Graphics::Element] frozen JSON declaration on read, self on assignment
|
|
17
|
+
# @raise [Sevgi::ArgumentError] when called outside a document root or with an invalid declaration
|
|
18
|
+
# @example Declare an editable outline
|
|
19
|
+
# drawing.Style do
|
|
20
|
+
# Target "ink", selectors: ["#outline"]
|
|
21
|
+
# Param "display", targets: ["ink"], property: "display",
|
|
22
|
+
# schema: {type: "string", enum: %w[inline none]}
|
|
23
|
+
# Param "opacity", targets: ["ink"], property: "stroke-opacity",
|
|
24
|
+
# schema: {type: "number", minimum: 0, maximum: 1}
|
|
25
|
+
# end
|
|
26
|
+
def Style(definition = Undefined, &block)
|
|
27
|
+
ArgumentError.("Style belongs to a document root") unless Root?()
|
|
28
|
+
return @style&.to_h if definition.equal?(Undefined) && !block
|
|
29
|
+
definition = {} if definition.equal?(Undefined)
|
|
30
|
+
@style = Declaration.new(definition, &block)
|
|
31
|
+
self
|
|
32
|
+
end
|
|
33
|
+
end
|
|
34
|
+
end
|
|
35
|
+
end
|
|
36
|
+
end
|
|
@@ -63,6 +63,7 @@ require_relative "mixtures/polyfills"
|
|
|
63
63
|
require_relative "mixtures/rdf"
|
|
64
64
|
require_relative "mixtures/render"
|
|
65
65
|
require_relative "mixtures/save"
|
|
66
|
+
require_relative "mixtures/style"
|
|
66
67
|
require_relative "mixtures/symbols"
|
|
67
68
|
require_relative "mixtures/tile"
|
|
68
69
|
require_relative "mixtures/transform"
|
data/lib/sevgi/graphics.rb
CHANGED
|
@@ -2,9 +2,8 @@
|
|
|
2
2
|
|
|
3
3
|
require "sevgi/function"
|
|
4
4
|
|
|
5
|
-
require_relative "graphics/xml"
|
|
6
|
-
require_relative "graphics/attribute"
|
|
7
5
|
require_relative "graphics/auxiliary"
|
|
6
|
+
require_relative "graphics/attribute"
|
|
8
7
|
require_relative "graphics/element"
|
|
9
8
|
require_relative "graphics/mixtures"
|
|
10
9
|
|
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: sevgi-graphics
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 1.
|
|
4
|
+
version: 1.1.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Recai Oktaş
|
|
@@ -9,20 +9,34 @@ bindir: bin
|
|
|
9
9
|
cert_chain: []
|
|
10
10
|
date: 1980-01-02 00:00:00.000000000 Z
|
|
11
11
|
dependencies:
|
|
12
|
+
- !ruby/object:Gem::Dependency
|
|
13
|
+
name: json
|
|
14
|
+
requirement: !ruby/object:Gem::Requirement
|
|
15
|
+
requirements:
|
|
16
|
+
- - ">="
|
|
17
|
+
- !ruby/object:Gem::Version
|
|
18
|
+
version: '3.0'
|
|
19
|
+
type: :runtime
|
|
20
|
+
prerelease: false
|
|
21
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
22
|
+
requirements:
|
|
23
|
+
- - ">="
|
|
24
|
+
- !ruby/object:Gem::Version
|
|
25
|
+
version: '3.0'
|
|
12
26
|
- !ruby/object:Gem::Dependency
|
|
13
27
|
name: sevgi-function
|
|
14
28
|
requirement: !ruby/object:Gem::Requirement
|
|
15
29
|
requirements:
|
|
16
30
|
- - '='
|
|
17
31
|
- !ruby/object:Gem::Version
|
|
18
|
-
version: 1.
|
|
32
|
+
version: 1.1.0
|
|
19
33
|
type: :runtime
|
|
20
34
|
prerelease: false
|
|
21
35
|
version_requirements: !ruby/object:Gem::Requirement
|
|
22
36
|
requirements:
|
|
23
37
|
- - '='
|
|
24
38
|
- !ruby/object:Gem::Version
|
|
25
|
-
version: 1.
|
|
39
|
+
version: 1.1.0
|
|
26
40
|
description: Builds and renders SVG document trees from Ruby.
|
|
27
41
|
email: roktas@gmail.com
|
|
28
42
|
executables: []
|
|
@@ -39,9 +53,8 @@ files:
|
|
|
39
53
|
- lib/sevgi/graphics/auxiliary/content.rb
|
|
40
54
|
- lib/sevgi/graphics/auxiliary/margin.rb
|
|
41
55
|
- lib/sevgi/graphics/auxiliary/paper.rb
|
|
42
|
-
- lib/sevgi/graphics/auxiliary/path.rb
|
|
43
56
|
- lib/sevgi/graphics/auxiliary/scalar.rb
|
|
44
|
-
- lib/sevgi/graphics/auxiliary/
|
|
57
|
+
- lib/sevgi/graphics/auxiliary/xml.rb
|
|
45
58
|
- lib/sevgi/graphics/document.rb
|
|
46
59
|
- lib/sevgi/graphics/document/base.rb
|
|
47
60
|
- lib/sevgi/graphics/document/default.rb
|
|
@@ -63,6 +76,8 @@ files:
|
|
|
63
76
|
- lib/sevgi/graphics/mixtures/rdf.rb
|
|
64
77
|
- lib/sevgi/graphics/mixtures/render.rb
|
|
65
78
|
- lib/sevgi/graphics/mixtures/save.rb
|
|
79
|
+
- lib/sevgi/graphics/mixtures/style.rb
|
|
80
|
+
- lib/sevgi/graphics/mixtures/style/declaration.rb
|
|
66
81
|
- lib/sevgi/graphics/mixtures/symbols.rb
|
|
67
82
|
- lib/sevgi/graphics/mixtures/tile.rb
|
|
68
83
|
- lib/sevgi/graphics/mixtures/transform.rb
|
|
@@ -70,7 +85,6 @@ files:
|
|
|
70
85
|
- lib/sevgi/graphics/mixtures/validate.rb
|
|
71
86
|
- lib/sevgi/graphics/mixtures/wrappers.rb
|
|
72
87
|
- lib/sevgi/graphics/version.rb
|
|
73
|
-
- lib/sevgi/graphics/xml.rb
|
|
74
88
|
homepage: https://sevgi.roktas.dev
|
|
75
89
|
licenses:
|
|
76
90
|
- GPL-3.0-or-later
|
|
@@ -1,44 +0,0 @@
|
|
|
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
|
|
@@ -1,62 +0,0 @@
|
|
|
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
|
|
File without changes
|