postsvg 0.2.6 → 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 -27
- data/Gemfile +2 -2
- data/README.adoc +4 -62
- data/Rakefile +0 -17
- 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/interpreter.rb +2 -1
- 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 +14 -23
- 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 +60 -82
- data/lib/postsvg/translation/handlers/polygon_handler.rb +2 -4
- data/lib/postsvg/translation/handlers/polyline_handler.rb +4 -5
- 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 +8 -17
- data/lib/postsvg/translation/handlers.rb +1 -2
- data/lib/postsvg/translation/ps_renderer.rb +4 -18
- data/lib/postsvg/translation/record_emitter.rb +0 -54
- 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 -15
- data/lib/postsvg/visitors/ps_visitor/graphics_state.rb +5 -7
- data/lib/postsvg/visitors/ps_visitor/painting.rb +3 -3
- data/lib/postsvg/visitors/ps_visitor/path.rb +33 -52
- data/lib/postsvg/visitors/ps_visitor/transformations.rb +7 -45
- data/lib/postsvg/visitors/ps_visitor.rb +2 -18
- data/lib/postsvg.rb +38 -43
- metadata +39 -20
- data/gemfiles/native-parity/Gemfile +0 -8
|
@@ -17,8 +17,7 @@ module Postsvg
|
|
|
17
17
|
autoload :TextHandler, "postsvg/translation/handlers/text_handler"
|
|
18
18
|
autoload :ImageHandler, "postsvg/translation/handlers/image_handler"
|
|
19
19
|
autoload :DefsHandler, "postsvg/translation/handlers/defs_handler"
|
|
20
|
-
autoload :ClipPathHandler,
|
|
21
|
-
"postsvg/translation/handlers/clip_path_handler"
|
|
20
|
+
autoload :ClipPathHandler, "postsvg/translation/handlers/clip_path_handler"
|
|
22
21
|
autoload :OpenHandler, "postsvg/translation/handlers/open_handler"
|
|
23
22
|
autoload :Shared, "postsvg/translation/handlers/shared"
|
|
24
23
|
end
|
|
@@ -72,12 +72,10 @@ module Postsvg
|
|
|
72
72
|
|
|
73
73
|
def compute_viewport_bbox
|
|
74
74
|
if document.viewbox
|
|
75
|
-
|
|
76
|
-
BoundingBox.new(min_x:
|
|
77
|
-
max_y: y + height)
|
|
75
|
+
llx, lly, urx, ury = document.viewbox
|
|
76
|
+
BoundingBox.new(min_x: llx, min_y: lly, max_x: urx, max_y: ury)
|
|
78
77
|
elsif document.width && document.height
|
|
79
|
-
BoundingBox.new(min_x: 0, min_y: 0, max_x: document.width,
|
|
80
|
-
max_y: document.height)
|
|
78
|
+
BoundingBox.new(min_x: 0, min_y: 0, max_x: document.width, max_y: document.height)
|
|
81
79
|
else
|
|
82
80
|
BoundingBox.empty
|
|
83
81
|
end
|
|
@@ -87,17 +85,6 @@ module Postsvg
|
|
|
87
85
|
registry.translate(element, context)
|
|
88
86
|
end
|
|
89
87
|
|
|
90
|
-
# SVG's y axis points down; PostScript's points up. Convert once at
|
|
91
|
-
# the document boundary, before any authored element transforms.
|
|
92
|
-
def coordinate_conversion(bbox)
|
|
93
|
-
emitter = RecordEmitter.new
|
|
94
|
-
emitter.emit(Model::Operators::Transformations::Translate.new(
|
|
95
|
-
tx: 0, ty: bbox[3] - bbox[1],
|
|
96
|
-
))
|
|
97
|
-
emitter.emit(Model::Operators::Transformations::Scale.new(sx: 1, sy: -1))
|
|
98
|
-
emitter.records
|
|
99
|
-
end
|
|
100
|
-
|
|
101
88
|
def build_program(emitter, context)
|
|
102
89
|
bbox = context.bounding_box
|
|
103
90
|
header_bbox =
|
|
@@ -111,8 +98,7 @@ module Postsvg
|
|
|
111
98
|
epsf: eps,
|
|
112
99
|
title: "Translated by Postsvg #{::Postsvg::VERSION}",
|
|
113
100
|
)
|
|
114
|
-
Model::Program.new(header: header,
|
|
115
|
-
body: coordinate_conversion(header_bbox) + emitter.records)
|
|
101
|
+
Model::Program.new(header: header, body: emitter.records)
|
|
116
102
|
end
|
|
117
103
|
end
|
|
118
104
|
end
|
|
@@ -7,54 +7,6 @@ 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
|
-
|
|
58
10
|
attr_reader :records
|
|
59
11
|
|
|
60
12
|
def initialize
|
|
@@ -62,12 +14,6 @@ module Postsvg
|
|
|
62
14
|
end
|
|
63
15
|
|
|
64
16
|
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))
|
|
71
17
|
@records << record
|
|
72
18
|
self
|
|
73
19
|
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 <<
|
|
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 <<
|
|
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 <<
|
|
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.to_f)
|
|
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 <<
|
|
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 <<
|
|
110
|
+
@stack << a.to_f**b.to_f
|
|
111
111
|
end
|
|
112
112
|
|
|
113
113
|
private
|
|
@@ -7,25 +7,22 @@ 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 = Postsvg::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 =
|
|
16
|
-
op.blue.to_f)
|
|
15
|
+
color = Postsvg::Color.rgb(op.red.to_f, op.green.to_f, op.blue.to_f)
|
|
17
16
|
@graphics.update(fill_color: color, stroke_color: color)
|
|
18
17
|
end
|
|
19
18
|
|
|
20
19
|
def visit_setcmykcolor(op, _ctx)
|
|
21
|
-
color =
|
|
22
|
-
op.yellow.to_f, op.key.to_f)
|
|
20
|
+
color = Postsvg::Color.cmyk(op.cyan.to_f, op.magenta.to_f, op.yellow.to_f, op.key.to_f)
|
|
23
21
|
@graphics.update(fill_color: color, stroke_color: color)
|
|
24
22
|
end
|
|
25
23
|
|
|
26
24
|
def visit_sethsbcolor(op, _ctx)
|
|
27
|
-
color = hsb_to_color(op.hue.to_f, op.saturation.to_f,
|
|
28
|
-
op.brightness.to_f)
|
|
25
|
+
color = hsb_to_color(op.hue.to_f, op.saturation.to_f, op.brightness.to_f)
|
|
29
26
|
@graphics.update(fill_color: color, stroke_color: color)
|
|
30
27
|
end
|
|
31
28
|
|
|
@@ -48,7 +45,7 @@ module Postsvg
|
|
|
48
45
|
when 4 then [t, p, v]
|
|
49
46
|
else [v, p, q]
|
|
50
47
|
end
|
|
51
|
-
|
|
48
|
+
Postsvg::Color.rgb(r, g, b)
|
|
52
49
|
end
|
|
53
50
|
end
|
|
54
51
|
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.sub(/\A\//, "")
|
|
17
17
|
else key.to_s
|
|
18
18
|
end
|
|
19
19
|
end
|
|
@@ -25,6 +25,7 @@ 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
|
|
28
29
|
end
|
|
29
30
|
end
|
|
30
31
|
|
|
@@ -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
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
54
|
+
case op.operand
|
|
55
|
+
when String
|
|
56
|
+
@stack << op.operand[op.start, op.count]
|
|
57
|
+
when Array
|
|
58
|
+
@stack << op.operand[op.start, op.count]
|
|
59
|
+
when Model::Literals::StringLiteral
|
|
60
|
+
@stack << op.operand.value[op.start, op.count]
|
|
61
|
+
when Model::Literals::ArrayLiteral
|
|
62
|
+
@stack << op.operand.elements[op.start, op.count]
|
|
63
|
+
else
|
|
64
|
+
@stack << ""
|
|
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 Postsvg::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 Postsvg::ExitSignal
|
|
78
78
|
end
|
|
79
79
|
|
|
80
80
|
def visit_quit(_op, _ctx)
|
|
81
|
-
raise
|
|
81
|
+
raise Postsvg::QuitSignal
|
|
82
82
|
end
|
|
83
83
|
|
|
84
84
|
def visit_exec(_op, _ctx)
|
|
@@ -72,14 +72,10 @@ 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
|
-
|
|
79
|
-
case operand
|
|
80
|
-
when Hash then operand.length
|
|
81
|
-
when Model::Literals::Dictionary then operand.entries.length
|
|
82
|
-
else 0
|
|
75
|
+
case op.operand
|
|
76
|
+
when Hash then @stack << op.operand.length
|
|
77
|
+
when Model::Literals::Dictionary then @stack << op.operand.entries.length
|
|
78
|
+
else @stack << 0
|
|
83
79
|
end
|
|
84
80
|
end
|
|
85
81
|
|
|
@@ -31,26 +31,15 @@ module Postsvg
|
|
|
31
31
|
end
|
|
32
32
|
|
|
33
33
|
def visit_scalefont(op, _ctx)
|
|
34
|
-
size = op.size.to_i
|
|
35
|
-
consume_numeric_operands(op.size)
|
|
34
|
+
size = op.size.to_i || op.size.to_f
|
|
36
35
|
font = @stack.pop
|
|
37
|
-
base =
|
|
38
|
-
font
|
|
39
|
-
else
|
|
40
|
-
FontRef.new(name: "Helvetica",
|
|
41
|
-
size: nil)
|
|
42
|
-
end
|
|
36
|
+
base = font.is_a?(FontRef) ? font : FontRef.new(name: "Helvetica", size: nil)
|
|
43
37
|
@stack << FontRef.new(name: base.name, size: size.to_f)
|
|
44
38
|
end
|
|
45
39
|
|
|
46
40
|
def visit_setfont(_op, _ctx)
|
|
47
41
|
font = @stack.pop
|
|
48
|
-
ref =
|
|
49
|
-
font
|
|
50
|
-
else
|
|
51
|
-
FontRef.new(name: "Helvetica",
|
|
52
|
-
size: 12.0)
|
|
53
|
-
end
|
|
42
|
+
ref = font.is_a?(FontRef) ? font : FontRef.new(name: "Helvetica", size: 12.0)
|
|
54
43
|
@graphics.update(font_name: ref.name || "Helvetica",
|
|
55
44
|
font_size: ref.size || 12.0)
|
|
56
45
|
end
|
|
@@ -78,7 +67,7 @@ module Postsvg
|
|
|
78
67
|
y: -screen_pos[:y],
|
|
79
68
|
font_family: @graphics.current.font_name,
|
|
80
69
|
font_size: @graphics.current.font_size,
|
|
81
|
-
color: color ||
|
|
70
|
+
color: color || Postsvg::Color::BLACK,
|
|
82
71
|
transform: "scale(1 -1)",
|
|
83
72
|
)
|
|
84
73
|
@path.reset
|
|
@@ -7,17 +7,15 @@ module Postsvg
|
|
|
7
7
|
# dash attribute setters.
|
|
8
8
|
module GraphicsState
|
|
9
9
|
def visit_gsave(_op, _ctx)
|
|
10
|
-
@saved_paths << @path.dup
|
|
11
10
|
@graphics.gsave
|
|
12
11
|
end
|
|
13
12
|
|
|
14
13
|
def visit_grestore(_op, _ctx)
|
|
15
|
-
@path = @saved_paths.pop if @graphics.depth > 1 && !@saved_paths.empty?
|
|
16
14
|
@graphics.grestore
|
|
17
15
|
end
|
|
18
16
|
|
|
19
17
|
def visit_grestoreall(_op, _ctx)
|
|
20
|
-
|
|
18
|
+
@graphics.grestore while @graphics.depth > 1
|
|
21
19
|
end
|
|
22
20
|
|
|
23
21
|
def visit_setlinewidth(op, _ctx)
|
|
@@ -25,14 +23,12 @@ module Postsvg
|
|
|
25
23
|
end
|
|
26
24
|
|
|
27
25
|
def visit_setlinecap(op, _ctx)
|
|
28
|
-
name = { 0 => :butt, 1 => :round, 2 => :square }.fetch(op.cap_code,
|
|
29
|
-
:butt)
|
|
26
|
+
name = { 0 => :butt, 1 => :round, 2 => :square }.fetch(op.cap_code, :butt)
|
|
30
27
|
@graphics.update(line_cap: name)
|
|
31
28
|
end
|
|
32
29
|
|
|
33
30
|
def visit_setlinejoin(op, _ctx)
|
|
34
|
-
name = { 0 => :miter, 1 => :round, 2 => :bevel }.fetch(op.join_code,
|
|
35
|
-
:miter)
|
|
31
|
+
name = { 0 => :miter, 1 => :round, 2 => :bevel }.fetch(op.join_code, :miter)
|
|
36
32
|
@graphics.update(line_join: name)
|
|
37
33
|
end
|
|
38
34
|
|
|
@@ -48,6 +44,8 @@ module Postsvg
|
|
|
48
44
|
pattern.map { |v| FormatNumber.call(v.to_f) }.join(" ")
|
|
49
45
|
when Numeric
|
|
50
46
|
FormatNumber.call(pattern.to_f)
|
|
47
|
+
else
|
|
48
|
+
nil
|
|
51
49
|
end
|
|
52
50
|
@graphics.update(dash: dash_str)
|
|
53
51
|
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
|
|
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
|
|
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,29 +5,25 @@ 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
|
-
#
|
|
11
|
-
# Other mid-path CTM changes retain the existing paint-time behavior.
|
|
8
|
+
# identity, the renderer will wrap emitted paths in a
|
|
9
|
+
# <g transform=>. Points are stored in user-space coordinates;
|
|
10
|
+
# the visitor delegates to the SvgBuilder for transform emission.
|
|
12
11
|
module Path
|
|
13
12
|
def visit_newpath(_op, _ctx)
|
|
14
13
|
@path.reset
|
|
15
14
|
end
|
|
16
15
|
|
|
17
16
|
def visit_moveto(op, _ctx)
|
|
18
|
-
@path.matrix ||= @graphics.current.ctm
|
|
19
17
|
@path.move_to(op.x, op.y)
|
|
20
18
|
@graphics.update(last_text_position: { x: op.x, y: op.y })
|
|
21
19
|
end
|
|
22
20
|
|
|
23
21
|
def visit_rmoveto(op, _ctx)
|
|
24
|
-
@path.matrix ||= @graphics.current.ctm
|
|
25
22
|
@path.move_to_rel(op.dx, op.dy)
|
|
26
23
|
pos = @graphics.current.last_text_position
|
|
27
24
|
base_x = pos ? pos[:x] : 0
|
|
28
25
|
base_y = pos ? pos[:y] : 0
|
|
29
|
-
@graphics.update(last_text_position: { x: base_x + op.dx,
|
|
30
|
-
y: base_y + op.dy })
|
|
26
|
+
@graphics.update(last_text_position: { x: base_x + op.dx, y: base_y + op.dy })
|
|
31
27
|
end
|
|
32
28
|
|
|
33
29
|
def visit_lineto(op, _ctx)
|
|
@@ -69,61 +65,46 @@ module Postsvg
|
|
|
69
65
|
# are parametrised by center, radius, angle range. SVG arcs
|
|
70
66
|
# are parametrised by endpoint, radii, large-arc/sweep flags.
|
|
71
67
|
def append_arc(cx, cy, radius, angle1, angle2, sweep:)
|
|
72
|
-
consume_numeric_operands(cx, cy, radius, angle1, angle2)
|
|
73
68
|
r = radius.abs
|
|
74
69
|
return if r.zero?
|
|
75
70
|
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
71
|
+
start_rad = angle1 * Math::PI / 180.0
|
|
72
|
+
start_x = cx + (r * Math.cos(start_rad))
|
|
73
|
+
start_y = cy + (r * Math.sin(start_rad))
|
|
74
|
+
end_rad = angle2 * Math::PI / 180.0
|
|
75
|
+
end_x = cx + (r * Math.cos(end_rad))
|
|
76
|
+
end_y = cy + (r * Math.sin(end_rad))
|
|
77
|
+
|
|
78
|
+
# If the current path is empty, the arc implicitly starts
|
|
79
|
+
# with a moveto to its starting point.
|
|
80
|
+
if @path.empty? || last_command_starts_with?("M")
|
|
81
|
+
@path.move_to(start_x, start_y) if @path.empty?
|
|
85
82
|
end
|
|
86
|
-
@path.append_deferred(command, end_x, end_y)
|
|
87
|
-
end
|
|
88
83
|
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
rx, ry, rotation = arc_radii(matrix, radius)
|
|
96
|
-
reflected = ((matrix.a * matrix.d) - (matrix.b * matrix.c)).negative?
|
|
97
|
-
svg_sweep = sweep != reflected
|
|
84
|
+
delta = (angle2 - angle1) % 360
|
|
85
|
+
delta += 360 if delta.negative?
|
|
86
|
+
large_arc = delta > 180
|
|
87
|
+
|
|
88
|
+
# SVG sweep flag is 1 when the arc is drawn clockwise.
|
|
89
|
+
svg_sweep = sweep ? (delta.positive? ? 1 : 0) : 0
|
|
98
90
|
if delta.abs < 1e-6
|
|
99
|
-
# Full
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
91
|
+
# Full circle: emit two arcs.
|
|
92
|
+
mid_angle = angle1 + 180
|
|
93
|
+
mid_rad = mid_angle * Math::PI / 180.0
|
|
94
|
+
mid_x = cx + (r * Math.cos(mid_rad))
|
|
95
|
+
mid_y = cy + (r * Math.sin(mid_rad))
|
|
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
|
|
104
99
|
else
|
|
105
|
-
path.arc_to(
|
|
100
|
+
@path.arc_to(r, r, 0, large_arc ? 1 : 0, svg_sweep, end_x, end_y)
|
|
106
101
|
end
|
|
107
|
-
path.to_path_d
|
|
108
102
|
end
|
|
109
103
|
|
|
110
|
-
def
|
|
111
|
-
|
|
112
|
-
matrix.apply_point(cx + (radius * Math.cos(radians)),
|
|
113
|
-
cy + (radius * Math.sin(radians))).values_at(:x, :y)
|
|
114
|
-
end
|
|
104
|
+
def last_command_starts_with?(prefix)
|
|
105
|
+
return false if @path.commands.empty?
|
|
115
106
|
|
|
116
|
-
|
|
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]
|
|
107
|
+
@path.commands.last.start_with?(prefix)
|
|
127
108
|
end
|
|
128
109
|
end
|
|
129
110
|
end
|
|
@@ -6,19 +6,12 @@ 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
|
-
|
|
15
9
|
def visit_translate(op, _ctx)
|
|
16
10
|
new_ctm = @graphics.current.ctm.translate(op.tx.to_f, op.ty.to_f)
|
|
17
11
|
@graphics.update(ctm: new_ctm)
|
|
18
12
|
end
|
|
19
13
|
|
|
20
14
|
def visit_scale(op, _ctx)
|
|
21
|
-
consume_numeric_operands(op.sx, op.sy)
|
|
22
15
|
new_ctm = @graphics.current.ctm.scale(op.sx.to_f, op.sy.to_f)
|
|
23
16
|
@graphics.update(ctm: new_ctm)
|
|
24
17
|
end
|
|
@@ -29,7 +22,7 @@ module Postsvg
|
|
|
29
22
|
end
|
|
30
23
|
|
|
31
24
|
def visit_concat(op, _ctx)
|
|
32
|
-
matrix = matrix_from_operand(
|
|
25
|
+
matrix = matrix_from_operand(op.matrix)
|
|
33
26
|
return unless matrix
|
|
34
27
|
|
|
35
28
|
new_ctm = @graphics.current.ctm.multiply(matrix)
|
|
@@ -37,17 +30,16 @@ module Postsvg
|
|
|
37
30
|
end
|
|
38
31
|
|
|
39
32
|
def visit_matrix(_op, _ctx)
|
|
40
|
-
@stack <<
|
|
33
|
+
@stack << [1, 0, 0, 1, 0, 0]
|
|
41
34
|
end
|
|
42
35
|
|
|
43
36
|
def visit_currentmatrix(_op, _ctx)
|
|
44
|
-
@stack.pop if @stack.last.is_a?(StackMatrix)
|
|
45
37
|
ctm = @graphics.current.ctm
|
|
46
|
-
@stack <<
|
|
38
|
+
@stack << [ctm.a, ctm.b, ctm.c, ctm.d, ctm.e, ctm.f]
|
|
47
39
|
end
|
|
48
40
|
|
|
49
41
|
def visit_setmatrix(op, _ctx)
|
|
50
|
-
matrix = matrix_from_operand(
|
|
42
|
+
matrix = matrix_from_operand(op.matrix)
|
|
51
43
|
return unless matrix
|
|
52
44
|
|
|
53
45
|
@graphics.update(ctm: matrix)
|
|
@@ -93,46 +85,16 @@ module Postsvg
|
|
|
93
85
|
|
|
94
86
|
private
|
|
95
87
|
|
|
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.
|
|
120
88
|
def matrix_from_operand(operand)
|
|
121
89
|
case operand
|
|
122
90
|
when Matrix then operand
|
|
123
|
-
when Model::Literals::ArrayLiteral then matrix_from_operand(operand.elements)
|
|
124
91
|
when Array
|
|
125
|
-
|
|
126
|
-
return nil unless numbers.length == 6 && numbers.all?
|
|
92
|
+
return nil unless operand.length == 6
|
|
127
93
|
|
|
128
|
-
Matrix.new(a:
|
|
129
|
-
d:
|
|
94
|
+
Matrix.new(a: operand[0], b: operand[1], c: operand[2],
|
|
95
|
+
d: operand[3], e: operand[4], f: operand[5])
|
|
130
96
|
end
|
|
131
97
|
end
|
|
132
|
-
|
|
133
|
-
def matrix_number(element)
|
|
134
|
-
numeric_value(element) if element.is_a?(Numeric) || element.is_a?(Model::Literals::Number)
|
|
135
|
-
end
|
|
136
98
|
end
|
|
137
99
|
end
|
|
138
100
|
end
|