fastpng icon indicating copy to clipboard operation
fastpng copied to clipboard

Read/write 8-bit/16-bit PNGs with rasters, native rasters, numeric+integer arrays, indexed images with palette, packed pixels in raw vector. Configurable compression settings allow for speed/size trad...

fastpng

R-CMD-check CRAN
status

{fastpng} reads and writes PNG images.

{fastpng} exposes configuration options so that the user can make a trade-off between speed of writing and PNG size. These options include:

  • Compression level
  • Filter use
  • Image transposition

For example, writing uncompressed PNG images can be 100x faster than writing with regular compression settings.

fastpng is an R wrapper for libspng - current v0.7.4

Features

Supported image data in R

Supported images each have examples in the test_image as part of this package.

  • Native rasters
  • Rasters
    • With hex colour formats: #RGB, #RGBA, #RRGGBB, #RRGGBBAA
    • Standard R colour names also supported e.g. ‘red’, ‘white’
  • Numeric arrays
    • Values in range [0,1]
    • 1-, 2-, 3- and 4-plane numeric arrays (interpreted as gray, gray+alpha, RGB and RGBA images)
  • Integer arrays
    • Values in range [0,255] treated as 8-bit values
    • Values in range [0,65535] treated as 16-bit for PNG writing
  • Integer matrix + an indexed palette of colours
  • Raw vectors with a specification for data layout

Supported PNG image types

  • 8-bit and 16-bit PNGs
  • RGBA, RGB, Gray + Alpha, Gray PNGs
  • Indexed colour PNGs
  • RGB PNGs with a specified transparency colour (using tRNS chunk)

Comparison to standard {png} library

{fastpng} {png}
Numeric arrays Yes Yes
Native rasters Yes Yes
Rasters Yes
Integer Arrays Yes
Indexed PNGs Yes
tRNS transparency Yes
Configurable compression Yes
Configurable filtering Yes
Configurable transposition Yes

Compression Settings: Speed / size tradeoff

Click to reveal benchmark code and results table

library(png)
im <- test_image$array$rgba

res <- bench::mark(
  fastpng::write_png(im, compression_level =  0),
  fastpng::write_png(im, compression_level =  1),
  fastpng::write_png(im, compression_level =  2),
  fastpng::write_png(im, compression_level =  3),
  fastpng::write_png(im, compression_level =  4),
  fastpng::write_png(im, compression_level =  5),
  fastpng::write_png(im, compression_level =  6),
  fastpng::write_png(im, compression_level =  7),
  fastpng::write_png(im, compression_level =  8),
  fastpng::write_png(im, compression_level =  9),
  check = FALSE,
  relative = FALSE
)

res <- res %>% 
  select(writes_per_second = `itr/sec`) %>%
  mutate(
    compression = 0:9,
    package = "fastpng"
  )


sizes <- vapply(0:9, \(x) fastpng::write_png(im, compression_level = x) |> length(), integer(1))
df <- data.frame(compression = 0:9, size = sizes)


plot_df <- left_join(res, df, by = 'compression')


png_speed <- bench::mark(writePNG(im))$`itr/sec`
png_size  <- length(writePNG(im))

plot_df <- plot_df %>% 
  add_row(writes_per_second = png_speed, size = png_size, package = "png") %>%
  mutate(
    compression_ratio = prod(dim(im)) * 8 / size
  )

knitr::kable(plot_df)
writes_per_second compression package size compression_ratio
6218.86981 0 fastpng 240651 7.978359
291.79719 1 fastpng 62456 30.741642
253.02288 2 fastpng 58017 33.093748
160.14992 3 fastpng 54119 35.477374
182.47226 4 fastpng 46436 41.347231
121.51956 5 fastpng 43177 44.468120
64.25097 6 fastpng 41303 46.485727
41.37887 7 fastpng 40799 47.059977
14.08140 8 fastpng 40758 47.107316
13.02791 9 fastpng 40776 47.086522
67.18115 NA png 41303 46.485727
#> Saving 8 x 6 in image

Installation

Install from CRAN with:

install.packages('fastpng')

You can install the latest development verion from GitHub with:

# install.package('remotes')
remotes::install_github('coolbutuseless/fastpng')

What’s in the box

  • read_png() to read a PNG from a file or a raw vector
  • write_png() to write an R image as a PNG file or PNG data in a raw vector
  • get_png_info() - interrogate a vector of raw values containing a PNG image to determine image information i.e. width, height, bit_depth, color_type, compression_method, filter_method, interlace_method.
  • test_image is a list of images. These are images contained in different datastructures and of differing bitdepth etc: RGBA and RGB numeric arrays, raster, native raster.

Example: Read a PNG into R

library(fastpng)
png_file <- system.file("img", "Rlogo.png", package="png")
fastpng::get_png_info(png_file)
#> $width
#> [1] 100
#> 
#> $height
#> [1] 76
#> 
#> $bit_depth
#> [1] 8
#> 
#> $color_type
#> [1] 6
#> 
#> $compression_method
#> [1] 0
#> 
#> $filter_method
#> [1] 0
#> 
#> $interlace_method
#> [1] 0
#> 
#> $color_desc
#> [1] "SPNG_COLOR_TYPE_TRUECOLOR_ALPHA"
#> 
#> $filter_desc
#> [1] "SPNG_FILTER_NONE"
#> 
#> $interlate_desc
#> [1] "SPNG_INTERLACE_NONE"

ras <- read_png(png_file, type = 'raster') 
grid::grid.raster(ras, interpolate = FALSE)

Read as a raster (of hex colours)

ras <- read_png(png_file, type = "raster")
ras[7:11, 79:83]
#>      [,1]        [,2]        [,3]        [,4]        [,5]       
#> [1,] "#686D6597" "#7579711F" "#00000000" "#00000000" "#00000000"
#> [2,] "#5F645CFF" "#5D635AF9" "#63696098" "#7B80781C" "#00000000"
#> [3,] "#686D64FF" "#61665DFF" "#5B6158FF" "#5C6158F5" "#656A6280"
#> [4,] "#71766EFF" "#6B6F67FF" "#656A61FF" "#5E635BFF" "#595E55FF"
#> [5,] "#797D75FF" "#747971FF" "#6E736AFF" "#686D64FF" "#60655DFF"

Read as a numeric array

ras <- read_png(png_file, type = "array")
ras[7:11, 79:83, 1] # red channel
#>           [,1]      [,2]      [,3]      [,4]      [,5]
#> [1,] 0.4078431 0.4588235 0.0000000 0.0000000 0.0000000
#> [2,] 0.3725490 0.3647059 0.3882353 0.4823529 0.0000000
#> [3,] 0.4078431 0.3803922 0.3568627 0.3607843 0.3960784
#> [4,] 0.4431373 0.4196078 0.3960784 0.3686275 0.3490196
#> [5,] 0.4745098 0.4549020 0.4313725 0.4078431 0.3764706

Read as an integer array

ras <- read_png(png_file, type = "array", array_type = 'integer')
ras[7:11, 79:83, 1] # red channel
#>      [,1] [,2] [,3] [,4] [,5]
#> [1,]  104  117    0    0    0
#> [2,]   95   93   99  123    0
#> [3,]  104   97   91   92  101
#> [4,]  113  107  101   94   89
#> [5,]  121  116  110  104   96

Read as a native raster

im <- read_png(png_file, type = "nativeraster")
im[7:11, 79:83]
#>          [,1] [,2] [,3]     [,4]      [,5]
#> [1,] -7235693    0    0 -7301485  -7767184
#> [2,] -7367279    0    0 -7367022  -5864589
#> [3,] -7498608    0    0 -7301485  -4219764
#> [4,] -7432815    0    0 -7235692   -995135
#> [5,] -6648959    0    0 -7501694 -10268343

Write an image to PNG with/without compression

png_file <- tempfile()
write_png(im, png_file)  # standard compression
file.size(png_file)
#> [1] 11938
write_png(im, png_file, compression_level = 0) # no compression, but fast!
file.size(png_file)
#> [1] 30580

Write integer matrix as indexed PNG

indices <- test_image$indexed$integer_index
palette <- test_image$indexed$palette

dim(indices)
#> [1] 200 300
indices[1:10, 1:10]
#>       [,1] [,2] [,3] [,4] [,5] [,6] [,7] [,8] [,9] [,10]
#>  [1,]    0    0    0    0    0    0    0    0    0     0
#>  [2,]    0    0    0    0    0    0    0    0    0     0
#>  [3,]    0    0    0    0    0    0    0    0    0     1
#>  [4,]    0    0    0    0    0    0    0    1    1     1
#>  [5,]    0    0    0    0    0    1    1    1    1     1
#>  [6,]    0    0    0    0    1    1    1    1    1     1
#>  [7,]    0    0    0    0    1    1    1    1    1     1
#>  [8,]    0    0    0    1    1    1    1    1    1     1
#>  [9,]    0    0    0    1    1    1    1    1    1     2
#> [10,]    0    0    1    1    1    1    1    1    2     2
palette[1:10]
#>  [1] "#440154FF" "#440256FF" "#450457FF" "#450559FF" "#46075AFF" "#46085CFF"
#>  [7] "#460A5DFF" "#460B5EFF" "#470D60FF" "#470E61FF"
tmp <- tempfile()
write_png(image = indices, palette = palette, file = tmp)