Module: Emfsvg::Svg::TransformParser

Defined in:
lib/emfsvg/svg/transform_parser.rb

Overview

Parse SVG transform="..." attribute syntax into a single Emf::Model::Geometry::Matrix. Handles translate(), scale(), rotate(), matrix(), skewX(), skewY().

Convention: the emf gem's Matrix uses EMF's row-vector layout (v_new = v * M) — same as emfsvg's existing emr_visitor multiply. SVG transform="A B" means "apply A first, then B to the result" in row-vector terms, which equals the matrix product A * B.

Constant Summary collapse

TRANSFORM_FN_RE =
/([a-zA-Z]+)\s*\(([^)]*)\)/

Class Method Summary collapse

Class Method Details

.build_function(name, args) ⇒ Object



32
33
34
35
36
37
38
39
40
41
42
43
# File 'lib/emfsvg/svg/transform_parser.rb', line 32

def build_function(name, args)
  case name
  when "matrix" then from_matrix_args(args)
  when "translate" then translate_matrix(*args)
  when "scale" then scale_matrix(*args)
  when "rotate" then rotate_matrix(*args)
  when "skewx" then skewx_matrix(args.first)
  when "skewy" then skewy_matrix(args.first)
  else
    raise FormatError, "unknown SVG transform function: #{name}"
  end
end

.from_matrix_args(args) ⇒ Object

Raises:



45
46
47
48
49
50
51
52
# File 'lib/emfsvg/svg/transform_parser.rb', line 45

def from_matrix_args(args)
  raise FormatError, "matrix() requires 6 args" if args.size != 6

  Emf::Model::Geometry::Matrix.new(
    m11: args[0], m12: args[1], m21: args[2], m22: args[3],
    dx: args[4], dy: args[5]
  )
end

.multiply(a, b) ⇒ Object

Row-vector matrix multiply: v * (a * b) = (v * a) * b. Matches emfsvg's emr_visitor#multiply_matrix formula.



111
112
113
114
115
116
117
118
119
120
# File 'lib/emfsvg/svg/transform_parser.rb', line 111

def multiply(a, b)
  Emf::Model::Geometry::Matrix.new(
    m11: (a.m11 * b.m11) + (a.m12 * b.m21),
    m12: (a.m11 * b.m12) + (a.m12 * b.m22),
    m21: (a.m21 * b.m11) + (a.m22 * b.m21),
    m22: (a.m21 * b.m12) + (a.m22 * b.m22),
    dx: (a.dx * b.m11) + (a.dy * b.m21) + b.dx,
    dy: (a.dx * b.m12) + (a.dy * b.m22) + b.dy
  )
end

.parse(string) ⇒ Object



18
19
20
21
22
23
24
25
26
27
28
29
30
# File 'lib/emfsvg/svg/transform_parser.rb', line 18

def parse(string)
  return Emf::Model::Geometry::Matrix.identity if string.nil? || string.strip.empty?

  # Row-vector convention: v_new = v * M. SVG `transform="A B"`
  # applies B first then A to local coords, which in row-vector
  # equals the product B * A. Scanning left-to-right we compose
  # on the LEFT so each new function pre-multiplies the accumulator.
  string.scan(TRANSFORM_FN_RE).inject(Emf::Model::Geometry::Matrix.identity) do |composed, (name, args_str)|
    args = args_str.split(/[\s,]+/).map(&:to_f)
    fn_matrix = build_function(name.downcase, args)
    multiply(fn_matrix, composed)
  end
end

.rotate_matrix(*args) ⇒ Object

SVG rotate(angle) is visually clockwise (Y grows downward). With the emf gem's row-vector matrix layout, the equivalent transform applied around (cx, cy) is: T(-cx,-cy) * R * T(cx,cy). (The SVG spec text writes it as T(cx,cy) * R * T(-cx,-cy), but that's column-vector notation — row-vector reverses the order.)



77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
# File 'lib/emfsvg/svg/transform_parser.rb', line 77

def rotate_matrix(*args)
  deg = args.fetch(0, 0.0)
  cx = args.fetch(1, 0.0)
  cy = args.fetch(2, 0.0)
  rad = deg * Math::PI / 180.0
  cos = Math.cos(rad)
  sin = Math.sin(rad)
  rot = Emf::Model::Geometry::Matrix.new(
    m11: cos, m12: sin, m21: -sin, m22: cos,
    dx: 0.0, dy: 0.0
  )
  return rot if cx.zero? && cy.zero?

  multiply(multiply(translate_matrix(-cx, -cy), rot), translate_matrix(cx, cy))
end

.scale_matrix(*args) ⇒ Object



63
64
65
66
67
68
69
70
# File 'lib/emfsvg/svg/transform_parser.rb', line 63

def scale_matrix(*args)
  sx = args.fetch(0, 1.0)
  sy = args.fetch(1, sx)
  Emf::Model::Geometry::Matrix.new(
    m11: sx, m12: 0.0, m21: 0.0, m22: sy,
    dx: 0.0, dy: 0.0
  )
end

.skewx_matrix(deg) ⇒ Object



93
94
95
96
97
98
99
# File 'lib/emfsvg/svg/transform_parser.rb', line 93

def skewx_matrix(deg)
  rad = deg * Math::PI / 180.0
  Emf::Model::Geometry::Matrix.new(
    m11: 1.0, m12: 0.0, m21: Math.tan(rad), m22: 1.0,
    dx: 0.0, dy: 0.0
  )
end

.skewy_matrix(deg) ⇒ Object



101
102
103
104
105
106
107
# File 'lib/emfsvg/svg/transform_parser.rb', line 101

def skewy_matrix(deg)
  rad = deg * Math::PI / 180.0
  Emf::Model::Geometry::Matrix.new(
    m11: 1.0, m12: Math.tan(rad), m21: 0.0, m22: 1.0,
    dx: 0.0, dy: 0.0
  )
end

.translate_matrix(*args) ⇒ Object



54
55
56
57
58
59
60
61
# File 'lib/emfsvg/svg/transform_parser.rb', line 54

def translate_matrix(*args)
  tx = args.fetch(0, 0.0)
  ty = args.fetch(1, 0.0)
  Emf::Model::Geometry::Matrix.new(
    m11: 1.0, m12: 0.0, m21: 0.0, m22: 1.0,
    dx: tx, dy: ty
  )
end