postsvg 0.3.0 → 0.3.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (91) hide show
  1. checksums.yaml +4 -4
  2. data/.rubocop_todo.yml +533 -49
  3. data/CLAUDE.md +27 -3
  4. data/Gemfile +2 -2
  5. data/README.adoc +62 -4
  6. data/Rakefile +17 -0
  7. data/gemfiles/native-parity/Gemfile +8 -0
  8. data/lib/postsvg/cli.rb +10 -5
  9. data/lib/postsvg/graphics_context.rb +2 -1
  10. data/lib/postsvg/interpreter.rb +1 -2
  11. data/lib/postsvg/options.rb +1 -1
  12. data/lib/postsvg/parser/postscript_parser.rb +7 -7
  13. data/lib/postsvg/renderer.rb +7 -3
  14. data/lib/postsvg/svg/clip_path_registry.rb +3 -1
  15. data/lib/postsvg/svg/element.rb +4 -2
  16. data/lib/postsvg/svg/elements/ellipse.rb +2 -1
  17. data/lib/postsvg/svg/elements/svg.rb +2 -1
  18. data/lib/postsvg/svg/paint.rb +1 -1
  19. data/lib/postsvg/svg/parser.rb +3 -3
  20. data/lib/postsvg/svg/path_data/parser.rb +10 -6
  21. data/lib/postsvg/svg/stroke.rb +4 -2
  22. data/lib/postsvg/svg/transform_list.rb +1 -1
  23. data/lib/postsvg/svg.rb +1 -1
  24. data/lib/postsvg/svg_builder.rb +15 -11
  25. data/lib/postsvg/translation/arc_converter.rb +23 -14
  26. data/lib/postsvg/translation/bounding_box.rb +7 -4
  27. data/lib/postsvg/translation/handlers/circle_handler.rb +2 -2
  28. data/lib/postsvg/translation/handlers/ellipse_handler.rb +8 -3
  29. data/lib/postsvg/translation/handlers/group_handler.rb +2 -1
  30. data/lib/postsvg/translation/handlers/image_handler.rb +3 -2
  31. data/lib/postsvg/translation/handlers/line_handler.rb +6 -2
  32. data/lib/postsvg/translation/handlers/open_handler.rb +3 -2
  33. data/lib/postsvg/translation/handlers/path_handler.rb +82 -60
  34. data/lib/postsvg/translation/handlers/polygon_handler.rb +4 -2
  35. data/lib/postsvg/translation/handlers/polyline_handler.rb +5 -4
  36. data/lib/postsvg/translation/handlers/rect_handler.rb +10 -5
  37. data/lib/postsvg/translation/handlers/shared.rb +11 -11
  38. data/lib/postsvg/translation/handlers/svg_handler.rb +3 -2
  39. data/lib/postsvg/translation/handlers/text_handler.rb +17 -8
  40. data/lib/postsvg/translation/handlers.rb +2 -1
  41. data/lib/postsvg/translation/ps_renderer.rb +18 -4
  42. data/lib/postsvg/translation/record_emitter.rb +54 -0
  43. data/lib/postsvg/version.rb +1 -1
  44. data/lib/postsvg/visitors/ps_visitor/arithmetic.rb +6 -6
  45. data/lib/postsvg/visitors/ps_visitor/color.rb +8 -5
  46. data/lib/postsvg/visitors/ps_visitor/common.rb +1 -2
  47. data/lib/postsvg/visitors/ps_visitor/container.rb +13 -13
  48. data/lib/postsvg/visitors/ps_visitor/control_flow.rb +3 -3
  49. data/lib/postsvg/visitors/ps_visitor/dictionary.rb +8 -4
  50. data/lib/postsvg/visitors/ps_visitor/font.rb +15 -4
  51. data/lib/postsvg/visitors/ps_visitor/graphics_state.rb +7 -5
  52. data/lib/postsvg/visitors/ps_visitor/painting.rb +3 -3
  53. data/lib/postsvg/visitors/ps_visitor/path.rb +52 -33
  54. data/lib/postsvg/visitors/ps_visitor/transformations.rb +45 -7
  55. data/lib/postsvg/visitors/ps_visitor.rb +18 -2
  56. data/lib/postsvg.rb +43 -38
  57. metadata +20 -39
  58. data/lib/postsvg/color.rb +0 -132
  59. data/lib/postsvg/format_number.rb +0 -22
  60. data/lib/postsvg/matrix.rb +0 -106
  61. data/lib/postsvg/model/literals/array.rb +0 -41
  62. data/lib/postsvg/model/literals/dictionary.rb +0 -34
  63. data/lib/postsvg/model/literals/hex.rb +0 -37
  64. data/lib/postsvg/model/literals/name.rb +0 -40
  65. data/lib/postsvg/model/literals/number.rb +0 -36
  66. data/lib/postsvg/model/literals/procedure.rb +0 -41
  67. data/lib/postsvg/model/literals/string.rb +0 -30
  68. data/lib/postsvg/model/literals.rb +0 -19
  69. data/lib/postsvg/model/operator.rb +0 -58
  70. data/lib/postsvg/model/operators/arithmetic.rb +0 -264
  71. data/lib/postsvg/model/operators/boolean.rb +0 -182
  72. data/lib/postsvg/model/operators/color.rb +0 -74
  73. data/lib/postsvg/model/operators/container.rb +0 -186
  74. data/lib/postsvg/model/operators/control_flow.rb +0 -119
  75. data/lib/postsvg/model/operators/device.rb +0 -21
  76. data/lib/postsvg/model/operators/dictionary.rb +0 -118
  77. data/lib/postsvg/model/operators/font.rb +0 -121
  78. data/lib/postsvg/model/operators/graphics_state.rb +0 -84
  79. data/lib/postsvg/model/operators/painting.rb +0 -29
  80. data/lib/postsvg/model/operators/path.rb +0 -169
  81. data/lib/postsvg/model/operators/stack.rb +0 -72
  82. data/lib/postsvg/model/operators/transformations.rb +0 -103
  83. data/lib/postsvg/model/operators.rb +0 -89
  84. data/lib/postsvg/model/program.rb +0 -68
  85. data/lib/postsvg/model/token.rb +0 -43
  86. data/lib/postsvg/model.rb +0 -17
  87. data/lib/postsvg/serializer.rb +0 -325
  88. data/lib/postsvg/source/ast_builder.rb +0 -308
  89. data/lib/postsvg/source/lexer.rb +0 -322
  90. data/lib/postsvg/source/operand_stack.rb +0 -55
  91. data/lib/postsvg/source.rb +0 -21
data/lib/postsvg.rb CHANGED
@@ -1,63 +1,68 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require "postscript"
3
4
  require "parslet"
4
5
  require "thor"
5
- require "nokogiri"
6
+ require "leptris"
6
7
 
7
8
  module Postsvg
8
9
  autoload :VERSION, "postsvg/version"
9
10
 
10
- # Errors
11
+ # ============================================================
12
+ # Posts created and managed by the +postscript+ gem.
13
+ # ============================================================
14
+ #
15
+ # `postscript` owns PS source parsing, the typed PS domain model,
16
+ # and the PS source serializer. The `Postscript` namespace is the
17
+ # canonical location; the aliases below keep existing user code
18
+ # working during the transition (deprecated, will be removed at
19
+ # postsvg 1.0).
20
+ #
21
+ # If you maintain postsvg, prefer `Postscript::*` in new code.
22
+
23
+ Source = Postscript::Source
24
+ Model = Postscript::Model
25
+ Serializer = Postscript::Serializer
26
+ Matrix = Postscript::Matrix
27
+ Color = Postscript::Color
28
+ FormatNumber = Postscript::FormatNumber
29
+
30
+ ParseError = Postscript::ParseError
31
+ LexError = Postscript::LexError
32
+ SyntaxError = Postscript::SyntaxError
33
+ StackUnderflowError = Postscript::StackUnderflowError
34
+ UndefinedOperatorError = Postscript::UndefinedOperatorError
35
+ RecursionLimitError = Postscript::RecursionLimitError
36
+ RenderError = Postscript::RenderError
37
+ SerializeError = Postscript::SerializeError
38
+ ExitSignal = Postscript::ExitSignal
39
+ QuitSignal = Postscript::QuitSignal
40
+
41
+ # ============================================================
42
+ # Errors that originate in the SVG side (kept here, not in postscript).
43
+ # ============================================================
11
44
  autoload :Error, "postsvg/errors"
12
- autoload :ParseError, "postsvg/errors"
13
- autoload :LexError, "postsvg/errors"
14
- autoload :SyntaxError, "postsvg/errors"
15
- autoload :RenderError, "postsvg/errors"
16
- autoload :StackUnderflowError, "postsvg/errors"
17
- autoload :UndefinedOperatorError, "postsvg/errors"
18
- autoload :RecursionLimitError, "postsvg/errors"
19
- autoload :SizeLimitError, "postsvg/errors"
20
45
  autoload :TranslationError, "postsvg/errors"
21
46
  autoload :UnsupportedElementError, "postsvg/errors"
22
47
  autoload :UnresolvedReferenceError, "postsvg/errors"
23
- autoload :SerializeError, "postsvg/errors"
24
- autoload :ConversionError, "postsvg/errors"
25
- autoload :UnsupportedOperatorError, "postsvg/errors"
26
- autoload :ExitSignal, "postsvg/errors"
27
- autoload :QuitSignal, "postsvg/errors"
28
48
 
29
- # Foundational value types
30
- autoload :FormatNumber, "postsvg/format_number"
31
- autoload :Matrix, "postsvg/matrix"
32
- autoload :Color, "postsvg/color"
49
+ # Legacy alias for RenderError (postsvg 0.1 API).
50
+ ConversionError = RenderError
51
+ # Legacy alias for UndefinedOperatorError (postsvg 0.1 API).
52
+ UnsupportedOperatorError = UndefinedOperatorError
33
53
 
34
- # Source-reading layer (PS source -> Model::Program)
35
- autoload :Source, "postsvg/source"
36
-
37
- # PS domain model
38
- autoload :Model, "postsvg/model"
39
-
40
- # Graphics state
54
+ # ============================================================
55
+ # Rendering / SVG-side types owned by postsvg.
56
+ # ============================================================
41
57
  autoload :GraphicsContext, "postsvg/graphics_context"
42
58
  autoload :GraphicsStack, "postsvg/graphics_stack"
43
59
  autoload :PathBuilder, "postsvg/path_builder"
44
-
45
- # SVG emission (forward direction)
46
60
  autoload :SvgBuilder, "postsvg/svg_builder"
47
-
48
- # Forward direction (PS -> SVG)
49
61
  autoload :Options, "postsvg/options"
50
62
  autoload :Renderer, "postsvg/renderer"
51
63
  autoload :Visitors, "postsvg/visitors"
52
-
53
- # SVG domain model (reverse direction input)
54
64
  autoload :Svg, "postsvg/svg"
55
-
56
- # Reverse direction (SVG -> PS)
57
65
  autoload :Translation, "postsvg/translation"
58
- autoload :Serializer, "postsvg/serializer"
59
-
60
- # CLI
61
66
  autoload :CLI, "postsvg/cli"
62
67
 
63
68
  # Legacy implementations retained on disk for reference. Not on the
@@ -73,7 +78,7 @@ module Postsvg
73
78
  class << self
74
79
  # PS/EPS source -> SVG string.
75
80
  def to_svg(source, **options)
76
- program = Source.parse(source)
81
+ program = Postscript::Source.parse(source)
77
82
  Renderer.call(program, Options.new(**options))
78
83
  end
79
84
 
metadata CHANGED
@@ -1,29 +1,29 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: postsvg
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.3.0
4
+ version: 0.3.1
5
5
  platform: ruby
6
6
  authors:
7
7
  - Ribose Inc.
8
8
  autorequire:
9
9
  bindir: exe
10
10
  cert_chain: []
11
- date: 2026-07-27 00:00:00.000000000 Z
11
+ date: 2026-11-02 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
- name: nokogiri
14
+ name: leptris
15
15
  requirement: !ruby/object:Gem::Requirement
16
16
  requirements:
17
17
  - - ">="
18
18
  - !ruby/object:Gem::Version
19
- version: '0'
19
+ version: 1.9.399.0
20
20
  type: :runtime
21
21
  prerelease: false
22
22
  version_requirements: !ruby/object:Gem::Requirement
23
23
  requirements:
24
24
  - - ">="
25
25
  - !ruby/object:Gem::Version
26
- version: '0'
26
+ version: 1.9.399.0
27
27
  - !ruby/object:Gem::Dependency
28
28
  name: parslet
29
29
  requirement: !ruby/object:Gem::Requirement
@@ -38,6 +38,20 @@ dependencies:
38
38
  - - ">="
39
39
  - !ruby/object:Gem::Version
40
40
  version: '0'
41
+ - !ruby/object:Gem::Dependency
42
+ name: postscript
43
+ requirement: !ruby/object:Gem::Requirement
44
+ requirements:
45
+ - - "~>"
46
+ - !ruby/object:Gem::Version
47
+ version: '0.2'
48
+ type: :runtime
49
+ prerelease: false
50
+ version_requirements: !ruby/object:Gem::Requirement
51
+ requirements:
52
+ - - "~>"
53
+ - !ruby/object:Gem::Version
54
+ version: '0.2'
41
55
  - !ruby/object:Gem::Dependency
42
56
  name: thor
43
57
  requirement: !ruby/object:Gem::Requirement
@@ -172,55 +186,22 @@ files:
172
186
  - docs/troubleshooting.adoc
173
187
  - docs/validation.adoc
174
188
  - exe/postsvg
189
+ - gemfiles/native-parity/Gemfile
175
190
  - lib/postsvg.rb
176
191
  - lib/postsvg/cli.rb
177
- - lib/postsvg/color.rb
178
192
  - lib/postsvg/colors.rb
179
193
  - lib/postsvg/converter.rb
180
194
  - lib/postsvg/errors.rb
181
- - lib/postsvg/format_number.rb
182
195
  - lib/postsvg/graphics_context.rb
183
196
  - lib/postsvg/graphics_stack.rb
184
197
  - lib/postsvg/graphics_state.rb
185
198
  - lib/postsvg/interpreter.rb
186
- - lib/postsvg/matrix.rb
187
- - lib/postsvg/model.rb
188
- - lib/postsvg/model/literals.rb
189
- - lib/postsvg/model/literals/array.rb
190
- - lib/postsvg/model/literals/dictionary.rb
191
- - lib/postsvg/model/literals/hex.rb
192
- - lib/postsvg/model/literals/name.rb
193
- - lib/postsvg/model/literals/number.rb
194
- - lib/postsvg/model/literals/procedure.rb
195
- - lib/postsvg/model/literals/string.rb
196
- - lib/postsvg/model/operator.rb
197
- - lib/postsvg/model/operators.rb
198
- - lib/postsvg/model/operators/arithmetic.rb
199
- - lib/postsvg/model/operators/boolean.rb
200
- - lib/postsvg/model/operators/color.rb
201
- - lib/postsvg/model/operators/container.rb
202
- - lib/postsvg/model/operators/control_flow.rb
203
- - lib/postsvg/model/operators/device.rb
204
- - lib/postsvg/model/operators/dictionary.rb
205
- - lib/postsvg/model/operators/font.rb
206
- - lib/postsvg/model/operators/graphics_state.rb
207
- - lib/postsvg/model/operators/painting.rb
208
- - lib/postsvg/model/operators/path.rb
209
- - lib/postsvg/model/operators/stack.rb
210
- - lib/postsvg/model/operators/transformations.rb
211
- - lib/postsvg/model/program.rb
212
- - lib/postsvg/model/token.rb
213
199
  - lib/postsvg/options.rb
214
200
  - lib/postsvg/parser.rb
215
201
  - lib/postsvg/parser/postscript_parser.rb
216
202
  - lib/postsvg/parser/transform.rb
217
203
  - lib/postsvg/path_builder.rb
218
204
  - lib/postsvg/renderer.rb
219
- - lib/postsvg/serializer.rb
220
- - lib/postsvg/source.rb
221
- - lib/postsvg/source/ast_builder.rb
222
- - lib/postsvg/source/lexer.rb
223
- - lib/postsvg/source/operand_stack.rb
224
205
  - lib/postsvg/svg.rb
225
206
  - lib/postsvg/svg/attribute_parser.rb
226
207
  - lib/postsvg/svg/clip_path_registry.rb
data/lib/postsvg/color.rb DELETED
@@ -1,132 +0,0 @@
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
@@ -1,22 +0,0 @@
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
@@ -1,106 +0,0 @@
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
@@ -1,41 +0,0 @@
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
@@ -1,34 +0,0 @@
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
@@ -1,37 +0,0 @@
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
@@ -1,40 +0,0 @@
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
@@ -1,36 +0,0 @@
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