postsvg 0.2.5 → 0.3.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/.rubocop_todo.yml +49 -533
- data/CLAUDE.md +3 -3
- data/Gemfile +2 -2
- data/lib/postsvg/cli.rb +5 -10
- data/lib/postsvg/color.rb +132 -0
- data/lib/postsvg/format_number.rb +22 -0
- data/lib/postsvg/graphics_context.rb +1 -2
- data/lib/postsvg/matrix.rb +106 -0
- data/lib/postsvg/model/literals/array.rb +41 -0
- data/lib/postsvg/model/literals/dictionary.rb +34 -0
- data/lib/postsvg/model/literals/hex.rb +37 -0
- data/lib/postsvg/model/literals/name.rb +40 -0
- data/lib/postsvg/model/literals/number.rb +36 -0
- data/lib/postsvg/model/literals/procedure.rb +41 -0
- data/lib/postsvg/model/literals/string.rb +30 -0
- data/lib/postsvg/model/literals.rb +19 -0
- data/lib/postsvg/model/operator.rb +58 -0
- data/lib/postsvg/model/operators/arithmetic.rb +264 -0
- data/lib/postsvg/model/operators/boolean.rb +182 -0
- data/lib/postsvg/model/operators/color.rb +74 -0
- data/lib/postsvg/model/operators/container.rb +186 -0
- data/lib/postsvg/model/operators/control_flow.rb +119 -0
- data/lib/postsvg/model/operators/device.rb +21 -0
- data/lib/postsvg/model/operators/dictionary.rb +118 -0
- data/lib/postsvg/model/operators/font.rb +121 -0
- data/lib/postsvg/model/operators/graphics_state.rb +84 -0
- data/lib/postsvg/model/operators/painting.rb +29 -0
- data/lib/postsvg/model/operators/path.rb +169 -0
- data/lib/postsvg/model/operators/stack.rb +72 -0
- data/lib/postsvg/model/operators/transformations.rb +103 -0
- data/lib/postsvg/model/operators.rb +89 -0
- data/lib/postsvg/model/program.rb +68 -0
- data/lib/postsvg/model/token.rb +43 -0
- data/lib/postsvg/model.rb +17 -0
- data/lib/postsvg/options.rb +1 -1
- data/lib/postsvg/parser/postscript_parser.rb +7 -7
- data/lib/postsvg/renderer.rb +3 -7
- data/lib/postsvg/serializer.rb +325 -0
- data/lib/postsvg/source/ast_builder.rb +308 -0
- data/lib/postsvg/source/lexer.rb +322 -0
- data/lib/postsvg/source/operand_stack.rb +55 -0
- data/lib/postsvg/source.rb +21 -0
- data/lib/postsvg/svg/clip_path_registry.rb +1 -3
- data/lib/postsvg/svg/element.rb +2 -4
- data/lib/postsvg/svg/elements/ellipse.rb +1 -2
- data/lib/postsvg/svg/elements/svg.rb +1 -2
- data/lib/postsvg/svg/paint.rb +1 -1
- data/lib/postsvg/svg/parser.rb +3 -3
- data/lib/postsvg/svg/path_data/parser.rb +6 -10
- data/lib/postsvg/svg/stroke.rb +2 -4
- data/lib/postsvg/svg/transform_list.rb +1 -1
- data/lib/postsvg/svg.rb +1 -1
- data/lib/postsvg/svg_builder.rb +11 -15
- data/lib/postsvg/translation/arc_converter.rb +12 -22
- data/lib/postsvg/translation/bounding_box.rb +4 -7
- data/lib/postsvg/translation/handlers/circle_handler.rb +2 -2
- data/lib/postsvg/translation/handlers/ellipse_handler.rb +3 -8
- data/lib/postsvg/translation/handlers/group_handler.rb +1 -2
- data/lib/postsvg/translation/handlers/image_handler.rb +2 -3
- data/lib/postsvg/translation/handlers/line_handler.rb +2 -6
- data/lib/postsvg/translation/handlers/open_handler.rb +2 -3
- data/lib/postsvg/translation/handlers/path_handler.rb +41 -57
- data/lib/postsvg/translation/handlers/polygon_handler.rb +2 -4
- data/lib/postsvg/translation/handlers/polyline_handler.rb +2 -4
- data/lib/postsvg/translation/handlers/rect_handler.rb +5 -10
- data/lib/postsvg/translation/handlers/shared.rb +11 -11
- data/lib/postsvg/translation/handlers/svg_handler.rb +2 -3
- data/lib/postsvg/translation/handlers/text_handler.rb +7 -8
- data/lib/postsvg/translation/handlers.rb +1 -2
- data/lib/postsvg/translation/ps_renderer.rb +1 -2
- data/lib/postsvg/translation/record_emitter.rb +0 -11
- data/lib/postsvg/version.rb +1 -1
- data/lib/postsvg/visitors/ps_visitor/arithmetic.rb +6 -6
- data/lib/postsvg/visitors/ps_visitor/color.rb +5 -8
- data/lib/postsvg/visitors/ps_visitor/common.rb +2 -1
- data/lib/postsvg/visitors/ps_visitor/container.rb +13 -13
- data/lib/postsvg/visitors/ps_visitor/control_flow.rb +3 -3
- data/lib/postsvg/visitors/ps_visitor/dictionary.rb +4 -8
- data/lib/postsvg/visitors/ps_visitor/font.rb +4 -14
- data/lib/postsvg/visitors/ps_visitor/graphics_state.rb +4 -4
- data/lib/postsvg/visitors/ps_visitor/painting.rb +1 -1
- data/lib/postsvg/visitors/ps_visitor/path.rb +4 -9
- data/lib/postsvg.rb +38 -43
- metadata +41 -21
data/CLAUDE.md
CHANGED
|
@@ -4,7 +4,7 @@ This file provides guidance to Claude Code (claude.ai/code) when working with co
|
|
|
4
4
|
|
|
5
5
|
## What this is
|
|
6
6
|
|
|
7
|
-
`postsvg` is a pure-Ruby bidirectional converter between PostScript/EPS and SVG. Both directions are implemented in the 0.2.x line; the SVg→PS direction is newer. The PS side uses a hand-written lexer + stack-machine interpreter; the SVG side uses
|
|
7
|
+
`postsvg` is a pure-Ruby bidirectional converter between PostScript/EPS and SVG. Both directions are implemented in the 0.2.x line; the SVg→PS direction is newer. The PS side uses a hand-written lexer + stack-machine interpreter; the SVG side uses Nokogiri. There is no Ghostscript, Inkscape, or other external renderer.
|
|
8
8
|
|
|
9
9
|
Code architecture mirrors `emfsvg`: a typed `Model::Program` value object is the single source of truth, with a Renderer / Visitor visiting it to emit SVG, and a Serializer walking it to emit PS.
|
|
10
10
|
|
|
@@ -54,7 +54,7 @@ PS/EPS bytes ──▶│ Postsvg::Lexer │
|
|
|
54
54
|
│ │
|
|
55
55
|
▼ │
|
|
56
56
|
SVG string │
|
|
57
|
-
│
|
|
57
|
+
│ Nokogiri │
|
|
58
58
|
▼ │
|
|
59
59
|
┌─────────────────┐ │
|
|
60
60
|
│ Postsvg:: │ │
|
|
@@ -163,7 +163,7 @@ Errors: `Postsvg::ParseError`, `LexError`, `SyntaxError`, `RenderError`, `StackU
|
|
|
163
163
|
|
|
164
164
|
## Dependencies
|
|
165
165
|
|
|
166
|
-
- `parslet`, `thor`, `
|
|
166
|
+
- `parslet`, `thor`, `nokogiri`, `lutaml/canon` (dev only, for XML matcher in integration specs) — see `Gemfile`.
|
|
167
167
|
|
|
168
168
|
## Limitations
|
|
169
169
|
|
data/Gemfile
CHANGED
data/lib/postsvg/cli.rb
CHANGED
|
@@ -8,8 +8,7 @@ module Postsvg
|
|
|
8
8
|
class CLI < Thor
|
|
9
9
|
package_name "postsvg"
|
|
10
10
|
|
|
11
|
-
desc "convert INPUT [OUTPUT]",
|
|
12
|
-
"Convert PostScript/EPS file to SVG (legacy alias for to-svg)"
|
|
11
|
+
desc "convert INPUT [OUTPUT]", "Convert PostScript/EPS file to SVG (legacy alias for to-svg)"
|
|
13
12
|
long_desc <<~DESC
|
|
14
13
|
Convert a PostScript (.ps) or Encapsulated PostScript (.eps) file to SVG.
|
|
15
14
|
|
|
@@ -21,8 +20,7 @@ module Postsvg
|
|
|
21
20
|
$ postsvg convert input.eps > output.svg
|
|
22
21
|
DESC
|
|
23
22
|
def convert(input_path, output_path = nil)
|
|
24
|
-
handle_input(input_path, output_path, method(:read_ps_to_svg),
|
|
25
|
-
direction: "PS/EPS -> SVG")
|
|
23
|
+
handle_input(input_path, output_path, method(:read_ps_to_svg), direction: "PS/EPS -> SVG")
|
|
26
24
|
end
|
|
27
25
|
|
|
28
26
|
desc "to-svg INPUT [OUTPUT]", "Convert PostScript/EPS to SVG"
|
|
@@ -44,8 +42,7 @@ module Postsvg
|
|
|
44
42
|
$ postsvg to-ps input.svg > output.ps
|
|
45
43
|
DESC
|
|
46
44
|
def to_ps(input_path, output_path = nil)
|
|
47
|
-
handle_input(input_path, output_path, method(:read_svg_to_ps),
|
|
48
|
-
direction: "SVG -> PS")
|
|
45
|
+
handle_input(input_path, output_path, method(:read_svg_to_ps), direction: "SVG -> PS")
|
|
49
46
|
end
|
|
50
47
|
|
|
51
48
|
desc "to-eps INPUT [OUTPUT]", "Convert SVG to Encapsulated PostScript"
|
|
@@ -54,12 +51,10 @@ module Postsvg
|
|
|
54
51
|
suitable for embedding.
|
|
55
52
|
DESC
|
|
56
53
|
def to_eps(input_path, output_path = nil)
|
|
57
|
-
handle_input(input_path, output_path, method(:read_svg_to_eps),
|
|
58
|
-
direction: "SVG -> EPS")
|
|
54
|
+
handle_input(input_path, output_path, method(:read_svg_to_eps), direction: "SVG -> EPS")
|
|
59
55
|
end
|
|
60
56
|
|
|
61
|
-
desc "batch INPUT_DIR [OUTPUT_DIR]",
|
|
62
|
-
"Convert all PS/EPS/SVG files in a directory"
|
|
57
|
+
desc "batch INPUT_DIR [OUTPUT_DIR]", "Convert all PS/EPS/SVG files in a directory"
|
|
63
58
|
long_desc <<~DESC
|
|
64
59
|
Auto-detects direction by file extension:
|
|
65
60
|
|
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Postsvg
|
|
4
|
+
# Immutable RGB color value object. Use the constructors (+.rgb+,
|
|
5
|
+
# +.gray+, +.cmyk+, +.parse+) instead of +new+ when constructing from
|
|
6
|
+
# other color models; they normalize to integer [0, 255] channels.
|
|
7
|
+
class Color
|
|
8
|
+
include Comparable
|
|
9
|
+
|
|
10
|
+
attr_reader :red, :green, :blue
|
|
11
|
+
|
|
12
|
+
def self.clamp_byte(value)
|
|
13
|
+
Integer === value ? value.clamp(0, 255) : value.to_i.clamp(0, 255)
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
def self.scale_unit_to_byte(value)
|
|
17
|
+
(value.clamp(0.0, 1.0) * 255.0).round
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
def initialize(red, green, blue)
|
|
21
|
+
@red = Color.clamp_byte(red)
|
|
22
|
+
@green = Color.clamp_byte(green)
|
|
23
|
+
@blue = Color.clamp_byte(blue)
|
|
24
|
+
freeze
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
# RGB triple in PS-native [0.0, 1.0] floats.
|
|
28
|
+
def self.rgb(r, g, b)
|
|
29
|
+
new(scale_unit_to_byte(r), scale_unit_to_byte(g), scale_unit_to_byte(b))
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
# Gray in [0.0, 1.0].
|
|
33
|
+
def self.gray(level)
|
|
34
|
+
v = scale_unit_to_byte(level)
|
|
35
|
+
new(v, v, v)
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
# CMYK in [0.0, 1.0]. Conversion matches PLRM §8.2.4 simplified
|
|
39
|
+
# formula; not colorimetrically exact.
|
|
40
|
+
def self.cmyk(c, m, y, k)
|
|
41
|
+
new(
|
|
42
|
+
scale_unit_to_byte((1 - c) * (1 - k)),
|
|
43
|
+
scale_unit_to_byte((1 - m) * (1 - k)),
|
|
44
|
+
scale_unit_to_byte((1 - y) * (1 - k)),
|
|
45
|
+
)
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
# Parse CSS / SVG color notations: #rgb, #rrggbb, rgb(r, g, b),
|
|
49
|
+
# rgb(r g b / a), named colors (X11 subset).
|
|
50
|
+
def self.parse(text)
|
|
51
|
+
return text if text.is_a?(Color)
|
|
52
|
+
|
|
53
|
+
case text.to_s.downcase
|
|
54
|
+
when /\A#([0-9a-f]{6})\z/
|
|
55
|
+
hex = ::Regexp.last_match(1)
|
|
56
|
+
new(hex[0, 2].to_i(16), hex[2, 2].to_i(16), hex[4, 2].to_i(16))
|
|
57
|
+
when /\A#([0-9a-f]{3})\z/
|
|
58
|
+
hex = ::Regexp.last_match(1)
|
|
59
|
+
new((hex[0] * 2).to_i(16), (hex[1] * 2).to_i(16), (hex[2] * 2).to_i(16))
|
|
60
|
+
when /\argb\(\s*(\d+)\s*,\s*(\d+)\s*,\s*(\d+)/
|
|
61
|
+
new(::Regexp.last_match(1).to_i, ::Regexp.last_match(2).to_i, ::Regexp.last_match(3).to_i)
|
|
62
|
+
when /\argb\(\s*(\d+)\s+(\d+)\s+(\d+)/
|
|
63
|
+
new(::Regexp.last_match(1).to_i, ::Regexp.last_match(2).to_i, ::Regexp.last_match(3).to_i)
|
|
64
|
+
when "none", "transparent"
|
|
65
|
+
nil
|
|
66
|
+
else
|
|
67
|
+
rgb = NAMED_COLORS[text.to_s]
|
|
68
|
+
return new(*rgb) if rgb
|
|
69
|
+
|
|
70
|
+
raise ArgumentError, "unrecognized color: #{text.inspect}"
|
|
71
|
+
end
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
def gray?
|
|
75
|
+
red == green && green == blue
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
def gray_level
|
|
79
|
+
(red + green + blue) / 765.0
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
def to_rgb_unit
|
|
83
|
+
[red / 255.0, green / 255.0, blue / 255.0]
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
def to_svg
|
|
87
|
+
format("#%02x%02x%02x", red, green, blue)
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
def to_ps_rgb
|
|
91
|
+
to_rgb_unit.map { |c| FormatNumber.call(c) }.join(" ")
|
|
92
|
+
end
|
|
93
|
+
|
|
94
|
+
def to_ps_gray
|
|
95
|
+
raise ArgumentError, "color is not gray" unless gray?
|
|
96
|
+
|
|
97
|
+
FormatNumber.call(gray_level)
|
|
98
|
+
end
|
|
99
|
+
|
|
100
|
+
def <=>(other)
|
|
101
|
+
return nil unless other.is_a?(Color)
|
|
102
|
+
|
|
103
|
+
[red, green, blue] <=> [other.red, other.green, other.blue]
|
|
104
|
+
end
|
|
105
|
+
|
|
106
|
+
def hash
|
|
107
|
+
[red, green, blue].hash
|
|
108
|
+
end
|
|
109
|
+
|
|
110
|
+
def eql?(other)
|
|
111
|
+
other.is_a?(Color) && other.red == red && other.green == green && other.blue == blue
|
|
112
|
+
end
|
|
113
|
+
alias == eql?
|
|
114
|
+
|
|
115
|
+
NAMED_COLORS = {
|
|
116
|
+
"black" => [0, 0, 0], "white" => [255, 255, 255],
|
|
117
|
+
"red" => [255, 0, 0], "green" => [0, 128, 0], "blue" => [0, 0, 255],
|
|
118
|
+
"yellow" => [255, 255, 0], "cyan" => [0, 255, 255],
|
|
119
|
+
"magenta" => [255, 0, 255], "gray" => [128, 128, 128],
|
|
120
|
+
"grey" => [128, 128, 128], "silver" => [192, 192, 192],
|
|
121
|
+
"maroon" => [128, 0, 0], "olive" => [128, 128, 0],
|
|
122
|
+
"navy" => [0, 0, 128], "purple" => [128, 0, 128],
|
|
123
|
+
"teal" => [0, 128, 128], "lime" => [0, 255, 0],
|
|
124
|
+
"aqua" => [0, 255, 255], "fuchsia" => [255, 0, 255],
|
|
125
|
+
"orange" => [255, 165, 0], "pink" => [255, 192, 203],
|
|
126
|
+
"brown" => [165, 42, 42],
|
|
127
|
+
}.freeze
|
|
128
|
+
|
|
129
|
+
BLACK = Color.new(0, 0, 0).freeze
|
|
130
|
+
WHITE = Color.new(255, 255, 255).freeze
|
|
131
|
+
end
|
|
132
|
+
end
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Postsvg
|
|
4
|
+
# Single source of truth for formatting numbers in both directions.
|
|
5
|
+
#
|
|
6
|
+
# - Integers print without a trailing ".0".
|
|
7
|
+
# - Floats print to 4 decimals, trailing zeros stripped.
|
|
8
|
+
# - -0 normalizes to 0 (so viewBoxes don't read "0 -0 100 100").
|
|
9
|
+
# - +Inf / -Inf / NaN raise ArgumentError; callers should validate.
|
|
10
|
+
module FormatNumber
|
|
11
|
+
module_function
|
|
12
|
+
|
|
13
|
+
def call(value)
|
|
14
|
+
raise ArgumentError, "non-finite number: #{value.inspect}" unless value.finite?
|
|
15
|
+
|
|
16
|
+
normalized = value.zero? ? 0.0 : value
|
|
17
|
+
return normalized.to_i.to_s if normalized == normalized.to_i
|
|
18
|
+
|
|
19
|
+
format("%.4f", normalized).sub(/\.?0+$/, "")
|
|
20
|
+
end
|
|
21
|
+
end
|
|
22
|
+
end
|
|
@@ -60,8 +60,7 @@ module Postsvg
|
|
|
60
60
|
font_name: overrides.fetch(:font_name, @font_name),
|
|
61
61
|
font_size: overrides.fetch(:font_size, @font_size),
|
|
62
62
|
clip_paths: overrides.fetch(:clip_paths, @clip_paths),
|
|
63
|
-
last_text_position: overrides.fetch(:last_text_position,
|
|
64
|
-
@last_text_position),
|
|
63
|
+
last_text_position: overrides.fetch(:last_text_position, @last_text_position),
|
|
65
64
|
fill_rule: overrides.fetch(:fill_rule, @fill_rule),
|
|
66
65
|
)
|
|
67
66
|
end
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Postsvg
|
|
4
|
+
# Matrix transformation class for PostScript coordinate transformations
|
|
5
|
+
# Implements affine transformation matrix [a b c d e f]
|
|
6
|
+
class Matrix
|
|
7
|
+
attr_accessor :a, :b, :c, :d, :e, :f
|
|
8
|
+
|
|
9
|
+
def initialize(a: 1, b: 0, c: 0, d: 1, e: 0, f: 0)
|
|
10
|
+
@a = a
|
|
11
|
+
@b = b
|
|
12
|
+
@c = c
|
|
13
|
+
@d = d
|
|
14
|
+
@e = e
|
|
15
|
+
@f = f
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
def multiply(matrix)
|
|
19
|
+
result = Matrix.new
|
|
20
|
+
result.a = (@a * matrix.a) + (@c * matrix.b)
|
|
21
|
+
result.b = (@b * matrix.a) + (@d * matrix.b)
|
|
22
|
+
result.c = (@a * matrix.c) + (@c * matrix.d)
|
|
23
|
+
result.d = (@b * matrix.c) + (@d * matrix.d)
|
|
24
|
+
result.e = (@a * matrix.e) + (@c * matrix.f) + @e
|
|
25
|
+
result.f = (@b * matrix.e) + (@d * matrix.f) + @f
|
|
26
|
+
result
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
def translate(tx, ty)
|
|
30
|
+
multiply(Matrix.new(e: tx, f: ty))
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
def scale(sx, sy)
|
|
34
|
+
multiply(Matrix.new(a: sx, d: sy))
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
def rotate(degrees)
|
|
38
|
+
radians = degrees * Math::PI / 180.0
|
|
39
|
+
m = Matrix.new
|
|
40
|
+
m.a = Math.cos(radians)
|
|
41
|
+
m.b = Math.sin(radians)
|
|
42
|
+
m.c = -Math.sin(radians)
|
|
43
|
+
m.d = Math.cos(radians)
|
|
44
|
+
multiply(m)
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
def skew_x(angle)
|
|
48
|
+
radians = angle * Math::PI / 180.0
|
|
49
|
+
multiply(Matrix.new(c: Math.tan(radians)))
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
def skew_y(angle)
|
|
53
|
+
radians = angle * Math::PI / 180.0
|
|
54
|
+
multiply(Matrix.new(b: Math.tan(radians)))
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
def to_transform_string
|
|
58
|
+
"matrix(#{@a} #{@b} #{@c} #{@d} #{@e} #{@f})"
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
def apply_point(x, y)
|
|
62
|
+
{
|
|
63
|
+
x: (x * @a) + (y * @c) + @e,
|
|
64
|
+
y: (x * @b) + (y * @d) + @f,
|
|
65
|
+
}
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
def decompose
|
|
69
|
+
scale_x = Math.hypot(@a, @b)
|
|
70
|
+
scale_y = ((@a * @d) - (@b * @c)) / scale_x
|
|
71
|
+
|
|
72
|
+
rotation = Math.atan2(@b, @a) * (180.0 / Math::PI)
|
|
73
|
+
|
|
74
|
+
skew_x = Math.atan2((@a * @c) + (@b * @d), scale_x * scale_x)
|
|
75
|
+
skew_y = Math.atan2((@a * @b) + (@c * @d), scale_y * scale_y)
|
|
76
|
+
|
|
77
|
+
{
|
|
78
|
+
translate: { x: @e, y: @f },
|
|
79
|
+
scale: { x: scale_x, y: scale_y },
|
|
80
|
+
rotate: rotation,
|
|
81
|
+
skew: {
|
|
82
|
+
x: skew_x * (180.0 / Math::PI),
|
|
83
|
+
y: skew_y * (180.0 / Math::PI),
|
|
84
|
+
},
|
|
85
|
+
}
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
def invert
|
|
89
|
+
det = (@a * @d) - (@b * @c)
|
|
90
|
+
return Matrix.new if det.abs < 1e-10
|
|
91
|
+
|
|
92
|
+
inv = Matrix.new
|
|
93
|
+
inv.a = @d / det
|
|
94
|
+
inv.b = -@b / det
|
|
95
|
+
inv.c = -@c / det
|
|
96
|
+
inv.d = @a / det
|
|
97
|
+
inv.e = ((@c * @f) - (@d * @e)) / det
|
|
98
|
+
inv.f = ((@b * @e) - (@a * @f)) / det
|
|
99
|
+
inv
|
|
100
|
+
end
|
|
101
|
+
|
|
102
|
+
def identity?
|
|
103
|
+
@a == 1 && @b.zero? && @c.zero? && @d == 1 && @e.zero? && @f.zero?
|
|
104
|
+
end
|
|
105
|
+
end
|
|
106
|
+
end
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Postsvg
|
|
4
|
+
module Model
|
|
5
|
+
module Literals
|
|
6
|
+
# Bracketed array literal: +[1 2 3]+. Elements may be any
|
|
7
|
+
# literal or operator.
|
|
8
|
+
class ArrayLiteral
|
|
9
|
+
include Enumerable
|
|
10
|
+
|
|
11
|
+
attr_reader :elements
|
|
12
|
+
|
|
13
|
+
def initialize(elements = [])
|
|
14
|
+
@elements = elements.freeze
|
|
15
|
+
freeze
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
def each(&block)
|
|
19
|
+
@elements.each(&block)
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
def length = @elements.length
|
|
23
|
+
def empty? = @elements.empty?
|
|
24
|
+
def [](idx) = @elements[idx]
|
|
25
|
+
|
|
26
|
+
def accept(visitor, ctx)
|
|
27
|
+
visitor.visit_array(self, ctx)
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
def ==(other)
|
|
31
|
+
other.is_a?(ArrayLiteral) && other.elements == @elements
|
|
32
|
+
end
|
|
33
|
+
alias eql? ==
|
|
34
|
+
|
|
35
|
+
def hash
|
|
36
|
+
@elements.hash
|
|
37
|
+
end
|
|
38
|
+
end
|
|
39
|
+
end
|
|
40
|
+
end
|
|
41
|
+
end
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Postsvg
|
|
4
|
+
module Model
|
|
5
|
+
module Literals
|
|
6
|
+
# Inline dictionary literal: +<< /Key (value) /Num 42 >>+.
|
|
7
|
+
class Dictionary
|
|
8
|
+
attr_reader :entries
|
|
9
|
+
|
|
10
|
+
def initialize(entries = {})
|
|
11
|
+
@entries = entries.dup.freeze
|
|
12
|
+
freeze
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
def [](key) = @entries[key]
|
|
16
|
+
def key?(key) = @entries.key?(key)
|
|
17
|
+
def each(&block) = @entries.each(&block)
|
|
18
|
+
|
|
19
|
+
def accept(visitor, ctx)
|
|
20
|
+
visitor.visit_dictionary(self, ctx)
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
def ==(other)
|
|
24
|
+
other.is_a?(Dictionary) && other.entries == @entries
|
|
25
|
+
end
|
|
26
|
+
alias eql? ==
|
|
27
|
+
|
|
28
|
+
def hash
|
|
29
|
+
@entries.hash
|
|
30
|
+
end
|
|
31
|
+
end
|
|
32
|
+
end
|
|
33
|
+
end
|
|
34
|
+
end
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Postsvg
|
|
4
|
+
module Model
|
|
5
|
+
module Literals
|
|
6
|
+
# Hex-string literal: +<DEADBEEF>+.
|
|
7
|
+
class HexLiteral
|
|
8
|
+
attr_reader :value
|
|
9
|
+
|
|
10
|
+
def initialize(value)
|
|
11
|
+
@value = value.to_s
|
|
12
|
+
freeze
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
# Raw bytes encoded by the hex literal.
|
|
16
|
+
def bytes
|
|
17
|
+
hex = value.delete("^0-9a-fA-F")
|
|
18
|
+
hex << "0" if hex.length.odd?
|
|
19
|
+
hex.chars.each_slice(2).map { |pair| pair.join.to_i(16) }.pack("C*")
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
def accept(visitor, ctx)
|
|
23
|
+
visitor.visit_hex(self, ctx)
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
def ==(other)
|
|
27
|
+
other.is_a?(HexLiteral) && other.value == value
|
|
28
|
+
end
|
|
29
|
+
alias eql? ==
|
|
30
|
+
|
|
31
|
+
def hash
|
|
32
|
+
value.hash
|
|
33
|
+
end
|
|
34
|
+
end
|
|
35
|
+
end
|
|
36
|
+
end
|
|
37
|
+
end
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Postsvg
|
|
4
|
+
module Model
|
|
5
|
+
module Literals
|
|
6
|
+
# A PostScript name. +literal:+ distinguishes +/foo+ (literal,
|
|
7
|
+
# pushed onto the stack) from +foo+ (executable, looked up and
|
|
8
|
+
# executed).
|
|
9
|
+
class Name
|
|
10
|
+
attr_reader :value, :literal
|
|
11
|
+
|
|
12
|
+
def initialize(value, literal: false)
|
|
13
|
+
@value = value.to_s
|
|
14
|
+
@literal = literal
|
|
15
|
+
freeze
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
def literal? = literal
|
|
19
|
+
def executable? = !literal
|
|
20
|
+
|
|
21
|
+
def to_s
|
|
22
|
+
@value
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
def accept(visitor, ctx)
|
|
26
|
+
visitor.visit_name(self, ctx)
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
def ==(other)
|
|
30
|
+
other.is_a?(Name) && other.value == value && other.literal == literal
|
|
31
|
+
end
|
|
32
|
+
alias eql? ==
|
|
33
|
+
|
|
34
|
+
def hash
|
|
35
|
+
[value, literal].hash
|
|
36
|
+
end
|
|
37
|
+
end
|
|
38
|
+
end
|
|
39
|
+
end
|
|
40
|
+
end
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Postsvg
|
|
4
|
+
module Model
|
|
5
|
+
module Literals
|
|
6
|
+
# A PostScript numeric literal. Stored as a Ruby Numeric so
|
|
7
|
+
# arithmetic Just Works; class carries the original source
|
|
8
|
+
# representation choice (int vs float) when known.
|
|
9
|
+
class Number
|
|
10
|
+
attr_reader :value
|
|
11
|
+
|
|
12
|
+
def initialize(value)
|
|
13
|
+
@value = value.is_a?(Numeric) ? value : Float(value)
|
|
14
|
+
freeze
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
def to_i = value.to_i
|
|
18
|
+
def to_f = value.to_f
|
|
19
|
+
def integer? = value.is_a?(Integer)
|
|
20
|
+
|
|
21
|
+
def accept(visitor, ctx)
|
|
22
|
+
visitor.visit_number(self, ctx)
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
def ==(other)
|
|
26
|
+
other.is_a?(Number) && other.value == value
|
|
27
|
+
end
|
|
28
|
+
alias eql? ==
|
|
29
|
+
|
|
30
|
+
def hash
|
|
31
|
+
value.hash
|
|
32
|
+
end
|
|
33
|
+
end
|
|
34
|
+
end
|
|
35
|
+
end
|
|
36
|
+
end
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Postsvg
|
|
4
|
+
module Model
|
|
5
|
+
module Literals
|
|
6
|
+
# Procedure body: +{ ... }+. Carries the inner token stream as a
|
|
7
|
+
# list of literal/operator nodes. Procedures are not executed by
|
|
8
|
+
# the parser; the renderer descends into them on InvokeProcedure.
|
|
9
|
+
class Procedure
|
|
10
|
+
include Enumerable
|
|
11
|
+
|
|
12
|
+
attr_reader :body
|
|
13
|
+
|
|
14
|
+
def initialize(body = [])
|
|
15
|
+
@body = body.freeze
|
|
16
|
+
freeze
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
def each(&block)
|
|
20
|
+
@body.each(&block)
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
def length = @body.length
|
|
24
|
+
def empty? = @body.empty?
|
|
25
|
+
|
|
26
|
+
def accept(visitor, ctx)
|
|
27
|
+
visitor.visit_procedure(self, ctx)
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
def ==(other)
|
|
31
|
+
other.is_a?(Procedure) && other.body == @body
|
|
32
|
+
end
|
|
33
|
+
alias eql? ==
|
|
34
|
+
|
|
35
|
+
def hash
|
|
36
|
+
@body.hash
|
|
37
|
+
end
|
|
38
|
+
end
|
|
39
|
+
end
|
|
40
|
+
end
|
|
41
|
+
end
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Postsvg
|
|
4
|
+
module Model
|
|
5
|
+
module Literals
|
|
6
|
+
# Parenthesized string literal: +(foo bar baz)+.
|
|
7
|
+
class StringLiteral
|
|
8
|
+
attr_reader :value
|
|
9
|
+
|
|
10
|
+
def initialize(value)
|
|
11
|
+
@value = value.to_s
|
|
12
|
+
freeze
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
def accept(visitor, ctx)
|
|
16
|
+
visitor.visit_string_literal(self, ctx)
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
def ==(other)
|
|
20
|
+
other.is_a?(StringLiteral) && other.value == value
|
|
21
|
+
end
|
|
22
|
+
alias eql? ==
|
|
23
|
+
|
|
24
|
+
def hash
|
|
25
|
+
value.hash
|
|
26
|
+
end
|
|
27
|
+
end
|
|
28
|
+
end
|
|
29
|
+
end
|
|
30
|
+
end
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Postsvg
|
|
4
|
+
module Model
|
|
5
|
+
# PS literal value objects. Each is immutable, has value equality,
|
|
6
|
+
# and knows how to (a) emit itself to a serializer and (b) be
|
|
7
|
+
# visited by a renderer. They never carry executable behaviour —
|
|
8
|
+
# they are data.
|
|
9
|
+
module Literals
|
|
10
|
+
autoload :Number, "postsvg/model/literals/number"
|
|
11
|
+
autoload :Name, "postsvg/model/literals/name"
|
|
12
|
+
autoload :StringLiteral, "postsvg/model/literals/string"
|
|
13
|
+
autoload :HexLiteral, "postsvg/model/literals/hex"
|
|
14
|
+
autoload :ArrayLiteral, "postsvg/model/literals/array"
|
|
15
|
+
autoload :Procedure, "postsvg/model/literals/procedure"
|
|
16
|
+
autoload :Dictionary, "postsvg/model/literals/dictionary"
|
|
17
|
+
end
|
|
18
|
+
end
|
|
19
|
+
end
|