postsvg 0.2.2 → 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/Gemfile +1 -1
- 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 +1 -3
- 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/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_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/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 +37 -42
- metadata +36 -16
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
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Postsvg
|
|
4
|
+
module Model
|
|
5
|
+
# Common base for every PS operator. Subclasses live under
|
|
6
|
+
# Model::Operators::* grouped by PLRM chapter.
|
|
7
|
+
#
|
|
8
|
+
# Each operator declares:
|
|
9
|
+
# - +keyword+ (the PS source token, e.g. "moveto")
|
|
10
|
+
# - typed operand fields via +attr_reader+
|
|
11
|
+
# - +self.from_operands(stack)+ that pops operands and returns a new
|
|
12
|
+
# frozen instance
|
|
13
|
+
# - +self.visit_name+ (defaults to the keyword)
|
|
14
|
+
# - +self.consumes+ / +self.produces+ (stack-arity, for parser
|
|
15
|
+
# placeholders)
|
|
16
|
+
#
|
|
17
|
+
# The visitor calls +operator.accept(visitor, ctx)+ which routes to
|
|
18
|
+
# +visitor.visit_<visit_name>(operator, ctx)+ via +public_send+.
|
|
19
|
+
class Operator
|
|
20
|
+
class << self
|
|
21
|
+
# Register this class under +keyword+ in the global Registry.
|
|
22
|
+
# Called at the bottom of each subclass definition.
|
|
23
|
+
def register_as(keyword, consumes: 0, produces: 0)
|
|
24
|
+
Operators.register(keyword, self)
|
|
25
|
+
define_method(:keyword) { keyword }
|
|
26
|
+
define_method(:visit_name) { keyword }
|
|
27
|
+
@consumes = consumes
|
|
28
|
+
@produces = produces
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
attr_reader :consumes, :produces
|
|
32
|
+
|
|
33
|
+
# Default: a no-op factory that pops nothing. Subclasses
|
|
34
|
+
# override to consume operands off the parse stack.
|
|
35
|
+
def from_operands(_stack)
|
|
36
|
+
new
|
|
37
|
+
end
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
def accept(visitor, ctx)
|
|
41
|
+
visitor.dispatch(:"visit_#{visit_name}", self, ctx)
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
def operator? = true
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
# Sentinel value the parser pushes onto the operand stack when an
|
|
48
|
+
# operator's runtime result is unknown at parse time (e.g. the
|
|
49
|
+
# output of +findfont+, +add+, +dup+). Carries no semantic value;
|
|
50
|
+
# its purpose is to keep the parse-stack shape in sync with the
|
|
51
|
+
# runtime stack so chained operators parse cleanly.
|
|
52
|
+
class Computed < ::Struct.new(:operator_keyword, keyword_init: true)
|
|
53
|
+
def to_s
|
|
54
|
+
"<computed:#{operator_keyword}>"
|
|
55
|
+
end
|
|
56
|
+
end
|
|
57
|
+
end
|
|
58
|
+
end
|