Skip to content

Commit

Permalink
Add crystal native implementation of PNG support
Browse files Browse the repository at this point in the history
  • Loading branch information
Vici37 committed Mar 2, 2024
1 parent d97535f commit d9f9468
Show file tree
Hide file tree
Showing 9 changed files with 100 additions and 6 deletions.
2 changes: 2 additions & 0 deletions BENCHMARKS.md
Original file line number Diff line number Diff line change
Expand Up @@ -81,6 +81,8 @@ For these tests, with `template[n]`, `template = CrImage::OneMap.new(n, n)`
| ------------------------------------ | -------- | ------- |
| to_gray | 1.95ms | 732kiB |
| to_ppm(IO::Memory.new) | 6.9ms | 4.01MiB |
|libspng to_png(IO::Memory.new) | 119.5ms | 1.55MiB |
|native crystal to_png(IO::Memory.new) | 117.86ms | 1.93MiB |
| to_jpeg(IO::Memory.new) | 10.24ms | 1.2MiB |
| to_webp(IO::Memory.new) | 213.93ms | 1.5MiB |
| to_webp(IO::Memory.new, lossy: true) | 38.07ms | 1.45MiB |
4 changes: 2 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -25,16 +25,16 @@ All sample images used are from [Unsplash](https://unsplash.com/).
CrImage supports the formats:
* PPM
* JPEG (requires `libturbojpeg`)
* PNG (requirens `libspng`)
* PNG (natively by default, or optionally through requires `libspng`)
* WebP (requires `libwebp`)

For the formats that require a linked library, they must be `require`d explicitly:

```crystal
require "cr-image"
require "cr-image/jpeg"
require "cr-image/png"
require "cr-image/webp"
require "cr-image/png" # replaces native crystal with libspng
# Or, alternatively
require "cr-image/all_formats"
Expand Down
7 changes: 6 additions & 1 deletion scripts/benchmark.cr
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
#!/usr/bin/env -S crystal run --no-debug --release

require "../src/cr-image"
require "../src/all_formats"
require "../src/webp"
require "../src/jpeg"

record Result, name : String, time : Float64, memory : Int64
alias Color = CrImage::Color
Expand Down Expand Up @@ -156,6 +157,10 @@ results.clear

results << benchmark { image.to_gray }
results << benchmark { image.to_ppm(IO::Memory.new) }
# NOTE: to get the benchmark for libspng, you need to:
# require "../src/png"
# Since the perf of both is so close, the default is for the native implementation
results << benchmark { image.to_png(IO::Memory.new) }
results << benchmark { image.to_jpeg(IO::Memory.new) }
results << benchmark { image.to_webp(IO::Memory.new) }
results << benchmark { image.to_webp(IO::Memory.new, lossy: true) }
Expand Down
4 changes: 4 additions & 0 deletions shard.yml
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,10 @@ version: 0.1.0
authors:
- Troy Sornson <[email protected]>

dependencies:
png:
github: sleepinginsomniac/png

development_dependencies:
spectator:
gitlab: arctic-fox/spectator
Expand Down
4 changes: 2 additions & 2 deletions spec/cr-image/format/png_spec.cr
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ Spectator.describe CrImage::Format::PNG do
io = IO::Memory.new
image.to_png(io)

expect_digest(io.to_s).to eq "c9450089873a4bbe98a2aa54f1bea959915ba088"
expect_digest(io.to_s).to eq "8635306d7a0201533e8812ebf3671b9c2e31c4b0"
end
end

Expand All @@ -20,7 +20,7 @@ Spectator.describe CrImage::Format::PNG do
io = IO::Memory.new
image.to_png(io)

expect_digest(io.to_s).to eq "4cac53568704ac1617cd96313658559f22c1a24b"
expect_digest(io.to_s).to eq "4623c3074b418ca58a9c083297baa3fefaad8f1c"
end
end
end
Expand Down
1 change: 0 additions & 1 deletion spec/spec_helper.cr
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@ require "digest"
require "spectator"
require "../src/cr-image"
require "../src/jpeg"
require "../src/png"
require "../src/webp"
require "./helpers/**"

Expand Down
1 change: 1 addition & 0 deletions src/cr-image.cr
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ require "./cr-image/format/open"

# Native crystal image format implementations
require "./cr-image/format/ppm"
require "./cr-image/format/png"

# Require `image` first, and then subclasses of it
require "./cr-image/image"
Expand Down
82 changes: 82 additions & 0 deletions src/cr-image/format/png.cr
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
require "png"

# Provides methods to read from and write to PNG. Requires `libspng` to function.
#
# ```
# image = File.open("image.png") { |file| CrImage::RGBAImage.from_png(file) }
# File.open("other_image.png") { |file| image.to_png(file) }
# ```
# Alternatively, you can use the convenience methods in the `Open` and `Save` modules
# to acheive the same thing:
# ```
# image = CrImage::RGBAImage.open("image.png")
# image.save("other_image.png")
# ```
module CrImage::Format::PNG
{% CrImage::Format::SUPPORTED_FORMATS << {extension: ".png", method: "png"} %}

macro included
# Read `image_data` and PNG encoded bytes
def self.from_png(image_data : Bytes) : self
from_png(IO::Memory.new(image_data))
end

# Construct an Image by reading bytes from `io`
def self.from_png(io : IO) : self
png = ::PNG.read(io)

width = png.width.to_i32
height = png.height.to_i32

red = Array.new(width * height) { ChannelType::Red.default }
green = Array.new(width * height) { ChannelType::Green.default }
blue = Array.new(width * height) { ChannelType::Blue.default }
alpha = Array.new(width * height) { ChannelType::Alpha.default }

red_offset = 0
green_offset = 1
blue_offset = 2
alpha_offset = 3
jump = png.color_type.channels

case jump
when 1
red_offset = green_offset = blue_offset = 0
alpha_offset = -1
when 2
red_offset = green_offset = blue_offset = 0
alpha_offset = 1
when 3
alpha_offset = -1
end

(width * height).times do |index|
png.data[index * jump]
red.unsafe_put(index, png.data[index * jump + red_offset])
green.unsafe_put(index, png.data[index * jump + green_offset])
blue.unsafe_put(index, png.data[index * jump + blue_offset])
if alpha_offset > -1
alpha.unsafe_put(index, png.data[index * jump + alpha_offset])
end
end

new(red, green, blue, alpha, width, height)
end
end

# Output the image as PNG to `io`
def to_png(io : IO) : Nil
bytes = Bytes.new(size * 4)
idx = 0
(size * 4).times.step(4).each do |index|
bytes.unsafe_put(index, red[idx])
bytes.unsafe_put(index + 1, green[idx])
bytes.unsafe_put(index + 2, blue[idx])
bytes.unsafe_put(index + 3, alpha[idx])
idx += 1
end
canvas = ::PNG::Canvas.new(::PNG::Header.new(width.to_u32, height.to_u32, color_type: ::PNG::ColorType::TrueColorAlpha), bytes)

::PNG.write(io, canvas)
end
end
1 change: 1 addition & 0 deletions src/cr-image/image.cr
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ abstract class CrImage::Image

macro inherited
include Format::PPM
include Format::PNG

include Operation::BilinearResize
include Operation::BoxBlur
Expand Down

0 comments on commit d9f9468

Please sign in to comment.