-
Notifications
You must be signed in to change notification settings - Fork 99
/
Copy pathPuppeteerSharpRenderer.fs
203 lines (157 loc) · 8.02 KB
/
PuppeteerSharpRenderer.fs
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
namespace Plotly.NET.ImageExport
open System.Threading
open System.Threading.Tasks
open Plotly.NET
open PuppeteerSharp
open System
open System.IO
open System.Text
open System.Text.RegularExpressions
open DynamicObj
module PuppeteerSharpRendererOptions =
let mutable launchOptions = LaunchOptions()
launchOptions.Timeout <- 60000
let mutable localBrowserExecutablePath =
None
let mutable navigationOptions = NavigationOptions()
type PuppeteerSharpRenderer() =
/// adapted from the original C# implementation by @ilyalatt : https://github.com/ilyalatt/Plotly.NET.PuppeteerRenderer
///
/// creates a full screen html site for the given chart
let toFullScreenHtml (gChart: GenericChart) =
gChart
|> GenericChart.mapConfig (fun c ->
c |> DynObj.withProperty "responsive" true
)
|> GenericChart.mapLayout (fun l ->
l
|> DynObj.withProperty "width" "100%"
|> DynObj.withProperty "height" "100%"
)
|> GenericChart.toEmbeddedHTML
// this should be done via regex, as this only captures the default width and height.
|> fun html -> html.Replace("width: 600px; height: 600px;", "width: 100%; height: 100%;")
/// adapted from the original C# implementation by @ilyalatt : https://github.com/ilyalatt/Plotly.NET.PuppeteerRenderer
///
/// adds the necessary js function calls to render an image with plotly.js
let patchHtml width height (scale: float) (format: StyleParam.ImageFormat) html =
let regex =
Regex(@"(Plotly\.newPlot\(.+?\))")
let patchedHtml =
regex.Replace(
html,
(fun (x: Match) ->
x.Result(
"$1"
+ $".then(x => Plotly.toImage(x, {{ format: '{StyleParam.ImageFormat.toString format}', scale: {scale}, width: {width}, height: {height} }}))"
+ ".then(img => window.plotlyImage = img)"
))
)
patchedHtml
/// adapted from the original C# implementation by @ilyalatt : https://github.com/ilyalatt/Plotly.NET.PuppeteerRenderer
///
/// attempts to render a chart as static image of the given format with the given dimensions from the given html string
let tryRenderAsync (browser: IBrowser) (width: int) (height: int) (scale: float) (format: StyleParam.ImageFormat) (html: string) =
task {
let! page = browser.NewPageAsync() |> Async.AwaitTask
try
let! _ =
page.SetContentAsync(
html = patchHtml width height scale format html,
options = PuppeteerSharpRendererOptions.navigationOptions
)
|> Async.AwaitTask
let! imgHandle = page.WaitForExpressionAsync("window.plotlyImage") |> Async.AwaitTask
let! imgStr = imgHandle.JsonValueAsync<string>() |> Async.AwaitTask
return imgStr
finally
page.CloseAsync() |> AsyncHelper.taskSyncUnit
}
/// Initalizes headless browser
let fetchAndLaunchBrowserAsync () =
task {
match PuppeteerSharpRendererOptions.localBrowserExecutablePath with
| None ->
let browserFetcher = new BrowserFetcher()
let! revision = browserFetcher.DownloadAsync()
let launchOptions =
PuppeteerSharpRendererOptions.launchOptions
launchOptions.ExecutablePath <- revision.GetExecutablePath()
return! Puppeteer.LaunchAsync(launchOptions)
| Some p ->
let launchOptions =
PuppeteerSharpRendererOptions.launchOptions
launchOptions.ExecutablePath <- p
return! Puppeteer.LaunchAsync(launchOptions)
}
/// skips the data type part of the given URI
let skipDataTypeString (base64: string) =
let imgBase64StartIdx =
base64.IndexOf(",", StringComparison.Ordinal) + 1
base64.Substring(imgBase64StartIdx)
/// converst a base64 encoded string URI to a byte array
let getBytesFromBase64String (base64: string) =
base64 |> skipDataTypeString |> Convert.FromBase64String
interface IGenericChartRenderer with
member this.RenderJPGAsync(width: int, height: int, scale: float, gChart: GenericChart) =
task {
use! browser = fetchAndLaunchBrowserAsync ()
return! tryRenderAsync browser width height scale StyleParam.ImageFormat.JPEG (gChart |> toFullScreenHtml)
}
member this.RenderJPG(width: int, height: int, scale: float, gChart: GenericChart) =
(this :> IGenericChartRenderer)
.RenderJPGAsync(width, height, scale, gChart)
|> AsyncHelper.taskSync
member this.SaveJPGAsync(path: string, width: int, height: int, scale: float, gChart: GenericChart) =
task {
let! rendered =
(this :> IGenericChartRenderer)
.RenderJPGAsync(width, height, scale, gChart)
return rendered |> getBytesFromBase64String |> (fun base64 -> File.WriteAllBytes($"{path}.jpg", base64))
}
member this.SaveJPG(path: string, width: int, height: int, scale: float, gChart: GenericChart) =
(this :> IGenericChartRenderer)
.SaveJPGAsync(path, width, height, scale, gChart)
|> AsyncHelper.taskSync
member this.RenderPNGAsync(width: int, height: int, scale: float, gChart: GenericChart) =
task {
use! browser = fetchAndLaunchBrowserAsync ()
return! tryRenderAsync browser width height scale StyleParam.ImageFormat.PNG (gChart |> toFullScreenHtml)
}
member this.RenderPNG(width: int, height: int, scale: float, gChart: GenericChart) =
(this :> IGenericChartRenderer)
.RenderPNGAsync(width, height, scale, gChart)
|> AsyncHelper.taskSync
member this.SavePNGAsync(path: string, width: int, height: int, scale: float, gChart: GenericChart) =
task {
let! rendered =
(this :> IGenericChartRenderer)
.RenderPNGAsync(width, height, scale, gChart)
return rendered |> getBytesFromBase64String |> (fun base64 -> File.WriteAllBytes($"{path}.png", base64))
}
member this.SavePNG(path: string, width: int, height: int, scale: float, gChart: GenericChart) =
(this :> IGenericChartRenderer)
.SavePNGAsync(path, width, height, scale, gChart)
|> AsyncHelper.taskSync
member this.RenderSVGAsync(width: int, height: int, scale: float, gChart: GenericChart) =
task {
use! browser = fetchAndLaunchBrowserAsync ()
let! renderedString =
tryRenderAsync browser width height scale StyleParam.ImageFormat.SVG (gChart |> toFullScreenHtml)
return renderedString |> fun svg -> System.Uri.UnescapeDataString(svg) |> skipDataTypeString
}
member this.RenderSVG(width: int, height: int, scale: float, gChart: GenericChart) =
(this :> IGenericChartRenderer)
.RenderSVGAsync(width, height, scale, gChart)
|> AsyncHelper.taskSync
member this.SaveSVGAsync(path: string, width: int, height: int, scale: float, gChart: GenericChart) =
task {
let! rendered =
(this :> IGenericChartRenderer)
.RenderSVGAsync(width, height, scale, gChart)
return rendered |> fun svg -> File.WriteAllText($"{path}.svg", svg)
}
member this.SaveSVG(path: string, width: int, height: int, scale: float, gChart: GenericChart) =
(this :> IGenericChartRenderer)
.SaveSVGAsync(path, width, height, scale, gChart)
|> AsyncHelper.taskSync