Skip to content

Commit dd2dd83

Browse files
authored
Add ability to write pixel density into pngs (#79)
1 parent cd3a532 commit dd2dd83

File tree

3 files changed

+62
-1
lines changed

3 files changed

+62
-1
lines changed

src/io.jl

Lines changed: 24 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -280,6 +280,18 @@ end
280280

281281
### Write ##########################################################################################
282282

283+
_dpi_to_ppm(::Nothing) = nothing
284+
function _dpi_to_ppm(dpi::Real)
285+
@assert dpi > 0
286+
pixels_per_meter = round(UInt32, dpi / 0.0254)
287+
return (pixels_per_meter, pixels_per_meter)
288+
end
289+
function _dpi_to_ppm(dpi::Tuple{<:Real,<:Real})
290+
@assert dpi[1] > 0
291+
@assert dpi[2] > 0
292+
return round.(UInt32, dpi ./ 0.0254)
293+
end
294+
283295
const SupportedPaletteColor = Union{
284296
AbstractRGB{<:Union{N0f8,AbstractFloat}},
285297
TransparentRGB{T,<:Union{N0f8,AbstractFloat}} where T,
@@ -328,6 +340,9 @@ Write out a julia `Array` as a PNG image.
328340
- `background`: optional background color to be stored in the `bKGD` chunk. Only meaningful for transparent images.
329341
Valid values are `nothing` for no background, `UInt8` as a palette index for palleted images,
330342
`Gray` for grayscale images and `RGB` for true color images.
343+
- `dpi`: stores the pixel density given in dots per inch into the `pHYs` chunk as pixels per meter.
344+
The density is given as `dpi` for ease of use as pixels per meter is an uncommon format. If set to `nothing`,
345+
no pixel density is written. If set to a 2-element tuple, a different density is written for x and y, respectively.
331346
332347
# Returns
333348
- `nothing`
@@ -340,6 +355,7 @@ function save(
340355
filters::Integer = Int(PNG_FILTER_PAETH),
341356
file_gamma::Union{Nothing,Float64} = nothing,
342357
background::Union{Nothing,UInt8,AbstractGray,AbstractRGB} = nothing,
358+
dpi::Union{Nothing,Real,Tuple{<:Real,<:Real}} = nothing,
343359
) where {
344360
T,
345361
S<:Union{AbstractMatrix{T},AbstractArray{T,3}}
@@ -363,7 +379,8 @@ function save(
363379
compression_strategy=compression_strategy,
364380
filters=filters,
365381
file_gamma=file_gamma,
366-
background=background
382+
background=background,
383+
pixels_per_meter = _dpi_to_ppm(dpi),
367384
)
368385

369386
close_png(fp)
@@ -377,6 +394,7 @@ function save(
377394
filters::Integer = Int(PNG_FILTER_PAETH),
378395
file_gamma::Union{Nothing,Float64} = nothing,
379396
background::Union{Nothing,UInt8,AbstractGray,AbstractRGB} = nothing,
397+
dpi::Union{Nothing,Real,Tuple{<:Real,<:Real}} = nothing,
380398
) where {
381399
S<:Union{AbstractMatrix,AbstractArray{<:Any,3}}
382400
}
@@ -399,6 +417,7 @@ function save(
399417
filters=filters,
400418
file_gamma=file_gamma,
401419
background=background,
420+
pixels_per_meter = _dpi_to_ppm(dpi),
402421
)
403422
end
404423
end
@@ -411,6 +430,7 @@ function _save(png_ptr, info_ptr, image::S;
411430
filters::Integer = Int(PNG_FILTER_PAETH),
412431
file_gamma::Union{Nothing,Float64} = nothing,
413432
background::Union{Nothing,UInt8,AbstractGray,AbstractRGB} = nothing,
433+
pixels_per_meter::Union{Nothing,Tuple{UInt32,UInt32}} = nothing,
414434
) where {
415435
T,
416436
S<:Union{AbstractMatrix{T},AbstractArray{T,3}}
@@ -426,6 +446,9 @@ function _save(png_ptr, info_ptr, image::S;
426446
png_set_compression_level(png_ptr, compression_level)
427447
png_set_compression_strategy(png_ptr, compression_strategy)
428448
png_set_compression_window_bits(png_ptr, min(15, max(8, _nextpow2exp(approx_bytes))))
449+
if pixels_per_meter !== nothing
450+
png_set_pHYs(png_ptr, info_ptr, pixels_per_meter..., PNG_RESOLUTION_METER)
451+
end
429452

430453
if color_type == PNG_COLOR_TYPE_PALETTE
431454
# TODO: 1, 2, 4 bit-depth indices for palleted

test/runtests.jl

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -60,6 +60,7 @@ ensure_imagemagick()
6060
include("test_images_with_background.jl")
6161
include("test_io.jl")
6262
include("test_various_array_types.jl")
63+
include("test_dpi.jl")
6364
end
6465

6566
# Cleanup

test/test_dpi.jl

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
@testset "dpi" begin
2+
io_none = IOBuffer()
3+
io_300 = IOBuffer()
4+
io_300_300 = IOBuffer()
5+
io_300_500 = IOBuffer()
6+
7+
img = rand(RGB{N0f8}, 2, 2)
8+
9+
PNGFiles.save(io_none, img; dpi = nothing)
10+
PNGFiles.save(io_300, img; dpi = 300)
11+
PNGFiles.save(io_300_300, img; dpi = (300f0, 300.0))
12+
PNGFiles.save(io_300_500, img; dpi = (300, 500))
13+
14+
for io in [io_none, io_300, io_300_300, io_300_500]
15+
seekstart(io)
16+
@test PNGFiles.load(io) == img
17+
end
18+
19+
s_none = String(take!(io_none))
20+
s_300 = String(take!(io_300))
21+
s_300_300 = String(take!(io_300_300))
22+
s_300_500 = String(take!(io_300_500))
23+
24+
function physblock(xdpi, ydpi)
25+
io = IOBuffer()
26+
write(io, "pHYs")
27+
write(io, hton(round(UInt32, xdpi / 0.0254)))
28+
write(io, hton(round(UInt32, ydpi / 0.0254)))
29+
write(io, hton(UInt8(1)))
30+
return String(take!(io))
31+
end
32+
33+
@test !occursin("pHYs", s_none)
34+
@test occursin(physblock(300, 300), s_300)
35+
@test occursin(physblock(300, 300), s_300_300)
36+
@test occursin(physblock(300, 500), s_300_500)
37+
end

0 commit comments

Comments
 (0)