Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

display png via html preserving size regardless of pixel density #2346

Closed
wants to merge 19 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
14 changes: 14 additions & 0 deletions CairoMakie/src/display.jl
Original file line number Diff line number Diff line change
Expand Up @@ -113,10 +113,24 @@ function Makie.backend_show(screen::Screen{IMAGE}, io::IO, ::MIME"image/png", sc
return screen
end

function Makie.backend_show(screen::Screen{IMAGE}, io::IO, ::Union{MIME"text/html",MIME"application/vnd.webio.application+html",MIME"application/prs.juno.plotpane+html",MIME"juliavscode/html"}, scene::Scene)
w, h = widths(scene.px_area[])
cairo_draw(screen, scene)
png_io = IOBuffer()
Cairo.write_to_png(screen.surface, png_io)
b64 = Base64.base64encode(String(take!(png_io)))
print(io, "<img width=$w height=$h style='object-fit: contain;' src=\"data:image/png;base64, $(b64)\"/>")
return screen
end

# Disabling mimes and showable

const DISABLED_MIMES = Set{String}()
const SUPPORTED_MIMES = Set([
"text/html",
"application/vnd.webio.application+html",
"application/prs.juno.plotpane+html",
"juliavscode/html",
"image/svg+xml",
"application/pdf",
"application/postscript",
Expand Down
17 changes: 10 additions & 7 deletions CairoMakie/src/screen.jl
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
using Base.Docs: doc

@enum RenderType SVG IMAGE PDF EPS
@enum RenderType SVG IMAGE PDF EPS HTML

Base.convert(::Type{RenderType}, ::MIME{SYM}) where SYM = mime_to_rendertype(SYM)
function Base.convert(::Type{RenderType}, type::String)
Expand All @@ -12,6 +12,8 @@ function Base.convert(::Type{RenderType}, type::String)
return PDF
elseif type == "eps"
return EPS
elseif type in ("html", "text/html", "application/vnd.webio.application+html", "application/prs.juno.plotpane+html", "juliavscode/html")
return HTML
else
error("Unsupported cairo render type: $type")
end
Expand All @@ -22,6 +24,7 @@ function to_mime(type::RenderType)
type == SVG && return MIME("image/svg+xml")
type == PDF && return MIME("application/pdf")
type == EPS && return MIME("application/postscript")
type == HTML && return MIME("text/html")
return MIME("image/png")
end

Expand All @@ -35,6 +38,8 @@ function mime_to_rendertype(mime::Symbol)::RenderType
return PDF
elseif mime == Symbol("application/postscript")
return EPS
elseif mime in (Symbol("text/html"), Symbol("text/html"), Symbol("application/vnd.webio.application+html"), Symbol("application/prs.juno.plotpane+html"), Symbol("juliavscode/html"))
return HTML
else
error("Unsupported mime: $mime")
end
Expand All @@ -55,7 +60,7 @@ function surface_from_output_type(type::RenderType, io, w, h)
return Cairo.CairoPDFSurface(io, w, h)
elseif type === EPS
return Cairo.CairoEPSSurface(io, w, h)
elseif type === IMAGE
elseif type === IMAGE || type === HTML
img = fill(ARGB32(0, 0, 0, 0), w, h)
return Cairo.CairoImageSurface(img)
else
Expand All @@ -76,8 +81,8 @@ end
to_cairo_antialias(aa::Int) = aa

"""
* `px_per_unit = 1.0`: see [figure size docs](https://docs.makie.org/v0.17.13/documentation/figure_size/index.html).
* `pt_per_unit = 0.75`: see [figure size docs](https://docs.makie.org/v0.17.13/documentation/figure_size/index.html).
* `px_per_unit = 2.0`: see [figure docs](https://docs.makie.org/stable/documentation/figure/).
* `pt_per_unit = 0.75`: see [figure docs](https://docs.makie.org/stable/documentation/figure/).
* `antialias::Union{Symbol, Int} = :best`: antialias modus Cairo uses to draw. Applicable options: `[:best => Cairo.ANTIALIAS_BEST, :good => Cairo.ANTIALIAS_GOOD, :subpixel => Cairo.ANTIALIAS_SUBPIXEL, :none => Cairo.ANTIALIAS_NONE]`.
* `visible::Bool`: if true, a browser/image viewer will open to display rendered output.
"""
Expand Down Expand Up @@ -120,9 +125,7 @@ function activate!(; inline=LAST_INLINE[], type="png", screen_config...)
# So, if we want to prefer the png mime, we disable the mimes that are usually higher up in the stack.
disable_mime!("svg", "pdf")
elseif type == "svg"
# SVG is usually pretty high up the priority, so we can just enable all mimes
# If we implement html display for CairoMakie, we might need to disable that.
disable_mime!()
disable_mime!("text/html", "application/vnd.webio.application+html", "application/prs.juno.plotpane+html", "juliavscode/html")
else
enable_only_mime!(type)
end
Expand Down
4 changes: 2 additions & 2 deletions CairoMakie/test/runtests.jl
Original file line number Diff line number Diff line change
Expand Up @@ -64,7 +64,7 @@ include(joinpath(@__DIR__, "rasterization_tests.jl"))
fig = scatter(1:4, figure=(; resolution=(800, 800)))
save("test.pdf", fig)
save("test.png", fig)
@test size(load("test.png")) == (800, 800)
@test size(load("test.png")) == (1600, 1600)
rm("test.pdf")
rm("test.png")
end
Expand Down Expand Up @@ -193,7 +193,7 @@ excludes = Set([
functions = [:volume, :volume!, :uv_mesh]

@testset "refimages" begin
CairoMakie.activate!(type = "png")
CairoMakie.activate!(type = "png", px_per_unit = 1)
ReferenceTests.mark_broken_tests(excludes, functions=functions)
recorded_files, recording_dir = @include_reference_tests "refimages.jl"
missing_images, scores = ReferenceTests.record_comparison(recording_dir)
Expand Down
2 changes: 1 addition & 1 deletion ReferenceTests/src/database.jl
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,7 @@ macro reference_test(name, code)
error("title must be unique. Duplicate title: $(title)")
end
println("running $(lpad(COUNTER[] += 1, 3)): $($title)")
Makie.set_theme!(resolution=(500, 500))
Makie.set_theme!(resolution=(500, 500), CairoMakie = (; px_per_unit = 1))
ReferenceTests.RNG.seed_rng!()
result = let
$(esc(code))
Expand Down
59 changes: 56 additions & 3 deletions docs/documentation/figure.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
The `Figure` object contains a top-level `Scene` and a `GridLayout`, as well as a list of blocks that have been placed into it, like `Axis`, `Colorbar`, `Slider`, `Legend`, etc.


## Creating a `Figure`
## Creating a Figure

You can create a figure explicitly with the `Figure()` function, and set attributes of the underlying scene.
The most important one of which is the `resolution`.
Expand Down Expand Up @@ -34,9 +34,9 @@ You can pass arguments to the created figure in a dict-like object to the specia
scatter(rand(100, 2), figure = (resolution = (600, 400),))
```

## Placing blocks into a `Figure`
## Placing Blocks into a Figure

All blocks take their parent figure as the first argument, then you can place them in the figure layout via indexing syntax.
All Blocks take their parent figure as the first argument, then you can place them in the figure layout via indexing syntax.

```julia
f = Figure()
Expand Down Expand Up @@ -166,3 +166,56 @@ ax = f[1, 1] = Axis(f)
contents(f[1, 1]) == [ax]
content(f[1, 1]) == ax
```

## Figure size

The size or resolution of a Figure is given without units, such as `resolution = (800, 600)`.
You can think of these values as "device-independent pixels".
Like the `px` unit in CSS, these values do not directly correspond to physical pixels of your screen or pixels in a png file.
Instead, they can be mapped to these device pixels using a scaling factor.

Currently, these scaling factors are only directly supported by CairoMakie, but in the future they should be available for GLMakie and WGLMakie as well.
Right now, the implicit scaling factor of GLMakie and WGLMakie is 1, which means that a window of a figure with resolution 800 x 600 will actually have 800 x 600 pixels in its frame buffer.
In the future, this should be adjustable, for example for "retina" or high-dpi displays, where the frame buffer for a 800 x 600 window typically has 1600 x 1200 pixels.

## Matching figure and font sizes to documents

Journal papers and other documents written in Word or LaTeX commonly use the `pt` unit to define font sizes.
The unit `pt` is a physical dimension and is typically defined as `1 inch / 72`.
To match font sizes of Makie plots with other text in these documents, you have to adjust both the figure size and font size together.

First, you need to convert the physical target size of your figure in the document to device-independent pixels.
For this, you have to decide a `px_per_unit` value if you're exporting a bitmap, or a `pt_per_unit` value if you export vector graphics.
With those, you can convert the target font size into device-independent pixels as well.

CairoMakie is the only backend that can export both bitmaps and vector graphics.
By default, its `px_per_unit` is `2` and `pt_per_unit` is `0.75`, but those values are chosen with interactive plotting with web-technology tools in mind.
The reason is that in normal web browsers, `1px` is equal to `0.75pt` and images with a density of 2 pixels for each device-independent `px` look sharper on modern high-dpi displays.
The default fontsize of `16` will by default look like `12pt` in web and print contexts this way.

### Example

Let's say we want to create a vector graphic for a scientific paper set with 12pt font size, and the figure size should be 5 x 4 inches which is equivalent to 360 x 288 pt (multiply by 72).

With the default `pt_per_unit = 0.75` we arrive at a necessary figure size of 480 x 384 device-independent pixels (divide by 0.75).

Equivalently, the font size we need to match 12pt is `12 / 0.75 = 16`.

Therefore, we can create our figure with `Figure(resolution = (480, 384), fontsize = 16)` and save with `save("figure.pdf", fig)`.

Let's say we now decide that our figure is too large in vector format, because it has a million scatter points, so we want to switch to bitmap format.

We keep our figure with its resolution and font size as it is.
The question is now only, how high should our dpi be.
With CairoMakie's default of `px_per_unit = 2`, we would get a pixel size of 960 x 768 for our image, if we divide that by 5 x 4 inches we get a dpi of 192.

Let's say this is not sharp enough for our purposes and we want to bump to 600 dpi.
The necessary pixel size of the image is 3000 x 2400.
With our figure size of 480 x 386 device-independent pixels, that gives a `px_per_unit` value of 6.25 to reach 600 dpi.
Note that we do not have to change anything about the font or other content sizes in the figure, we just scale up the render size.
We only need to run `save("figure.png", fig, px_per_unit = 6.25)` and take care to insert the image with the correct size of 5 x 4 inches, as image files usually don't store what physical size they are intended to be.

!!! note
If you keep the intended physical size of an image constant and increase the dpi by increasing `px_per_unit`, the size of text and other content relative to the figure will stay constant.
However, if you instead try to increase the dpi by increasing the Figure size itself, the relative size of text and other content will shrink when viewed at the same physical size.
The first option is usually much more convenient, as it keeps the look and layout of the overall figure exactly the same, just with higher resolution.
51 changes: 0 additions & 51 deletions docs/documentation/figure_size.md

This file was deleted.

2 changes: 1 addition & 1 deletion src/theming.jl
Original file line number Diff line number Diff line change
Expand Up @@ -71,7 +71,7 @@ const minimal_default = Attributes(
inspectable = true,

CairoMakie = Attributes(
px_per_unit = 1.0,
px_per_unit = 2.0,
pt_per_unit = 0.75,
antialias = :best,
visible = true,
Expand Down