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.
- checksums.yaml +4 -4
- data/.rubocop_todo.yml +533 -49
- data/CLAUDE.md +27 -3
- data/Gemfile +2 -2
- data/README.adoc +62 -4
- data/Rakefile +17 -0
- data/gemfiles/native-parity/Gemfile +8 -0
- data/lib/postsvg/cli.rb +10 -5
- data/lib/postsvg/graphics_context.rb +2 -1
- data/lib/postsvg/interpreter.rb +1 -2
- data/lib/postsvg/options.rb +1 -1
- data/lib/postsvg/parser/postscript_parser.rb +7 -7
- data/lib/postsvg/renderer.rb +7 -3
- data/lib/postsvg/svg/clip_path_registry.rb +3 -1
- data/lib/postsvg/svg/element.rb +4 -2
- data/lib/postsvg/svg/elements/ellipse.rb +2 -1
- data/lib/postsvg/svg/elements/svg.rb +2 -1
- data/lib/postsvg/svg/paint.rb +1 -1
- data/lib/postsvg/svg/parser.rb +3 -3
- data/lib/postsvg/svg/path_data/parser.rb +10 -6
- data/lib/postsvg/svg/stroke.rb +4 -2
- data/lib/postsvg/svg/transform_list.rb +1 -1
- data/lib/postsvg/svg.rb +1 -1
- data/lib/postsvg/svg_builder.rb +15 -11
- data/lib/postsvg/translation/arc_converter.rb +23 -14
- data/lib/postsvg/translation/bounding_box.rb +7 -4
- data/lib/postsvg/translation/handlers/circle_handler.rb +2 -2
- data/lib/postsvg/translation/handlers/ellipse_handler.rb +8 -3
- data/lib/postsvg/translation/handlers/group_handler.rb +2 -1
- data/lib/postsvg/translation/handlers/image_handler.rb +3 -2
- data/lib/postsvg/translation/handlers/line_handler.rb +6 -2
- data/lib/postsvg/translation/handlers/open_handler.rb +3 -2
- data/lib/postsvg/translation/handlers/path_handler.rb +82 -60
- data/lib/postsvg/translation/handlers/polygon_handler.rb +4 -2
- data/lib/postsvg/translation/handlers/polyline_handler.rb +5 -4
- data/lib/postsvg/translation/handlers/rect_handler.rb +10 -5
- data/lib/postsvg/translation/handlers/shared.rb +11 -11
- data/lib/postsvg/translation/handlers/svg_handler.rb +3 -2
- data/lib/postsvg/translation/handlers/text_handler.rb +17 -8
- data/lib/postsvg/translation/handlers.rb +2 -1
- data/lib/postsvg/translation/ps_renderer.rb +18 -4
- data/lib/postsvg/translation/record_emitter.rb +54 -0
- 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 +8 -5
- data/lib/postsvg/visitors/ps_visitor/common.rb +1 -2
- 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 +8 -4
- data/lib/postsvg/visitors/ps_visitor/font.rb +15 -4
- data/lib/postsvg/visitors/ps_visitor/graphics_state.rb +7 -5
- data/lib/postsvg/visitors/ps_visitor/painting.rb +3 -3
- data/lib/postsvg/visitors/ps_visitor/path.rb +52 -33
- data/lib/postsvg/visitors/ps_visitor/transformations.rb +45 -7
- data/lib/postsvg/visitors/ps_visitor.rb +18 -2
- data/lib/postsvg.rb +43 -38
- metadata +20 -39
- data/lib/postsvg/color.rb +0 -132
- data/lib/postsvg/format_number.rb +0 -22
- data/lib/postsvg/matrix.rb +0 -106
- data/lib/postsvg/model/literals/array.rb +0 -41
- data/lib/postsvg/model/literals/dictionary.rb +0 -34
- data/lib/postsvg/model/literals/hex.rb +0 -37
- data/lib/postsvg/model/literals/name.rb +0 -40
- data/lib/postsvg/model/literals/number.rb +0 -36
- data/lib/postsvg/model/literals/procedure.rb +0 -41
- data/lib/postsvg/model/literals/string.rb +0 -30
- data/lib/postsvg/model/literals.rb +0 -19
- data/lib/postsvg/model/operator.rb +0 -58
- data/lib/postsvg/model/operators/arithmetic.rb +0 -264
- data/lib/postsvg/model/operators/boolean.rb +0 -182
- data/lib/postsvg/model/operators/color.rb +0 -74
- data/lib/postsvg/model/operators/container.rb +0 -186
- data/lib/postsvg/model/operators/control_flow.rb +0 -119
- data/lib/postsvg/model/operators/device.rb +0 -21
- data/lib/postsvg/model/operators/dictionary.rb +0 -118
- data/lib/postsvg/model/operators/font.rb +0 -121
- data/lib/postsvg/model/operators/graphics_state.rb +0 -84
- data/lib/postsvg/model/operators/painting.rb +0 -29
- data/lib/postsvg/model/operators/path.rb +0 -169
- data/lib/postsvg/model/operators/stack.rb +0 -72
- data/lib/postsvg/model/operators/transformations.rb +0 -103
- data/lib/postsvg/model/operators.rb +0 -89
- data/lib/postsvg/model/program.rb +0 -68
- data/lib/postsvg/model/token.rb +0 -43
- data/lib/postsvg/model.rb +0 -17
- data/lib/postsvg/serializer.rb +0 -325
- data/lib/postsvg/source/ast_builder.rb +0 -308
- data/lib/postsvg/source/lexer.rb +0 -322
- data/lib/postsvg/source/operand_stack.rb +0 -55
- data/lib/postsvg/source.rb +0 -21
|
@@ -7,6 +7,54 @@ module Postsvg
|
|
|
7
7
|
# surface so handlers don't all need to know how the program is
|
|
8
8
|
# stored.
|
|
9
9
|
class RecordEmitter
|
|
10
|
+
NUMBER_OPERANDS = lambda do |record|
|
|
11
|
+
record.operand_values.map { |value| Model::Literals::Number.new(value) }
|
|
12
|
+
end
|
|
13
|
+
NO_OPERANDS = ->(_record) { [] }
|
|
14
|
+
|
|
15
|
+
OPERAND_BUILDERS = {
|
|
16
|
+
Model::Operators::Color::Setgray => NUMBER_OPERANDS,
|
|
17
|
+
Model::Operators::Color::Setrgbcolor => NUMBER_OPERANDS,
|
|
18
|
+
Model::Operators::GraphicsState::Setlinewidth => NUMBER_OPERANDS,
|
|
19
|
+
Model::Operators::Path::Moveto => NUMBER_OPERANDS,
|
|
20
|
+
Model::Operators::Path::Rmoveto => lambda do |record|
|
|
21
|
+
[Model::Literals::Number.new(record.dx), Model::Literals::Number.new(record.dy)]
|
|
22
|
+
end,
|
|
23
|
+
Model::Operators::Path::Lineto => NUMBER_OPERANDS,
|
|
24
|
+
Model::Operators::Path::Rlineto => NUMBER_OPERANDS,
|
|
25
|
+
Model::Operators::Path::Curveto => NUMBER_OPERANDS,
|
|
26
|
+
Model::Operators::Path::Rcurveto => NUMBER_OPERANDS,
|
|
27
|
+
Model::Operators::Path::Arc => NUMBER_OPERANDS,
|
|
28
|
+
Model::Operators::Path::Arcn => NUMBER_OPERANDS,
|
|
29
|
+
Model::Operators::Transformations::Translate => NUMBER_OPERANDS,
|
|
30
|
+
Model::Operators::Transformations::Scale => NUMBER_OPERANDS,
|
|
31
|
+
Model::Operators::Font::Findfont => lambda do |record|
|
|
32
|
+
# findfont accepts strings as well as names; whitespace must not
|
|
33
|
+
# split a family name into separate PostScript tokens.
|
|
34
|
+
if record.name.match?(/\s/)
|
|
35
|
+
[Model::Literals::StringLiteral.new(record.name)]
|
|
36
|
+
else
|
|
37
|
+
[Model::Literals::Name.new(record.name, literal: true)]
|
|
38
|
+
end
|
|
39
|
+
end,
|
|
40
|
+
Model::Operators::Font::Scalefont => lambda do |record|
|
|
41
|
+
[Model::Literals::Number.new(record.size)]
|
|
42
|
+
end,
|
|
43
|
+
Model::Operators::Font::Show => lambda do |record|
|
|
44
|
+
[Model::Literals::StringLiteral.new(record.text)]
|
|
45
|
+
end,
|
|
46
|
+
Model::Operators::GraphicsState::Setlinecap => lambda do |record|
|
|
47
|
+
[Model::Literals::Number.new(record.cap_code)]
|
|
48
|
+
end,
|
|
49
|
+
Model::Operators::GraphicsState::Setlinejoin => lambda do |record|
|
|
50
|
+
[Model::Literals::Number.new(record.join_code)]
|
|
51
|
+
end,
|
|
52
|
+
Model::Operators::Transformations::Concat => lambda do |record|
|
|
53
|
+
elements = record.matrix.map { |value| Model::Literals::Number.new(value) }
|
|
54
|
+
[Model::Literals::ArrayLiteral.new(elements)]
|
|
55
|
+
end,
|
|
56
|
+
}.freeze
|
|
57
|
+
|
|
10
58
|
attr_reader :records
|
|
11
59
|
|
|
12
60
|
def initialize
|
|
@@ -14,6 +62,12 @@ module Postsvg
|
|
|
14
62
|
end
|
|
15
63
|
|
|
16
64
|
def emit(record)
|
|
65
|
+
# Operators built by the svg -> ps handlers carry their
|
|
66
|
+
# operands as attributes; the serializer writes keywords only
|
|
67
|
+
# and expects the operands as separate literal statements
|
|
68
|
+
# ahead of the keyword (the shape the PS parse path produces).
|
|
69
|
+
# Splice them in so the record stream round-trips.
|
|
70
|
+
@records.concat(OPERAND_BUILDERS.fetch(record.class, NO_OPERANDS).call(record))
|
|
17
71
|
@records << record
|
|
18
72
|
self
|
|
19
73
|
end
|
data/lib/postsvg/version.rb
CHANGED
|
@@ -12,19 +12,19 @@ module Postsvg
|
|
|
12
12
|
def visit_add(_op, _ctx)
|
|
13
13
|
b = pop_runtime_number
|
|
14
14
|
a = pop_runtime_number
|
|
15
|
-
@stack << a + b
|
|
15
|
+
@stack << (a + b)
|
|
16
16
|
end
|
|
17
17
|
|
|
18
18
|
def visit_sub(_op, _ctx)
|
|
19
19
|
b = pop_runtime_number
|
|
20
20
|
a = pop_runtime_number
|
|
21
|
-
@stack << a - b
|
|
21
|
+
@stack << (a - b)
|
|
22
22
|
end
|
|
23
23
|
|
|
24
24
|
def visit_mul(_op, _ctx)
|
|
25
25
|
b = pop_runtime_number
|
|
26
26
|
a = pop_runtime_number
|
|
27
|
-
@stack << a * b
|
|
27
|
+
@stack << (a * b)
|
|
28
28
|
end
|
|
29
29
|
|
|
30
30
|
def visit_div(_op, _ctx)
|
|
@@ -32,7 +32,7 @@ module Postsvg
|
|
|
32
32
|
a = pop_runtime_number
|
|
33
33
|
raise RenderError, "division by zero" if divisor.zero?
|
|
34
34
|
|
|
35
|
-
@stack << (a.to_f / divisor
|
|
35
|
+
@stack << (a.to_f / divisor)
|
|
36
36
|
end
|
|
37
37
|
|
|
38
38
|
def visit_idiv(_op, _ctx)
|
|
@@ -85,7 +85,7 @@ module Postsvg
|
|
|
85
85
|
def visit_atan(_op, _ctx)
|
|
86
86
|
b = pop_runtime_number
|
|
87
87
|
a = pop_runtime_number
|
|
88
|
-
@stack << Math.atan2(a.to_f, b.to_f) * (180.0 / Math::PI)
|
|
88
|
+
@stack << (Math.atan2(a.to_f, b.to_f) * (180.0 / Math::PI))
|
|
89
89
|
end
|
|
90
90
|
|
|
91
91
|
def visit_cos(_op, _ctx)
|
|
@@ -107,7 +107,7 @@ module Postsvg
|
|
|
107
107
|
def visit_exp(_op, _ctx)
|
|
108
108
|
b = pop_runtime_number
|
|
109
109
|
a = pop_runtime_number
|
|
110
|
-
@stack << a.to_f**b.to_f
|
|
110
|
+
@stack << (a.to_f**b.to_f)
|
|
111
111
|
end
|
|
112
112
|
|
|
113
113
|
private
|
|
@@ -7,22 +7,25 @@ module Postsvg
|
|
|
7
7
|
# GraphicsContext; the next paint operator reads the new colors.
|
|
8
8
|
module Color
|
|
9
9
|
def visit_setgray(op, _ctx)
|
|
10
|
-
color =
|
|
10
|
+
color = Postscript::Color.gray(op.gray.to_f)
|
|
11
11
|
@graphics.update(fill_color: color, stroke_color: color)
|
|
12
12
|
end
|
|
13
13
|
|
|
14
14
|
def visit_setrgbcolor(op, _ctx)
|
|
15
|
-
color =
|
|
15
|
+
color = Postscript::Color.rgb(op.red.to_f, op.green.to_f,
|
|
16
|
+
op.blue.to_f)
|
|
16
17
|
@graphics.update(fill_color: color, stroke_color: color)
|
|
17
18
|
end
|
|
18
19
|
|
|
19
20
|
def visit_setcmykcolor(op, _ctx)
|
|
20
|
-
color =
|
|
21
|
+
color = Postscript::Color.cmyk(op.cyan.to_f, op.magenta.to_f,
|
|
22
|
+
op.yellow.to_f, op.key.to_f)
|
|
21
23
|
@graphics.update(fill_color: color, stroke_color: color)
|
|
22
24
|
end
|
|
23
25
|
|
|
24
26
|
def visit_sethsbcolor(op, _ctx)
|
|
25
|
-
color = hsb_to_color(op.hue.to_f, op.saturation.to_f,
|
|
27
|
+
color = hsb_to_color(op.hue.to_f, op.saturation.to_f,
|
|
28
|
+
op.brightness.to_f)
|
|
26
29
|
@graphics.update(fill_color: color, stroke_color: color)
|
|
27
30
|
end
|
|
28
31
|
|
|
@@ -45,7 +48,7 @@ module Postsvg
|
|
|
45
48
|
when 4 then [t, p, v]
|
|
46
49
|
else [v, p, q]
|
|
47
50
|
end
|
|
48
|
-
|
|
51
|
+
Postscript::Color.rgb(r, g, b)
|
|
49
52
|
end
|
|
50
53
|
end
|
|
51
54
|
end
|
|
@@ -13,7 +13,7 @@ module Postsvg
|
|
|
13
13
|
def normalize_key(key)
|
|
14
14
|
case key
|
|
15
15
|
when Model::Literals::Name then key.value
|
|
16
|
-
when String then key.to_s.
|
|
16
|
+
when String then key.to_s.delete_prefix("/")
|
|
17
17
|
else key.to_s
|
|
18
18
|
end
|
|
19
19
|
end
|
|
@@ -25,7 +25,6 @@ module Postsvg
|
|
|
25
25
|
when String then node
|
|
26
26
|
when Model::Literals::StringLiteral then node.value
|
|
27
27
|
when Model::Literals::HexLiteral then node.bytes
|
|
28
|
-
else nil
|
|
29
28
|
end
|
|
30
29
|
end
|
|
31
30
|
|
|
@@ -44,25 +44,25 @@ module Postsvg
|
|
|
44
44
|
@stack << value
|
|
45
45
|
end
|
|
46
46
|
|
|
47
|
-
def visit_put(
|
|
47
|
+
def visit_put(_op, _ctx)
|
|
48
48
|
# PS put mutates in place. Our value objects are immutable.
|
|
49
49
|
# Marked as a comment in the output so the gap is visible.
|
|
50
50
|
@builder.comment("put: in-place mutation not supported")
|
|
51
51
|
end
|
|
52
52
|
|
|
53
53
|
def visit_getinterval(op, _ctx)
|
|
54
|
-
case op.operand
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
54
|
+
@stack << case op.operand
|
|
55
|
+
when String
|
|
56
|
+
op.operand[op.start, op.count]
|
|
57
|
+
when Array
|
|
58
|
+
op.operand[op.start, op.count]
|
|
59
|
+
when Model::Literals::StringLiteral
|
|
60
|
+
op.operand.value[op.start, op.count]
|
|
61
|
+
when Model::Literals::ArrayLiteral
|
|
62
|
+
op.operand.elements[op.start, op.count]
|
|
63
|
+
else
|
|
64
|
+
""
|
|
65
|
+
end
|
|
66
66
|
end
|
|
67
67
|
|
|
68
68
|
def visit_putinterval(_op, _ctx)
|
|
@@ -39,7 +39,7 @@ module Postsvg
|
|
|
39
39
|
body = @stack.pop
|
|
40
40
|
LOOP_LIMIT.times do
|
|
41
41
|
descend_into_procedure(body)
|
|
42
|
-
rescue
|
|
42
|
+
rescue Postscript::ExitSignal
|
|
43
43
|
break
|
|
44
44
|
end
|
|
45
45
|
end
|
|
@@ -74,11 +74,11 @@ module Postsvg
|
|
|
74
74
|
end
|
|
75
75
|
|
|
76
76
|
def visit_exit(_op, _ctx)
|
|
77
|
-
raise
|
|
77
|
+
raise Postscript::ExitSignal
|
|
78
78
|
end
|
|
79
79
|
|
|
80
80
|
def visit_quit(_op, _ctx)
|
|
81
|
-
raise
|
|
81
|
+
raise Postscript::QuitSignal
|
|
82
82
|
end
|
|
83
83
|
|
|
84
84
|
def visit_exec(_op, _ctx)
|
|
@@ -72,10 +72,14 @@ module Postsvg
|
|
|
72
72
|
|
|
73
73
|
def visit_maxlength(op, _ctx)
|
|
74
74
|
# PS dictionaries can grow dynamically; report current size.
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
75
|
+
@stack << dict_size(op.operand)
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
def dict_size(operand)
|
|
79
|
+
case operand
|
|
80
|
+
when Hash then operand.length
|
|
81
|
+
when Model::Literals::Dictionary then operand.entries.length
|
|
82
|
+
else 0
|
|
79
83
|
end
|
|
80
84
|
end
|
|
81
85
|
|
|
@@ -31,15 +31,26 @@ module Postsvg
|
|
|
31
31
|
end
|
|
32
32
|
|
|
33
33
|
def visit_scalefont(op, _ctx)
|
|
34
|
-
size = op.size.to_i
|
|
34
|
+
size = op.size.to_i
|
|
35
|
+
consume_numeric_operands(op.size)
|
|
35
36
|
font = @stack.pop
|
|
36
|
-
base = font.is_a?(FontRef)
|
|
37
|
+
base = if font.is_a?(FontRef)
|
|
38
|
+
font
|
|
39
|
+
else
|
|
40
|
+
FontRef.new(name: "Helvetica",
|
|
41
|
+
size: nil)
|
|
42
|
+
end
|
|
37
43
|
@stack << FontRef.new(name: base.name, size: size.to_f)
|
|
38
44
|
end
|
|
39
45
|
|
|
40
46
|
def visit_setfont(_op, _ctx)
|
|
41
47
|
font = @stack.pop
|
|
42
|
-
ref = font.is_a?(FontRef)
|
|
48
|
+
ref = if font.is_a?(FontRef)
|
|
49
|
+
font
|
|
50
|
+
else
|
|
51
|
+
FontRef.new(name: "Helvetica",
|
|
52
|
+
size: 12.0)
|
|
53
|
+
end
|
|
43
54
|
@graphics.update(font_name: ref.name || "Helvetica",
|
|
44
55
|
font_size: ref.size || 12.0)
|
|
45
56
|
end
|
|
@@ -67,7 +78,7 @@ module Postsvg
|
|
|
67
78
|
y: -screen_pos[:y],
|
|
68
79
|
font_family: @graphics.current.font_name,
|
|
69
80
|
font_size: @graphics.current.font_size,
|
|
70
|
-
color: color ||
|
|
81
|
+
color: color || Postscript::Color::BLACK,
|
|
71
82
|
transform: "scale(1 -1)",
|
|
72
83
|
)
|
|
73
84
|
@path.reset
|
|
@@ -7,15 +7,17 @@ module Postsvg
|
|
|
7
7
|
# dash attribute setters.
|
|
8
8
|
module GraphicsState
|
|
9
9
|
def visit_gsave(_op, _ctx)
|
|
10
|
+
@saved_paths << @path.dup
|
|
10
11
|
@graphics.gsave
|
|
11
12
|
end
|
|
12
13
|
|
|
13
14
|
def visit_grestore(_op, _ctx)
|
|
15
|
+
@path = @saved_paths.pop if @graphics.depth > 1 && !@saved_paths.empty?
|
|
14
16
|
@graphics.grestore
|
|
15
17
|
end
|
|
16
18
|
|
|
17
19
|
def visit_grestoreall(_op, _ctx)
|
|
18
|
-
|
|
20
|
+
visit_grestore(nil, nil) while @graphics.depth > 1
|
|
19
21
|
end
|
|
20
22
|
|
|
21
23
|
def visit_setlinewidth(op, _ctx)
|
|
@@ -23,12 +25,14 @@ module Postsvg
|
|
|
23
25
|
end
|
|
24
26
|
|
|
25
27
|
def visit_setlinecap(op, _ctx)
|
|
26
|
-
name = { 0 => :butt, 1 => :round, 2 => :square }.fetch(op.cap_code,
|
|
28
|
+
name = { 0 => :butt, 1 => :round, 2 => :square }.fetch(op.cap_code,
|
|
29
|
+
:butt)
|
|
27
30
|
@graphics.update(line_cap: name)
|
|
28
31
|
end
|
|
29
32
|
|
|
30
33
|
def visit_setlinejoin(op, _ctx)
|
|
31
|
-
name = { 0 => :miter, 1 => :round, 2 => :bevel }.fetch(op.join_code,
|
|
34
|
+
name = { 0 => :miter, 1 => :round, 2 => :bevel }.fetch(op.join_code,
|
|
35
|
+
:miter)
|
|
32
36
|
@graphics.update(line_join: name)
|
|
33
37
|
end
|
|
34
38
|
|
|
@@ -44,8 +48,6 @@ module Postsvg
|
|
|
44
48
|
pattern.map { |v| FormatNumber.call(v.to_f) }.join(" ")
|
|
45
49
|
when Numeric
|
|
46
50
|
FormatNumber.call(pattern.to_f)
|
|
47
|
-
else
|
|
48
|
-
nil
|
|
49
51
|
end
|
|
50
52
|
@graphics.update(dash: dash_str)
|
|
51
53
|
end
|
|
@@ -25,7 +25,7 @@ module Postsvg
|
|
|
25
25
|
def visit_clip(_op, _ctx)
|
|
26
26
|
return if @path.empty?
|
|
27
27
|
|
|
28
|
-
path_d = @path.to_path_d
|
|
28
|
+
path_d = @path.to_path_d(@graphics.current.ctm)
|
|
29
29
|
clip_id = @builder.register_clip_path(path_d)
|
|
30
30
|
@graphics.update(clip_paths: @graphics.current.clip_paths + [clip_id])
|
|
31
31
|
@path.reset
|
|
@@ -43,7 +43,7 @@ module Postsvg
|
|
|
43
43
|
def flush_path(mode)
|
|
44
44
|
return if @path.empty?
|
|
45
45
|
|
|
46
|
-
d = @path.to_path_d
|
|
46
|
+
d = @path.to_path_d(@graphics.current.ctm)
|
|
47
47
|
ctx = @graphics.current
|
|
48
48
|
transform_str = build_transform_string(ctx)
|
|
49
49
|
clip_id = ctx.clip_paths.last
|
|
@@ -66,7 +66,7 @@ module Postsvg
|
|
|
66
66
|
line_cap: ctx.line_cap,
|
|
67
67
|
line_join: ctx.line_join,
|
|
68
68
|
dash: ctx.dash,
|
|
69
|
-
clip_path_id: clip_path_id
|
|
69
|
+
clip_path_id: clip_path_id
|
|
70
70
|
)
|
|
71
71
|
end
|
|
72
72
|
|
|
@@ -5,25 +5,29 @@ module Postsvg
|
|
|
5
5
|
class PsVisitor
|
|
6
6
|
# Path construction operator handlers. Each appends a command to
|
|
7
7
|
# +@path+ (PathState). CTM-aware: when the current CTM is not
|
|
8
|
-
# identity, the renderer
|
|
9
|
-
#
|
|
10
|
-
#
|
|
8
|
+
# identity, the renderer wraps painted paths in a <g transform=>.
|
|
9
|
+
# Arcs retain their construction CTM and are mapped into the paint CTM
|
|
10
|
+
# at emission, whether or not the temporary transform was restored.
|
|
11
|
+
# Other mid-path CTM changes retain the existing paint-time behavior.
|
|
11
12
|
module Path
|
|
12
13
|
def visit_newpath(_op, _ctx)
|
|
13
14
|
@path.reset
|
|
14
15
|
end
|
|
15
16
|
|
|
16
17
|
def visit_moveto(op, _ctx)
|
|
18
|
+
@path.matrix ||= @graphics.current.ctm
|
|
17
19
|
@path.move_to(op.x, op.y)
|
|
18
20
|
@graphics.update(last_text_position: { x: op.x, y: op.y })
|
|
19
21
|
end
|
|
20
22
|
|
|
21
23
|
def visit_rmoveto(op, _ctx)
|
|
24
|
+
@path.matrix ||= @graphics.current.ctm
|
|
22
25
|
@path.move_to_rel(op.dx, op.dy)
|
|
23
26
|
pos = @graphics.current.last_text_position
|
|
24
27
|
base_x = pos ? pos[:x] : 0
|
|
25
28
|
base_y = pos ? pos[:y] : 0
|
|
26
|
-
@graphics.update(last_text_position: { x: base_x + op.dx,
|
|
29
|
+
@graphics.update(last_text_position: { x: base_x + op.dx,
|
|
30
|
+
y: base_y + op.dy })
|
|
27
31
|
end
|
|
28
32
|
|
|
29
33
|
def visit_lineto(op, _ctx)
|
|
@@ -65,46 +69,61 @@ module Postsvg
|
|
|
65
69
|
# are parametrised by center, radius, angle range. SVG arcs
|
|
66
70
|
# are parametrised by endpoint, radii, large-arc/sweep flags.
|
|
67
71
|
def append_arc(cx, cy, radius, angle1, angle2, sweep:)
|
|
72
|
+
consume_numeric_operands(cx, cy, radius, angle1, angle2)
|
|
68
73
|
r = radius.abs
|
|
69
74
|
return if r.zero?
|
|
70
75
|
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
if @path.empty? || last_command_starts_with?("M")
|
|
81
|
-
@path.move_to(start_x, start_y) if @path.empty?
|
|
76
|
+
construction_matrix = @graphics.current.ctm
|
|
77
|
+
@path.matrix ||= construction_matrix
|
|
78
|
+
point_matrix = @path.matrix.invert.multiply(construction_matrix)
|
|
79
|
+
end_x, end_y = arc_point(point_matrix, cx, cy, r, angle2)
|
|
80
|
+
starts_path = @path.empty?
|
|
81
|
+
command = lambda do |paint_matrix|
|
|
82
|
+
matrix = paint_matrix.invert.multiply(construction_matrix)
|
|
83
|
+
render_arc(cx, cy, r, angle1, angle2, sweep: sweep,
|
|
84
|
+
matrix: matrix, starts_path: starts_path)
|
|
82
85
|
end
|
|
86
|
+
@path.append_deferred(command, end_x, end_y)
|
|
87
|
+
end
|
|
83
88
|
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
89
|
+
def render_arc(cx, cy, radius, angle1, angle2, sweep:, matrix:, starts_path:)
|
|
90
|
+
path = PathState.new
|
|
91
|
+
start_x, start_y = arc_point(matrix, cx, cy, radius, angle1)
|
|
92
|
+
end_x, end_y = arc_point(matrix, cx, cy, radius, angle2)
|
|
93
|
+
path.move_to(start_x, start_y) if starts_path
|
|
94
|
+
delta = sweep ? (angle2 - angle1) % 360 : (angle1 - angle2) % 360
|
|
95
|
+
rx, ry, rotation = arc_radii(matrix, radius)
|
|
96
|
+
reflected = ((matrix.a * matrix.d) - (matrix.b * matrix.c)).negative?
|
|
97
|
+
svg_sweep = sweep != reflected
|
|
90
98
|
if delta.abs < 1e-6
|
|
91
|
-
# Full
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
@path.arc_to(r, r, 0, 0, svg_sweep, mid_x, mid_y)
|
|
97
|
-
@path.arc_to(r, r, 0, 0, svg_sweep, start_x, start_y)
|
|
98
|
-
@path.close
|
|
99
|
+
# Full circles need two arcs; retain the corpus's large-arc spelling.
|
|
100
|
+
mid_x, mid_y = arc_point(matrix, cx, cy, radius, angle1 + 180)
|
|
101
|
+
path.arc_to(rx, ry, rotation, true, svg_sweep, mid_x, mid_y)
|
|
102
|
+
path.arc_to(rx, ry, rotation, true, svg_sweep, start_x, start_y)
|
|
103
|
+
path.close
|
|
99
104
|
else
|
|
100
|
-
|
|
105
|
+
path.arc_to(rx, ry, rotation, delta > 180, svg_sweep, end_x, end_y)
|
|
101
106
|
end
|
|
107
|
+
path.to_path_d
|
|
102
108
|
end
|
|
103
109
|
|
|
104
|
-
def
|
|
105
|
-
|
|
110
|
+
def arc_point(matrix, cx, cy, radius, angle)
|
|
111
|
+
radians = angle * Math::PI / 180.0
|
|
112
|
+
matrix.apply_point(cx + (radius * Math.cos(radians)),
|
|
113
|
+
cy + (radius * Math.sin(radians))).values_at(:x, :y)
|
|
114
|
+
end
|
|
106
115
|
|
|
107
|
-
|
|
116
|
+
# Singular values give the ellipse radii even when its frame is rotated
|
|
117
|
+
# or sheared. The principal eigenvector gives its SVG axis rotation.
|
|
118
|
+
def arc_radii(matrix, radius)
|
|
119
|
+
xx = (matrix.a**2) + (matrix.c**2)
|
|
120
|
+
xy = (matrix.a * matrix.b) + (matrix.c * matrix.d)
|
|
121
|
+
yy = (matrix.b**2) + (matrix.d**2)
|
|
122
|
+
root = Math.hypot(xx - yy, 2 * xy)
|
|
123
|
+
rx = radius * Math.sqrt([(xx + yy + root) / 2, 0].max)
|
|
124
|
+
ry = radius * Math.sqrt([(xx + yy - root) / 2, 0].max)
|
|
125
|
+
rotation = root < 1e-12 ? 0.0 : Math.atan2(2 * xy, xx - yy) * 90 / Math::PI
|
|
126
|
+
[rx, ry, rotation]
|
|
108
127
|
end
|
|
109
128
|
end
|
|
110
129
|
end
|
|
@@ -6,12 +6,19 @@ module Postsvg
|
|
|
6
6
|
# Coordinate transformation operator handlers. Each returns a
|
|
7
7
|
# new CTM via Matrix composition and updates the GraphicsContext.
|
|
8
8
|
module Transformations
|
|
9
|
+
# A matrix this visitor produced (`matrix`, `currentmatrix`). The
|
|
10
|
+
# runtime stack also holds values nothing popped (`def` operands,
|
|
11
|
+
# unresolved names, path operands), so only these are trusted as a
|
|
12
|
+
# matrix the parser could not see.
|
|
13
|
+
class StackMatrix < Array; end
|
|
14
|
+
|
|
9
15
|
def visit_translate(op, _ctx)
|
|
10
16
|
new_ctm = @graphics.current.ctm.translate(op.tx.to_f, op.ty.to_f)
|
|
11
17
|
@graphics.update(ctm: new_ctm)
|
|
12
18
|
end
|
|
13
19
|
|
|
14
20
|
def visit_scale(op, _ctx)
|
|
21
|
+
consume_numeric_operands(op.sx, op.sy)
|
|
15
22
|
new_ctm = @graphics.current.ctm.scale(op.sx.to_f, op.sy.to_f)
|
|
16
23
|
@graphics.update(ctm: new_ctm)
|
|
17
24
|
end
|
|
@@ -22,7 +29,7 @@ module Postsvg
|
|
|
22
29
|
end
|
|
23
30
|
|
|
24
31
|
def visit_concat(op, _ctx)
|
|
25
|
-
matrix = matrix_from_operand(op
|
|
32
|
+
matrix = matrix_from_operand(matrix_operand(op))
|
|
26
33
|
return unless matrix
|
|
27
34
|
|
|
28
35
|
new_ctm = @graphics.current.ctm.multiply(matrix)
|
|
@@ -30,16 +37,17 @@ module Postsvg
|
|
|
30
37
|
end
|
|
31
38
|
|
|
32
39
|
def visit_matrix(_op, _ctx)
|
|
33
|
-
@stack << [1, 0, 0, 1, 0, 0]
|
|
40
|
+
@stack << StackMatrix[1, 0, 0, 1, 0, 0]
|
|
34
41
|
end
|
|
35
42
|
|
|
36
43
|
def visit_currentmatrix(_op, _ctx)
|
|
44
|
+
@stack.pop if @stack.last.is_a?(StackMatrix)
|
|
37
45
|
ctm = @graphics.current.ctm
|
|
38
|
-
@stack << [ctm.a, ctm.b, ctm.c, ctm.d, ctm.e, ctm.f]
|
|
46
|
+
@stack << StackMatrix[ctm.a, ctm.b, ctm.c, ctm.d, ctm.e, ctm.f]
|
|
39
47
|
end
|
|
40
48
|
|
|
41
49
|
def visit_setmatrix(op, _ctx)
|
|
42
|
-
matrix = matrix_from_operand(op
|
|
50
|
+
matrix = matrix_from_operand(matrix_operand(op))
|
|
43
51
|
return unless matrix
|
|
44
52
|
|
|
45
53
|
@graphics.update(ctm: matrix)
|
|
@@ -85,16 +93,46 @@ module Postsvg
|
|
|
85
93
|
|
|
86
94
|
private
|
|
87
95
|
|
|
96
|
+
# Parsed literal operands remain on the visitor stack. Consume only
|
|
97
|
+
# an exact numeric suffix, leaving directly constructed operators alone.
|
|
98
|
+
def consume_numeric_operands(*values)
|
|
99
|
+
@stack.pop(values.length) if @stack.last(values.length) == values
|
|
100
|
+
end
|
|
101
|
+
|
|
102
|
+
# A literal matrix was also pushed by visit_array; consume that copy
|
|
103
|
+
# so a later operator does not apply it again. A computed matrix is
|
|
104
|
+
# taken from the stack only when this visitor produced it.
|
|
105
|
+
def matrix_operand(op)
|
|
106
|
+
unless op.matrix.is_a?(Model::Computed)
|
|
107
|
+
@stack.pop if literal_on_stack?(op.matrix)
|
|
108
|
+
return op.matrix
|
|
109
|
+
end
|
|
110
|
+
|
|
111
|
+
@stack.pop if @stack.last.is_a?(StackMatrix)
|
|
112
|
+
end
|
|
113
|
+
|
|
114
|
+
def literal_on_stack?(matrix)
|
|
115
|
+
matrix.is_a?(Model::Literals::ArrayLiteral) && @stack.last.equal?(matrix.elements)
|
|
116
|
+
end
|
|
117
|
+
|
|
118
|
+
# Arrays on the runtime stack hold either plain numbers or parsed
|
|
119
|
+
# Number literals. Anything other than six numbers is not a matrix.
|
|
88
120
|
def matrix_from_operand(operand)
|
|
89
121
|
case operand
|
|
90
122
|
when Matrix then operand
|
|
123
|
+
when Model::Literals::ArrayLiteral then matrix_from_operand(operand.elements)
|
|
91
124
|
when Array
|
|
92
|
-
|
|
125
|
+
numbers = operand.map { |element| matrix_number(element) }
|
|
126
|
+
return nil unless numbers.length == 6 && numbers.all?
|
|
93
127
|
|
|
94
|
-
Matrix.new(a:
|
|
95
|
-
d:
|
|
128
|
+
Matrix.new(a: numbers[0], b: numbers[1], c: numbers[2],
|
|
129
|
+
d: numbers[3], e: numbers[4], f: numbers[5])
|
|
96
130
|
end
|
|
97
131
|
end
|
|
132
|
+
|
|
133
|
+
def matrix_number(element)
|
|
134
|
+
numeric_value(element) if element.is_a?(Numeric) || element.is_a?(Model::Literals::Number)
|
|
135
|
+
end
|
|
98
136
|
end
|
|
99
137
|
end
|
|
100
138
|
end
|
|
@@ -36,6 +36,7 @@ module Postsvg
|
|
|
36
36
|
@graphics = graphics
|
|
37
37
|
@stack = [] # operand stack for execution semantics
|
|
38
38
|
@path = PathState.new
|
|
39
|
+
@saved_paths = []
|
|
39
40
|
@dict_stack = [{}] # runtime dict stack (mirrors PLRM dict stack)
|
|
40
41
|
@recursion_depth = 0
|
|
41
42
|
end
|
|
@@ -116,6 +117,7 @@ module Postsvg
|
|
|
116
117
|
# geometry bookkeeping.
|
|
117
118
|
class PathState
|
|
118
119
|
attr_reader :commands, :current_x, :current_y
|
|
120
|
+
attr_accessor :matrix
|
|
119
121
|
|
|
120
122
|
def initialize
|
|
121
123
|
@commands = []
|
|
@@ -127,6 +129,11 @@ module Postsvg
|
|
|
127
129
|
@commands.empty?
|
|
128
130
|
end
|
|
129
131
|
|
|
132
|
+
def initialize_copy(other)
|
|
133
|
+
super
|
|
134
|
+
@commands = other.commands.map(&:dup)
|
|
135
|
+
end
|
|
136
|
+
|
|
130
137
|
def move_to(x, y)
|
|
131
138
|
@commands << "M #{fmt(x)} #{fmt(y)}"
|
|
132
139
|
@current_x = x.to_f
|
|
@@ -170,18 +177,27 @@ module Postsvg
|
|
|
170
177
|
@current_y = y.to_f
|
|
171
178
|
end
|
|
172
179
|
|
|
180
|
+
def append_deferred(command, x, y)
|
|
181
|
+
@commands << command
|
|
182
|
+
@current_x = x
|
|
183
|
+
@current_y = y
|
|
184
|
+
end
|
|
185
|
+
|
|
173
186
|
def close
|
|
174
187
|
@commands << "Z"
|
|
175
188
|
end
|
|
176
189
|
|
|
177
190
|
def reset
|
|
191
|
+
@matrix = nil
|
|
178
192
|
@commands = []
|
|
179
193
|
@current_x = 0.0
|
|
180
194
|
@current_y = 0.0
|
|
181
195
|
end
|
|
182
196
|
|
|
183
|
-
def to_path_d
|
|
184
|
-
@commands.
|
|
197
|
+
def to_path_d(paint_matrix = @matrix || Matrix.new)
|
|
198
|
+
@commands.map do |command|
|
|
199
|
+
command.is_a?(Proc) ? command.call(paint_matrix) : command
|
|
200
|
+
end.join(" ")
|
|
185
201
|
end
|
|
186
202
|
|
|
187
203
|
def fmt(value)
|