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.
Files changed (100) hide show
  1. checksums.yaml +4 -4
  2. data/Gemfile.lock +1 -1
  3. data/README.md +55 -35
  4. data/lib/prawn/svg/attributes/clip_path.rb +32 -2
  5. data/lib/prawn/svg/attributes/stroke.rb +11 -1
  6. data/lib/prawn/svg/calculators/arc_to_bezier_curve.rb +15 -74
  7. data/lib/prawn/svg/calculators/path_length.rb +168 -0
  8. data/lib/prawn/svg/calculators/pixels.rb +15 -0
  9. data/lib/prawn/svg/css/font_face_parser.rb +72 -0
  10. data/lib/prawn/svg/css/stylesheets.rb +91 -4
  11. data/lib/prawn/svg/document.rb +90 -2
  12. data/lib/prawn/svg/elements/base.rb +9 -0
  13. data/lib/prawn/svg/elements/bbox_scaling.rb +78 -0
  14. data/lib/prawn/svg/elements/clip_path.rb +17 -2
  15. data/lib/prawn/svg/elements/container.rb +0 -7
  16. data/lib/prawn/svg/elements/gradient.rb +0 -19
  17. data/lib/prawn/svg/elements/image.rb +65 -2
  18. data/lib/prawn/svg/elements/marker.rb +1 -2
  19. data/lib/prawn/svg/elements/mask.rb +1 -89
  20. data/lib/prawn/svg/elements/path.rb +9 -16
  21. data/lib/prawn/svg/elements/pattern.rb +150 -0
  22. data/lib/prawn/svg/elements/rect.rb +40 -9
  23. data/lib/prawn/svg/elements/switch.rb +92 -0
  24. data/lib/prawn/svg/elements/text.rb +25 -1
  25. data/lib/prawn/svg/elements/text_component.rb +105 -8
  26. data/lib/prawn/svg/elements/text_node.rb +60 -17
  27. data/lib/prawn/svg/elements/text_path.rb +153 -0
  28. data/lib/prawn/svg/elements/use.rb +97 -19
  29. data/lib/prawn/svg/elements.rb +7 -4
  30. data/lib/prawn/svg/font.rb +8 -9
  31. data/lib/prawn/svg/font_metrics.rb +46 -2
  32. data/lib/prawn/svg/font_registry.rb +201 -26
  33. data/lib/prawn/svg/gradients.rb +5 -2
  34. data/lib/prawn/svg/interface.rb +4 -1
  35. data/lib/prawn/svg/pathable.rb +8 -5
  36. data/lib/prawn/svg/pattern_renderer.rb +112 -0
  37. data/lib/prawn/svg/properties.rb +125 -33
  38. data/lib/prawn/svg/renderer.rb +18 -6
  39. data/lib/prawn/svg/ttc.rb +35 -0
  40. data/lib/prawn/svg/ttf.rb +77 -47
  41. data/lib/prawn/svg/version.rb +1 -1
  42. data/lib/prawn-svg.rb +4 -0
  43. data/spec/integration_spec.rb +16 -16
  44. data/spec/prawn/svg/attributes/clip_path_spec.rb +246 -0
  45. data/spec/prawn/svg/attributes/stroke_spec.rb +126 -0
  46. data/spec/prawn/svg/calculators/path_length_spec.rb +141 -0
  47. data/spec/prawn/svg/css/font_face_parser_spec.rb +72 -0
  48. data/spec/prawn/svg/css/selector_parser_spec.rb +6 -0
  49. data/spec/prawn/svg/css/stylesheets_spec.rb +369 -0
  50. data/spec/prawn/svg/document_spec.rb +199 -2
  51. data/spec/prawn/svg/elements/base_spec.rb +2 -2
  52. data/spec/prawn/svg/elements/gradient_spec.rb +1 -1
  53. data/spec/prawn/svg/elements/image_spec.rb +169 -0
  54. data/spec/prawn/svg/elements/line_spec.rb +1 -1
  55. data/spec/prawn/svg/elements/marker_spec.rb +2 -1
  56. data/spec/prawn/svg/elements/mask_spec.rb +1 -1
  57. data/spec/prawn/svg/elements/path_spec.rb +2 -2
  58. data/spec/prawn/svg/elements/pattern_spec.rb +232 -0
  59. data/spec/prawn/svg/elements/polygon_spec.rb +1 -1
  60. data/spec/prawn/svg/elements/polyline_spec.rb +1 -1
  61. data/spec/prawn/svg/elements/switch_spec.rb +310 -0
  62. data/spec/prawn/svg/elements/text_path_spec.rb +183 -0
  63. data/spec/prawn/svg/elements/text_spec.rb +512 -12
  64. data/spec/prawn/svg/elements/use_spec.rb +274 -0
  65. data/spec/prawn/svg/font_registry_spec.rb +223 -20
  66. data/spec/prawn/svg/font_spec.rb +37 -0
  67. data/spec/prawn/svg/interface_spec.rb +32 -12
  68. data/spec/prawn/svg/pathable_spec.rb +2 -2
  69. data/spec/prawn/svg/properties_spec.rb +198 -0
  70. data/spec/prawn/svg/ttc_spec.rb +31 -0
  71. data/spec/sample_css/import_base.css +4 -0
  72. data/spec/sample_css/import_nested.css +1 -0
  73. data/spec/sample_svg/alignment_baseline.svg +61 -0
  74. data/spec/sample_svg/arc_test_a.svg +186 -0
  75. data/spec/sample_svg/arc_test_b.svg +194 -0
  76. data/spec/sample_svg/baseline_shift.svg +55 -0
  77. data/spec/sample_svg/clip_path_text.svg +104 -0
  78. data/spec/sample_svg/clip_path_units.svg +102 -0
  79. data/spec/sample_svg/css_import.svg +38 -0
  80. data/spec/sample_svg/dominant_baseline.svg +41 -0
  81. data/spec/sample_svg/font_face.svg +77 -0
  82. data/spec/sample_svg/font_stretch.svg +54 -0
  83. data/spec/sample_svg/font_weights.svg +140 -0
  84. data/spec/sample_svg/kerning.svg +45 -0
  85. data/spec/sample_svg/lang.svg +117 -0
  86. data/spec/sample_svg/marker_shorthand.svg +48 -0
  87. data/spec/sample_svg/pattern.svg +80 -0
  88. data/spec/sample_svg/rounded_rectangle.svg +101 -0
  89. data/spec/sample_svg/stroke_dashoffset.svg +48 -0
  90. data/spec/sample_svg/stroke_miterlimit.svg +48 -0
  91. data/spec/sample_svg/switch.svg +106 -0
  92. data/spec/sample_svg/text-decoration.svg +5 -3
  93. data/spec/sample_svg/text_path.svg +25 -0
  94. data/spec/sample_svg/use_external.svg +51 -0
  95. data/spec/sample_svg/use_external_icons.svg +29 -0
  96. data/spec/sample_svg/vertical-text.svg +152 -0
  97. data/spec/sample_svg/view.svg +48 -0
  98. data/spec/sample_svg/word_spacing.svg +45 -0
  99. data/spec/sample_ttf/TestFamily.ttc +0 -0
  100. metadata +48 -2
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 3c524e2dc0d6754bcd60ca3d4e056f64069621b4412ff90b2f07c01123837e80
4
- data.tar.gz: 3edb96730b5a94d0c5db17ac5cd47e6dd4ab4af4e93b409555fc94e69b12dde4
3
+ metadata.gz: d49edc3094a7c32e0a28d5652485f9f41760559041b5dbf75fd2deebe0861059
4
+ data.tar.gz: 74ac334d02477ae3cb00173b4dd04b5fde9a614cc17a57bfaf102d5e81b756e0
5
5
  SHA512:
6
- metadata.gz: 344edee9e2156c94d4d82aa995d38df4a73cad64966979d70da1220a30875da3837dbc962d968347b52d1bc55ee843e141275f62e013de31362ea064fbc24f92
7
- data.tar.gz: a65858effa7cabdff362876b3e877bed5d1e35a9b9dcc70fbc68d23849faea060f14f63fe710baf510e011e5f5433014b365146ac47acef4fd35f2ad02221fbd
6
+ metadata.gz: 8d65777391564f1e2515b3f695d97b9d517a368c5b0e2d9af62c47866da82174f0ba25146ab29251af90175c95d0400443d5a2e1171bddebe113eeba9b75df7e
7
+ data.tar.gz: 890c36c783060f51f1ff6e80e7455b314b2a3acb948a427e985bfd29825de5f0b6bdcddd646e29f699180f85a85f05656759e107a8d1ff3ce3b075f098085aa0
data/Gemfile.lock CHANGED
@@ -1,7 +1,7 @@
1
1
  PATH
2
2
  remote: .
3
3
  specs:
4
- prawn-svg (0.38.1)
4
+ prawn-svg (0.40.0)
5
5
  css_parser (~> 1.6)
6
6
  matrix (~> 0.4.2)
7
7
  prawn (>= 0.11.1, < 3)
data/README.md CHANGED
@@ -1,16 +1,14 @@
1
1
  # prawn-svg
2
2
 
3
3
  [![Gem Version](https://badge.fury.io/rb/prawn-svg.svg)](https://badge.fury.io/rb/prawn-svg)
4
- ![Build Status](https://github.com/mogest/prawn-svg/actions/workflows/test.yml/badge.svg?branch=master)
4
+ ![Build Status](https://github.com/mogest/prawn-svg/actions/workflows/test.yml/badge.svg?branch=main)
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. Find out more about the Prawn PDF library at:
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
- http://github.com/prawnpdf/prawn
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. Defaults to true.
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 most but not all of the full SVG 1.1 specification. It currently 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
- - `<rect>`. Rounded rects are supported, but only one radius is applied to all corners.
57
+ - `<line>`, `<polyline>`, `<rect>`, `<polygon>`, `<circle>` and `<ellipse>`
60
58
 
61
- - `<path>` supports all commands defined in SVG 1.1, although the
62
- implementation of elliptical arc is a bit rough at the moment.
59
+ - `<path>`
63
60
 
64
- - `<text>`, `<tspan>` and `<tref>` with attributes `x`, `y`, `dx`, `dy`, `rotate`, `textLength`, `lengthAdjust`,
65
- and with extra properties `text-anchor`, `text-decoration` (underline only), `font`, `font-size`, `font-family`,
66
- `font-weight`, `font-style`, `letter-spacing`, `dominant-baseline` (middle only)
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
- - `<switch>` and `<foreignObject>`, although prawn-svg cannot handle any data that is not SVG so `<foreignObject>`
88
- tags are always ignored.
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 the
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 CSS, both in `<style>` blocks and `style` attributes.
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
- The pseudo-classes `:first-child`, `:last-child` and `:nth-child(n)` (where n is a number) also work.
119
- `!important` is supported.
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
- Pseudo-elements and the other pseudo-classes are not supported.
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 patterns or filters.
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
- It does not support text in the clip area, but you can clip shapes and text by any shape.
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
- add_calls_from_element clip_path_element
13
- add_call 'clip'
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
- add_call('dash', values)
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
- def calculate_bezier_curve_points_for_arc(cx, cy, a, b, lambda_1, lambda_2, theta)
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
- d_lambda = lambda_2 - lambda_1
26
+ d_eta = eta_2 - eta_1
24
27
 
25
28
  while iterations < 1024
26
- if d_lambda.abs <= Math::PI / 2.0
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
- d_lambda = (lambda_2 - lambda_1) / iterations
32
+ d_eta = (eta_2 - eta_1) / iterations
34
33
  end
35
34
 
36
35
  (0...iterations).collect do |iteration|
37
- eta_a, eta_b = calculate_eta_from_lambda(a, b, lambda_1 + (iteration * d_lambda),
38
- lambda_1 + ((iteration + 1) * d_lambda))
39
- d_eta = eta_b - eta_a
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(d_eta) * ((Math.sqrt(4 + (3 * (Math.tan(d_eta / 2)**2))) - 1) / 3)
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[eta_a]
44
- x2, y2 = e[eta_b]
42
+ x1, y1 = e[seg_eta_a]
43
+ x2, y2 = e[seg_eta_b]
45
44
 
46
- ep_eta1_x, ep_eta1_y = ep[eta_a]
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[eta_b]
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