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

Correct png image size if dpi metadata is present #224

Merged
merged 1 commit into from
Feb 26, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Binary file added docs/src/assets/plot_with_dpi.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added docs/src/assets/plot_without_dpi.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
11 changes: 10 additions & 1 deletion docs/src/mime_examples.md
Original file line number Diff line number Diff line change
Expand Up @@ -20,9 +20,18 @@ Base.show(io, ::MimeType, media::MediaOutput{MimeType}) where MimeType = write(i
# MediaOutput{MIME"text/plain"}("Hello there!")
```

PNG images that carry pixel density metadata will be shown at the correct size. This is a plot rendered at high pixel density which does not carry dpi metadata.

```@example mime-examples
using DocumenterVitepress
MediaOutput{MIME"image/png"}(read(joinpath(pathof(DocumenterVitepress) |> dirname |> dirname, "docs", "src", "assets", "plot_without_dpi.png")))
```

And this is the same plot but with dpi metadata embedded. DocumenterVitepress annotates the corrected size in its Markdown output.

```@example mime-examples
using DocumenterVitepress
MediaOutput{MIME"image/png"}(read(joinpath(pathof(DocumenterVitepress) |> dirname |> dirname, "docs", "src", "assets", "logo.png")))
MediaOutput{MIME"image/png"}(read(joinpath(pathof(DocumenterVitepress) |> dirname |> dirname, "docs", "src", "assets", "plot_with_dpi.png")))
```

```@example mime-examples
Expand Down
66 changes: 63 additions & 3 deletions src/writer.jl
Original file line number Diff line number Diff line change
Expand Up @@ -578,11 +578,71 @@
end
end

# Taken from https://github.com/PumasAI/QuartoNotebookRunner.jl/, MIT licensed
function png_image_metadata(bytes::Vector{UInt8})
if @view(bytes[1:8]) != b"\x89PNG\r\n\x1a\n"
throw(ArgumentError("Not a png file"))
end

chunk_start::Int = 9

_load(T, bytes, index) = ntoh(reinterpret(T, @view(bytes[index:index+sizeof(T)-1]))[])

function read_chunk!()
chunk_start > lastindex(bytes) && return nothing
chunk_data_length = _load(UInt32, bytes, chunk_start)
type = @view(bytes[chunk_start+4:chunk_start+7])
data = @view(bytes[chunk_start+8:chunk_start+8+chunk_data_length-1])
result = (; chunk_start, type, data)

# advance the chunk_start state variable
chunk_start += 4 + 4 + chunk_data_length + 4 # length, type, data, crc

return result
end

chunk = read_chunk!()
if chunk === nothing
error("PNG file had no chunks")
end
if chunk.type != b"IHDR"
error("PNG file must start with IHDR chunk, started with $(chunk.type)")
end

width = Int(_load(UInt32, chunk.data, 1))
height = Int(_load(UInt32, chunk.data, 5))

# if the png reports a physical pixel size, i.e., it has a pHYs chunk
# with the pixels per meter unit flag set, correct the basic width and height
# by those physical pixel sizes
while true
chunk = read_chunk!()
chunk === nothing && break
chunk.type == b"IDAT" && break
if chunk.type == b"pHYs"
is_in_meters = Bool(_load(UInt8, chunk.data, 9))
is_in_meters || break
x_px_per_meter = _load(UInt32, chunk.data, 1)
y_px_per_meter = _load(UInt32, chunk.data, 5)
# it seems sensible to round the final image size to full CSS pixels,
# especially given that png doesn't store dpi but px per meter
# in an integer format, losing some precision
width = round(Int, width / x_px_per_meter * (96 / 0.0254))
height = round(Int, height / y_px_per_meter * (96 / 0.0254))
break
end
end

return (; width, height)
end

function render_mime(io::IO, mime::MIME"image/png", node, element, page, doc; md_output_path, kwargs...)
filename = String(rand('a':'z', 7))
write(joinpath(doc.user.build, md_output_path, dirname(relpath(page.build, doc.user.build)), "$(filename).png"),
base64decode(element))
println(io, "![]($(filename).png)")
pngpath = joinpath(doc.user.build, md_output_path, dirname(relpath(page.build, doc.user.build)), "$(filename).png")
bytes = base64decode(element)
write(pngpath, bytes)
(; width, height) = png_image_metadata(bytes)
println(io, "![]($(filename).png){width=$(width)px height=$(height)px}")
end

function render_mime(io::IO, mime::MIME"image/webp", node, element, page, doc; md_output_path, kwargs...)
Expand Down Expand Up @@ -751,7 +811,7 @@
# Code blocks
function render(io::IO, mime::MIME"text/plain", node::Documenter.MarkdownAST.Node, code::MarkdownAST.CodeBlock, page, doc; kwargs...)
if startswith(code.info, "@")
@warn """

Check warning on line 814 in src/writer.jl

View workflow job for this annotation

GitHub Actions / build

DocumenterVitepress: un-expanded `@doctest` block encountered on page src/code_example.md. The first few lines of code in this node are: ``` julia> 1 + 1 2 ```
DocumenterVitepress: un-expanded `$(code.info)` block encountered on page $(page.source).
The first few lines of code in this node are:
```
Expand Down
Loading