sevgi-sundries 0.93.1 → 0.94.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 +4 -0
- data/LICENSE +5 -0
- data/README.md +52 -2
- data/lib/sevgi/sundries/export/native.rb +53 -56
- data/lib/sevgi/sundries/export.rb +115 -1
- data/lib/sevgi/sundries/grid.rb +0 -7
- data/lib/sevgi/sundries/ruler.rb +68 -16
- data/lib/sevgi/sundries/tile.rb +18 -1
- data/lib/sevgi/sundries/version.rb +1 -1
- data/lib/sevgi/sundries.rb +6 -0
- metadata +16 -28
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: e8ddc5568e2938adbef615c7bc12663083287016955cd708bd342ec95423bfea
|
|
4
|
+
data.tar.gz: 64b06db206dcf954131c8348e0e1871a39c80677f83a15fc683cc84ab943d2aa
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: de63cbabb66ebaa15dfbb78d4aa5cb14384fb8414c15cd13f4a5e1665c3e8a28d66bfef4e7014acb51c486024efcc1e545f04d10172b228716e27050ac1854ff
|
|
7
|
+
data.tar.gz: 9b74f16efebe20b184b0ba277d588e20403cb3633256868b03b8d9bcf608f846bb266283283bbf7316e65fb0de5c065d69e20ab3f5fd5c7c597ecf74b0f1bc57
|
data/CHANGELOG.md
ADDED
data/LICENSE
ADDED
data/README.md
CHANGED
|
@@ -1,5 +1,55 @@
|
|
|
1
1
|
# Sevgi Sundries
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
Helper objects and export tools for Sevgi drawings.
|
|
4
4
|
|
|
5
|
-
|
|
5
|
+
## Install
|
|
6
|
+
|
|
7
|
+
```sh
|
|
8
|
+
gem install sevgi-sundries
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
## Require
|
|
12
|
+
|
|
13
|
+
```ruby
|
|
14
|
+
require "sevgi/sundries"
|
|
15
|
+
```
|
|
16
|
+
|
|
17
|
+
## Example
|
|
18
|
+
|
|
19
|
+
```ruby
|
|
20
|
+
rect = Sevgi::Geometry::Rect[3, 5]
|
|
21
|
+
tile = Sevgi::Sundries::Tile.new(rect)
|
|
22
|
+
tile.box.height
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
## Ruby compatibility
|
|
26
|
+
|
|
27
|
+
Requires Ruby 3.4.0 or newer. CI verifies Ruby 3.4 and the current development Ruby from `.ruby-version`.
|
|
28
|
+
|
|
29
|
+
## Native prerequisites
|
|
30
|
+
|
|
31
|
+
Basic ruler, grid, and tile helpers need only Ruby dependencies. Installing `sevgi-sundries` does not install native
|
|
32
|
+
export gems.
|
|
33
|
+
|
|
34
|
+
PDF/PNG export helpers load the optional Ruby gems `cairo`, `rsvg2`, and `hexapdf` only when export is used. Install
|
|
35
|
+
their system libraries and gems separately:
|
|
36
|
+
|
|
37
|
+
```sh
|
|
38
|
+
sudo apt-get update
|
|
39
|
+
sudo apt-get install -y libcairo2-dev libgdk-pixbuf-2.0-dev libgirepository1.0-dev libglib2.0-dev librsvg2-dev pkg-config
|
|
40
|
+
gem install cairo rsvg2 hexapdf
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
On macOS with Homebrew:
|
|
44
|
+
|
|
45
|
+
```sh
|
|
46
|
+
brew install cairo gdk-pixbuf gobject-introspection librsvg pkg-config
|
|
47
|
+
gem install cairo rsvg2 hexapdf
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
## Links
|
|
51
|
+
|
|
52
|
+
- Documentation: https://sevgi.roktas.dev
|
|
53
|
+
- API documentation: https://www.rubydoc.info/gems/sevgi-sundries
|
|
54
|
+
- Source: https://github.com/roktas/sevgi/tree/main/sundries
|
|
55
|
+
- Changelog: https://github.com/roktas/sevgi/blob/main/CHANGELOG.md
|
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
+
require_relative "../export"
|
|
4
|
+
|
|
3
5
|
require "cairo"
|
|
4
6
|
require "fileutils"
|
|
5
7
|
require "hexapdf"
|
|
@@ -8,13 +10,12 @@ require "tempfile"
|
|
|
8
10
|
|
|
9
11
|
module Sevgi
|
|
10
12
|
module Sundries
|
|
11
|
-
# Exports SVG content and post-processes PDF output.
|
|
12
13
|
module Export
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
14
|
+
%i[
|
|
15
|
+
call
|
|
16
|
+
stamp
|
|
17
|
+
stamp!
|
|
18
|
+
].each { remove_method(it) if method_defined?(it) }
|
|
18
19
|
|
|
19
20
|
# Exports SVG source to a PDF or PNG file using librsvg and Cairo.
|
|
20
21
|
# @param svg [String] SVG source content
|
|
@@ -30,7 +31,7 @@ module Sevgi
|
|
|
30
31
|
# @return [Object] the original output argument
|
|
31
32
|
# @raise [Sevgi::ArgumentError] when SVG content is not a string or output is blank
|
|
32
33
|
# @raise [Sevgi::Sundries::Export::ExportError] when format, SVG parsing, SVG dimensions, or render dimensions are invalid
|
|
33
|
-
def
|
|
34
|
+
def call(svg, output, format: nil, width: nil, height: nil, dpi: DEFAULT_DPI, css: nil, &block)
|
|
34
35
|
ArgumentError.("SVG content must be a String") unless svg.is_a?(String)
|
|
35
36
|
ArgumentError.("Export output must be provided") if output.nil? || output.to_s.strip.empty?
|
|
36
37
|
|
|
@@ -71,81 +72,49 @@ module Sevgi
|
|
|
71
72
|
output
|
|
72
73
|
end
|
|
73
74
|
|
|
74
|
-
#
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
}.freeze)
|
|
79
|
-
.invert
|
|
80
|
-
.freeze
|
|
81
|
-
|
|
82
|
-
# Resolves the export format from an explicit value or output extension.
|
|
83
|
-
# @param format [Symbol, String, nil] explicit format
|
|
84
|
-
# @param output [String, #to_s] output path
|
|
85
|
-
# @return [Symbol] resolved format
|
|
86
|
-
# @raise [Sevgi::Sundries::Export::ExportError] when the explicit format or output extension is unsupported
|
|
87
|
-
def format_for!(format, output)
|
|
88
|
-
if format
|
|
89
|
-
format = format.to_sym
|
|
90
|
-
ExportError.("Unsupported export format: #{format}") unless AVAILABLE.key?(format)
|
|
91
|
-
|
|
92
|
-
format
|
|
93
|
-
else
|
|
94
|
-
ext = File.extname(output.to_s).downcase
|
|
95
|
-
ExportError.("Unrecognized file extension: #{ext}") unless EXTENSIONS.key?(ext)
|
|
96
|
-
|
|
97
|
-
EXTENSIONS[ext]
|
|
98
|
-
end
|
|
99
|
-
end
|
|
100
|
-
|
|
101
|
-
# Inserts CSS before the closing svg tag.
|
|
102
|
-
# @param svg [String] SVG source content
|
|
103
|
-
# @param css [String] CSS source content
|
|
104
|
-
# @return [String] SVG source with an added style element when a closing svg tag is present
|
|
105
|
-
def inject(svg, css) = svg.sub("</svg>", "<style>#{css}</style></svg>")
|
|
106
|
-
|
|
107
|
-
# Replaces a placeholder text object in a PDF stream.
|
|
75
|
+
# Replaces exact placeholder text objects in PDF streams.
|
|
76
|
+
# The placeholder must appear as a PDF literal string in a white text object matching the export stamp pattern.
|
|
77
|
+
# Replacement text is escaped as a PDF literal string. When no exact match is replaced, the output file is not
|
|
78
|
+
# written.
|
|
108
79
|
# @param infile [String] source PDF file path
|
|
109
80
|
# @param outfile [String] destination PDF file path
|
|
110
81
|
# @param stamp [String] replacement text
|
|
111
82
|
# @param placeholder [String] placeholder text to replace
|
|
112
|
-
# @return [Boolean] true when
|
|
113
|
-
# @raise [Sevgi::Sundries::Export::ExportError] when the PDF
|
|
83
|
+
# @return [Boolean] true when at least one matching placeholder was replaced
|
|
84
|
+
# @raise [Sevgi::Sundries::Export::ExportError] when the PDF cannot be read, rewritten, or stamped
|
|
114
85
|
def stamp(infile, outfile, stamp:, placeholder:)
|
|
115
86
|
doc = HexaPDF::Document.open(infile)
|
|
116
|
-
|
|
87
|
+
replacements = 0
|
|
117
88
|
|
|
118
89
|
doc.pages.each do |page|
|
|
119
90
|
Array(page[:Contents]).each do |ref|
|
|
120
91
|
obj = doc.object(ref)
|
|
121
92
|
next unless obj.respond_to?(:stream)
|
|
122
93
|
|
|
123
|
-
data = obj.stream
|
|
124
|
-
next
|
|
94
|
+
data, count = stamp_stream(obj.stream, stamp:, placeholder:)
|
|
95
|
+
next if count.zero?
|
|
125
96
|
|
|
126
|
-
|
|
127
|
-
%r{1 1 1 rg (BT\s+.*?/\S+ \d+ Tf\s+)\(#{Regexp.escape(placeholder)}\)Tj}m,
|
|
128
|
-
"0.101961 0.101961 0.101961 rg \\1(#{stamp})Tj"
|
|
129
|
-
)
|
|
97
|
+
replacements += count
|
|
130
98
|
|
|
131
99
|
obj.stream = data
|
|
132
100
|
obj.set_filter(:FlateDecode)
|
|
133
|
-
stamped = true
|
|
134
101
|
end
|
|
135
102
|
end
|
|
136
103
|
|
|
137
|
-
doc.write(outfile, optimize: true) if
|
|
138
|
-
|
|
104
|
+
doc.write(outfile, optimize: true) if replacements.positive?
|
|
105
|
+
replacements.positive?
|
|
139
106
|
rescue HexaPDF::Error, ::SystemCallError => e
|
|
140
107
|
ExportError.("PDF stamp error: #{e.message}")
|
|
141
108
|
end
|
|
142
109
|
|
|
143
|
-
# Replaces
|
|
110
|
+
# Replaces exact placeholder text objects inside a PDF file in place.
|
|
111
|
+
# The input file is replaced only after at least one exact placeholder match is rewritten into a non-empty output
|
|
112
|
+
# file.
|
|
144
113
|
# @param infile [String] PDF file path to modify
|
|
145
114
|
# @param stamp [String] replacement text
|
|
146
115
|
# @param placeholder [String] placeholder text to replace
|
|
147
|
-
# @return [Boolean] true when
|
|
148
|
-
# @raise [Sevgi::Sundries::Export::ExportError] when the PDF
|
|
116
|
+
# @return [Boolean] true when at least one matching placeholder was replaced
|
|
117
|
+
# @raise [Sevgi::Sundries::Export::ExportError] when the PDF cannot be read, rewritten, stamped, or replaced
|
|
149
118
|
def stamp!(infile, stamp:, placeholder:)
|
|
150
119
|
temp = Tempfile.new(%w[stamp .pdf], File.dirname(infile))
|
|
151
120
|
stamped = stamp(infile, temp.path, stamp:, placeholder:)
|
|
@@ -234,6 +203,34 @@ module Sevgi
|
|
|
234
203
|
|
|
235
204
|
width.round.positive? && height.round.positive?
|
|
236
205
|
end
|
|
206
|
+
|
|
207
|
+
PDF_LITERAL_ESCAPE = {
|
|
208
|
+
"\\" => "\\\\",
|
|
209
|
+
"(" => "\\(",
|
|
210
|
+
")" => "\\)",
|
|
211
|
+
"\b" => "\\b",
|
|
212
|
+
"\f" => "\\f",
|
|
213
|
+
"\n" => "\\n",
|
|
214
|
+
"\r" => "\\r",
|
|
215
|
+
"\t" => "\\t"
|
|
216
|
+
}.freeze
|
|
217
|
+
|
|
218
|
+
private_constant :PDF_LITERAL_ESCAPE
|
|
219
|
+
|
|
220
|
+
def pdf_literal(text)
|
|
221
|
+
"(#{text.to_s.each_char.map { PDF_LITERAL_ESCAPE.fetch(it, it) }.join})"
|
|
222
|
+
end
|
|
223
|
+
|
|
224
|
+
def stamp_stream(data, stamp:, placeholder:)
|
|
225
|
+
count = 0
|
|
226
|
+
pattern = %r{1 1 1 rg (BT\s+.*?/\S+ \d+ Tf\s+)#{Regexp.escape(pdf_literal(placeholder))}Tj}m
|
|
227
|
+
stamped = data.gsub(pattern) do
|
|
228
|
+
count += 1
|
|
229
|
+
"0.101961 0.101961 0.101961 rg #{Regexp.last_match(1)}#{pdf_literal(stamp)}Tj"
|
|
230
|
+
end
|
|
231
|
+
|
|
232
|
+
[stamped, count]
|
|
233
|
+
end
|
|
237
234
|
end
|
|
238
235
|
|
|
239
236
|
# Low-level format renderers used by {Export.call}.
|
|
@@ -1,4 +1,118 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
require "sevgi/function"
|
|
4
|
+
|
|
4
5
|
require_relative "export/system"
|
|
6
|
+
|
|
7
|
+
module Sevgi
|
|
8
|
+
module Sundries
|
|
9
|
+
# Exports SVG content and post-processes PDF output.
|
|
10
|
+
#
|
|
11
|
+
# Native PDF/PNG rendering is loaded lazily so installing `sevgi-sundries` for SVG-only helpers does not require the
|
|
12
|
+
# Cairo, RSVG, or HexaPDF gems. Native export entrypoints raise {Sevgi::MissingComponentError} when those optional
|
|
13
|
+
# gems are unavailable.
|
|
14
|
+
module Export
|
|
15
|
+
# Supported export format names mapped to file extensions.
|
|
16
|
+
AVAILABLE = (EXTENSIONS = {
|
|
17
|
+
".pdf" => :pdf,
|
|
18
|
+
".png" => :png
|
|
19
|
+
}.freeze)
|
|
20
|
+
.invert
|
|
21
|
+
.freeze
|
|
22
|
+
|
|
23
|
+
# Default SVG CSS pixel density.
|
|
24
|
+
DEFAULT_DPI = 96.0
|
|
25
|
+
|
|
26
|
+
# Raised when SVG export or PDF post-processing cannot be completed.
|
|
27
|
+
ExportError = Class.new(Error)
|
|
28
|
+
|
|
29
|
+
NATIVE_COMPONENTS = %w[
|
|
30
|
+
cairo
|
|
31
|
+
hexapdf
|
|
32
|
+
rsvg2
|
|
33
|
+
].freeze
|
|
34
|
+
|
|
35
|
+
private_constant :NATIVE_COMPONENTS
|
|
36
|
+
|
|
37
|
+
# @overload call(svg, output, format: nil, width: nil, height: nil, dpi: DEFAULT_DPI, css: nil)
|
|
38
|
+
# Exports SVG source to a PDF or PNG file using the optional native export gems.
|
|
39
|
+
# @param svg [String] SVG source content
|
|
40
|
+
# @param output [String, #to_s] output file path
|
|
41
|
+
# @param format [Symbol, String, nil] explicit output format, or nil to infer from output extension
|
|
42
|
+
# @param width [Numeric, nil] target width in output pixels for PNG, or CSS pixels before PDF point conversion
|
|
43
|
+
# @param height [Numeric, nil] target height in output pixels for PNG, or CSS pixels before PDF point conversion
|
|
44
|
+
# @param dpi [Numeric] CSS pixel density used for absolute SVG units and PDF point conversion
|
|
45
|
+
# @param css [String, nil] CSS inserted before the closing svg tag before rendering
|
|
46
|
+
# @yield [svg] optional source transformation applied before rendering
|
|
47
|
+
# @yieldparam svg [String] SVG source after optional CSS injection
|
|
48
|
+
# @yieldreturn [String] SVG source to render
|
|
49
|
+
# @return [Object] the original output argument
|
|
50
|
+
# @raise [Sevgi::ArgumentError] when SVG content is not a string or output is blank
|
|
51
|
+
# @raise [Sevgi::MissingComponentError] when cairo, hexapdf, or rsvg2 is unavailable
|
|
52
|
+
# @raise [Sevgi::Sundries::Export::ExportError] when format, SVG parsing, SVG dimensions, or render dimensions
|
|
53
|
+
# are invalid
|
|
54
|
+
def call(*args, **kwargs, &block) = native!.call(*args, **kwargs, &block)
|
|
55
|
+
|
|
56
|
+
# Resolves the export format from an explicit value or output extension.
|
|
57
|
+
# @param format [Symbol, String, nil] explicit format
|
|
58
|
+
# @param output [String, #to_s] output path
|
|
59
|
+
# @return [Symbol] resolved format
|
|
60
|
+
# @raise [Sevgi::Sundries::Export::ExportError] when the explicit format or output extension is unsupported
|
|
61
|
+
def format_for!(format, output)
|
|
62
|
+
if format
|
|
63
|
+
format = format.to_sym
|
|
64
|
+
ExportError.("Unsupported export format: #{format}") unless AVAILABLE.key?(format)
|
|
65
|
+
|
|
66
|
+
format
|
|
67
|
+
else
|
|
68
|
+
ext = File.extname(output.to_s).downcase
|
|
69
|
+
ExportError.("Unrecognized file extension: #{ext}") unless EXTENSIONS.key?(ext)
|
|
70
|
+
|
|
71
|
+
EXTENSIONS[ext]
|
|
72
|
+
end
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
# Inserts CSS before the closing svg tag.
|
|
76
|
+
# @param svg [String] SVG source content
|
|
77
|
+
# @param css [String] CSS source content
|
|
78
|
+
# @return [String] SVG source with an added style element when a closing svg tag is present
|
|
79
|
+
# @raise [NoMethodError] when SVG content does not support string substitution
|
|
80
|
+
def inject(svg, css) = svg.sub("</svg>", "<style>#{css}</style></svg>")
|
|
81
|
+
|
|
82
|
+
# Replaces exact placeholder text objects in PDF streams.
|
|
83
|
+
# @param infile [String] source PDF file path
|
|
84
|
+
# @param outfile [String] destination PDF file path
|
|
85
|
+
# @param stamp [String] replacement text
|
|
86
|
+
# @param placeholder [String] placeholder text to replace
|
|
87
|
+
# @return [Boolean] true when at least one matching placeholder was replaced
|
|
88
|
+
# @raise [Sevgi::MissingComponentError] when native export gems are unavailable
|
|
89
|
+
# @raise [Sevgi::Sundries::Export::ExportError] when the PDF cannot be read, rewritten, or stamped
|
|
90
|
+
def stamp(infile, outfile, stamp:, placeholder:) = native!.stamp(infile, outfile, stamp:, placeholder:)
|
|
91
|
+
|
|
92
|
+
# Replaces exact placeholder text objects inside a PDF file in place.
|
|
93
|
+
# @param infile [String] PDF file path to modify
|
|
94
|
+
# @param stamp [String] replacement text
|
|
95
|
+
# @param placeholder [String] placeholder text to replace
|
|
96
|
+
# @return [Boolean] true when at least one matching placeholder was replaced
|
|
97
|
+
# @raise [Sevgi::MissingComponentError] when native export gems are unavailable
|
|
98
|
+
# @raise [Sevgi::Sundries::Export::ExportError] when the PDF cannot be read, rewritten, stamped, or replaced
|
|
99
|
+
def stamp!(infile, stamp:, placeholder:) = native!.stamp!(infile, stamp:, placeholder:)
|
|
100
|
+
|
|
101
|
+
extend self
|
|
102
|
+
|
|
103
|
+
class << self
|
|
104
|
+
private
|
|
105
|
+
|
|
106
|
+
def native!
|
|
107
|
+
require_relative "export/native"
|
|
108
|
+
|
|
109
|
+
self
|
|
110
|
+
rescue ::LoadError => e
|
|
111
|
+
raise unless NATIVE_COMPONENTS.include?(e.path)
|
|
112
|
+
|
|
113
|
+
MissingComponentError.(NATIVE_COMPONENTS.join(", "))
|
|
114
|
+
end
|
|
115
|
+
end
|
|
116
|
+
end
|
|
117
|
+
end
|
|
118
|
+
end
|
data/lib/sevgi/sundries/grid.rb
CHANGED
|
@@ -51,7 +51,6 @@ module Sevgi
|
|
|
51
51
|
def width = x.d
|
|
52
52
|
|
|
53
53
|
# Axis wrapper exposing grid line queries for one ruler direction.
|
|
54
|
-
# @api private
|
|
55
54
|
class Axis < DelegateClass(Ruler)
|
|
56
55
|
# Returns the major line query.
|
|
57
56
|
# @return [Sevgi::Sundries::Grid::Axis::Major]
|
|
@@ -78,7 +77,6 @@ module Sevgi
|
|
|
78
77
|
end
|
|
79
78
|
|
|
80
79
|
# Memoized grid line query for an axis.
|
|
81
|
-
# @api private
|
|
82
80
|
class Query
|
|
83
81
|
# Creates a query.
|
|
84
82
|
# @param this [Sevgi::Sundries::Grid::Axis] axis receiving generated lines
|
|
@@ -104,7 +102,6 @@ module Sevgi
|
|
|
104
102
|
end
|
|
105
103
|
|
|
106
104
|
# Major grid line query.
|
|
107
|
-
# @api private
|
|
108
105
|
class Major < Query
|
|
109
106
|
# Returns lines at major tick distances.
|
|
110
107
|
# @return [Array<Sevgi::Geometry::Line>]
|
|
@@ -112,7 +109,6 @@ module Sevgi
|
|
|
112
109
|
end
|
|
113
110
|
|
|
114
111
|
# Midpoint grid line query.
|
|
115
|
-
# @api private
|
|
116
112
|
class Halve < Query
|
|
117
113
|
# Returns lines at midpoint tick distances.
|
|
118
114
|
# @return [Array<Sevgi::Geometry::Line>]
|
|
@@ -120,7 +116,6 @@ module Sevgi
|
|
|
120
116
|
end
|
|
121
117
|
|
|
122
118
|
# Minor grid line query.
|
|
123
|
-
# @api private
|
|
124
119
|
class Minor < Query
|
|
125
120
|
# Returns lines at minor tick distances.
|
|
126
121
|
# @return [Array<Sevgi::Geometry::Line>]
|
|
@@ -129,7 +124,6 @@ module Sevgi
|
|
|
129
124
|
end
|
|
130
125
|
|
|
131
126
|
# Horizontal grid axis.
|
|
132
|
-
# @api private
|
|
133
127
|
class X < Axis
|
|
134
128
|
# Returns the base horizontal line for this axis.
|
|
135
129
|
# @return [Sevgi::Geometry::Line]
|
|
@@ -142,7 +136,6 @@ module Sevgi
|
|
|
142
136
|
end
|
|
143
137
|
|
|
144
138
|
# Vertical grid axis.
|
|
145
|
-
# @api private
|
|
146
139
|
class Y < Axis
|
|
147
140
|
# Creates a vertical axis wrapper.
|
|
148
141
|
# @param this [Sevgi::Sundries::Ruler] horizontal ruler
|
data/lib/sevgi/sundries/ruler.rb
CHANGED
|
@@ -20,7 +20,10 @@ module Sevgi
|
|
|
20
20
|
# @param n [Integer] non-negative interval count
|
|
21
21
|
# @return [Sevgi::Sundries::Interval]
|
|
22
22
|
# @raise [Sevgi::ArgumentError] when count is not a non-negative integer
|
|
23
|
-
# @raise [Sevgi::ArgumentError] when the unit
|
|
23
|
+
# @raise [Sevgi::ArgumentError] when the unit object does not expose length
|
|
24
|
+
# @raise [Sevgi::ArgumentError] when the measured unit length is not numeric
|
|
25
|
+
# @raise [Sevgi::ArgumentError] when the measured unit length is not finite
|
|
26
|
+
# @raise [Sevgi::ArgumentError] when the measured unit length is not positive
|
|
24
27
|
def self.[](e, n) = new(e, n)
|
|
25
28
|
|
|
26
29
|
# @!attribute [r] n
|
|
@@ -34,12 +37,13 @@ module Sevgi
|
|
|
34
37
|
# @param n [Integer] non-negative interval count
|
|
35
38
|
# @return [void]
|
|
36
39
|
# @raise [Sevgi::ArgumentError] when count is not a non-negative integer
|
|
37
|
-
# @raise [Sevgi::ArgumentError] when the unit
|
|
40
|
+
# @raise [Sevgi::ArgumentError] when the unit object does not expose length
|
|
41
|
+
# @raise [Sevgi::ArgumentError] when the measured unit length is not numeric
|
|
42
|
+
# @raise [Sevgi::ArgumentError] when the measured unit length is not finite
|
|
43
|
+
# @raise [Sevgi::ArgumentError] when the measured unit length is not positive
|
|
38
44
|
def initialize(e, n)
|
|
39
|
-
|
|
40
|
-
|
|
45
|
+
@n = non_negative_integer(n, "Interval count")
|
|
41
46
|
@u = measure(e)
|
|
42
|
-
@n = n
|
|
43
47
|
end
|
|
44
48
|
|
|
45
49
|
# Returns a major tick distance by index.
|
|
@@ -50,7 +54,10 @@ module Sevgi
|
|
|
50
54
|
# Counts how many whole lengths fit into this interval.
|
|
51
55
|
# @param length [Numeric] candidate length
|
|
52
56
|
# @return [Integer]
|
|
53
|
-
|
|
57
|
+
# @raise [Sevgi::ArgumentError] when length is not numeric
|
|
58
|
+
# @raise [Sevgi::ArgumentError] when length is not finite
|
|
59
|
+
# @raise [Sevgi::ArgumentError] when length is not positive
|
|
60
|
+
def count(length) = (d / positive_number(length, "Interval count length")).to_i
|
|
54
61
|
|
|
55
62
|
# Returns the total interval distance.
|
|
56
63
|
# @return [Float]
|
|
@@ -82,11 +89,50 @@ module Sevgi
|
|
|
82
89
|
private
|
|
83
90
|
|
|
84
91
|
def measure(e)
|
|
85
|
-
|
|
92
|
+
value = if e.is_a?(::Numeric)
|
|
93
|
+
e
|
|
94
|
+
else
|
|
95
|
+
ArgumentError.("#{e.class}#length must be implemented") unless e.respond_to?(:length)
|
|
96
|
+
|
|
97
|
+
e.length
|
|
98
|
+
end
|
|
99
|
+
|
|
100
|
+
positive_number(value, "Interval unit length")
|
|
101
|
+
end
|
|
102
|
+
|
|
103
|
+
def non_negative_integer(value, field)
|
|
104
|
+
ArgumentError.("#{field} must be a non-negative Integer") unless value.is_a?(::Integer) && !value.negative?
|
|
105
|
+
|
|
106
|
+
value
|
|
107
|
+
end
|
|
108
|
+
|
|
109
|
+
def non_negative_number(value, field)
|
|
110
|
+
number = numeric(value, field)
|
|
111
|
+
ArgumentError.("#{field} must be non-negative") if number.negative?
|
|
112
|
+
|
|
113
|
+
number
|
|
114
|
+
end
|
|
115
|
+
|
|
116
|
+
def numeric(value, field)
|
|
117
|
+
ArgumentError.("#{field} must be Numeric") unless value.is_a?(::Numeric)
|
|
118
|
+
|
|
119
|
+
number = value.to_f
|
|
120
|
+
ArgumentError.("#{field} must be finite") unless number.finite?
|
|
121
|
+
|
|
122
|
+
number
|
|
123
|
+
end
|
|
86
124
|
|
|
87
|
-
|
|
125
|
+
def positive_integer(value, field)
|
|
126
|
+
ArgumentError.("#{field} must be a positive Integer") unless value.is_a?(::Integer) && value.positive?
|
|
88
127
|
|
|
89
|
-
|
|
128
|
+
value
|
|
129
|
+
end
|
|
130
|
+
|
|
131
|
+
def positive_number(value, field)
|
|
132
|
+
number = numeric(value, field)
|
|
133
|
+
ArgumentError.("#{field} must be positive") unless number.positive?
|
|
134
|
+
|
|
135
|
+
number
|
|
90
136
|
end
|
|
91
137
|
end
|
|
92
138
|
|
|
@@ -116,16 +162,22 @@ module Sevgi
|
|
|
116
162
|
# @param multiple [Integer] number of subinterval units per major interval
|
|
117
163
|
# @param margin [Numeric] minimum margin on each side
|
|
118
164
|
# @return [void]
|
|
119
|
-
# @raise [Sevgi::ArgumentError] when
|
|
165
|
+
# @raise [Sevgi::ArgumentError] when brut is not numeric
|
|
166
|
+
# @raise [Sevgi::ArgumentError] when brut is not finite
|
|
167
|
+
# @raise [Sevgi::ArgumentError] when brut is negative
|
|
168
|
+
# @raise [Sevgi::ArgumentError] when unit is not numeric
|
|
169
|
+
# @raise [Sevgi::ArgumentError] when unit is not finite
|
|
170
|
+
# @raise [Sevgi::ArgumentError] when unit is not positive
|
|
171
|
+
# @raise [Sevgi::ArgumentError] when multiple is not a positive integer
|
|
172
|
+
# @raise [Sevgi::ArgumentError] when margin is not numeric
|
|
173
|
+
# @raise [Sevgi::ArgumentError] when margin is not finite
|
|
120
174
|
# @raise [Sevgi::ArgumentError] when margin is negative
|
|
121
175
|
# @raise [Sevgi::ArgumentError] when the fitting span is negative
|
|
122
176
|
def initialize(brut:, unit:, multiple:, margin: 0.0)
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
@brut = brut.to_f
|
|
128
|
-
margin = margin.to_f
|
|
177
|
+
@brut = non_negative_number(brut, "Ruler brut")
|
|
178
|
+
unit = positive_number(unit, "Ruler unit")
|
|
179
|
+
multiple = positive_integer(multiple, "Ruler multiple")
|
|
180
|
+
margin = non_negative_number(margin, "Ruler margin")
|
|
129
181
|
@sub = Interval.new(unit, multiple)
|
|
130
182
|
|
|
131
183
|
n = divide(unit:, multiple:, brut: @brut, margin:)
|
data/lib/sevgi/sundries/tile.rb
CHANGED
|
@@ -24,13 +24,14 @@ module Sevgi
|
|
|
24
24
|
# @return [void]
|
|
25
25
|
# @raise [Sevgi::ArgumentError] when element is not a geometry element
|
|
26
26
|
# @raise [Sevgi::ArgumentError] when nx or ny is not a positive integer
|
|
27
|
+
# @raise [Sevgi::ArgumentError] when position is not a point or two-number array
|
|
27
28
|
def initialize(element, position: Geometry::Origin, nx: 1, ny: 1)
|
|
28
29
|
ArgumentError.("Must be an Element object: #{element}") unless element.is_a?(Geometry::Element)
|
|
29
30
|
ArgumentError.("Tile nx must be positive") unless nx.is_a?(::Integer) && nx.positive?
|
|
30
31
|
ArgumentError.("Tile ny must be positive") unless ny.is_a?(::Integer) && ny.positive?
|
|
31
32
|
|
|
32
33
|
@element = element
|
|
33
|
-
@position = position
|
|
34
|
+
@position = self.class.send(:position, position)
|
|
34
35
|
|
|
35
36
|
@nx = nx
|
|
36
37
|
@ny = ny
|
|
@@ -97,6 +98,22 @@ module Sevgi
|
|
|
97
98
|
|
|
98
99
|
private
|
|
99
100
|
|
|
101
|
+
# Coerces a public tile position.
|
|
102
|
+
# @param position [Sevgi::Geometry::Point, Array<Numeric>] tile origin
|
|
103
|
+
# @return [Sevgi::Geometry::Point]
|
|
104
|
+
# @raise [Sevgi::ArgumentError] when position is not a point or two-number array
|
|
105
|
+
def self.position(position)
|
|
106
|
+
return position if position.is_a?(Geometry::Point)
|
|
107
|
+
|
|
108
|
+
unless position.is_a?(::Array) && position.size == 2 && position.all?(::Numeric)
|
|
109
|
+
ArgumentError.("Tile position must be a Point or two-number Array")
|
|
110
|
+
end
|
|
111
|
+
|
|
112
|
+
Geometry::Point[*position]
|
|
113
|
+
end
|
|
114
|
+
|
|
115
|
+
private_class_method :position
|
|
116
|
+
|
|
100
117
|
def coordinate(i, j = 0) = position.translate(j * element.box.width, i * element.box.height)
|
|
101
118
|
end
|
|
102
119
|
end
|
data/lib/sevgi/sundries.rb
CHANGED
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
require "sevgi/function"
|
|
4
|
+
require "sevgi/geometry"
|
|
5
|
+
require "sevgi/graphics"
|
|
4
6
|
|
|
5
7
|
require_relative "sundries/ruler"
|
|
6
8
|
require_relative "sundries/tile"
|
|
@@ -12,6 +14,10 @@ require_relative "sundries/version"
|
|
|
12
14
|
|
|
13
15
|
module Sevgi
|
|
14
16
|
# Layout, tiling, grid, and export helpers shared by Sevgi consumers.
|
|
17
|
+
#
|
|
18
|
+
# This component loads its eager public surfaces directly. `Ruler`, `Tile`, and `Grid` therefore require
|
|
19
|
+
# `sevgi-function`, `sevgi-geometry`, and `sevgi-graphics` as runtime dependencies of the `sevgi-sundries` gem.
|
|
20
|
+
# Native PDF/PNG export gems are optional and loaded only by {Export} when export is used.
|
|
15
21
|
module Sundries
|
|
16
22
|
end
|
|
17
23
|
end
|
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: sevgi-sundries
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.
|
|
4
|
+
version: 0.94.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Recai Oktaş
|
|
@@ -15,62 +15,50 @@ dependencies:
|
|
|
15
15
|
requirements:
|
|
16
16
|
- - '='
|
|
17
17
|
- !ruby/object:Gem::Version
|
|
18
|
-
version: 0.
|
|
18
|
+
version: 0.94.0
|
|
19
19
|
type: :runtime
|
|
20
20
|
prerelease: false
|
|
21
21
|
version_requirements: !ruby/object:Gem::Requirement
|
|
22
22
|
requirements:
|
|
23
23
|
- - '='
|
|
24
24
|
- !ruby/object:Gem::Version
|
|
25
|
-
version: 0.
|
|
25
|
+
version: 0.94.0
|
|
26
26
|
- !ruby/object:Gem::Dependency
|
|
27
|
-
name:
|
|
27
|
+
name: sevgi-geometry
|
|
28
28
|
requirement: !ruby/object:Gem::Requirement
|
|
29
29
|
requirements:
|
|
30
|
-
- -
|
|
31
|
-
- !ruby/object:Gem::Version
|
|
32
|
-
version: '0'
|
|
33
|
-
type: :runtime
|
|
34
|
-
prerelease: false
|
|
35
|
-
version_requirements: !ruby/object:Gem::Requirement
|
|
36
|
-
requirements:
|
|
37
|
-
- - ">="
|
|
38
|
-
- !ruby/object:Gem::Version
|
|
39
|
-
version: '0'
|
|
40
|
-
- !ruby/object:Gem::Dependency
|
|
41
|
-
name: hexapdf
|
|
42
|
-
requirement: !ruby/object:Gem::Requirement
|
|
43
|
-
requirements:
|
|
44
|
-
- - "~>"
|
|
30
|
+
- - '='
|
|
45
31
|
- !ruby/object:Gem::Version
|
|
46
|
-
version:
|
|
32
|
+
version: 0.94.0
|
|
47
33
|
type: :runtime
|
|
48
34
|
prerelease: false
|
|
49
35
|
version_requirements: !ruby/object:Gem::Requirement
|
|
50
36
|
requirements:
|
|
51
|
-
- -
|
|
37
|
+
- - '='
|
|
52
38
|
- !ruby/object:Gem::Version
|
|
53
|
-
version:
|
|
39
|
+
version: 0.94.0
|
|
54
40
|
- !ruby/object:Gem::Dependency
|
|
55
|
-
name:
|
|
41
|
+
name: sevgi-graphics
|
|
56
42
|
requirement: !ruby/object:Gem::Requirement
|
|
57
43
|
requirements:
|
|
58
|
-
- -
|
|
44
|
+
- - '='
|
|
59
45
|
- !ruby/object:Gem::Version
|
|
60
|
-
version:
|
|
46
|
+
version: 0.94.0
|
|
61
47
|
type: :runtime
|
|
62
48
|
prerelease: false
|
|
63
49
|
version_requirements: !ruby/object:Gem::Requirement
|
|
64
50
|
requirements:
|
|
65
|
-
- -
|
|
51
|
+
- - '='
|
|
66
52
|
- !ruby/object:Gem::Version
|
|
67
|
-
version:
|
|
53
|
+
version: 0.94.0
|
|
68
54
|
description: Enhances the Sevgi toolkit with helper objects.
|
|
69
55
|
email: roktas@gmail.com
|
|
70
56
|
executables: []
|
|
71
57
|
extensions: []
|
|
72
58
|
extra_rdoc_files: []
|
|
73
59
|
files:
|
|
60
|
+
- CHANGELOG.md
|
|
61
|
+
- LICENSE
|
|
74
62
|
- README.md
|
|
75
63
|
- lib/sevgi/sundries.rb
|
|
76
64
|
- lib/sevgi/sundries/export.rb
|
|
@@ -95,7 +83,7 @@ required_ruby_version: !ruby/object:Gem::Requirement
|
|
|
95
83
|
requirements:
|
|
96
84
|
- - ">="
|
|
97
85
|
- !ruby/object:Gem::Version
|
|
98
|
-
version: 3.4.0
|
|
86
|
+
version: 3.4.0
|
|
99
87
|
required_rubygems_version: !ruby/object:Gem::Requirement
|
|
100
88
|
requirements:
|
|
101
89
|
- - ">="
|