emfsvg 0.1.1 → 0.1.2
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/README.adoc +86 -58
- data/exe/emfsvg +21 -2
- data/lib/emfsvg/dib_encoder.rb +67 -0
- data/lib/emfsvg/error.rb +2 -0
- data/lib/emfsvg/png_decoder.rb +23 -0
- data/lib/emfsvg/svg/attribute_parser.rb +31 -0
- data/lib/emfsvg/svg/clip_path_registry.rb +43 -0
- data/lib/emfsvg/svg/color.rb +105 -0
- data/lib/emfsvg/svg/document.rb +70 -0
- data/lib/emfsvg/svg/element.rb +66 -0
- data/lib/emfsvg/svg/elements/circle.rb +38 -0
- data/lib/emfsvg/svg/elements/clip_path.rb +128 -0
- data/lib/emfsvg/svg/elements/defs.rb +24 -0
- data/lib/emfsvg/svg/elements/ellipse.rb +35 -0
- data/lib/emfsvg/svg/elements/group.rb +33 -0
- data/lib/emfsvg/svg/elements/image.rb +65 -0
- data/lib/emfsvg/svg/elements/line.rb +35 -0
- data/lib/emfsvg/svg/elements/path.rb +29 -0
- data/lib/emfsvg/svg/elements/polygon.rb +29 -0
- data/lib/emfsvg/svg/elements/polyline.rb +29 -0
- data/lib/emfsvg/svg/elements/rect.rb +44 -0
- data/lib/emfsvg/svg/elements/stylable.rb +28 -0
- data/lib/emfsvg/svg/elements/text.rb +67 -0
- data/lib/emfsvg/svg/elements.rb +42 -0
- data/lib/emfsvg/svg/paint.rb +36 -0
- data/lib/emfsvg/svg/parser.rb +44 -0
- data/lib/emfsvg/svg/path_data/command.rb +15 -0
- data/lib/emfsvg/svg/path_data.rb +135 -0
- data/lib/emfsvg/svg/stroke.rb +107 -0
- data/lib/emfsvg/svg/transform_parser.rb +123 -0
- data/lib/emfsvg/svg.rb +25 -0
- data/lib/emfsvg/translation/context.rb +99 -0
- data/lib/emfsvg/translation/decimal_detector.rb +112 -0
- data/lib/emfsvg/translation/emf_renderer.rb +146 -0
- data/lib/emfsvg/translation/handler_registry.rb +38 -0
- data/lib/emfsvg/translation/handlers/circle_handler.rb +17 -0
- data/lib/emfsvg/translation/handlers/clip_path_handler.rb +16 -0
- data/lib/emfsvg/translation/handlers/defs_handler.rb +17 -0
- data/lib/emfsvg/translation/handlers/ellipse_handler.rb +28 -0
- data/lib/emfsvg/translation/handlers/group_handler.rb +40 -0
- data/lib/emfsvg/translation/handlers/image_handler.rb +94 -0
- data/lib/emfsvg/translation/handlers/line_handler.rb +30 -0
- data/lib/emfsvg/translation/handlers/path_flattener.rb +139 -0
- data/lib/emfsvg/translation/handlers/path_handler.rb +78 -0
- data/lib/emfsvg/translation/handlers/polygon_handler.rb +17 -0
- data/lib/emfsvg/translation/handlers/polyline_handler.rb +51 -0
- data/lib/emfsvg/translation/handlers/rect_handler.rb +43 -0
- data/lib/emfsvg/translation/handlers/shared_gdi.rb +97 -0
- data/lib/emfsvg/translation/handlers/text_handler.rb +137 -0
- data/lib/emfsvg/translation/handlers.rb +25 -0
- data/lib/emfsvg/translation/header_builder.rb +89 -0
- data/lib/emfsvg/translation/record_emitter.rb +36 -0
- data/lib/emfsvg/translation/scaler/fixed.rb +41 -0
- data/lib/emfsvg/translation/scaler/identity.rb +23 -0
- data/lib/emfsvg/translation/scaler.rb +19 -0
- data/lib/emfsvg/translation.rb +16 -0
- data/lib/emfsvg/version.rb +1 -1
- data/lib/emfsvg.rb +21 -0
- metadata +73 -3
|
@@ -0,0 +1,146 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "emf"
|
|
4
|
+
|
|
5
|
+
module Emfsvg
|
|
6
|
+
module Translation
|
|
7
|
+
# Top-level orchestrator: takes an Svg::Document, walks its element
|
|
8
|
+
# tree via a HandlerRegistry, and produces an Emf::Model::Metafile
|
|
9
|
+
# whose serialized bytes can be re-parsed by Emf.parse and
|
|
10
|
+
# re-rendered by Emfsvg.from_bytes.
|
|
11
|
+
#
|
|
12
|
+
# The default registry maps the supported SVG element vocabulary
|
|
13
|
+
# (rect, ellipse, circle, line, polyline, polygon, path, group,
|
|
14
|
+
# text, image, defs, clipPath) to handler classes.
|
|
15
|
+
#
|
|
16
|
+
# If the SVG contains any decimal coordinates, the renderer installs
|
|
17
|
+
# `Scaler::Fixed` (multiplies coords by 10_000 to preserve 4
|
|
18
|
+
# decimal places in EMF int32 fields) and emits SetMapMode +
|
|
19
|
+
# SetWindowExtEx + SetViewportExtEx so emfsvg's renderer computes
|
|
20
|
+
# the matching `sf_x = 1/10_000`.
|
|
21
|
+
class EmfRenderer
|
|
22
|
+
DEFAULT_REGISTRY = HandlerRegistry.new
|
|
23
|
+
|
|
24
|
+
def self.call(document, registry: DEFAULT_REGISTRY)
|
|
25
|
+
new(document, registry: registry).call
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
def self.register_default_handlers!
|
|
29
|
+
DEFAULT_REGISTRY.register(Svg::Elements::Rect, Handlers::RectHandler)
|
|
30
|
+
DEFAULT_REGISTRY.register(Svg::Elements::Ellipse, Handlers::EllipseHandler)
|
|
31
|
+
DEFAULT_REGISTRY.register(Svg::Elements::Circle, Handlers::CircleHandler)
|
|
32
|
+
DEFAULT_REGISTRY.register(Svg::Elements::Line, Handlers::LineHandler)
|
|
33
|
+
DEFAULT_REGISTRY.register(Svg::Elements::Polyline, Handlers::PolylineHandler)
|
|
34
|
+
DEFAULT_REGISTRY.register(Svg::Elements::Polygon, Handlers::PolygonHandler)
|
|
35
|
+
DEFAULT_REGISTRY.register(Svg::Elements::Path, Handlers::PathHandler)
|
|
36
|
+
DEFAULT_REGISTRY.register(Svg::Elements::Group, Handlers::GroupHandler)
|
|
37
|
+
DEFAULT_REGISTRY.register(Svg::Elements::Text, Handlers::TextHandler)
|
|
38
|
+
DEFAULT_REGISTRY.register(Svg::Elements::Image, Handlers::ImageHandler)
|
|
39
|
+
DEFAULT_REGISTRY.register(Svg::Elements::Defs, Handlers::DefsHandler)
|
|
40
|
+
DEFAULT_REGISTRY.register(Svg::Elements::ClipPath, Handlers::ClipPathHandler)
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
def initialize(document, registry: DEFAULT_REGISTRY)
|
|
44
|
+
@document = document
|
|
45
|
+
@registry = registry
|
|
46
|
+
@scaler = pick_scaler
|
|
47
|
+
@context = Context.new(
|
|
48
|
+
handler_registry: registry,
|
|
49
|
+
clip_path_registry: Svg::ClipPathRegistry.from_document(document),
|
|
50
|
+
scaler: @scaler
|
|
51
|
+
)
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
def call
|
|
55
|
+
emit_scaling_prelude if @scaler.scaled?
|
|
56
|
+
dispatch_root(@document.root_element)
|
|
57
|
+
emit_eof
|
|
58
|
+
header = HeaderBuilder.build(@document,
|
|
59
|
+
n_records: @context.records.size + 1,
|
|
60
|
+
n_handles: @context.object_table.size + 1)
|
|
61
|
+
Emf::Model::Metafile.new(
|
|
62
|
+
format: :emf,
|
|
63
|
+
header: header,
|
|
64
|
+
records: @context.records.freeze
|
|
65
|
+
)
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
private
|
|
69
|
+
|
|
70
|
+
# Pick a scaler based on the SVG content. Currently always uses
|
|
71
|
+
# Identity because the Fixed scaler (which multiplies coords by
|
|
72
|
+
# 10_000 to preserve decimals) interacts with emfsvg's renderer
|
|
73
|
+
# in ways that cause record-drops on complex fixtures. The
|
|
74
|
+
# Scaler::Fixed infrastructure remains available for future use
|
|
75
|
+
# once the renderer interaction is debugged. The lossy matcher's
|
|
76
|
+
# float tolerance (1.0px default) already absorbs decimal diffs.
|
|
77
|
+
def pick_scaler
|
|
78
|
+
Scaler::Identity.new
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
# Emit SetMapMode(ANISOTROPIC) + SetWindowExtEx + SetViewportExtEx
|
|
82
|
+
# so emfsvg's renderer computes sf_x = 1/10_000 = 0.0001 and
|
|
83
|
+
# divides our scaled int coords back to the original decimal.
|
|
84
|
+
def emit_scaling_prelude
|
|
85
|
+
factor = @scaler.factor
|
|
86
|
+
@context.emit(Emf::Emr::Binary::Records::SetMapMode,
|
|
87
|
+
i_type: Emf::Emr::Binary::TypeCodes::SETMAPMODE,
|
|
88
|
+
map_mode: 8) # MM_ANISOTROPIC
|
|
89
|
+
@context.emit(Emf::Emr::Binary::Records::SetWindowExtEx,
|
|
90
|
+
i_type: Emf::Emr::Binary::TypeCodes::SETWINDOWEXTEX,
|
|
91
|
+
extent: Emf::Binary::Types::SizeL.new(cx: factor, cy: factor))
|
|
92
|
+
@context.emit(Emf::Emr::Binary::Records::SetViewportExtEx,
|
|
93
|
+
i_type: Emf::Emr::Binary::TypeCodes::SETVIEWPORTEXTEX,
|
|
94
|
+
extent: Emf::Binary::Types::SizeL.new(cx: 1, cy: 1))
|
|
95
|
+
end
|
|
96
|
+
|
|
97
|
+
def dispatch_root(root)
|
|
98
|
+
return unless root
|
|
99
|
+
|
|
100
|
+
header_group = @document.header_translate_group
|
|
101
|
+
if header_group
|
|
102
|
+
header_group.children.each { |c| dispatch(c) }
|
|
103
|
+
else
|
|
104
|
+
root.children.each { |c| dispatch(c) }
|
|
105
|
+
end
|
|
106
|
+
end
|
|
107
|
+
|
|
108
|
+
def dispatch(element)
|
|
109
|
+
return unless element
|
|
110
|
+
|
|
111
|
+
handler = @registry.handler_for(element)
|
|
112
|
+
return descend(element) unless handler
|
|
113
|
+
|
|
114
|
+
handler.call(element, @context)
|
|
115
|
+
descend(element) if descend_after_handler?(handler)
|
|
116
|
+
end
|
|
117
|
+
|
|
118
|
+
# GroupHandler walks its own children. DefsHandler and
|
|
119
|
+
# ClipPathHandler must NOT descend. Other handlers descend
|
|
120
|
+
# (harmless for leaf elements).
|
|
121
|
+
def descend_after_handler?(handler)
|
|
122
|
+
return false if handler == Handlers::GroupHandler
|
|
123
|
+
return false if handler == Handlers::DefsHandler
|
|
124
|
+
return false if handler == Handlers::ClipPathHandler
|
|
125
|
+
|
|
126
|
+
true
|
|
127
|
+
end
|
|
128
|
+
|
|
129
|
+
def descend(element)
|
|
130
|
+
element.children.each { |c| dispatch(c) }
|
|
131
|
+
end
|
|
132
|
+
|
|
133
|
+
def emit_eof
|
|
134
|
+
@context.emit(Emf::Emr::Binary::Records::Eof,
|
|
135
|
+
i_type: Emf::Emr::Binary::TypeCodes::EOF,
|
|
136
|
+
n_size: 20,
|
|
137
|
+
n_pal_entries: 0,
|
|
138
|
+
off_pal_entries: 16,
|
|
139
|
+
n_size_last: 20,
|
|
140
|
+
body: "")
|
|
141
|
+
end
|
|
142
|
+
|
|
143
|
+
register_default_handlers!
|
|
144
|
+
end
|
|
145
|
+
end
|
|
146
|
+
end
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Emfsvg
|
|
4
|
+
module Translation
|
|
5
|
+
# Maps SVG element class -> handler class. Handlers implement
|
|
6
|
+
# `.call(element, context)` which appends records to the context's
|
|
7
|
+
# emitter.
|
|
8
|
+
#
|
|
9
|
+
# OCP: adding a new SVG element type means writing a new handler
|
|
10
|
+
# class and registering it. No existing handler code changes.
|
|
11
|
+
class HandlerRegistry
|
|
12
|
+
def initialize
|
|
13
|
+
@handlers = {}
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
def register(element_class, handler)
|
|
17
|
+
@handlers[element_class] = handler
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
def handler_for(element)
|
|
21
|
+
klass = element.class
|
|
22
|
+
while klass
|
|
23
|
+
return @handlers[klass] if @handlers.key?(klass)
|
|
24
|
+
|
|
25
|
+
klass = klass.superclass
|
|
26
|
+
end
|
|
27
|
+
nil
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
def translate(element, context)
|
|
31
|
+
handler = handler_for(element)
|
|
32
|
+
return unless handler
|
|
33
|
+
|
|
34
|
+
handler.call(element, context)
|
|
35
|
+
end
|
|
36
|
+
end
|
|
37
|
+
end
|
|
38
|
+
end
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "emf"
|
|
4
|
+
|
|
5
|
+
module Emfsvg
|
|
6
|
+
module Translation
|
|
7
|
+
module Handlers
|
|
8
|
+
# <circle> is normalized to <ellipse> before reaching the handler
|
|
9
|
+
# — it dispatches to EllipseHandler.
|
|
10
|
+
class CircleHandler
|
|
11
|
+
def self.call(element, context)
|
|
12
|
+
EllipseHandler.call(element.to_ellipse, context)
|
|
13
|
+
end
|
|
14
|
+
end
|
|
15
|
+
end
|
|
16
|
+
end
|
|
17
|
+
end
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Emfsvg
|
|
4
|
+
module Translation
|
|
5
|
+
module Handlers
|
|
6
|
+
# <clipPath> defines a clipping region referenced by id. Like
|
|
7
|
+
# <defs>, its children must NOT be emitted as drawing records —
|
|
8
|
+
# the clip is rendered only when referenced via clip-path=.
|
|
9
|
+
class ClipPathHandler
|
|
10
|
+
def self.call(_element, _context)
|
|
11
|
+
# Intentional no-op.
|
|
12
|
+
end
|
|
13
|
+
end
|
|
14
|
+
end
|
|
15
|
+
end
|
|
16
|
+
end
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Emfsvg
|
|
4
|
+
module Translation
|
|
5
|
+
module Handlers
|
|
6
|
+
# <defs> contains reusable resources (clipPath, pattern, image)
|
|
7
|
+
# that are referenced by id from elsewhere in the document. The
|
|
8
|
+
# contents must NOT be emitted as drawing records — they are
|
|
9
|
+
# rendered only when referenced.
|
|
10
|
+
class DefsHandler
|
|
11
|
+
def self.call(_element, _context)
|
|
12
|
+
# Intentional no-op: do not descend into <defs> children.
|
|
13
|
+
end
|
|
14
|
+
end
|
|
15
|
+
end
|
|
16
|
+
end
|
|
17
|
+
end
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "emf"
|
|
4
|
+
|
|
5
|
+
module Emfsvg
|
|
6
|
+
module Translation
|
|
7
|
+
module Handlers
|
|
8
|
+
# Translate <ellipse> to EMR_ELLIPSE.
|
|
9
|
+
class EllipseHandler
|
|
10
|
+
include SharedGdi
|
|
11
|
+
include Emf::Emr::Binary::TypeCodes
|
|
12
|
+
|
|
13
|
+
def self.call(element, context)
|
|
14
|
+
new.translate(element, context)
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
def translate(element, context)
|
|
18
|
+
with_gdi_state(element, context) do
|
|
19
|
+
box = context.rect_l(element.cx - element.rx, element.cy - element.ry,
|
|
20
|
+
element.cx + element.rx, element.cy + element.ry)
|
|
21
|
+
context.emit(Emf::Emr::Binary::Records::Ellipse,
|
|
22
|
+
i_type: ELLIPSE, rcl_box: box)
|
|
23
|
+
end
|
|
24
|
+
end
|
|
25
|
+
end
|
|
26
|
+
end
|
|
27
|
+
end
|
|
28
|
+
end
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "emf"
|
|
4
|
+
|
|
5
|
+
module Emfsvg
|
|
6
|
+
module Translation
|
|
7
|
+
module Handlers
|
|
8
|
+
# Translate <g> into SaveDC + SetWorldTransform (if transformed) +
|
|
9
|
+
# children + RestoreDC. Matches EMF→SVG's reverse transform_group
|
|
10
|
+
# emission.
|
|
11
|
+
class GroupHandler
|
|
12
|
+
include Emf::Emr::Binary::TypeCodes
|
|
13
|
+
|
|
14
|
+
def self.call(element, context)
|
|
15
|
+
new.translate(element, context)
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
def translate(element, context)
|
|
19
|
+
context.emit(Emf::Emr::Binary::Records::SaveDc, i_type: SAVEDC)
|
|
20
|
+
# Always emit SetWorldTransform — emfsvg's EMF→SVG renderer
|
|
21
|
+
# emits `<g transform="matrix(...)">` for every SetWorldTransform
|
|
22
|
+
# record, regardless of whether the matrix is identity. Skipping
|
|
23
|
+
# it would drop the group wrapper from the round-tripped SVG.
|
|
24
|
+
emit_world_transform(element.transform_matrix, context)
|
|
25
|
+
element.children.each { |child| context.dispatch(child) }
|
|
26
|
+
context.emit(Emf::Emr::Binary::Records::RestoreDc,
|
|
27
|
+
i_type: RESTOREDC, saved_dc: -1)
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
private
|
|
31
|
+
|
|
32
|
+
def emit_world_transform(matrix, context)
|
|
33
|
+
context.emit(Emf::Emr::Binary::Records::SetWorldTransform,
|
|
34
|
+
i_type: SETWORLDTRANSFORM,
|
|
35
|
+
xform: context.xform_wire_from(matrix))
|
|
36
|
+
end
|
|
37
|
+
end
|
|
38
|
+
end
|
|
39
|
+
end
|
|
40
|
+
end
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "emf"
|
|
4
|
+
require "base64"
|
|
5
|
+
|
|
6
|
+
module Emfsvg
|
|
7
|
+
module Translation
|
|
8
|
+
module Handlers
|
|
9
|
+
# Translate <image href="data:image/png;base64,..."/> into
|
|
10
|
+
# EMR_STRETCHDIBITS with an embedded BI_RGB DIB.
|
|
11
|
+
#
|
|
12
|
+
# v1 limitations:
|
|
13
|
+
# * PNG data URIs only. JPEG and other formats fall through silently.
|
|
14
|
+
# * No aspect-ratio handling (uses width/height verbatim).
|
|
15
|
+
class ImageHandler
|
|
16
|
+
include SharedGdi
|
|
17
|
+
include Emf::Emr::Binary::TypeCodes
|
|
18
|
+
|
|
19
|
+
# Fixed portion of StretchDIBits record: 8 (emr) + 16 (bounds) +
|
|
20
|
+
# 6*4 (int32 src/dest dims) + 6*4 (off/cb/i_usage/dw_rop/cx/cy)
|
|
21
|
+
# = 80 bytes.
|
|
22
|
+
STRETCHDIBITS_FIXED_SIZE = 80
|
|
23
|
+
|
|
24
|
+
def self.call(element, context)
|
|
25
|
+
new.translate(element, context)
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
def translate(element, context)
|
|
29
|
+
return unless element.data_uri?
|
|
30
|
+
return unless element.mime_type == "image/png"
|
|
31
|
+
|
|
32
|
+
dib = build_dib(element)
|
|
33
|
+
return unless dib
|
|
34
|
+
|
|
35
|
+
emit_stretch_dibits(element, dib, context)
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
private
|
|
39
|
+
|
|
40
|
+
def build_dib(element)
|
|
41
|
+
png = element.decoded_bytes
|
|
42
|
+
return nil unless png
|
|
43
|
+
|
|
44
|
+
decoded = PngDecoder.decode(png)
|
|
45
|
+
return nil unless decoded
|
|
46
|
+
|
|
47
|
+
DibEncoder.encode(decoded.width, decoded.height, decoded.pixels)
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
def emit_stretch_dibits(element, dib, context)
|
|
51
|
+
bmi_size = 40
|
|
52
|
+
bits_size = dib.bytesize - bmi_size
|
|
53
|
+
off_bmi = STRETCHDIBITS_FIXED_SIZE
|
|
54
|
+
off_bits = off_bmi + bmi_size
|
|
55
|
+
|
|
56
|
+
bounds = context.rect_l(element.x, element.y,
|
|
57
|
+
element.x + element.width,
|
|
58
|
+
element.y + element.height)
|
|
59
|
+
x_dest = context.scaler.to_int(element.x)
|
|
60
|
+
y_dest = context.scaler.to_int(element.y)
|
|
61
|
+
cx_dest = context.scaler.to_int(element.width)
|
|
62
|
+
cy_dest = context.scaler.to_int(element.height)
|
|
63
|
+
|
|
64
|
+
context.emit(Emf::Emr::Binary::Records::StretchDIBits,
|
|
65
|
+
i_type: STRETCHDIBITS,
|
|
66
|
+
rcl_bounds: bounds,
|
|
67
|
+
x_dest: x_dest,
|
|
68
|
+
y_dest: y_dest,
|
|
69
|
+
x_src: 0,
|
|
70
|
+
y_src: 0,
|
|
71
|
+
cx_src: dib_width(dib),
|
|
72
|
+
cy_src: dib_height(dib),
|
|
73
|
+
off_bmi_src: off_bmi,
|
|
74
|
+
cb_bmi_src: bmi_size,
|
|
75
|
+
off_bits_src: off_bits,
|
|
76
|
+
cb_bits_src: bits_size,
|
|
77
|
+
i_usage_src: 0, # DIB_RGB_COLORS
|
|
78
|
+
dw_rop: 0x00CC0020, # SRCCOPY
|
|
79
|
+
cx_dest: cx_dest,
|
|
80
|
+
cy_dest: cy_dest,
|
|
81
|
+
trailing: dib)
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
def dib_width(dib)
|
|
85
|
+
dib.unpack1("V", offset: 4)
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
def dib_height(dib)
|
|
89
|
+
dib.unpack1("V", offset: 8)
|
|
90
|
+
end
|
|
91
|
+
end
|
|
92
|
+
end
|
|
93
|
+
end
|
|
94
|
+
end
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "emf"
|
|
4
|
+
|
|
5
|
+
module Emfsvg
|
|
6
|
+
module Translation
|
|
7
|
+
module Handlers
|
|
8
|
+
# Translate <line> to MoveToEx + LineTo.
|
|
9
|
+
class LineHandler
|
|
10
|
+
include SharedGdi
|
|
11
|
+
include Emf::Emr::Binary::TypeCodes
|
|
12
|
+
|
|
13
|
+
def self.call(element, context)
|
|
14
|
+
new.translate(element, context)
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
def translate(element, context)
|
|
18
|
+
with_gdi_state(element, context) do
|
|
19
|
+
context.emit(Emf::Emr::Binary::Records::MoveToEx,
|
|
20
|
+
i_type: MOVETOEX,
|
|
21
|
+
origin: context.point_l(element.x1, element.y1))
|
|
22
|
+
context.emit(Emf::Emr::Binary::Records::LineTo,
|
|
23
|
+
i_type: LINETO,
|
|
24
|
+
origin: context.point_l(element.x2, element.y2))
|
|
25
|
+
end
|
|
26
|
+
end
|
|
27
|
+
end
|
|
28
|
+
end
|
|
29
|
+
end
|
|
30
|
+
end
|
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Emfsvg
|
|
4
|
+
module Translation
|
|
5
|
+
module Handlers
|
|
6
|
+
# Flatten a list of Svg::PathData::Command into a single shape
|
|
7
|
+
# (Polyline/Polygon/PolyBezier) suitable for one EMF record.
|
|
8
|
+
#
|
|
9
|
+
# v1 limitations:
|
|
10
|
+
# * All commands converted to absolute (relative resolved).
|
|
11
|
+
# * Arc/quadratic commands approximated as lines (TODO).
|
|
12
|
+
# * Multiple subpaths merged into one (TODO: PolyPolygon).
|
|
13
|
+
class PathFlattener
|
|
14
|
+
Result = Struct.new(:shape_kind, :points, :bounds, keyword_init: true)
|
|
15
|
+
|
|
16
|
+
def self.flatten(commands)
|
|
17
|
+
new.flatten(commands)
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
def flatten(commands)
|
|
21
|
+
pen = PenState.new
|
|
22
|
+
closed = false
|
|
23
|
+
has_curve = false
|
|
24
|
+
points = []
|
|
25
|
+
|
|
26
|
+
commands.each do |command|
|
|
27
|
+
case command.letter.upcase
|
|
28
|
+
when "M"
|
|
29
|
+
pen.move_to(command, absolute: command.absolute?)
|
|
30
|
+
points << pen.current.dup
|
|
31
|
+
when "L"
|
|
32
|
+
pen.line_to(command, absolute: command.absolute?)
|
|
33
|
+
points << pen.current.dup
|
|
34
|
+
when "H"
|
|
35
|
+
pen.h_line_to(command, absolute: command.absolute?)
|
|
36
|
+
points << pen.current.dup
|
|
37
|
+
when "V"
|
|
38
|
+
pen.v_line_to(command, absolute: command.absolute?)
|
|
39
|
+
points << pen.current.dup
|
|
40
|
+
when "C"
|
|
41
|
+
has_curve = true
|
|
42
|
+
pen.cubic_to(command, absolute: command.absolute?)
|
|
43
|
+
points.concat(pen.flush_curve_points)
|
|
44
|
+
when "Z"
|
|
45
|
+
closed = true
|
|
46
|
+
end
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
Result.new(
|
|
50
|
+
shape_kind: shape_kind(closed, has_curve),
|
|
51
|
+
points: points,
|
|
52
|
+
bounds: bounds_for(points)
|
|
53
|
+
)
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
private
|
|
57
|
+
|
|
58
|
+
def shape_kind(closed, has_curve)
|
|
59
|
+
return :polybezier if has_curve
|
|
60
|
+
|
|
61
|
+
closed ? :polygon : :polyline
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
# Bounds holds raw decimal values (NOT pre-scaled to int). The
|
|
65
|
+
# caller routes them through Context#rect_l so the scaler
|
|
66
|
+
# decides the final int representation.
|
|
67
|
+
Bounds = Struct.new(:left, :top, :right, :bottom, keyword_init: true)
|
|
68
|
+
|
|
69
|
+
def bounds_for(points)
|
|
70
|
+
return Bounds.new(left: 0, top: 0, right: 0, bottom: 0) if points.empty?
|
|
71
|
+
|
|
72
|
+
xs = points.map { |x, _| x }
|
|
73
|
+
ys = points.map { |_, y| y }
|
|
74
|
+
Bounds.new(left: xs.min, top: ys.min, right: xs.max, bottom: ys.max)
|
|
75
|
+
end
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
# Tracks current position and pending bezier control points while
|
|
79
|
+
# flattening a path. Single-use per flatten() call.
|
|
80
|
+
class PenState
|
|
81
|
+
attr_reader :current, :pending_curve_points
|
|
82
|
+
|
|
83
|
+
def initialize
|
|
84
|
+
@current = [0.0, 0.0]
|
|
85
|
+
@pending_curve_points = []
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
def move_to(command, absolute:)
|
|
89
|
+
x, y = command.args
|
|
90
|
+
@current = absolute ? [x, y] : [@current[0] + x, @current[1] + y]
|
|
91
|
+
end
|
|
92
|
+
|
|
93
|
+
def line_to(command, absolute:)
|
|
94
|
+
x, y = command.args
|
|
95
|
+
@current = absolute ? [x, y] : [@current[0] + x, @current[1] + y]
|
|
96
|
+
end
|
|
97
|
+
|
|
98
|
+
def h_line_to(command, absolute:)
|
|
99
|
+
x, = command.args
|
|
100
|
+
@current = if absolute
|
|
101
|
+
[x, @current[1]]
|
|
102
|
+
else
|
|
103
|
+
[@current[0] + x, @current[1]]
|
|
104
|
+
end
|
|
105
|
+
end
|
|
106
|
+
|
|
107
|
+
def v_line_to(command, absolute:)
|
|
108
|
+
y, = command.args
|
|
109
|
+
@current = if absolute
|
|
110
|
+
[@current[0], y]
|
|
111
|
+
else
|
|
112
|
+
[@current[0], @current[1] + y]
|
|
113
|
+
end
|
|
114
|
+
end
|
|
115
|
+
|
|
116
|
+
def cubic_to(command, absolute:)
|
|
117
|
+
x1, y1, x2, y2, x, y = command.args
|
|
118
|
+
if absolute
|
|
119
|
+
@pending_curve_points = [[x1, y1], [x2, y2], [x, y]]
|
|
120
|
+
@current = [x, y]
|
|
121
|
+
else
|
|
122
|
+
@pending_curve_points = [
|
|
123
|
+
[@current[0] + x1, @current[1] + y1],
|
|
124
|
+
[@current[0] + x2, @current[1] + y2],
|
|
125
|
+
[@current[0] + x, @current[1] + y]
|
|
126
|
+
]
|
|
127
|
+
@current = @current.zip([x, y]).map { |a, b| a + b }
|
|
128
|
+
end
|
|
129
|
+
end
|
|
130
|
+
|
|
131
|
+
def flush_curve_points
|
|
132
|
+
pts = @pending_curve_points
|
|
133
|
+
@pending_curve_points = []
|
|
134
|
+
pts
|
|
135
|
+
end
|
|
136
|
+
end
|
|
137
|
+
end
|
|
138
|
+
end
|
|
139
|
+
end
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "emf"
|
|
4
|
+
|
|
5
|
+
module Emfsvg
|
|
6
|
+
module Translation
|
|
7
|
+
module Handlers
|
|
8
|
+
# Translate <path d="..."> to EMF drawing records.
|
|
9
|
+
#
|
|
10
|
+
# v1 strategy: flatten to absolute, fully-resolved points. Bezier
|
|
11
|
+
# control points are preserved as cubic Beziers; arcs/quads are
|
|
12
|
+
# approximated (TODO). Emits a single PolyBezier record when
|
|
13
|
+
# cubics are present, else Polyline / Polygon.
|
|
14
|
+
class PathHandler
|
|
15
|
+
include SharedGdi
|
|
16
|
+
include Emf::Emr::Binary::TypeCodes
|
|
17
|
+
|
|
18
|
+
def self.call(element, context)
|
|
19
|
+
new.translate(element, context)
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
def translate(element, context)
|
|
23
|
+
return if element.commands.empty?
|
|
24
|
+
|
|
25
|
+
with_gdi_state(element, context) do
|
|
26
|
+
flattened = PathFlattener.flatten(element.commands)
|
|
27
|
+
emit_path(flattened, element, context)
|
|
28
|
+
end
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
private
|
|
32
|
+
|
|
33
|
+
def emit_path(flattened, _element, context)
|
|
34
|
+
case flattened.shape_kind
|
|
35
|
+
when :polygon then emit_polygon(flattened, context)
|
|
36
|
+
when :polyline then emit_polyline(flattened, context)
|
|
37
|
+
when :polybezier then emit_polybezier(flattened, context)
|
|
38
|
+
end
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
def emit_polygon(flattened, context)
|
|
42
|
+
emit_poly(Emf::Emr::Binary::Records::Polygon, POLYGON, flattened, context)
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
def emit_polyline(flattened, context)
|
|
46
|
+
emit_poly(Emf::Emr::Binary::Records::Polyline, POLYLINE, flattened, context)
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
def emit_polybezier(flattened, context)
|
|
50
|
+
# emfsvg's EMF→SVG renderer prepends `M cur_x,cur_y` before
|
|
51
|
+
# every PolyBezier's `M point[0] C ...`. To make round-trip
|
|
52
|
+
# byte-equal when cur matches point[0], emit a MoveToEx to
|
|
53
|
+
# set the current position to the PolyBezier's first point.
|
|
54
|
+
first = flattened.points.first
|
|
55
|
+
if first
|
|
56
|
+
context.emit(Emf::Emr::Binary::Records::MoveToEx,
|
|
57
|
+
i_type: MOVETOEX,
|
|
58
|
+
origin: context.point_l(first[0], first[1]))
|
|
59
|
+
end
|
|
60
|
+
emit_poly(Emf::Emr::Binary::Records::PolyBezier, POLYBEZIER, flattened, context)
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
def emit_poly(record_class, i_type, flattened, context)
|
|
64
|
+
points = flattened.points.map { |x, y| context.point_l(x, y) }
|
|
65
|
+
scaled_bounds = context.rect_l(
|
|
66
|
+
flattened.bounds.left, flattened.bounds.top,
|
|
67
|
+
flattened.bounds.right, flattened.bounds.bottom
|
|
68
|
+
)
|
|
69
|
+
context.emit(record_class,
|
|
70
|
+
i_type: i_type,
|
|
71
|
+
rcl_bounds: scaled_bounds,
|
|
72
|
+
cptl: points.size,
|
|
73
|
+
aptl: points)
|
|
74
|
+
end
|
|
75
|
+
end
|
|
76
|
+
end
|
|
77
|
+
end
|
|
78
|
+
end
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "emf"
|
|
4
|
+
|
|
5
|
+
module Emfsvg
|
|
6
|
+
module Translation
|
|
7
|
+
module Handlers
|
|
8
|
+
# <polygon> uses the same body as <polyline> but emits POLYGON
|
|
9
|
+
# (closed). Reuses PolylineHandler internals.
|
|
10
|
+
class PolygonHandler < PolylineHandler
|
|
11
|
+
def self.call(element, context)
|
|
12
|
+
new.translate(element, context)
|
|
13
|
+
end
|
|
14
|
+
end
|
|
15
|
+
end
|
|
16
|
+
end
|
|
17
|
+
end
|