prawn-svg 0.39.0 → 0.40.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/Gemfile.lock +1 -1
- data/README.md +55 -35
- data/lib/prawn/svg/attributes/clip_path.rb +32 -2
- data/lib/prawn/svg/attributes/stroke.rb +11 -1
- data/lib/prawn/svg/calculators/arc_to_bezier_curve.rb +15 -74
- data/lib/prawn/svg/calculators/path_length.rb +168 -0
- data/lib/prawn/svg/calculators/pixels.rb +15 -0
- data/lib/prawn/svg/css/font_face_parser.rb +72 -0
- data/lib/prawn/svg/css/stylesheets.rb +91 -4
- data/lib/prawn/svg/document.rb +90 -2
- data/lib/prawn/svg/elements/base.rb +9 -0
- data/lib/prawn/svg/elements/bbox_scaling.rb +78 -0
- data/lib/prawn/svg/elements/clip_path.rb +17 -2
- data/lib/prawn/svg/elements/container.rb +0 -7
- data/lib/prawn/svg/elements/gradient.rb +0 -19
- data/lib/prawn/svg/elements/image.rb +65 -2
- data/lib/prawn/svg/elements/marker.rb +1 -2
- data/lib/prawn/svg/elements/mask.rb +1 -89
- data/lib/prawn/svg/elements/path.rb +9 -16
- data/lib/prawn/svg/elements/pattern.rb +150 -0
- data/lib/prawn/svg/elements/rect.rb +40 -9
- data/lib/prawn/svg/elements/switch.rb +92 -0
- data/lib/prawn/svg/elements/text.rb +25 -1
- data/lib/prawn/svg/elements/text_component.rb +105 -8
- data/lib/prawn/svg/elements/text_node.rb +60 -17
- data/lib/prawn/svg/elements/text_path.rb +153 -0
- data/lib/prawn/svg/elements/use.rb +97 -19
- data/lib/prawn/svg/elements.rb +7 -4
- data/lib/prawn/svg/font.rb +8 -9
- data/lib/prawn/svg/font_metrics.rb +46 -2
- data/lib/prawn/svg/font_registry.rb +201 -26
- data/lib/prawn/svg/gradients.rb +5 -2
- data/lib/prawn/svg/interface.rb +4 -1
- data/lib/prawn/svg/pathable.rb +8 -5
- data/lib/prawn/svg/pattern_renderer.rb +112 -0
- data/lib/prawn/svg/properties.rb +125 -33
- data/lib/prawn/svg/renderer.rb +18 -6
- data/lib/prawn/svg/ttc.rb +35 -0
- data/lib/prawn/svg/ttf.rb +77 -47
- data/lib/prawn/svg/version.rb +1 -1
- data/lib/prawn-svg.rb +4 -0
- data/spec/integration_spec.rb +16 -16
- data/spec/prawn/svg/attributes/clip_path_spec.rb +246 -0
- data/spec/prawn/svg/attributes/stroke_spec.rb +126 -0
- data/spec/prawn/svg/calculators/path_length_spec.rb +141 -0
- data/spec/prawn/svg/css/font_face_parser_spec.rb +72 -0
- data/spec/prawn/svg/css/selector_parser_spec.rb +6 -0
- data/spec/prawn/svg/css/stylesheets_spec.rb +369 -0
- data/spec/prawn/svg/document_spec.rb +199 -2
- data/spec/prawn/svg/elements/base_spec.rb +2 -2
- data/spec/prawn/svg/elements/gradient_spec.rb +1 -1
- data/spec/prawn/svg/elements/image_spec.rb +169 -0
- data/spec/prawn/svg/elements/line_spec.rb +1 -1
- data/spec/prawn/svg/elements/marker_spec.rb +2 -1
- data/spec/prawn/svg/elements/mask_spec.rb +1 -1
- data/spec/prawn/svg/elements/path_spec.rb +2 -2
- data/spec/prawn/svg/elements/pattern_spec.rb +232 -0
- data/spec/prawn/svg/elements/polygon_spec.rb +1 -1
- data/spec/prawn/svg/elements/polyline_spec.rb +1 -1
- data/spec/prawn/svg/elements/switch_spec.rb +310 -0
- data/spec/prawn/svg/elements/text_path_spec.rb +183 -0
- data/spec/prawn/svg/elements/text_spec.rb +512 -12
- data/spec/prawn/svg/elements/use_spec.rb +274 -0
- data/spec/prawn/svg/font_registry_spec.rb +223 -20
- data/spec/prawn/svg/font_spec.rb +37 -0
- data/spec/prawn/svg/interface_spec.rb +32 -12
- data/spec/prawn/svg/pathable_spec.rb +2 -2
- data/spec/prawn/svg/properties_spec.rb +198 -0
- data/spec/prawn/svg/ttc_spec.rb +31 -0
- data/spec/sample_css/import_base.css +4 -0
- data/spec/sample_css/import_nested.css +1 -0
- data/spec/sample_svg/alignment_baseline.svg +61 -0
- data/spec/sample_svg/arc_test_a.svg +186 -0
- data/spec/sample_svg/arc_test_b.svg +194 -0
- data/spec/sample_svg/baseline_shift.svg +55 -0
- data/spec/sample_svg/clip_path_text.svg +104 -0
- data/spec/sample_svg/clip_path_units.svg +102 -0
- data/spec/sample_svg/css_import.svg +38 -0
- data/spec/sample_svg/dominant_baseline.svg +41 -0
- data/spec/sample_svg/font_face.svg +77 -0
- data/spec/sample_svg/font_stretch.svg +54 -0
- data/spec/sample_svg/font_weights.svg +140 -0
- data/spec/sample_svg/kerning.svg +45 -0
- data/spec/sample_svg/lang.svg +117 -0
- data/spec/sample_svg/marker_shorthand.svg +48 -0
- data/spec/sample_svg/pattern.svg +80 -0
- data/spec/sample_svg/rounded_rectangle.svg +101 -0
- data/spec/sample_svg/stroke_dashoffset.svg +48 -0
- data/spec/sample_svg/stroke_miterlimit.svg +48 -0
- data/spec/sample_svg/switch.svg +106 -0
- data/spec/sample_svg/text-decoration.svg +5 -3
- data/spec/sample_svg/text_path.svg +25 -0
- data/spec/sample_svg/use_external.svg +51 -0
- data/spec/sample_svg/use_external_icons.svg +29 -0
- data/spec/sample_svg/vertical-text.svg +152 -0
- data/spec/sample_svg/view.svg +48 -0
- data/spec/sample_svg/word_spacing.svg +45 -0
- data/spec/sample_ttf/TestFamily.ttc +0 -0
- metadata +48 -2
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: d49edc3094a7c32e0a28d5652485f9f41760559041b5dbf75fd2deebe0861059
|
|
4
|
+
data.tar.gz: 74ac334d02477ae3cb00173b4dd04b5fde9a614cc17a57bfaf102d5e81b756e0
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 8d65777391564f1e2515b3f695d97b9d517a368c5b0e2d9af62c47866da82174f0ba25146ab29251af90175c95d0400443d5a2e1171bddebe113eeba9b75df7e
|
|
7
|
+
data.tar.gz: 890c36c783060f51f1ff6e80e7455b314b2a3acb948a427e985bfd29825de5f0b6bdcddd646e29f699180f85a85f05656759e107a8d1ff3ce3b075f098085aa0
|
data/Gemfile.lock
CHANGED
data/README.md
CHANGED
|
@@ -1,16 +1,14 @@
|
|
|
1
1
|
# prawn-svg
|
|
2
2
|
|
|
3
3
|
[](https://badge.fury.io/rb/prawn-svg)
|
|
4
|
-

|
|
5
5
|
|
|
6
|
-
An SVG renderer for the Prawn PDF library.
|
|
6
|
+
An SVG renderer for the [Prawn PDF library](https://github.com/prawnpdf/prawn).
|
|
7
7
|
|
|
8
|
-
This will take an SVG document as input and render it into your PDF
|
|
8
|
+
This will take an SVG document as input and render it into your PDF, along with whatever else you build with Prawn.
|
|
9
9
|
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
prawn-svg is compatible with all versions of Prawn from 0.11.1 onwards, including the 1.x and 2.x series.
|
|
13
|
-
The minimum Ruby version required is 2.7.
|
|
10
|
+
prawn-svg is compatible with all versions of Prawn from 0.11.1 onwards, including the 1.x and 2.x series, although
|
|
11
|
+
you'll need version 2.2.0 onwards if you want color gradients. The minimum Ruby version required is 2.7.
|
|
14
12
|
|
|
15
13
|
## Using prawn-svg
|
|
16
14
|
|
|
@@ -30,11 +28,13 @@ Option | Data type | Description
|
|
|
30
28
|
:vposition | :top, :center, :bottom, integer | If :at not specified, specifies the vertical position to show the SVG. Defaults to current cursor position.
|
|
31
29
|
:width | integer | Desired width of the SVG. Defaults to horizontal space available.
|
|
32
30
|
:height | integer | Desired height of the SVG. Defaults to vertical space available.
|
|
33
|
-
:enable_web_requests | boolean | If true, prawn-svg will make http and https requests to fetch images
|
|
34
|
-
:enable_file_requests_with_root | string | If not nil, prawn-svg will serve `file:` URLs from your local disk if the file is located under the specified directory. It is very dangerous to specify the root path ("/") if you're not fully in control of your input SVG. Defaults to `nil` (off).
|
|
31
|
+
:enable_web_requests | boolean | If true, prawn-svg will make http and https requests to fetch images, `@font-face` fonts, and external `<use>` references.<br><br>Defaults to true, but will **default to false** in the upcoming 1.0 release. It's recommended you explicitly set this option for now.
|
|
32
|
+
:enable_file_requests_with_root | string | If not nil, prawn-svg will serve `file:` URLs and relative paths from your local disk if the file is located under the specified directory. Required for `@font-face` fonts and external `<use>` references loaded via file paths.<br><br>It is very dangerous to specify the root path ("/") if you're not fully in control of your input SVG. Defaults to `nil` (off).
|
|
35
33
|
:cache_images | boolean | If true, prawn-svg will cache the result of all URL requests. Defaults to false.
|
|
36
34
|
:fallback_font_name | string | A font name which will override the default fallback font of Times-Roman. If this value is set to `nil`, prawn-svg will ignore a request for an unknown font and log a warning.
|
|
37
35
|
:color_mode | :rgb, :cmyk | Output color mode. Defaults to :rgb.
|
|
36
|
+
:language | string | BCP 47 language tag for `<switch>` `systemLanguage` matching. Defaults to `"en"`.
|
|
37
|
+
:log_warnings | boolean | If true, warnings that occur when parsing/rendering the SVG are output to stderr via `warn`. Use this when you're not getting the output you expect. Defaults to false.
|
|
38
38
|
|
|
39
39
|
## Examples
|
|
40
40
|
|
|
@@ -47,27 +47,25 @@ Option | Data type | Description
|
|
|
47
47
|
|
|
48
48
|
# Render the logo at the current Y cursor position, and serve file: links relative to its directory
|
|
49
49
|
root_path = "/apps/myapp/current/images"
|
|
50
|
-
svg IO.read("#{root_path}/logo.svg"), enable_file_requests_with_root: root_path
|
|
50
|
+
svg IO.read("#{root_path}/logo.svg"), enable_file_requests_with_root: root_path, log_warnings: true
|
|
51
51
|
```
|
|
52
52
|
|
|
53
53
|
## Supported features
|
|
54
54
|
|
|
55
|
-
prawn-svg supports
|
|
56
|
-
|
|
57
|
-
- `<line>`, `<polyline>`, `<polygon>`, `<circle>` and `<ellipse>`
|
|
55
|
+
prawn-svg supports almost all of the full SVG 1.1 specification:
|
|
58
56
|
|
|
59
|
-
- `<
|
|
57
|
+
- `<line>`, `<polyline>`, `<rect>`, `<polygon>`, `<circle>` and `<ellipse>`
|
|
60
58
|
|
|
61
|
-
- `<path>`
|
|
62
|
-
implementation of elliptical arc is a bit rough at the moment.
|
|
59
|
+
- `<path>`
|
|
63
60
|
|
|
64
|
-
- `<text>`, `<tspan>` and `<
|
|
65
|
-
and with extra properties `text-anchor`, `text-decoration
|
|
66
|
-
`font-weight`, `font-style`, `letter-spacing`, `dominant-baseline`
|
|
61
|
+
- `<text>`, `<tspan>`, `<tref>` and `<textPath>` with attributes `x`, `y`, `dx`, `dy`, `rotate`, `textLength`,
|
|
62
|
+
`lengthAdjust`, and with extra properties `text-anchor`, `text-decoration`, `font`, `font-size`, `font-family`,
|
|
63
|
+
`font-weight`, `font-style`, `font-stretch`, `kerning`, `letter-spacing`, `word-spacing`, `dominant-baseline`, `alignment-baseline`, `baseline-shift`.
|
|
64
|
+
`<textPath>` supports `href`/`xlink:href` and `startOffset`.
|
|
67
65
|
|
|
68
66
|
- `<svg>`, `<g>` and `<symbol>`
|
|
69
67
|
|
|
70
|
-
- `<use>`
|
|
68
|
+
- `<use>` with local and external references (e.g. `other.svg#elementId`), subject to `enable_web_requests` and `enable_file_requests_with_root`
|
|
71
69
|
|
|
72
70
|
- `<style>` (see CSS section below)
|
|
73
71
|
|
|
@@ -75,7 +73,7 @@ prawn-svg supports most but not all of the full SVG 1.1 specification. It curre
|
|
|
75
73
|
`data:image/png;base64`, `data:image/svg+xml;base64` and `file:` schemes (`file:` is disabled by default for
|
|
76
74
|
security reasons, see Options section above)
|
|
77
75
|
|
|
78
|
-
- `<clipPath>`
|
|
76
|
+
- `<clipPath>` with `clipPathUnits` attribute and text content
|
|
79
77
|
|
|
80
78
|
- `<mask>` with attributes `maskUnits` and `maskContentUnits`
|
|
81
79
|
|
|
@@ -84,14 +82,21 @@ prawn-svg supports most but not all of the full SVG 1.1 specification. It curre
|
|
|
84
82
|
- `<linearGradient>` and `<radialGradient>` are implemented on Prawn 2.2.0+ with attributes `gradientUnits` and
|
|
85
83
|
`gradientTransform`
|
|
86
84
|
|
|
87
|
-
- `<
|
|
88
|
-
|
|
85
|
+
- `<pattern>` with attributes `patternUnits`, `patternContentUnits`, `patternTransform`, `viewBox`,
|
|
86
|
+
`preserveAspectRatio`, and `href` inheritance. Patterns can be used for both fill and stroke.
|
|
87
|
+
Nested patterns (a pattern whose content references another pattern) are not supported.
|
|
88
|
+
|
|
89
|
+
- `<switch>` with conditional processing (`requiredFeatures`, `requiredExtensions`, `systemLanguage`).
|
|
90
|
+
`<foreignObject>` tags are always ignored as prawn-svg cannot handle non-SVG data.
|
|
91
|
+
|
|
92
|
+
- `<view>` element for defining named views. Fragment identifiers (`#viewId` and `#svgView(...)`) on
|
|
93
|
+
`<image>` hrefs select which view to render an external SVG with.
|
|
89
94
|
|
|
90
|
-
- properties: `clip-path`, `color`, `display`, `fill`, `fill-opacity`, `fill-rule`, `opacity`, `overflow`,
|
|
91
|
-
`stroke`, `stroke-dasharray`, `stroke-linecap`, `stroke-linejoin`, `stroke-opacity`, `stroke-width`,
|
|
95
|
+
- properties: `clip-path`, `clip-rule`, `color`, `display`, `fill`, `fill-opacity`, `fill-rule`, `opacity`, `overflow`,
|
|
96
|
+
`stroke`, `stroke-dasharray`, `stroke-dashoffset`, `stroke-linecap`, `stroke-linejoin`, `stroke-miterlimit`, `stroke-opacity`, `stroke-width`,
|
|
92
97
|
`visibility`
|
|
93
98
|
|
|
94
|
-
- properties on lines, polylines, polygons and paths: `marker-end`, `marker-mid`, `marker-start`
|
|
99
|
+
- properties on lines, polylines, polygons and paths: `marker`, `marker-end`, `marker-mid`, `marker-start`
|
|
95
100
|
|
|
96
101
|
- attributes on all elements: `class`, `id`, `style`, `transform`, `xml:space`
|
|
97
102
|
|
|
@@ -106,25 +111,40 @@ prawn-svg supports most but not all of the full SVG 1.1 specification. It curre
|
|
|
106
111
|
|
|
107
112
|
- measurements specified in `pt`, `cm`, `dm`, `ft`, `in`, `m`, `mm`, `yd`, `pc`, `%`
|
|
108
113
|
|
|
109
|
-
- fonts: generic CSS fonts, built-in PDF fonts, and any TTF fonts in your fonts path, specified in any of
|
|
110
|
-
measurements above plus `em` or `rem`
|
|
114
|
+
- fonts: generic CSS fonts, built-in PDF fonts, and any TTF or TTC fonts in your fonts path, specified in any of
|
|
115
|
+
the measurements above plus `em` or `rem`
|
|
116
|
+
|
|
117
|
+
- `@font-face` in `<style>` blocks: `url()` sources (TTF/OTF formats) and `local()` references, with `font-weight`,
|
|
118
|
+
`font-style` and `font-stretch` descriptors. Font loading is subject to the `enable_web_requests` and
|
|
119
|
+
`enable_file_requests_with_root` options.
|
|
111
120
|
|
|
112
121
|
## CSS
|
|
113
122
|
|
|
114
|
-
prawn-svg supports
|
|
123
|
+
prawn-svg fully supports CSS2, both in `<style>` blocks and `style` attributes.
|
|
124
|
+
|
|
125
|
+
`@import` rules are supported for loading external stylesheets (requires `enable_web_requests` or
|
|
126
|
+
`enable_file_requests_with_root` options enabled).
|
|
115
127
|
|
|
116
128
|
In CSS selectors you can use element names, IDs, classes, attributes (existence, `=`, `^=`, `$=`, `*=`, `~=`, `|=`)
|
|
117
|
-
and all combinators (` `, `>`, `+`, `~`).
|
|
118
|
-
|
|
119
|
-
|
|
129
|
+
and all combinators (` `, `>`, `+`, `~`). The pseudo-classes `:first-child`, `:last-child`, `:link`, `:nth-child()`
|
|
130
|
+
and `:lang()` also work. `:visited`, `:hover`, `:active`, and `:focus` are never matched. `!important` is
|
|
131
|
+
supported.
|
|
120
132
|
|
|
121
|
-
|
|
133
|
+
We made the call to ignore `@media` because it's not clear whether you're making a PDF to print it or show it on
|
|
134
|
+
the screen.
|
|
122
135
|
|
|
123
136
|
## Not supported
|
|
124
137
|
|
|
125
|
-
prawn-svg does not support
|
|
138
|
+
prawn-svg does not support filters, as rasterised effects is not something the PDF format was designed to handle.
|
|
139
|
+
|
|
140
|
+
writing-mode is partially supported: vertical-rl and vertical-lr rotate the text 90 degrees, which
|
|
141
|
+
handles the common case of sideways Latin text. CJK upright glyph orientation is not supported.
|
|
142
|
+
|
|
143
|
+
direction and unicode-bidi are not supported.
|
|
126
144
|
|
|
127
|
-
|
|
145
|
+
Features that will probably never be supported because either they don't make sense for PDF, they were deprecated
|
|
146
|
+
in SVG 2, and/or they are rarely used: filters, SVG fonts, altGlyph, font-size-adjust, glyph-orientation, rendering
|
|
147
|
+
hints, ICC color profiles, color-interpolation, enable-background, deprecated CSS clip, @media.
|
|
128
148
|
|
|
129
149
|
## Configuration
|
|
130
150
|
|
|
@@ -8,9 +8,39 @@ module Prawn::SVG::Attributes::ClipPath
|
|
|
8
8
|
if clip_path_element.nil?
|
|
9
9
|
document.warnings << 'Could not resolve clip-path URI to a clipPath element'
|
|
10
10
|
else
|
|
11
|
+
clip_calls = clip_path_element.build_clip_calls(self)
|
|
12
|
+
return if clip_calls.nil?
|
|
13
|
+
|
|
11
14
|
add_call_and_enter 'save_graphics_state'
|
|
12
|
-
|
|
13
|
-
|
|
15
|
+
|
|
16
|
+
if clip_path_contains_text?(clip_calls)
|
|
17
|
+
# Text in clip paths is implemented using a PDF soft mask. The text
|
|
18
|
+
# renders in white fill inside the mask to define the visible region.
|
|
19
|
+
# This correctly unions multiple text elements, unlike PDF text
|
|
20
|
+
# rendering mode 7 which intersects separate text objects.
|
|
21
|
+
@calls << ['soft_mask', [], {}, clip_calls]
|
|
22
|
+
else
|
|
23
|
+
@calls.concat clip_calls
|
|
24
|
+
|
|
25
|
+
# SVG's clip-rule applies per-element (determining each shape's interior),
|
|
26
|
+
# then elements are unioned. PDF's W* applies even-odd to the entire combined
|
|
27
|
+
# path, which incorrectly creates holes between separate shapes. Only use W*
|
|
28
|
+
# when there's a single child element, where self-intersection matters.
|
|
29
|
+
child_elements = clip_path_element.svg_child_elements
|
|
30
|
+
if child_elements.length == 1 && clip_path_element.computed_properties.clip_rule == 'evenodd'
|
|
31
|
+
add_call 'clip', clip_rule: :even_odd
|
|
32
|
+
else
|
|
33
|
+
add_call 'clip'
|
|
34
|
+
end
|
|
35
|
+
end
|
|
36
|
+
end
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
private
|
|
40
|
+
|
|
41
|
+
def clip_path_contains_text?(calls)
|
|
42
|
+
calls.any? do |call, _arguments, _kwarguments, children|
|
|
43
|
+
call == 'svg:render' || clip_path_contains_text?(children)
|
|
14
44
|
end
|
|
15
45
|
end
|
|
16
46
|
end
|
|
@@ -17,6 +17,10 @@ module Prawn::SVG::Attributes::Stroke
|
|
|
17
17
|
add_call('join_style', JOIN_STYLE_TRANSLATIONS.fetch(linejoin, :miter))
|
|
18
18
|
end
|
|
19
19
|
|
|
20
|
+
if (miterlimit = properties.stroke_miterlimit) && miterlimit != 'inherit'
|
|
21
|
+
add_call('miter_limit', miterlimit)
|
|
22
|
+
end
|
|
23
|
+
|
|
20
24
|
if (dasharray = properties.stroke_dasharray)
|
|
21
25
|
case dasharray
|
|
22
26
|
when 'inherit'
|
|
@@ -30,7 +34,13 @@ module Prawn::SVG::Attributes::Stroke
|
|
|
30
34
|
if values.inject(0) { |a, b| a + b }.zero?
|
|
31
35
|
add_call('undash')
|
|
32
36
|
else
|
|
33
|
-
|
|
37
|
+
options = {}
|
|
38
|
+
offset = computed_properties.stroke_dashoffset
|
|
39
|
+
if offset && offset != 0 && offset != 'inherit'
|
|
40
|
+
phase = pixels(offset)
|
|
41
|
+
options[:phase] = phase unless phase.zero?
|
|
42
|
+
end
|
|
43
|
+
add_call('dash', values, **options)
|
|
34
44
|
end
|
|
35
45
|
else
|
|
36
46
|
raise "Unknown dasharray value: #{dasharray.inspect}"
|
|
@@ -4,7 +4,10 @@ module Prawn::SVG::Calculators
|
|
|
4
4
|
|
|
5
5
|
# Convert the elliptical arc to a cubic bézier curve using this algorithm:
|
|
6
6
|
# http://www.spaceroots.org/documents/ellipse/elliptical-arc.pdf
|
|
7
|
-
|
|
7
|
+
#
|
|
8
|
+
# eta_1 and eta_2 are the eccentric anomaly angles (the parametric angles on the
|
|
9
|
+
# unit circle that map to points on the ellipse). theta is the ellipse rotation angle.
|
|
10
|
+
def calculate_bezier_curve_points_for_arc(cx, cy, a, b, eta_1, eta_2, theta)
|
|
8
11
|
e = lambda do |eta|
|
|
9
12
|
[
|
|
10
13
|
cx + (a * Math.cos(theta) * Math.cos(eta)) - (b * Math.sin(theta) * Math.sin(eta)),
|
|
@@ -20,97 +23,35 @@ module Prawn::SVG::Calculators
|
|
|
20
23
|
end
|
|
21
24
|
|
|
22
25
|
iterations = 1
|
|
23
|
-
|
|
26
|
+
d_eta = eta_2 - eta_1
|
|
24
27
|
|
|
25
28
|
while iterations < 1024
|
|
26
|
-
if
|
|
27
|
-
# TODO : run error algorithm, see whether it meets threshold or not
|
|
28
|
-
# puts "error = #{calculate_curve_approximation_error(a, b, eta1, eta1 + d_eta)}"
|
|
29
|
-
break
|
|
30
|
-
end
|
|
29
|
+
break if d_eta.abs <= Math::PI / 2.0
|
|
31
30
|
|
|
32
31
|
iterations *= 2
|
|
33
|
-
|
|
32
|
+
d_eta = (eta_2 - eta_1) / iterations
|
|
34
33
|
end
|
|
35
34
|
|
|
36
35
|
(0...iterations).collect do |iteration|
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
36
|
+
seg_eta_a = eta_1 + (iteration * d_eta)
|
|
37
|
+
seg_eta_b = eta_1 + ((iteration + 1) * d_eta)
|
|
38
|
+
seg_d_eta = seg_eta_b - seg_eta_a
|
|
40
39
|
|
|
41
|
-
alpha = Math.sin(
|
|
40
|
+
alpha = Math.sin(seg_d_eta) * ((Math.sqrt(4 + (3 * (Math.tan(seg_d_eta / 2)**2))) - 1) / 3)
|
|
42
41
|
|
|
43
|
-
x1, y1 = e[
|
|
44
|
-
x2, y2 = e[
|
|
42
|
+
x1, y1 = e[seg_eta_a]
|
|
43
|
+
x2, y2 = e[seg_eta_b]
|
|
45
44
|
|
|
46
|
-
ep_eta1_x, ep_eta1_y = ep[
|
|
45
|
+
ep_eta1_x, ep_eta1_y = ep[seg_eta_a]
|
|
47
46
|
q1_x = x1 + (alpha * ep_eta1_x)
|
|
48
47
|
q1_y = y1 + (alpha * ep_eta1_y)
|
|
49
48
|
|
|
50
|
-
ep_eta2_x, ep_eta2_y = ep[
|
|
49
|
+
ep_eta2_x, ep_eta2_y = ep[seg_eta_b]
|
|
51
50
|
q2_x = x2 - (alpha * ep_eta2_x)
|
|
52
51
|
q2_y = y2 - (alpha * ep_eta2_y)
|
|
53
52
|
|
|
54
53
|
{ p2: [x2, y2], q1: [q1_x, q1_y], q2: [q2_x, q2_y] }
|
|
55
54
|
end
|
|
56
55
|
end
|
|
57
|
-
|
|
58
|
-
private
|
|
59
|
-
|
|
60
|
-
ERROR_COEFFICIENTS_A = [
|
|
61
|
-
[
|
|
62
|
-
[3.85268, -21.229, -0.330434, 0.0127842],
|
|
63
|
-
[-1.61486, 0.706564, 0.225945, 0.263682],
|
|
64
|
-
[-0.910164, 0.388383, 0.00551445, 0.00671814],
|
|
65
|
-
[-0.630184, 0.192402, 0.0098871, 0.0102527]
|
|
66
|
-
],
|
|
67
|
-
[
|
|
68
|
-
[-0.162211, 9.94329, 0.13723, 0.0124084],
|
|
69
|
-
[-0.253135, 0.00187735, 0.0230286, 0.01264],
|
|
70
|
-
[-0.0695069, -0.0437594, 0.0120636, 0.0163087],
|
|
71
|
-
[-0.0328856, -0.00926032, -0.00173573, 0.00527385]
|
|
72
|
-
]
|
|
73
|
-
].freeze
|
|
74
|
-
|
|
75
|
-
ERROR_COEFFICIENTS_B = [
|
|
76
|
-
[
|
|
77
|
-
[0.0899116, -19.2349, -4.11711, 0.183362],
|
|
78
|
-
[0.138148, -1.45804, 1.32044, 1.38474],
|
|
79
|
-
[0.230903, -0.450262, 0.219963, 0.414038],
|
|
80
|
-
[0.0590565, -0.101062, 0.0430592, 0.0204699]
|
|
81
|
-
],
|
|
82
|
-
[
|
|
83
|
-
[0.0164649, 9.89394, 0.0919496, 0.00760802],
|
|
84
|
-
[0.0191603, -0.0322058, 0.0134667, -0.0825018],
|
|
85
|
-
[0.0156192, -0.017535, 0.00326508, -0.228157],
|
|
86
|
-
[-0.0236752, 0.0405821, -0.0173086, 0.176187]
|
|
87
|
-
]
|
|
88
|
-
].freeze
|
|
89
|
-
|
|
90
|
-
def calculate_curve_approximation_error(a, b, eta1, eta2)
|
|
91
|
-
b_over_a = b / a
|
|
92
|
-
coefficents = b_over_a < 0.25 ? ERROR_COEFFICIENTS_A : ERROR_COEFFICIENTS_B
|
|
93
|
-
|
|
94
|
-
c = lambda do |i|
|
|
95
|
-
(0..3).inject(0) do |accumulator, j|
|
|
96
|
-
coef = coefficents[i][j]
|
|
97
|
-
accumulator + ((((coef[0] * (b_over_a**2)) + (coef[1] * b_over_a) + coef[2]) / (b_over_a * coef[3])) * Math.cos(j * (eta1 + eta2)))
|
|
98
|
-
end
|
|
99
|
-
end
|
|
100
|
-
|
|
101
|
-
(((0.001 * (b_over_a**2)) + (4.98 * b_over_a) + 0.207) / (b_over_a * 0.0067)) * a * Math.exp(c[0] + (c[1] * (eta2 - eta1)))
|
|
102
|
-
end
|
|
103
|
-
|
|
104
|
-
def calculate_eta_from_lambda(a, b, lambda_1, lambda_2)
|
|
105
|
-
# 2.2.1
|
|
106
|
-
eta1 = Math.atan2(Math.sin(lambda_1) / b, Math.cos(lambda_1) / a)
|
|
107
|
-
eta2 = Math.atan2(Math.sin(lambda_2) / b, Math.cos(lambda_2) / a)
|
|
108
|
-
|
|
109
|
-
# ensure eta1 <= eta2 <= eta1 + 2*PI
|
|
110
|
-
eta2 -= 2 * Math::PI * ((eta2 - eta1) / (2 * Math::PI)).floor
|
|
111
|
-
eta2 += 2 * Math::PI if lambda_2 - lambda_1 > Math::PI && eta2 - eta1 < Math::PI
|
|
112
|
-
|
|
113
|
-
[eta1, eta2]
|
|
114
|
-
end
|
|
115
56
|
end
|
|
116
57
|
end
|
|
@@ -0,0 +1,168 @@
|
|
|
1
|
+
module Prawn::SVG::Calculators
|
|
2
|
+
class PathLength
|
|
3
|
+
Segment = Struct.new(:start_point, :end_point, :type, :control_points, :segment_length, :cumulative_length, :lookup_table)
|
|
4
|
+
|
|
5
|
+
SUBDIVISION_TOLERANCE = 0.01
|
|
6
|
+
LOOKUP_TABLE_STEPS = 64
|
|
7
|
+
|
|
8
|
+
attr_reader :total_length
|
|
9
|
+
|
|
10
|
+
def initialize(commands)
|
|
11
|
+
@segments = []
|
|
12
|
+
@total_length = 0.0
|
|
13
|
+
|
|
14
|
+
current_point = nil
|
|
15
|
+
subpath_start = nil
|
|
16
|
+
|
|
17
|
+
commands.each do |command|
|
|
18
|
+
case command
|
|
19
|
+
when Prawn::SVG::Pathable::Move
|
|
20
|
+
current_point = command.destination
|
|
21
|
+
subpath_start = current_point
|
|
22
|
+
|
|
23
|
+
when Prawn::SVG::Pathable::Close
|
|
24
|
+
if current_point && subpath_start && current_point != subpath_start
|
|
25
|
+
add_line_segment(current_point, subpath_start)
|
|
26
|
+
current_point = subpath_start
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
when Prawn::SVG::Pathable::Line
|
|
30
|
+
if current_point
|
|
31
|
+
add_line_segment(current_point, command.destination)
|
|
32
|
+
current_point = command.destination
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
when Prawn::SVG::Pathable::Curve
|
|
36
|
+
if current_point
|
|
37
|
+
add_curve_segment(current_point, command.point1, command.point2, command.destination)
|
|
38
|
+
current_point = command.destination
|
|
39
|
+
end
|
|
40
|
+
end
|
|
41
|
+
end
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
def point_at(distance)
|
|
45
|
+
return nil if distance.negative? || distance > @total_length || @segments.empty?
|
|
46
|
+
|
|
47
|
+
@segments.each do |segment|
|
|
48
|
+
start_distance = segment.cumulative_length - segment.segment_length
|
|
49
|
+
|
|
50
|
+
next unless distance <= segment.cumulative_length
|
|
51
|
+
|
|
52
|
+
local_distance = distance - start_distance
|
|
53
|
+
|
|
54
|
+
case segment.type
|
|
55
|
+
when :line
|
|
56
|
+
t = segment.segment_length.positive? ? local_distance / segment.segment_length : 0.0
|
|
57
|
+
x = segment.start_point[0] + (t * (segment.end_point[0] - segment.start_point[0]))
|
|
58
|
+
y = segment.start_point[1] + (t * (segment.end_point[1] - segment.start_point[1]))
|
|
59
|
+
dx = segment.end_point[0] - segment.start_point[0]
|
|
60
|
+
dy = segment.end_point[1] - segment.start_point[1]
|
|
61
|
+
angle = Math.atan2(dy, dx) * 180.0 / Math::PI
|
|
62
|
+
return [x, y, angle]
|
|
63
|
+
|
|
64
|
+
when :curve
|
|
65
|
+
t = find_t_for_distance(segment, local_distance)
|
|
66
|
+
p0, p1, p2, p3 = segment.start_point, *segment.control_points, segment.end_point
|
|
67
|
+
x, y = evaluate_cubic(p0, p1, p2, p3, t)
|
|
68
|
+
dx, dy = evaluate_cubic_derivative(p0, p1, p2, p3, t)
|
|
69
|
+
angle = Math.atan2(dy, dx) * 180.0 / Math::PI
|
|
70
|
+
return [x, y, angle]
|
|
71
|
+
end
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
# Exactly at the end
|
|
75
|
+
segment = @segments.last
|
|
76
|
+
[segment.end_point[0], segment.end_point[1], end_angle(segment)]
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
private
|
|
80
|
+
|
|
81
|
+
def add_line_segment(start_point, end_point)
|
|
82
|
+
length = Math.sqrt(((end_point[0] - start_point[0])**2) + ((end_point[1] - start_point[1])**2))
|
|
83
|
+
@total_length += length
|
|
84
|
+
@segments << Segment.new(start_point, end_point, :line, nil, length, @total_length, nil)
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
def add_curve_segment(start_point, control1, control2, end_point)
|
|
88
|
+
lookup_table = build_lookup_table(start_point, control1, control2, end_point)
|
|
89
|
+
length = lookup_table.last
|
|
90
|
+
@total_length += length
|
|
91
|
+
@segments << Segment.new(start_point, end_point, :curve, [control1, control2], length, @total_length, lookup_table)
|
|
92
|
+
end
|
|
93
|
+
|
|
94
|
+
def build_lookup_table(p0, p1, p2, p3)
|
|
95
|
+
table = [0.0]
|
|
96
|
+
prev_point = p0
|
|
97
|
+
cumulative = 0.0
|
|
98
|
+
|
|
99
|
+
1.upto(LOOKUP_TABLE_STEPS) do |i|
|
|
100
|
+
t = i.to_f / LOOKUP_TABLE_STEPS
|
|
101
|
+
point = evaluate_cubic(p0, p1, p2, p3, t)
|
|
102
|
+
cumulative += Math.sqrt(((point[0] - prev_point[0])**2) + ((point[1] - prev_point[1])**2))
|
|
103
|
+
table << cumulative
|
|
104
|
+
prev_point = point
|
|
105
|
+
end
|
|
106
|
+
|
|
107
|
+
table
|
|
108
|
+
end
|
|
109
|
+
|
|
110
|
+
def find_t_for_distance(segment, target_distance)
|
|
111
|
+
table = segment.lookup_table
|
|
112
|
+
return 0.0 if target_distance <= 0
|
|
113
|
+
return 1.0 if target_distance >= segment.segment_length
|
|
114
|
+
|
|
115
|
+
# Binary search in the lookup table
|
|
116
|
+
low = 0
|
|
117
|
+
high = LOOKUP_TABLE_STEPS
|
|
118
|
+
|
|
119
|
+
while low < high - 1
|
|
120
|
+
mid = (low + high) / 2
|
|
121
|
+
if table[mid] < target_distance
|
|
122
|
+
low = mid
|
|
123
|
+
else
|
|
124
|
+
high = mid
|
|
125
|
+
end
|
|
126
|
+
end
|
|
127
|
+
|
|
128
|
+
# Linear interpolation between low and high
|
|
129
|
+
d_low = table[low]
|
|
130
|
+
d_high = table[high]
|
|
131
|
+
fraction = d_high > d_low ? (target_distance - d_low) / (d_high - d_low) : 0.0
|
|
132
|
+
(low + fraction) / LOOKUP_TABLE_STEPS
|
|
133
|
+
end
|
|
134
|
+
|
|
135
|
+
def evaluate_cubic(p0, p1, p2, p3, t)
|
|
136
|
+
mt = 1.0 - t
|
|
137
|
+
mt2 = mt * mt
|
|
138
|
+
mt3 = mt2 * mt
|
|
139
|
+
t2 = t * t
|
|
140
|
+
t3 = t2 * t
|
|
141
|
+
|
|
142
|
+
x = (mt3 * p0[0]) + (3 * mt2 * t * p1[0]) + (3 * mt * t2 * p2[0]) + (t3 * p3[0])
|
|
143
|
+
y = (mt3 * p0[1]) + (3 * mt2 * t * p1[1]) + (3 * mt * t2 * p2[1]) + (t3 * p3[1])
|
|
144
|
+
[x, y]
|
|
145
|
+
end
|
|
146
|
+
|
|
147
|
+
def evaluate_cubic_derivative(p0, p1, p2, p3, t)
|
|
148
|
+
mt = 1.0 - t
|
|
149
|
+
|
|
150
|
+
dx = (3 * mt * mt * (p1[0] - p0[0])) + (6 * mt * t * (p2[0] - p1[0])) + (3 * t * t * (p3[0] - p2[0]))
|
|
151
|
+
dy = (3 * mt * mt * (p1[1] - p0[1])) + (6 * mt * t * (p2[1] - p1[1])) + (3 * t * t * (p3[1] - p2[1]))
|
|
152
|
+
[dx, dy]
|
|
153
|
+
end
|
|
154
|
+
|
|
155
|
+
def end_angle(segment)
|
|
156
|
+
case segment.type
|
|
157
|
+
when :line
|
|
158
|
+
dx = segment.end_point[0] - segment.start_point[0]
|
|
159
|
+
dy = segment.end_point[1] - segment.start_point[1]
|
|
160
|
+
Math.atan2(dy, dx) * 180.0 / Math::PI
|
|
161
|
+
when :curve
|
|
162
|
+
p0, p1, p2, p3 = segment.start_point, *segment.control_points, segment.end_point
|
|
163
|
+
dx, dy = evaluate_cubic_derivative(p0, p1, p2, p3, 1.0)
|
|
164
|
+
Math.atan2(dy, dx) * 180.0 / Math::PI
|
|
165
|
+
end
|
|
166
|
+
end
|
|
167
|
+
end
|
|
168
|
+
end
|
|
@@ -39,4 +39,19 @@ module Prawn::SVG::Calculators::Pixels
|
|
|
39
39
|
value && Measurement.to_pixels(value, state.viewport_sizing.viewport_height,
|
|
40
40
|
font_size: computed_properties.numeric_font_size)
|
|
41
41
|
end
|
|
42
|
+
|
|
43
|
+
def percentage_or_proportion(string, default = 0)
|
|
44
|
+
string = string.to_s.strip
|
|
45
|
+
percentage = false
|
|
46
|
+
|
|
47
|
+
if string[-1] == '%'
|
|
48
|
+
percentage = true
|
|
49
|
+
string = string[0..-2]
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
value = Float(string, exception: false)
|
|
53
|
+
return default unless value
|
|
54
|
+
|
|
55
|
+
percentage ? value / 100.0 : value
|
|
56
|
+
end
|
|
42
57
|
end
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
module Prawn::SVG::CSS
|
|
2
|
+
class FontFaceParser
|
|
3
|
+
SUPPORTED_FORMATS = %w[truetype opentype].freeze
|
|
4
|
+
|
|
5
|
+
class << self
|
|
6
|
+
def parse_src(src)
|
|
7
|
+
split_sources(src).filter_map { |entry| parse_source_entry(entry.strip) }
|
|
8
|
+
end
|
|
9
|
+
|
|
10
|
+
private
|
|
11
|
+
|
|
12
|
+
def split_sources(value)
|
|
13
|
+
entries = []
|
|
14
|
+
current = +''
|
|
15
|
+
depth = 0
|
|
16
|
+
in_quote = nil
|
|
17
|
+
|
|
18
|
+
value.each_char do |char|
|
|
19
|
+
if in_quote
|
|
20
|
+
current << char
|
|
21
|
+
in_quote = nil if char == in_quote
|
|
22
|
+
elsif ['"', "'"].include?(char)
|
|
23
|
+
in_quote = char
|
|
24
|
+
current << char
|
|
25
|
+
elsif char == '('
|
|
26
|
+
depth += 1
|
|
27
|
+
current << char
|
|
28
|
+
elsif char == ')'
|
|
29
|
+
depth -= 1
|
|
30
|
+
current << char
|
|
31
|
+
elsif char == ',' && depth.zero?
|
|
32
|
+
entries << current
|
|
33
|
+
current = +''
|
|
34
|
+
else
|
|
35
|
+
current << char
|
|
36
|
+
end
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
entries << current unless current.strip.empty?
|
|
40
|
+
entries
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
def parse_source_entry(entry)
|
|
44
|
+
parts = Prawn::SVG::CSS::ValuesParser.parse(entry)
|
|
45
|
+
return if parts.empty?
|
|
46
|
+
|
|
47
|
+
first = parts[0]
|
|
48
|
+
return unless first.is_a?(Array)
|
|
49
|
+
|
|
50
|
+
case first[0]
|
|
51
|
+
when 'url'
|
|
52
|
+
url = first[1][0]
|
|
53
|
+
return unless url
|
|
54
|
+
|
|
55
|
+
format = extract_format(parts[1])
|
|
56
|
+
{ type: :url, url: url, format: format }
|
|
57
|
+
when 'local'
|
|
58
|
+
name = first[1][0]
|
|
59
|
+
return unless name
|
|
60
|
+
|
|
61
|
+
{ type: :local, name: name }
|
|
62
|
+
end
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
def extract_format(part)
|
|
66
|
+
return unless part.is_a?(Array) && part[0] == 'format'
|
|
67
|
+
|
|
68
|
+
part[1][0]
|
|
69
|
+
end
|
|
70
|
+
end
|
|
71
|
+
end
|
|
72
|
+
end
|