Skip to content

Commit e19c26d

Browse files
authored
Merge pull request #12 from JuliaPluto/screenshot-cells
2 parents a620310 + 6bf048a commit e19c26d

File tree

4 files changed

+109
-40
lines changed

4 files changed

+109
-40
lines changed

node/bin.js

+8-14
Original file line numberDiff line numberDiff line change
@@ -3,22 +3,16 @@ import { pdf } from "./export.js"
33
import path from "path"
44
import fileUrl from "file-url"
55

6-
const fileInput = process.argv[2]
7-
const fileOutput = process.argv[3]
6+
const html_path_arg = process.argv[2]
7+
const pdf_path_arg = process.argv[3]
88
const options = JSON.parse(process.argv[4])
9+
const screenshot_path_arg = process.argv[5]
10+
const screenshot_options = JSON.parse(process.argv[6])
911

10-
if (!fileInput) {
11-
console.error("ERROR: First program argument must be a Pluto notebook path or URL")
12-
process.exit(1)
13-
}
14-
if (!fileOutput) {
15-
console.error("ERROR: Second program argument must be the PDF output path")
16-
process.exit(1)
17-
}
12+
const input_url = html_path_arg.startsWith("http://") || html_path_arg.startsWith("https://") ? html_path_arg : fileUrl(path.resolve(html_path_arg))
13+
const pdf_path = path.resolve(pdf_path_arg)
14+
const screenshot_path = screenshot_path_arg == "" ? null : path.resolve(screenshot_path_arg)
1815

19-
const exportUrl = fileInput.startsWith("http://") || fileInput.startsWith("https://") ? fileInput : fileUrl(path.resolve(fileInput))
20-
const pdf_path = path.resolve(fileOutput)
21-
22-
await pdf(exportUrl, pdf_path, options)
16+
await pdf(input_url, pdf_path, options, screenshot_path, screenshot_options)
2317

2418
process.exit()

node/export.js

+53-20
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import p from "puppeteer"
22
import chalk from "chalk"
3+
import path from "path"
34

45
function sleep(time) {
56
return new Promise((resolve, reject) => {
@@ -9,7 +10,7 @@ function sleep(time) {
910
})
1011
}
1112

12-
export async function pdf(url, pdf_path, options, beforeClose = async () => {}) {
13+
export async function pdf(url, pdf_path, options, screenshot_dir, screenshot_options, { beforeClose = async () => {} } = {}) {
1314
const browser = await p.launch()
1415
console.log("Initiated headless browser")
1516
const page = await browser.newPage()
@@ -24,34 +25,66 @@ export async function pdf(url, pdf_path, options, beforeClose = async () => {})
2425
height: 1000,
2526
})
2627

27-
while (true) {
28-
const queued = await page.evaluate(`Array.from(document.getElementsByClassName('queued')).map(x => x.id)`)
29-
const running = await page.evaluate(`Array.from(document.getElementsByClassName('running')).map(x => x.id)`)
30-
const cells = await page.evaluate(`Array.from(document.getElementsByTagName('pluto-cell')).map(x => x.id)`)
31-
const bodyClasses = await page.evaluate(`document.body.getAttribute('class')`)
32-
33-
if (running.length > 0) {
34-
process.stdout.write(`\rRunning cell ${chalk.yellow(`${cells.length - queued.length}/${cells.length}`)} ${chalk.cyan(`[${running[0]}]`)}`)
35-
}
36-
37-
if (!(bodyClasses.includes("loading") || queued.length > 0 || running.length > 0)) {
38-
process.stdout.write(`\rRunning cell ${chalk.yellow(`${cells.length}/${cells.length}`)}`)
39-
console.log()
40-
break
41-
}
42-
43-
await sleep(250)
44-
}
28+
await waitForPlutoBusy(page, false, { timeout: 30 * 1000 })
4529

4630
console.log("Exporting as pdf...")
4731
await page.pdf({
4832
path: pdf_path,
4933
...options,
5034
})
35+
if (screenshot_dir != null) {
36+
await screenshot_cells(page, screenshot_dir, screenshot_options)
37+
}
5138

5239
console.log(chalk.green("Exported ✓") + " ... cleaning up")
5340

5441
await beforeClose()
55-
5642
await browser.close()
5743
}
44+
45+
/**
46+
* @param {p.Page} page
47+
* @param {string} screenshot_dir
48+
*/
49+
async function screenshot_cells(page, screenshot_dir, { outputOnly, scale }) {
50+
const cells = /** @type {String[]} */ (await page.evaluate(`Array.from(document.querySelectorAll('pluto-cell')).map(x => x.id)`))
51+
52+
for (let cell_id of cells) {
53+
const cell = await page.$(`[id="${cell_id}"]${outputOnly ? " > pluto-output" : ""}`)
54+
if (cell) {
55+
await cell.scrollIntoView()
56+
const rect = await cell.boundingBox()
57+
if (rect == null) {
58+
throw new Error(`Cell ${cell_id} is not visible`)
59+
}
60+
const imgpath = path.join(screenshot_dir, `${cell_id}.png`)
61+
62+
await cell.screenshot({ path: imgpath, clip: { ...rect, scale }, omitBackground: false })
63+
console.log(`Screenshot ${cell_id} saved to ${imgpath}`)
64+
}
65+
}
66+
}
67+
68+
const timeout = (delay) =>
69+
new Promise((r) => {
70+
setTimeout(r, delay)
71+
})
72+
73+
const waitForPlutoBusy = async (page, iWantBusiness, options) => {
74+
await timeout(1000)
75+
await page.waitForFunction(
76+
(iWantBusiness) => {
77+
let quiet = //@ts-ignore
78+
(document?.body?._update_is_ongoing ?? false) === false &&
79+
//@ts-ignore
80+
(document?.body?._js_init_set?.size ?? 0) === 0 &&
81+
document?.body?.classList?.contains("loading") === false &&
82+
document?.querySelector(`pluto-cell.running, pluto-cell.queued, pluto-cell.internal_test_queued`) == null
83+
84+
return iWantBusiness ? !quiet : quiet
85+
},
86+
options,
87+
iWantBusiness
88+
)
89+
await timeout(1000)
90+
}

src/PlutoPDF.jl

+25-5
Original file line numberDiff line numberDiff line change
@@ -36,19 +36,34 @@ const default_options = (
3636
displayHeaderFooter=false,
3737
)
3838

39-
function html_to_pdf(html_path::AbstractString, output_path::Union{AbstractString,Nothing}=nothing;
40-
options=default_options,
39+
const screenshot_default_options = (
40+
outputOnly=false,
41+
scale=2,
42+
)
43+
44+
function html_to_pdf(
45+
html_path::AbstractString,
46+
output_path::Union{AbstractString,Nothing}=nothing,
47+
screenshot_dir_path::Union{AbstractString,Nothing}=nothing;
48+
options=default_options,
49+
screenshot_options=screenshot_default_options,
4150
open=true,
4251
console_output=true
4352
)
4453
bin_script = normpath(joinpath(@__DIR__, "../node/bin.js"))
4554

4655
output_path = tamepath(something(output_path, Pluto.numbered_until_new(splitext(html_path)[1]; suffix=".pdf", create_file=false)))
56+
57+
screenshot_dir_path = if screenshot_dir_path === nothing
58+
nothing
59+
else
60+
mkpath(tamepath(screenshot_dir_path))
61+
end
4762

4863
@info "Generating pdf..."
4964
cmd = `$(node()) $bin_script $(tamepath(html_path)) $(output_path) $(JSON.json(
5065
(; default_options..., options...)
51-
))`
66+
)) $(something(screenshot_dir_path, "")) $(JSON.json((; screenshot_default_options..., screenshot_options...)))`
5267
if console_output
5368
run(cmd)
5469
else
@@ -77,7 +92,12 @@ Run a notebook, generate an Export HTML and then print it to a PDF file!
7792
# Options
7893
The `options` keyword argument can be a named tuple to configure the PDF export. The possible options can be seen in the [docs for `puppeteer.PDFOptions`](https://pptr.dev/api/puppeteer.pdfoptions). You don't need to specify all options, for example: `options=(format="A5",)` will work.
7994
"""
80-
function pluto_to_pdf(notebook_path::AbstractString, output_path::Union{AbstractString,Nothing}=nothing; kwargs...)
95+
function pluto_to_pdf(
96+
notebook_path::AbstractString,
97+
output_path::Union{AbstractString,Nothing}=nothing,
98+
screenshot_dir_path::Union{AbstractString,Nothing}=nothing;
99+
kwargs...
100+
)
81101
c = Pluto.Configuration.from_flat_kwargs(;
82102
disable_writing_notebook_files = true,
83103
lazy_workspace_creation = true,
@@ -93,7 +113,7 @@ function pluto_to_pdf(notebook_path::AbstractString, output_path::Union{Abstract
93113

94114
output_path = something(output_path, Pluto.numbered_until_new(Pluto.without_pluto_file_extension(notebook_path); suffix=".pdf", create_file=false))
95115

96-
html_to_pdf(filename, output_path; kwargs...)
116+
html_to_pdf(filename, output_path, screenshot_dir_path; kwargs...)
97117
end
98118

99119
function __init__()

test/runtests.jl

+23-1
Original file line numberDiff line numberDiff line change
@@ -6,16 +6,38 @@ testfile = download(
66
"https://raw.githubusercontent.com/fonsp/Pluto.jl/main/sample/Tower%20of%20Hanoi.jl",
77
)
88

9+
testfile2 = download("https://raw.githubusercontent.com/JuliaPluto/PlutoSliderServer.jl/08dafcca4073551cb4192d442dc3e8c33123b952/test/dir1/a.jl")
10+
911

1012
is_CI = get(ENV, "CI", "no") == "no"
1113

12-
outfile = pluto_to_pdf(testfile; open=is_CI, options=(format="A5",))
14+
outfile = tempname(; cleanup=false) * ".pdf"
15+
outdir = tempname(; cleanup=false)
16+
17+
@info "Files" outfile outdir testfile testfile2
1318

19+
result = pluto_to_pdf(testfile, outfile, outdir; open=is_CI, options=(format="A5",))
20+
21+
@test result == outfile
1422

1523
@test isfile(outfile)
1624
@test dirname(outfile) == dirname(testfile)
1725
@test endswith(outfile, ".pdf")
1826

27+
@test isdir(outdir)
28+
filez = readdir(outdir)
29+
@test length(filez) == 28
30+
@test all(endswith.(filez, ".png"))
31+
@test length(read(joinpath(outdir, filez[1]))) > 1000
32+
33+
34+
35+
outfile2 = pluto_to_pdf(testfile2; open=is_CI, options=(format="A5",))
36+
@info "Result" outfile2
37+
@test isfile(outfile2)
38+
@test dirname(outfile2) == dirname(testfile2)
39+
@test endswith(outfile2, ".pdf")
40+
1941
output_dir = get(ENV, "TEST_OUTPUT_DIR", nothing)
2042

2143
if @show(output_dir) isa String

0 commit comments

Comments
 (0)