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

Improve encoding #18

Merged
merged 4 commits into from
Feb 11, 2021
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
26 changes: 26 additions & 0 deletions bench/compression_bench.exs
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
defmodule CompressionBench do
use Benchfella

{:ok, kitten} = ExPng.Image.from_file("prof/kitten.png")
@kitten kitten

teardown_all nil do
File.rm("write_bench.png")
end

bench "zero compression" do
ExPng.Image.to_file(@kitten, "write_bench.png", compression: 0)
end

bench "compression level 1" do
ExPng.Image.to_file(@kitten, "write_bench.png", compression: 1)
end

bench "compression level 6" do
ExPng.Image.to_file(@kitten, "write_bench.png", compression: 6)
end

bench "compression level 9" do
ExPng.Image.to_file(@kitten, "write_bench.png", compression: 9)
end
end
6 changes: 6 additions & 0 deletions bench/snapshots/2021-02-09_14-16-23.snapshot
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
duration:1.0;mem stats:false;sys mem stats:false
module;test;tags;iterations;elapsed
ReadBench read larger image 1 5324485
ReadBench read smaller image 2 1914293
WriteBench write larger image 2 1193331
WriteBench write smaller image 20 1774304
6 changes: 6 additions & 0 deletions bench/snapshots/2021-02-09_15-10-41.snapshot
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
duration:10.0;mem stats:false;sys mem stats:false
module;test;tags;iterations;elapsed
ReadBench read larger image 2 10522213
ReadBench read smaller image 100 15114772
WriteBench write larger image 50 35312460
WriteBench write smaller image 1000 16980348
6 changes: 6 additions & 0 deletions bench/snapshots/2021-02-09_15-15-05.snapshot
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
duration:10.0;mem stats:false;sys mem stats:false
module;test;tags;iterations;elapsed
ReadBench read larger image 2 10316593
ReadBench read smaller image 100 15597511
WriteBench write larger image 50 32282283
WriteBench write smaller image 1000 15765622
10 changes: 10 additions & 0 deletions bench/snapshots/2021-02-09_15-35-37.snapshot
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
duration:10.0;mem stats:false;sys mem stats:false
module;test;tags;iterations;elapsed
CompressionBench compression level 1 1000 13639322
CompressionBench compression level 6 1000 13629043
CompressionBench compression level 9 1000 13697677
CompressionBench zero compression 1000 13636369
ReadBench read larger image 2 10661480
ReadBench read smaller image 100 15488693
WriteBench write larger image 50 31929886
WriteBench write smaller image 1000 16258235
10 changes: 10 additions & 0 deletions bench/snapshots/2021-02-09_15-44-58.snapshot
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
duration:10.0;mem stats:false;sys mem stats:false
module;test;tags;iterations;elapsed
CompressionBench compression level 1 2000 18362529
CompressionBench compression level 6 1000 13788383
CompressionBench compression level 9 1000 22295698
CompressionBench zero compression 5000 28055988
ReadBench read larger image 2 11168409
ReadBench read smaller image 100 15574840
WriteBench write larger image 50 32651972
WriteBench write smaller image 1000 15187876
2 changes: 1 addition & 1 deletion lib/ex_png/chunks/end.ex
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ defmodule ExPng.Chunks.End do
@behaviour ExPng.Encodeable

@impl true
def to_bytes(%__MODULE__{}) do
def to_bytes(%__MODULE__{}, _opts \\ []) do
length = <<0::32>>
type = <<73, 69, 78, 68>>
crc = :erlang.crc32([type, <<>>])
Expand Down
2 changes: 1 addition & 1 deletion lib/ex_png/chunks/header.ex
Original file line number Diff line number Diff line change
Expand Up @@ -66,7 +66,7 @@ defmodule ExPng.Chunks.Header do
@behaviour ExPng.Encodeable

@impl true
def to_bytes(%__MODULE__{} = header) do
def to_bytes(%__MODULE__{} = header, _opts \\ []) do
with {:ok, bit_depth} <- validate_bit_depth(header.bit_depth),
{:ok, color_mode} <- validate_color_mode(header.color_mode) do
length = <<13::32>>
Expand Down
79 changes: 62 additions & 17 deletions lib/ex_png/chunks/image_data.ex
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,11 @@ defmodule ExPng.Chunks.ImageData do
decoded by `ExPng`.
"""

use ExPng.Constants

alias ExPng.Image
import ExPng.Utilities, only: [reduce_to_binary: 1]

@type t :: %__MODULE__{
data: binary,
type: :IDAT
Expand Down Expand Up @@ -40,32 +45,73 @@ defmodule ExPng.Chunks.ImageData do
@behaviour ExPng.Encodeable

@impl true
def to_bytes(%__MODULE__{data: data}) do
data = deflate(data)
def to_bytes(%__MODULE__{data: data}, encoding_options \\ []) do
compression = Keyword.get(encoding_options, :compression, 6)
data = deflate(data, compression)
length = byte_size(data)
type = <<73, 68, 65, 84>>
crc = :erlang.crc32([type, data])
<<length::32>> <> type <> data <> <<crc::32>>
end

def from_pixels(pixels) do
def from_pixels(image, header) do
palette = Image.unique_pixels(image)
data =
Enum.map(pixels, fn line ->
Enum.reduce(line, <<0>>, fn pixel, acc ->
acc <> <<pixel.r, pixel.g, pixel.b, pixel.a>>
end)
Enum.map(image.pixels, fn line ->
Task.async(fn -> line_to_binary(line, header, palette) end)
end)
|> Enum.map(fn task ->
Task.await(task)
end)
|> Enum.reverse()
|> Enum.reduce(&Kernel.<>/2)
|> reduce_to_binary()

%__MODULE__{data: data}
{%__MODULE__{data: data}, %ExPng.Chunks.Palette{palette: palette}}
end

## PRIVATE

defp reduce_to_binary(chunks) do
Enum.reduce(chunks, <<>>, fn chunk, acc ->
acc <> chunk
defp line_to_binary(line, %{color_mode: @indexed} = header, palette) do
bit_depth = header.bit_depth
chunk_size = div(8, bit_depth)
line =
line
|> Enum.map(fn pixel -> Enum.find_index(palette, fn p -> p == pixel end) end)
|> Enum.map(fn i ->
Integer.to_string(i, 2)
|> String.pad_leading(bit_depth, "0")
end)
|> Enum.chunk_every(chunk_size, chunk_size)
|> Enum.map(fn byte ->
byte =
byte
|> Enum.join("")
|> String.pad_trailing(8, "0")
|> String.to_integer(2)
<<byte>>
end)
|> reduce_to_binary()
<<0>> <> line
end

defp line_to_binary(line, %{bit_depth: 1} = _header, _palette) do
line =
line
|> Enum.map(& div(&1.b, 255))
|> Enum.chunk_every(8)
|> Enum.map(fn bits -> Enum.join(bits, "") |> String.to_integer(2) end)
|> Enum.map(fn byte -> <<byte>> end)
|> reduce_to_binary()
<<0>> <> line
end

defp line_to_binary(line, %{bit_depth: 8} = header, _palette) do
Enum.reduce(line, <<0>>, fn pixel, acc ->
acc <> case header.color_mode do
@truecolor_alpha -> <<pixel.r, pixel.g, pixel.b, pixel.a>>
@truecolor -> <<pixel.r, pixel.g, pixel.b>>
@grayscale_alpha -> <<pixel.b, pixel.a>>
@grayscale -> <<pixel.b>>
end
end)
end

Expand All @@ -78,15 +124,14 @@ defmodule ExPng.Chunks.ImageData do
inflated_data
end

defp deflate(data) do
defp deflate(data, compression) do
zstream = :zlib.open()
:zlib.deflateInit(zstream)
:zlib.deflateInit(zstream, compression)
deflated_data = :zlib.deflate(zstream, data, :finish)
:zlib.deflateEnd(zstream)
:zlib.close(zstream)
deflated_data
|> List.flatten()
|> Enum.reverse()
|> Enum.reduce(&Kernel.<>/2)
|> reduce_to_binary()
end
end
16 changes: 16 additions & 0 deletions lib/ex_png/chunks/palette.ex
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@ defmodule ExPng.Chunks.Palette do

alias ExPng.Pixel

import ExPng.Utilities, only: [reduce_to_binary: 1]

@type t :: %__MODULE__{
type: :PLTE,
data: binary(),
Expand All @@ -20,6 +22,20 @@ defmodule ExPng.Chunks.Palette do
end
end

@behaviour ExPng.Encodeable

@impl true
def to_bytes(%__MODULE__{palette: palette}, _encoding_options \\ []) do
data =
Enum.map(palette, fn pixel -> <<pixel.r, pixel.g, pixel.b>> end)
|> reduce_to_binary()
length = byte_size(data)
type = <<80, 76, 84, 69>>
crc = :erlang.crc32([type, data])

<<length::32>> <> type <> data <> <<crc::32>>
end

## PRIVATE

defp parse_palette(data) do
Expand Down
2 changes: 1 addition & 1 deletion lib/ex_png/encodeable.ex
Original file line number Diff line number Diff line change
Expand Up @@ -4,5 +4,5 @@ defmodule ExPng.Encodeable do
and provide a callback to convert the struct to a binary bytestring to be
written to a PNG file.
"""
@callback to_bytes(term) :: binary()
@callback to_bytes(term, keyword | nil) :: binary()
end
13 changes: 10 additions & 3 deletions lib/ex_png/image.ex
Original file line number Diff line number Diff line change
Expand Up @@ -69,14 +69,21 @@ defmodule ExPng.Image do
end
end

@spec to_file(__MODULE__.t, filename) :: {:ok, filename}
def to_file(%__MODULE__{} = image, filename) do
@spec to_file(__MODULE__.t, filename, keyword | nil) :: {:ok, filename}
def to_file(%__MODULE__{} = image, filename, encoding_options \\ []) do
with {:ok, raw_data} <- Encoding.to_raw_data(image) do
RawData.to_file(raw_data, filename)
RawData.to_file(raw_data, filename, encoding_options)
{:ok, filename}
end
end

@spec unique_pixels(__MODULE__.t) :: integer()
def unique_pixels(%__MODULE__{pixels: pixels}) do
pixels
|> List.flatten()
|> Enum.uniq()
end

defdelegate erase(image), to: Drawing
defdelegate draw(image, coordinates, color), to: Drawing
defdelegate at(image, coordinates), to: Drawing
Expand Down
2 changes: 1 addition & 1 deletion lib/ex_png/image/decoding.ex
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@ defmodule ExPng.Image.Decoding do
image.header_chunk.bit_depth,
image.header_chunk.color_mode,
image.palette_chunk
)
) |> Enum.take(data.header_chunk.width)
end)

%Image{
Expand Down
80 changes: 73 additions & 7 deletions lib/ex_png/image/encoding.ex
Original file line number Diff line number Diff line change
Expand Up @@ -7,31 +7,97 @@ defmodule ExPng.Image.Encoding do
use ExPng.Constants

alias ExPng.Chunks.{End, Header, ImageData}
alias ExPng.{Image, RawData}
alias ExPng.{Image, Pixel, RawData}

def to_raw_data(%Image{} = image) do
header = build_header(image)
image_data_chunk = ImageData.from_pixels(image.pixels)
{image_data_chunk, palette} = ImageData.from_pixels(image, header)

{
:ok,
raw_data =
%RawData{
header_chunk: header,
data_chunk: image_data_chunk,
end_chunk: %End{}
}
}

raw_data = case header.color_mode do
@indexed ->
%{raw_data |
palette_chunk: palette
}
_ -> raw_data
end

{:ok, raw_data}
end

defp build_header(%Image{} = image) do
{bit_depth, color_mode} = bit_depth_and_color_mode(image)
%Header{
width: image.width,
height: image.height,
bit_depth: 8,
color_mode: @truecolor_alpha,
bit_depth: bit_depth,
color_mode: color_mode,
compression: 0,
filter: 0,
interlace: 0
}
end

# 1. black and white
# 2. grayscale (alpha)
# 3. indexed
# 4. truecolor (alpha)
defp bit_depth_and_color_mode(%Image{} = image) do
case black_and_white?(image) do
true -> {1, @grayscale}
false ->
case {indexable?(image), opaque?(image), grayscale?(image)} do
{_, true, true} -> {8, @grayscale}
{_, false, true} -> {8, @grayscale_alpha}
{true, _, _} -> {indexed_bit_depth(image), @indexed}
{_, true, false} -> {8, @truecolor}
{_, false, false} -> {8, @truecolor_alpha}
end
end
end

defp indexed_bit_depth(%Image{} = image) do
case unique_pixel_count(image) do
i when i <= 2 -> 1
i when i <= 4 -> 2
i when i <= 16 -> 4
i when i <= 256 -> 8
end
end

defp indexable?(%Image{} = image) do
image
|> unique_pixel_count()
|> Kernel.<=(256)
end

defp unique_pixel_count(%Image{} = image) do
image
|> Image.unique_pixels()
|> length()
end

defp opaque?(%Image{pixels: pixels}) do
pixels
|> List.flatten()
|> Enum.all?(&Pixel.opaque?/1)
end

defp grayscale?(%Image{pixels: pixels}) do
pixels
|> List.flatten()
|> Enum.all?(&Pixel.grayscale?/1)
end

defp black_and_white?(%Image{pixels: pixels}) do
pixels
|> List.flatten()
|> Enum.all?(&Pixel.black_or_white?/1)
end
end
Loading