diff --git a/LICENSE b/LICENSE
new file mode 100644
index 0000000..be37d04
--- /dev/null
+++ b/LICENSE
@@ -0,0 +1,21 @@
+The MIT License (MIT)
+
+Copyright (c) 2014 Chris Maddox
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in
+all copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+THE SOFTWARE.
\ No newline at end of file
diff --git a/README.md b/README.md
index cc04c5b..8944e60 100644
--- a/README.md
+++ b/README.md
@@ -1,36 +1,27 @@
Imagineer
=========
+**N.B.**
+Until 1.0 is reached, each beta release my include backwards incompatible changes.
+1.0 will include parsing and writing of PNGs and JPEGs.
+
Image parsing in Elixir.
-Currently Imagineer only supports PNGs. To load an image, create a new `Imagineer.Image` and pass it to the load function. Once the image is processed (or the raw binary is placed in the `raw` field by some other means), passing it to `process` will parse all of its data.
+Currently Imagineer only supports PNGs. To load an image, call the load function on the Imagineer module. Once the image is processed (or the raw binary is placed in the `raw` field by some other means), passing it to `process` will parse all of its data.
```elixir
-alias Imagineer.Image
-image = %Image{uri: "./test/support/images/alpaca.png"} |>
- Image.load() |>
- Image.process()
+Imagineer.load("./test/support/images/png/alpaca.png")
# =>
-# %Imagineer.Image{
-# alias: nil,
-# attributes: %{
-# color_type: 2,
-# compression: 0,
-# filter_method: 0,
-# interface_method: 0,
-# pixel_dimensions: {
-# 5669,
-# 5669,
-# :meter
-# }
-# },
-# bit_depth: 8,
-# color_format: :rgb8,
-# content: <<120, 1, 141, 189, 7, 148, 92, 213, 149, 254, 123, 99, 229, 208, 213, 57, 75, 106, 229, 0, 66, 66, 18, 32, 178, 49, 57, 216, 132, 193, 9, 99, 96, 108, 6, 131, 3, 14, 51, 255, 97, 198, 30, 71, 156, ...>>,
-# format: :png,
-# height: 96,
-# mask: nil,
-# raw: <<137, 80, 78, 71, 13, 10, 26, 10, 0, 0, 0, 13, 73, 72, 68, 82, 0, 0, 0, 96, 0, 0, 0, 96, 8, 2, 0, 0, 0, 109, 250, 224, 111, 0, 0, 12, 70, 105, 67, 67, 80, ...>>,
-# uri: "./test/support/images/alpaca.png",
-# width: 96}
+{:ok,
+ %Imagineer.Image.PNG{alias: nil,
+ attributes: %{"XML:com.adobe.xmp": "\n \n \n 96\n 96\n \n \n\n",
+ pixel_dimensions: {5669, 5669, :meter}}, bit_depth: 8, color_format: :rgb8,
+ color_type: 2, comment: nil, compression: :zlib,
+ data_content: <<120, 1, 141, 189, 7, 148, 92, 213, 149, 254, 123, 99, 229, 208, 213, 57, 75, 106, 229, 0,
+ 66, 66, 18, 32, 178, 49, 57, 216, 132, 193, 9, 99, 96, 108, 6, 131, 3, 14, 51, 255, 97, ...>>,
+ decompressed_data: nil, filter_method: :five_basics, format: :png, gamma: nil,
+ height: 96, interface_method: 0, mask: nil, palette: [],
+ pixels: [], # 96 rows of 96 3-element tuples each omitted for sanity.
+ raw: <<137, 80, 78, 71, 13, 10, 26, 10, 0, 0, 0, 13, 73, 72, 68, 82, 0, 0, 0, 96, 0, 0, 0, 96, 8, 2, 0, 0, #0, 109, 250, ...>>,
+ scanlines: [], unfiltered_rows: [], uri: nil, width: 96}}
```
diff --git a/config/config.exs b/config/config.exs
index 6dfa82f..c6d35a4 100644
--- a/config/config.exs
+++ b/config/config.exs
@@ -1,24 +1,11 @@
-# This file is responsible for configuring your application
-# and its dependencies with the aid of the Mix.Config module.
use Mix.Config
-# This configuration is loaded before any dependency and is restricted
-# to this project. If another project depends on this project, this
-# file won't be loaded nor affect the parent project. For this reason,
-# if you want to provide default values for your application for third-
-# party users, it should be done in your mix.exs file.
+level = if System.get_env("DEBUG") do
+ :debug
+else
+ :info
+end
-# Sample configuration:
-#
-# config :logger, :console,
-# level: :info,
-# format: "$date $time [$level] $metadata$message\n",
-# metadata: [:user_id]
-
-# It is also possible to import configuration files, relative to this
-# directory. For example, you can emulate configuration per environment
-# by uncommenting the line below and defining dev.exs, test.exs and such.
-# Configuration from the imported file will override the ones defined
-# here (which is why it is important to import them last).
-#
-# import_config "#{Mix.env}.exs"
+config :logger, :console,
+ level: level,
+ format: "$date $time [$level] $metadata$message\n"
diff --git a/lib/imagineer.ex b/lib/imagineer.ex
index 6b75548..08927c0 100644
--- a/lib/imagineer.ex
+++ b/lib/imagineer.ex
@@ -1,2 +1,33 @@
defmodule Imagineer do
+ alias Imagineer.FormatDetector
+ alias Imagineer.Image.PNG
+ alias Imagineer.Image.JPG
+
+ @doc """
+ Loads the file from the given location and processes it into the correct
+ file type.
+ Returns `{:ok, image}` if successful or `{:error, error_message}` otherwise
+ """
+ @spec load(uri :: binary) :: {:ok, %{}} | {:error, binary}
+ def load(uri) do
+ case File.read(uri) do
+ {:ok, file} ->
+ detect_type_and_process(file)
+ {:error, reason} -> {:error, "Could not open #{uri} #{file_error_description(reason)}" }
+ end
+ end
+
+ defp detect_type_and_process(content) do
+ case FormatDetector.detect(content) do
+ :png ->
+ {:ok, PNG.process(content)}
+ :jpg ->
+ {:ok, JPG.process(content)}
+ :unknown ->
+ {:error, "Unknown or unsupported image format."}
+ end
+ end
+
+ defp file_error_description(:enoent), do: "because the file does not exist."
+ defp file_error_description(reason), do: "due to #{reason}."
end
diff --git a/lib/imagineer/format_detector.ex b/lib/imagineer/format_detector.ex
new file mode 100644
index 0000000..b0ae2e2
--- /dev/null
+++ b/lib/imagineer/format_detector.ex
@@ -0,0 +1,9 @@
+defmodule Imagineer.FormatDetector do
+ @png_signature <<137::size(8), ?P, ?N, ?G,
+ 13::size(8), 10::size(8), 26::size(8), 10::size(8)>>
+ @jpg_signature <<255::size(8), 216::size(8)>>
+
+ def detect(<<@png_signature, _rest::binary>>), do: :png
+ def detect(<<@jpg_signature, _rest::binary>>), do: :jpg
+ def detect(_), do: :unknown
+end
diff --git a/lib/imagineer/image.ex b/lib/imagineer/image.ex
index 92a0659..0502578 100644
--- a/lib/imagineer/image.ex
+++ b/lib/imagineer/image.ex
@@ -1,40 +1,5 @@
defmodule Imagineer.Image do
- defstruct alias: nil, width: nil, height: nil, mask: nil, bit_depth: nil,
- color_format: nil, uri: nil, format: nil, attributes: %{}, content: <<>>,
- raw: nil, comment: nil, mime_type: nil, components: nil
- alias Imagineer.Image
- alias Imagineer.Image.PNG
- alias Imagineer.Image.JPG
-
- @png_signature <<137::size(8), 80::size(8), 78::size(8), 71::size(8),
- 13::size(8), 10::size(8), 26::size(8), 10::size(8)>>
- @jpg_signature <<255::size(8), 216::size(8)>>
-
- def load(%Image{uri: uri}=image) do
- case File.read(uri) do
- {:ok, file} ->
- %Image{image | raw: file}
- {:error, reason} -> {:error, "Could not open #{uri} due to #{reason}" }
- end
- end
-
- def process(%Image{format: :png}=image) do
- PNG.process(image)
- end
-
- def process(%Image{format: :jpg}=image) do
- JPG.process(image)
- end
-
- def process(%Image{raw: raw}=image) when not is_nil(raw) do
- process %Image{ image | format: detect_format(image.raw) }
- end
-
- defp detect_format(<<@png_signature, _png_body::binary>>) do
- :png
- end
-
- defp detect_format(<<@jpg_signature, _png_body::binary>>) do
- :jpg
- end
+ use Behaviour
+ @type supported_image :: %{}
+ defcallback process(raw_content :: bitstring) :: supported_image
end
diff --git a/lib/imagineer/image/jpg.ex b/lib/imagineer/image/jpg.ex
index 0bf3b21..cac325c 100644
--- a/lib/imagineer/image/jpg.ex
+++ b/lib/imagineer/image/jpg.ex
@@ -1,5 +1,31 @@
defmodule Imagineer.Image.JPG do
- alias Imagineer.Image
+ require Logger
+ alias Imagineer.Image.JPG
+ defstruct alias: nil,
+ width: nil,
+ height: nil,
+ bit_depth: nil,
+ color_type: nil,
+ color_format: nil,
+ uri: nil,
+ format: :jpg,
+ attributes: %{},
+ data_content: <<>>,
+ raw: nil,
+ comment: "",
+ mask: nil,
+ compression: :dct,
+ decompressed_data: nil,
+ unfiltered_rows: nil,
+ scanlines: [],
+ filter_method: nil,
+ interface_method: nil,
+ gamma: nil,
+ palette: [],
+ pixels: [],
+ components: nil
+
+ @behaviour Imagineer.Image
@moduledoc """
YCbCr
@@ -31,11 +57,24 @@ defmodule Imagineer.Image.JPG do
@comment <<255::size(8), 254::size(8)>>
- @start_of_baseline_frame <<255::size(8), 192::size(8)>>
+ @start_of_baseline_frame <<255::size(8), 192::size(8)>>
+ @start_of_progressive_frame <<255::size(8), 194::size(8)>>
@define_huffman_table <<255::size(8), 196::size(8)>>
@define_quantization_table <<255::size(8), 219::size(8)>>
@start_of_scan <<255::size(8), 218::size(8)>>
+ @restart_interval <<255::size(8), 221::size(8)>>
+ @restart0 <<255::size(8), 208::size(8)>>
+ @restart1 <<255::size(8), 209::size(8)>>
+ @restart2 <<255::size(8), 210::size(8)>>
+ @restart3 <<255::size(8), 211::size(8)>>
+ @restart4 <<255::size(8), 212::size(8)>>
+ @restart5 <<255::size(8), 213::size(8)>>
+ @restart6 <<255::size(8), 214::size(8)>>
+ @restart7 <<255::size(8), 215::size(8)>>
+
+ @padding <<0::size(8), 4::size(8)>>
+
@jfif_identifier <<74::size(8), 70::size(8), 73::size(8), 70::size(8)>>
## Colors
@@ -43,74 +82,149 @@ defmodule Imagineer.Image.JPG do
@kr 0.299
@kb 0.114
- def process(%Image{format: :jpg}=image) do
- IO.puts inspect image.raw
- process image, image.raw
+ def process(<<@start_of_image, rest::binary>>=raw) do
+ IO.puts "START"
+ process(rest, %JPG{raw: raw})
end
- defp process(image, <<@start_of_image, rest::binary>>) do
- process(image, rest)
+ def process(<<@start_of_image, rest::binary>>, %JPG{}=image) do
+ process(rest, image)
end
- defp process(image, <<@start_of_baseline_frame, rest::binary>>) do
+ def process(<<@start_of_baseline_frame, rest::binary>>, image) do
+ IO.puts "BASELINE"
{content, rest} = marker_content(rest)
- process_start_of_frame(content, image)
- |> process(rest)
+ image = process_start_of_frame(content, image)
+ process(rest, image)
end
- defp process(image, <<@app0, rest::binary>>) do
+ def process(<<@start_of_progressive_frame, rest::binary>>, image) do
+ IO.puts "PROGRESSIVE"
+ {content, rest} = marker_content(rest)
+ image = process_start_of_frame(content, image)
+ process(rest, image)
+ end
+
+ def process(<<@app0, rest::binary>>, image) do
IO.puts "APP0"
+ Logger.debug("APP0")
{marker_content, rest} = marker_content(rest)
- process_app0(image, marker_content)
- |> process(rest)
+ image = process_app0(image, marker_content)
+ process(rest, image)
end
- defp process(%Image{}=image, <<@define_quantization_table, rest::binary>>) do
- IO.puts("DQT...skipping...")
+ def process(<<@define_quantization_table, rest::binary>>, %JPG{}=image) do
+ IO.puts "DQT"
+ Logger.debug("DQT...skipping...")
{marker_content, rest} = marker_content(rest)
- process_quantization_table(image, marker_content)
- |> process(rest)
+ image = process_quantization_table(image, marker_content)
+ process(rest, image)
end
- # From [Wikipedia](http://en.wikibooks.org/wiki/JPEG_-_Idea_and_Practice/The_header_part#The_Quantization_table_segment_DQT):
- # > A quantization table is specified in a DQT segment. A DQT segment begins with
- # > the marker DQT = 219 and the length, which is (0, 67). Then comes a byte the
- # > first half of which here is 0, meaning that the table consists of bytes
- # > (8 bit numbers - for the extended mode it is 1, meaning that the table consists
- # > of words, 16 bit numbers), and the last half of which is the destination
- # > identifier of the table (0-3), for instance 0 for the Y component and 1 for the
- # > colour components. Next follow the 64 numbers of the table (bytes).
- defp process_quantization_table(image, content) do
- image
+ def process(<<@app13, rest::binary>>, image) do
+ IO.puts "APP13"
+ Logger.debug("APP13!")
+ process(process_app13(rest, image), image)
end
- defp process(image, <<@app13, rest::binary>>) do
- IO.puts "APP13!"
+ def process(<<@comment, rest::binary>>, image) do
+ IO.puts "COMMENT"
+ {marker_content, rest} = marker_content(rest)
+ image = %JPG{image | comment: image.comment <> marker_content}
+ process(rest, image)
+ end
- process(image, process_app13(image, rest))
+ def process(<<@padding, rest::binary>>, image) do
+ IO.puts "PADDING"
+ {marker_content, rest} = marker_content(rest)
+ process(rest, image)
end
- defp process(image, <<@comment, rest::binary>>) do
+ def process(<<@define_huffman_table, rest::binary>>, image) do
+ IO.puts "DFT"
{marker_content, rest} = marker_content(rest)
- %Image{image | comment: marker_content}
- |> process(rest)
+ Logger.debug(marker_content, raw: true)
+ image = process_huffman_table(marker_content, image)
+ process(rest, image)
+ end
+
+ def process(<<@restart_interval, marker::size(32), rest::binary>>, image) do
+ IO.puts "RESTART INTERVAL"
+ # IO.puts marker
+ process(rest, image)
+ end
+
+ def process(<<@restart0, rest::binary>>, image) do
+ IO.puts "RESTART0"
+ process(rest, image)
+ end
+
+ def process(<<@restart1, rest::binary>>, image) do
+ IO.puts "RESTART1"
+ process(rest, image)
+ end
+
+ def process(<<@restart2, rest::binary>>, image) do
+ IO.puts "RESTART2"
+ process(rest, image)
+ end
+
+ def process(<<@restart3, rest::binary>>, image) do
+ IO.puts "RESTART3"
+ process(rest, image)
end
- defp process(image, <<@define_huffman_table, rest::binary>>) do
+ def process(<<@restart4, rest::binary>>, image) do
+ IO.puts "RESTART4"
+ process(rest, image)
+ end
+
+ def process(<<@restart5, rest::binary>>, image) do
+ IO.puts "RESTART5"
+ process(rest, image)
+ end
+
+ def process(<<@restart6, rest::binary>>, image) do
+ IO.puts "RESTART6"
+ process(rest, image)
+ end
+
+ def process(<<@restart7, rest::binary>>, image) do
+ IO.puts "RESTART7"
+ process(rest, image)
+ end
+
+ def process(<<@start_of_scan, rest::binary>>, image) do
+ IO.puts "START SCAN"
{marker_content, rest} = marker_content(rest)
- IO.puts inspect(marker_content, raw: true)
- process_huffman_table(marker_content, image)
- |> process(rest)
+ process_start_of_scan(marker_content, image)
+ process(rest, image)
end
- defp process(image, <<@end_of_image>>) do
+ def process(<<@end_of_image>>, image) do
+ IO.puts "END"
image
end
- defp process(image, <<255::size(8), marker::size(8), rest::binary>>) do
- IO.puts "Skipping unknown marker #{marker}"
- {marker_content, rest} = marker_content(rest)
- process image, rest
+ def process(<<255::size(8), marker::size(8), rest::binary>>, image) do
+ IO.puts "UNKNOWN"
+ IO.puts marker
+ Logger.debug("Skipping unknown marker #{marker}")
+ # {marker_content, rest} = marker_content(rest)
+ process(rest, image)
+ end
+
+ # From [Wikipedia](http://en.wikibooks.org/wiki/JPEG_-_Idea_and_Practice/The_header_part#The_Quantization_table_segment_DQT):
+ # > A quantization table is specified in a DQT segment. A DQT segment begins with
+ # > the marker DQT = 219 and the length, which is (0, 67). Then comes a byte the
+ # > first half of which here is 0, meaning that the table consists of bytes
+ # > (8 bit numbers - for the extended mode it is 1, meaning that the table consists
+ # > of words, 16 bit numbers), and the last half of which is the destination
+ # > identifier of the table (0-3), for instance 0 for the Y component and 1 for the
+ # > colour components. Next follow the 64 numbers of the table (bytes).
+ defp process_quantization_table(image, content) do
+ IO.puts "PQT"
+ image
end
# The leading byte tells us the number of bits to a color value. 8 is normal (
@@ -121,8 +235,18 @@ defmodule Imagineer.Image.JPG do
image)
do
components = parse_components(components)
- IO.puts "components: " <> inspect(components)
- %Image{image | height: height, width: width, components: components}
+ %JPG{image | height: height, width: width, components: components}
+ end
+
+ defp process_start_of_scan(<>, image) do
+ IO.puts "SoS"
+ # <<3, 1, 0, 2, 17, 3, 17, 0, 63, 0>>
+ component_size = num_components * 16
+ <> = sos_rest
+ IO.puts spectral_start
+ IO.puts spectral_end
+ # components = parse_components(components)
+ # %JPG{image | components: components}
end
defp parse_components(components) do
@@ -144,11 +268,12 @@ defmodule Imagineer.Image.JPG do
## DC Y Component
defp process_huffman_table(<<0::size(4), 0::size(4), rest::binary>>, image) do
+ image
end
## DC Color Component
defp process_huffman_table(<<0::size(4), 1::size(4), rest::binary>>, image) do
-
+ image
end
## AC Y Component
@@ -156,15 +281,14 @@ defmodule Imagineer.Image.JPG do
image
end
-
## DC Color component
defp process_huffman_table(<<1::size(4), 1::size(4), rest::binary>>, image) do
image
end
- defp process_app13(image, bin) do
- IO.puts "skipping APP13"
- {_marker_content, rest} = marker_content(bin)
+ defp process_app13(rest, image) do
+ Logger.debug("skipping APP13")
+ {_marker_content, rest} = marker_content(rest)
rest
end
@@ -175,15 +299,6 @@ defmodule Imagineer.Image.JPG do
rest::binary>>) do
thumbnail_data_size = 3 * thumbnail_width * thumbnail_height
<> = rest
- IO.puts "Data in app0:" <> inspect %{
- version_major: version_major,
- version_minor: version_minor,
- density_units: density_units,
- x_density: x_density,
- y_density: y_density,
- thumbnail_data_size: thumbnail_data_size,
- thumbnail_data: thumbnail_data
- }
image
end
@@ -192,6 +307,8 @@ defmodule Imagineer.Image.JPG do
# two to get the length of the actual content
defp marker_content(<>) do
content_length = total_length - 2
+ IO.puts "CONTENT LENGTH"
+ IO.puts content_length
<> = rest
{content, rest}
end
diff --git a/lib/imagineer/image/jpg/compression.ex b/lib/imagineer/image/jpg/compression.ex
new file mode 100644
index 0000000..0bef94f
--- /dev/null
+++ b/lib/imagineer/image/jpg/compression.ex
@@ -0,0 +1,20 @@
+defmodule Imagineer.Image.JPG.Compression do
+ alias Imagineer.Image.JPG
+ @moduledoc """
+ Handles
+ """
+
+ @doc """
+ Decompresses JPG data based on the passed in compression method.
+
+ Currently only dct is supported.
+ """
+ def decompress(%JPG{compression: compression}=image) do
+ decompressed_data = decompress(compression, image)
+ Map.put(image, :decompressed_data, decompressed_data)
+ end
+
+ defp decompress(:dct, image) do
+ JPG.Compression.DCT.decompress(image)
+ end
+end
diff --git a/lib/imagineer/image/jpg/compression/dct.ex b/lib/imagineer/image/jpg/compression/dct.ex
new file mode 100644
index 0000000..6001c23
--- /dev/null
+++ b/lib/imagineer/image/jpg/compression/dct.ex
@@ -0,0 +1,26 @@
+defmodule Imagineer.Image.JPG.Compression.DCT do
+ alias Imagineer.Image.JPG
+
+ @doc """
+ Takes in a JPG and returns its `data_content` inflated with dct.
+
+ ## Example
+
+ Let's get real deep with some Tallest Man on Earth.
+
+ iex> alias Imagineer.Image.JPG
+ iex> JPG.Compression.DCT.decompress(%JPG{
+ ...> data_content: <<120, 156, 29, 140, 65, 14, 128, 32, 12, 4, 191, 178,
+ ...> 55, 46, 198, 15, 112, 242, 200, 51, 208, 86, 37, 129, 150, 88, 136,
+ ...> 241, 247, 162, 215, 217, 217, 89, 132, 208, 206, 100, 176, 72, 194,
+ ...> 102, 19, 2, 172, 215, 170, 198, 30, 3, 31, 42, 18, 113, 106, 38,
+ ...> 20, 70, 211, 33, 51, 142, 75, 187, 144, 199, 50, 206, 193, 21, 236,
+ ...> 122, 109, 76, 223, 186, 167, 191, 199, 176, 150, 114, 246, 8, 130,
+ ...> 136, 154, 227, 198, 120, 180, 227, 86, 113, 13, 43, 195, 253, 133,
+ ...> 249, 5, 207, 168, 43, 42>>
+ ...> })
+ "And this sadness, I suppose; is gonna hold me to the ground; And I'm forced to find the still; In a place you won't be 'round."
+ """
+ def decompress(%JPG{data_content: compressed_data}) do
+ end
+end
diff --git a/lib/imagineer/image/jpg/data_content.ex b/lib/imagineer/image/jpg/data_content.ex
new file mode 100644
index 0000000..3ff145c
--- /dev/null
+++ b/lib/imagineer/image/jpg/data_content.ex
@@ -0,0 +1,70 @@
+defmodule Imagineer.Image.JPG.DataContent do
+ alias Imagineer.Image.JPG
+ # import Imagineer.Image.JPG.Helpers, only: [bytes_per_row: 2]
+
+ @doc """
+ Takes in an image whose chunks have been processed and decompresses,
+ defilters, de-interlaces, and pulls apart its pixels.
+ """
+ def process(%JPG{}=image) do
+ JPG.Compression.decompress(image)
+ |> extract_scanlines()
+ |> JPG.Filter.unfilter
+ |> JPG.Pixels.extract
+ |> cleanup
+ end
+
+ # Removes fields that are used in intermediate steps that don't make sense
+ # otherwise
+ defp cleanup(%JPG{}=image) do
+ %JPG{image | scanlines: [], decompressed_data: nil, unfiltered_rows: []}
+ end
+
+ @doc """
+ Takes in a JPG with decompressed data and splits up the individual scanlines,
+ returning the image with scanlines attached.
+ A scanline is 1 byte containing the filter followed by the binary
+ representation of that row of pixels (as filtered through the filter indicated
+ by the first byte.)
+
+ ## Example
+
+ iex> alias Imagineer.Image.JPG
+ iex> decompressed_data = <<1, 127, 138, 255, 20, 21, 107, 0, 233, 1, 77,
+ ...> 78, 191, 144, 2, 1, 77, 16, 234, 234, 154, 3, 67, 123, 98, 142,
+ ...> 117, 3, 4, 104, 44, 87, 33, 91, 188>>
+ iex> updated_image = %JPG{
+ ...> decompressed_data: decompressed_data,
+ ...> color_format: :rgb8,
+ ...> width: 2
+ ...> } |>
+ ...> JPG.DataContent.extract_scanlines()
+ iex> updated_image.scanlines
+ [
+ <<1, 127, 138, 255, 20, 21, 107>>,
+ <<0, 233, 1, 77, 78, 191, 144>>,
+ <<2, 1, 77, 16, 234, 234, 154>>,
+ <<3, 67, 123, 98, 142, 117, 3>>,
+ <<4, 104, 44, 87, 33, 91, 188>>
+ ]
+ """
+ def extract_scanlines(%JPG{decompressed_data: decompressed_data}=image) do
+ scanlines = extract_scanlines(decompressed_data, bytes_per_scanline(image), [])
+ Map.put(image, :scanlines, scanlines)
+ end
+
+ defp extract_scanlines(<<>>, _, scanlines) do
+ Enum.reverse scanlines
+ end
+
+ defp extract_scanlines(decompressed_data, bytes_per_scanline, scanlines) do
+ <> = decompressed_data
+ extract_scanlines(rest, bytes_per_scanline, [scanline | scanlines])
+ end
+
+ # The number of bytes per scanline is equal to the number of bytes per row
+ # plus one byte for the filter method.
+ defp bytes_per_scanline(%JPG{color_format: color_format, width: width}) do
+ # bytes_per_row(color_format, width) + 1
+ end
+end
diff --git a/lib/imagineer/image/png.ex b/lib/imagineer/image/png.ex
index a23b252..22cb79d 100644
--- a/lib/imagineer/image/png.ex
+++ b/lib/imagineer/image/png.ex
@@ -1,138 +1,221 @@
defmodule Imagineer.Image.PNG do
- alias Imagineer.Image
+ require Logger
+ alias Imagineer.Image.PNG
+ import Imagineer.Image.PNG.Helpers
+ defstruct alias: nil,
+ width: nil,
+ height: nil,
+ bit_depth: nil,
+ color_type: nil,
+ color_format: nil,
+ uri: nil,
+ format: :png,
+ attributes: %{},
+ data_content: <<>>,
+ raw: nil,
+ comment: nil,
+ mask: nil,
+ compression: :zlib,
+ decompressed_data: nil,
+ unfiltered_rows: nil,
+ scanlines: [],
+ filter_method: nil,
+ interface_method: nil,
+ gamma: nil,
+ palette: [],
+ pixels: []
- @png_signiture <<137::size(8), 80::size(8), 78::size(8), 71::size(8),
- 13::size(8), 10::size(8), 26::size(8), 10::size(8)>>
+ @behaviour Imagineer.Image
+
+ @png_signature <<137::size(8), ?P, ?N, ?G, ?\r, ?\n, 26::size(8), ?\n>>
# Required headers
- @ihdr_header <<73::size(8), 72::size(8), 68::size(8), 82::size(8)>>
- @plte_header <<80::size(8), 76::size(8), 84::size(8), 69::size(8)>>
- @idat_header <<73::size(8), 68::size(8), 65::size(8), 84::size(8)>>
- @iend_header <<73::size(8), 69::size(8), 78::size(8), 68::size(8)>>
+ @ihdr_header <>
+ @plte_header <>
+ @idat_header <>
+ @iend_header <>
# Auxillary headers
- @bkgd_header <<98::size(8), 75::size(8), 82::size(8), 68::size(8)>>
- @iccp_header <<105::size(8), 67::size(8), 67::size(8), 80::size(8)>>
- @phys_header <<112::size(8), 72::size(8), 89::size(8), 115::size(8)>>
- @text_header <<105::size(8), 84::size(8), 88::size(8), 116::size(8)>>
+ @bkgd_header <>
+ @iccp_header <>
+ @phys_header <>
+ @itxt_header <>
+ @gama_header <>
+
+ # Compression
+ @zlib 0
+
+ # Filter Methods
+ @filter_five_basics 0
- @mime_type "image/png"
+ # Filter Types
+ # http://www.w3.org/TR/PNG-Filters.html
+ @filter_0 :none
+ @filter_1 :sub
+ @filter_2 :up
+ @filter_3 :average
+ @filter_4 :paeth
+
+ # Color Types
+ # Color type is a single-byte integer that describes the interpretation of the
+ # image data. Color type codes represent sums of the following values:
+ # - 1 (palette used)
+ # - 2 (color used)
+ # - 4 (alpha channel used)
+ # Valid values are 0, 2, 3, 4, and 6.
+ @color_type_raw 0
+ @color_type_palette 1
+ @color_type_color 2
+ @color_type_palette_and_color 3
+ @color_type_alpha 4
+ @color_type_palette_color_and_alpha 6
+
+ def process(<<@png_signature, rest::binary>>=raw) do
+ process(rest, %PNG{raw: raw})
+ end
- def process(%Image{format: :png, raw: <<@png_signiture, rest::binary>>}=image) do
- IO.puts("processing")
- %Image{ image | mime_type: @mime_type }
- |> process(rest)
+ def process(<<@png_signature, rest::binary>>, %PNG{}=image) do
+ process(rest, image)
end
# Processes the "IHDR" chunk
- def process(%Image{} = image, <>) do
+ def process(<>, %PNG{} = image) do
<> = content
- attributes = Map.merge image.attributes, %{
+ image = %PNG{
+ image |
+ width: width,
+ height: height,
+ bit_depth: bit_depth,
+ color_format: color_format(color_type, bit_depth),
color_type: color_type,
- compression: compression,
- filter_method: filter_method,
+ compression: compression_format(compression),
+ filter_method: filter_method(filter_method),
interface_method: interface_method
}
-
- image = %Image{ image | attributes: attributes, width: width, height: height, bit_depth: bit_depth, color_format: color_format(color_type, bit_depth) }
- process(image, rest)
+ process(rest, image)
end
# Process "PLTE" chunk
- def process(%Image{} = image, <>) do
- image = %Image{ image | attributes: set_attribute(image, :palette, read_pallete(content))}
- process(image, rest)
+ def process(<>,%PNG{}=image) do
+ image = %PNG{ image | palette: read_palette(content)}
+ process(rest, image)
end
# Process "pHYs" chunk
- def process(%Image{} = image, <<_content_length::integer-size(32), @phys_header,
- <>,
- _crc::size(32), rest::binary >>) do
+ def process(<<_content_length::integer-size(32), @phys_header,
+ x_pixels_per_unit::integer-size(32), y_pixels_per_unit::integer-size(32),
+ _unit::binary-size(1), _crc::size(32), rest::binary >>, %PNG{}=image) do
pixel_dimensions = {
x_pixels_per_unit,
y_pixels_per_unit,
:meter}
- image = %Image{ image | attributes: set_attribute(image, :pixel_dimensions, pixel_dimensions)}
- process(image, rest)
+ image = %PNG{ image | attributes: set_attribute(image, :pixel_dimensions, pixel_dimensions)}
+ process(rest, image)
end
# Process the "IDAT" chunk
# There can be multiple IDAT chunks to allow the encoding system to control
# memory consumption. Append the content
- def process(%Image{} = image, <>) do
- process(%Image{ image | content: image.content <> content}, rest)
+ def process(<>, image) do
+ new_content = image.data_content <> data_content
+ process(rest, Map.put(image, :data_content, new_content))
end
# Process the "IEND" chunk
# The end of the PNG
- def process(%Image{} = image, <<_length::size(32), @iend_header, _rest::binary>>) do
- image
+ def process(<<_length::size(32), @iend_header, _rest::binary>>, %PNG{}=image) do
+ PNG.DataContent.process(image)
end
# Process the auxillary "bKGD" chunk
- def process(%Image{} = image, <>) do
- color_type = image.attributes.color_type
- background_color = case content do
- <> when color_type == 3 ->
- index
- <> when color_type == 0 or color_type == 4 ->
- gray
- <> when color_type == 2 or color_type == 6 ->
- {red, green, blue}
- _ ->
- :undefined
- end
- image = %Image{ image | attributes: set_attribute(image, :background_color, background_color)}
- process(image, rest)
+ def process(
+ <<_content_length::size(32), @bkgd_header, gray::size(16), _crc::size(32), rest::binary>>,
+ %PNG{color_type: @color_type_raw}=image)
+ do
+ process_with_background_color(image, gray, rest)
end
- # Process the auxillary "tEXt" chunk
- def process(%Image{} = image, <>) do
+ def process(
+ <<_content_length::size(32), @bkgd_header, red::size(16), green::size(16), blue::size(16), _crc::size(32), rest::binary>>,
+ %PNG{color_type: @color_type_color}=image)
+ do
+ process_with_background_color(image, {red, green, blue}, rest)
+ end
+
+ def process(
+ <<_content_length::size(32), @bkgd_header, index::size(8), _crc::size(32), rest::binary>>,
+ %PNG{color_type: @color_type_palette_and_color}=image)
+ do
+ process_with_background_color(image, elem(image.palatte, index), rest)
+ end
+
+ def process(
+ <<_content_length::size(32), @bkgd_header, gray::size(16), _crc::size(32), rest::binary>>,
+ %PNG{color_type: @color_type_alpha}=image)
+ do
+ process_with_background_color(image, gray, rest)
+ end
+
+ def process(
+ <<_content_length::size(32), @bkgd_header, red::size(16), green::size(16), blue::size(16), _crc::size(32), rest::binary>>,
+ %PNG{color_type: @color_type_palette_color_and_alpha}=image)
+ do
+ process_with_background_color(image, {red, green, blue}, rest)
+ end
+
+ # Process the auxillary "gAMA" chunk
+ def process(
+ <<_content_length::size(32), @gama_header, gamma::integer-size(32), _crc::size(32), rest::binary>>,
+ %PNG{}=image)
+ do
+ process(rest, %PNG{image| gamma: gamma/100_000})
+ end
+
+ # Process the auxillary "iTXt" chunk
+ def process(<>, %PNG{}=image) do
image = process_text_chunk(image, content)
- process(image, rest)
+ process(rest, image)
end
# For headers that we don't understand, skip them
- def process(%Image{} = image, <>) do
- IO.puts("Don't understand what to do with #{header}")
- process(image, rest)
+ def process(<>,
+ %PNG{}=image) do
+ Logger.debug("Skipping unknown header #{header}")
+ process(rest, image)
+ end
+
+ defp process_with_background_color(background_color, image, rest) do
+ image = %PNG{ image | attributes: set_attribute(image, :background_color, background_color)}
+ process(rest, image)
end
# Private helper functions
- defp set_attribute(%Image{} = image, attribute, value) do
+ defp set_attribute(%PNG{} = image, attribute, value) do
Map.put image.attributes, attribute, value
end
- # Color formats, taking in the color_type and bit_depth
- defp color_format(0, 1) , do: :grayscale1
- defp color_format(0, 2) , do: :grayscale2
- defp color_format(0, 4) , do: :grayscale4
- defp color_format(0, 8) , do: :grayscale8
- defp color_format(0, 16), do: :grayscale16
- defp color_format(2, 8) , do: :rgb8
- defp color_format(2, 16), do: :rgb16
- defp color_format(3, 1) , do: :palette1
- defp color_format(3, 2) , do: :palette2
- defp color_format(3, 4) , do: :palette4
- defp color_format(3, 8) , do: :palette8
- defp color_format(4, 8) , do: :grayscale_alpha8
- defp color_format(4, 16), do: :grayscale_alpha16
- defp color_format(6, 8) , do: :rgb_alpha8
- defp color_format(6, 16), do: :rgb_alpha16
+ # Check the compression byte. Purposefully raise if not zlib
+ defp compression_format(@zlib), do: :zlib
- defp read_pallete(content) do
- Enum.reverse read_pallete(content, [])
+ # Check for the filter method. Purposefully raise if not the only one defined
+ defp filter_method(@filter_five_basics), do: :five_basics
+
+ # We store as an array because we need to access by index
+ defp read_palette(content) do
+ read_palette(content, [])
+ |> Enum.reverse
+ |> :array.from_list
end
- defp read_pallete(<>, acc) do
- read_pallete(more_pallete, [{red, green, blue}| acc])
+ defp read_palette(<>, acc) do
+ read_palette(more_palette, [{red, green, blue}| acc])
end
defp process_text_chunk(image, content) do
@@ -165,15 +248,14 @@ defmodule Imagineer.Image.PNG do
content
end
-
# Sets the attribute relevant to whatever is held in the text chunk,
# returns the image
defp set_text_attribute(image, key, value) do
case key do
:Comment ->
- %Image{image | comment: value}
+ %PNG{image | comment: value}
_ ->
- %Image{image | attributes: set_attribute(image, key, value)}
+ %PNG{image | attributes: set_attribute(image, key, value)}
end
end
diff --git a/lib/imagineer/image/png/compression.ex b/lib/imagineer/image/png/compression.ex
new file mode 100644
index 0000000..4f40acf
--- /dev/null
+++ b/lib/imagineer/image/png/compression.ex
@@ -0,0 +1,20 @@
+defmodule Imagineer.Image.PNG.Compression do
+ alias Imagineer.Image.PNG
+ @moduledoc """
+ Handles
+ """
+
+ @doc """
+ Decompresses PNG data based on the passed in compression method.
+
+ Currently only zlib is supported.
+ """
+ def decompress(%PNG{compression: compression}=image) do
+ decompressed_data = decompress(compression, image)
+ Map.put(image, :decompressed_data, decompressed_data)
+ end
+
+ defp decompress(:zlib, image) do
+ PNG.Compression.Zlib.decompress(image)
+ end
+end
diff --git a/lib/imagineer/image/png/compression/zlib.ex b/lib/imagineer/image/png/compression/zlib.ex
new file mode 100644
index 0000000..fcac39d
--- /dev/null
+++ b/lib/imagineer/image/png/compression/zlib.ex
@@ -0,0 +1,32 @@
+defmodule Imagineer.Image.PNG.Compression.Zlib do
+ alias Imagineer.Image.PNG
+
+ @doc """
+ Takes in a PNG and returns its `data_content` inflated with zlib.
+
+ ## Example
+
+ Let's get real deep with some Tallest Man on Earth.
+
+ iex> alias Imagineer.Image.PNG
+ iex> PNG.Compression.Zlib.decompress(%PNG{
+ ...> data_content: <<120, 156, 29, 140, 65, 14, 128, 32, 12, 4, 191, 178,
+ ...> 55, 46, 198, 15, 112, 242, 200, 51, 208, 86, 37, 129, 150, 88, 136,
+ ...> 241, 247, 162, 215, 217, 217, 89, 132, 208, 206, 100, 176, 72, 194,
+ ...> 102, 19, 2, 172, 215, 170, 198, 30, 3, 31, 42, 18, 113, 106, 38,
+ ...> 20, 70, 211, 33, 51, 142, 75, 187, 144, 199, 50, 206, 193, 21, 236,
+ ...> 122, 109, 76, 223, 186, 167, 191, 199, 176, 150, 114, 246, 8, 130,
+ ...> 136, 154, 227, 198, 120, 180, 227, 86, 113, 13, 43, 195, 253, 133,
+ ...> 249, 5, 207, 168, 43, 42>>
+ ...> })
+ "And this sadness, I suppose; is gonna hold me to the ground; And I'm forced to find the still; In a place you won't be 'round."
+ """
+ def decompress(%PNG{data_content: compressed_data}) do
+ zlib_stream = :zlib.open()
+ :ok = :zlib.inflateInit(zlib_stream)
+ decompressed_data = Enum.join(:zlib.inflate(zlib_stream, compressed_data), "")
+ :ok = :zlib.inflateEnd(zlib_stream)
+ :ok = :zlib.close(zlib_stream)
+ decompressed_data
+ end
+end
diff --git a/lib/imagineer/image/png/data_content.ex b/lib/imagineer/image/png/data_content.ex
new file mode 100644
index 0000000..3d80218
--- /dev/null
+++ b/lib/imagineer/image/png/data_content.ex
@@ -0,0 +1,70 @@
+defmodule Imagineer.Image.PNG.DataContent do
+ alias Imagineer.Image.PNG
+ import Imagineer.Image.PNG.Helpers, only: [bytes_per_row: 2]
+
+ @doc """
+ Takes in an image whose chunks have been processed and decompresses,
+ defilters, de-interlaces, and pulls apart its pixels.
+ """
+ def process(%PNG{}=image) do
+ PNG.Compression.decompress(image)
+ |> extract_scanlines()
+ |> PNG.Filter.unfilter
+ |> PNG.Pixels.extract
+ |> cleanup
+ end
+
+ # Removes fields that are used in intermediate steps that don't make sense
+ # otherwise
+ defp cleanup(%PNG{}=image) do
+ %PNG{image | scanlines: [], decompressed_data: nil, unfiltered_rows: []}
+ end
+
+ @doc """
+ Takes in a PNG with decompressed data and splits up the individual scanlines,
+ returning the image with scanlines attached.
+ A scanline is 1 byte containing the filter followed by the binary
+ representation of that row of pixels (as filtered through the filter indicated
+ by the first byte.)
+
+ ## Example
+
+ iex> alias Imagineer.Image.PNG
+ iex> decompressed_data = <<1, 127, 138, 255, 20, 21, 107, 0, 233, 1, 77,
+ ...> 78, 191, 144, 2, 1, 77, 16, 234, 234, 154, 3, 67, 123, 98, 142,
+ ...> 117, 3, 4, 104, 44, 87, 33, 91, 188>>
+ iex> updated_image = %PNG{
+ ...> decompressed_data: decompressed_data,
+ ...> color_format: :rgb8,
+ ...> width: 2
+ ...> } |>
+ ...> PNG.DataContent.extract_scanlines()
+ iex> updated_image.scanlines
+ [
+ <<1, 127, 138, 255, 20, 21, 107>>,
+ <<0, 233, 1, 77, 78, 191, 144>>,
+ <<2, 1, 77, 16, 234, 234, 154>>,
+ <<3, 67, 123, 98, 142, 117, 3>>,
+ <<4, 104, 44, 87, 33, 91, 188>>
+ ]
+ """
+ def extract_scanlines(%PNG{decompressed_data: decompressed_data}=image) do
+ scanlines = extract_scanlines(decompressed_data, bytes_per_scanline(image), [])
+ Map.put(image, :scanlines, scanlines)
+ end
+
+ defp extract_scanlines(<<>>, _, scanlines) do
+ Enum.reverse scanlines
+ end
+
+ defp extract_scanlines(decompressed_data, bytes_per_scanline, scanlines) do
+ <> = decompressed_data
+ extract_scanlines(rest, bytes_per_scanline, [scanline | scanlines])
+ end
+
+ # The number of bytes per scanline is equal to the number of bytes per row
+ # plus one byte for the filter method.
+ defp bytes_per_scanline(%PNG{color_format: color_format, width: width}) do
+ bytes_per_row(color_format, width) + 1
+ end
+end
diff --git a/lib/imagineer/image/png/filter.ex b/lib/imagineer/image/png/filter.ex
new file mode 100644
index 0000000..ccd71c9
--- /dev/null
+++ b/lib/imagineer/image/png/filter.ex
@@ -0,0 +1,17 @@
+defmodule Imagineer.Image.PNG.Filter do
+ alias Imagineer.Image.PNG
+
+ @doc """
+ Takes in a png and converts its `scanlines` into `unfiltered_rows`. Returns
+ the png with the `unfiltered_rows` attribute set to a list of tuples of the
+ for `{row_index, unfiltered_row_binary}`
+ """
+ def unfilter(%PNG{filter_method: filter_method}=image) do
+ unfiltered_rows = unfilter(filter_method, image)
+ Map.put(image, :unfiltered_rows, unfiltered_rows)
+ end
+
+ defp unfilter(:five_basics, %PNG{}=image) do
+ PNG.Filter.Basic.unfilter(image)
+ end
+end
diff --git a/lib/imagineer/image/png/filter/basic.ex b/lib/imagineer/image/png/filter/basic.ex
new file mode 100644
index 0000000..4ba5cd4
--- /dev/null
+++ b/lib/imagineer/image/png/filter/basic.ex
@@ -0,0 +1,51 @@
+defmodule Imagineer.Image.PNG.Filter.Basic do
+ alias Imagineer.Image.PNG
+ alias PNG.Filter.Basic
+ import PNG.Helpers, only: [ bytes_per_pixel: 1, bytes_per_row: 2, null_binary: 1 ]
+ @none 0
+ @sub 1
+ @up 2
+ @average 3
+ @paeth 4
+
+ @doc """
+ Takes an image and its decompressed content. Returns the rows unfiltered with
+ their respective index.
+
+ Types are defined [here](http://www.w3.org/TR/PNG-Filters.html).
+ """
+ def unfilter(%PNG{scanlines: scanlines}=image) when is_list(scanlines) do
+ # For unfiltering, the row prior to the first is assumed to be all 0s
+ ghost_row = null_binary(bytes_per_row(image.color_format, image.width))
+ unfilter(scanlines, ghost_row, 0, bytes_per_pixel(image.color_format), [])
+ end
+
+ defp unfilter([], _prior_row, _current_index, _bytes_per_pixel, unfiltered) do
+ Enum.reverse unfiltered
+ end
+
+ defp unfilter([filtered_row | filtered_rows], prior_row, row_index, bytes_per_pixel, unfiltered) do
+ unfiltered_row = unfilter_scanline(filtered_row, bytes_per_pixel, prior_row)
+ unfilter(filtered_rows, unfiltered_row, row_index+1, bytes_per_pixel, [{row_index, unfiltered_row} | unfiltered])
+ end
+
+ defp unfilter_scanline(<<@none::size(8), row_content::binary>>, _bytes_per_pixel, _prior) do
+ row_content
+ end
+
+ defp unfilter_scanline(<<@sub::size(8), row_content::binary>>, bytes_per_pixel, _prior) do
+ Basic.Sub.unfilter(row_content, bytes_per_pixel)
+ end
+
+ defp unfilter_scanline(<<@up::size(8), row_content::binary>>, _bytes_per_pixel, prior_row) do
+ Basic.Up.unfilter(row_content, prior_row)
+ end
+
+ defp unfilter_scanline(<<@average::size(8), row_content::binary>>, bytes_per_pixel, prior_row) do
+ Basic.Average.unfilter(row_content, prior_row, bytes_per_pixel)
+ end
+
+ defp unfilter_scanline(<<@paeth::size(8), row_content::binary>>, bytes_per_pixel, prior_row) do
+ Basic.Paeth.unfilter(row_content, prior_row, bytes_per_pixel)
+ end
+end
diff --git a/lib/imagineer/image/png/filter/basic/average.ex b/lib/imagineer/image/png/filter/basic/average.ex
new file mode 100644
index 0000000..4756107
--- /dev/null
+++ b/lib/imagineer/image/png/filter/basic/average.ex
@@ -0,0 +1,79 @@
+defmodule Imagineer.Image.PNG.Filter.Basic.Average do
+ import Imagineer.Image.PNG.Helpers, only: [ null_binary: 1 ]
+ @moduledoc """
+ The Average filter computes value for a pixel based on the average between the
+ pixel to its left and the pixel directly above it.
+ """
+
+ @doc """
+ Takes in the uncompressed binary for an average-filtered row of pixels, the
+ unfiltered binary of the preceding row, and the number of bytes per pixel. It
+ returns the a binary of the row as unfiltered pixel data.
+
+ For more information, see the PNG Filter [documentation for the Average filter type
+ ](http://www.w3.org/TR/PNG-Filters.html#Filter-type-3-Average).
+
+ ## Example
+
+ iex> filtered = <<13, 191, 74, 228, 149, 158>>
+ iex> prior_unfiltered_row = <<8, 215, 35, 113, 28, 112>>
+ iex> Imagineer.Image.PNG.Filter.Basic.Average.unfilter(filtered, prior_unfiltered_row, 3)
+ <<17, 42, 91, 37, 184, 3>>
+
+ iex> filtered = <<13, 191, 74, 228, 149, 158>>
+ iex> prior_unfiltered_row = <<8, 215, 35, 113, 28, 112>>
+ iex> Imagineer.Image.PNG.Filter.Basic.Average.unfilter(filtered, prior_unfiltered_row, 2)
+ <<17, 42, 100, 49, 213, 238>>
+ """
+ def unfilter(row, prior_row, bytes_per_pixel) do
+ # For the first row, the preceding pixel bytes are all zeros
+ ghost_pixel = null_binary(bytes_per_pixel)
+ unfilter(row, prior_row, ghost_pixel, bytes_per_pixel, [])
+ |> Enum.join
+ end
+
+ # In the base case, `unfiltered_pixels` will be a reversed list of lists, each
+ # sublist of which contains the unfiltered bytes for that pixel
+ defp unfilter(<<>>, <<>>, _prior_pixel, _bytes_per_pixel, unfiltered_pixels) do
+ Enum.reverse unfiltered_pixels
+ end
+
+ defp unfilter(row, prior_row, prior_pixel, bytes_per_pixel, unfiltered_pixels) do
+ <> = row
+ <> = prior_row
+ unfiltered_pixel = unfilter_pixel(row_pixel, prior_row_pixel, prior_pixel)
+ unfilter(row_rest, prior_row_rest, unfiltered_pixel, bytes_per_pixel, [unfiltered_pixel | unfiltered_pixels])
+ end
+
+ defp unfilter_pixel(row_pixel, prior_row_pixel, prior_pixel) do
+ unfilter_pixel(row_pixel, prior_row_pixel, prior_pixel, [])
+ |> Enum.join
+ end
+
+ # In the base case, `unfiltered_bytes` is a reveresed list of the unfiltered
+ # bytes for this one pixel
+ defp unfilter_pixel(<<>>, <<>>, <<>>, unfiltered_bytes) do
+ Enum.reverse unfiltered_bytes
+ end
+
+ # To unfilter a given pixel's byte, we take the average of the corresponding
+ # byte of the pixel above it (`prior_row_pixel`) and the the corresponding
+ # byte of the pixel immediately to its left (`prior_pixel`.) We then take the
+ # floor of the average of these two and add it to our filtered byte (
+ # `row_pixel_byte`. That number modulo 256 (so it fits into a byte) is our
+ # answer.
+ #
+ # Who comes up with this shit?
+ defp unfilter_pixel(
+ <>,
+ <>,
+ <>,
+ unfiltered_bytes)
+ do
+ # We use `Kernel.trunc` to turn get the floor of the average as an integer
+ unfiltered_byte = row_pixel_byte + trunc((prior_pixel_byte + prior_row_pixel_byte)/2)
+ unfiltered_bytes = [<> | unfiltered_bytes]
+ unfilter_pixel(row_pixel_rest, prior_row_pixel_rest, prior_pixel_rest, unfiltered_bytes)
+ end
+
+end
diff --git a/lib/imagineer/image/png/filter/basic/paeth.ex b/lib/imagineer/image/png/filter/basic/paeth.ex
new file mode 100644
index 0000000..9f25361
--- /dev/null
+++ b/lib/imagineer/image/png/filter/basic/paeth.ex
@@ -0,0 +1,117 @@
+defmodule Imagineer.Image.PNG.Filter.Basic.Paeth do
+ import Imagineer.Image.PNG.Helpers, only: [ null_binary: 1 ]
+
+ @doc """
+ Takes in the uncompressed binary representation of a row, the unfiltered row
+ row above it, and the number of bytes per pixel. Decodes according to the
+ Paeth filter.
+
+ For more information, see the PNG documentation for the [Paeth filter type]
+ (http://www.w3.org/TR/PNG-Filters.html#Filter-type-4-Paeth)
+
+ ## Examples
+
+ iex> unfiltered_prior_row = <<18, 39, 117, 39, 201, 7>>
+ iex> filtered_row = <<86, 5, 226, 185, 146, 181>>
+ iex> Imagineer.Image.PNG.Filter.Basic.Paeth.unfilter(filtered_row, unfiltered_prior_row, 3)
+ <<104, 44, 87, 33, 91, 188>>
+
+ iex> unfiltered_prior_row = <<18, 39, 117, 39, 201, 7>>
+ iex> filtered_row = <<86, 5, 226, 245, 146, 181>>
+ iex> Imagineer.Image.PNG.Filter.Basic.Paeth.unfilter(filtered_row, unfiltered_prior_row, 2)
+ <<104, 44, 87, 33, 91, 188>>
+ """
+ def unfilter(row, prior_row, bytes_per_pixel) do
+ # For the first pixel, which has no upper left or left, we fill them in as
+ # null-filled binaries (`<<0>>`.)
+ upper_left_ghost_pixel = left_ghost_pixel = null_binary(bytes_per_pixel)
+ unfilter(row, prior_row, left_ghost_pixel, upper_left_ghost_pixel, bytes_per_pixel, [])
+ |> Enum.join()
+ end
+
+ # In the base case, we'll have a reversed list of binaries, each containing
+ # the unfiltered bytes of their respective pixel
+ defp unfilter(<<>>, <<>>, _left_pixel, _upper_left_pixel, _bytes_per_pixel, unfiltered_pixels) do
+ Enum.reverse unfiltered_pixels
+ end
+
+ defp unfilter(row, prior_row, left_pixel, upper_left_pixel, bytes_per_pixel, unfiltered_pixels) do
+ <> = row
+ <> = prior_row
+ unfiltered_pixel = unfilter_pixel(row_pixel, left_pixel, above_pixel, upper_left_pixel)
+ unfilter(row_rest, prior_row_rest, unfiltered_pixel, above_pixel, bytes_per_pixel, [unfiltered_pixel | unfiltered_pixels])
+ end
+
+ defp unfilter_pixel(row_pixel, left_pixel, above_pixel, upper_left_pixel) do
+ unfilter_pixel(row_pixel, left_pixel, above_pixel, upper_left_pixel, [])
+ |> Enum.join()
+ end
+
+ # In the base case, we'll have run through each of the bytes and have a
+ # reversed list of unfiltered bytes
+ defp unfilter_pixel(<<>>, <<>>, <<>>, <<>>, unfiltered_pixel_bytes) do
+ Enum.reverse unfiltered_pixel_bytes
+ end
+
+# Paeth(x) + PaethPredictor(Raw(x-bpp), Prior(x), Prior(x-bpp))
+
+ defp unfilter_pixel(
+ <>,
+ <>,
+ <>,
+ <>,
+ unfiltered_pixel_bytes)
+ do
+ nearest_byte = predictor(left_pixel_byte, above_pixel_byte, upper_left_pixel_byte)
+ unfiltered_byte = <>
+ unfilter_pixel(
+ filtered_pixel_rest,
+ left_pixel_rest,
+ above_pixel_rest,
+ upper_left_pixel_rest,
+ [unfiltered_byte | unfiltered_pixel_bytes]
+ )
+ end
+
+ @doc """
+ The Paeth prediction is calculated as `left + above - upper_left`.
+ This function returns the value nearest to the Paeth prediction, breaking ties
+ in the order of left, above, upper_left.
+
+ For more information, see the PNG documentation for the [Paeth filter type]
+ (http://www.w3.org/TR/PNG-Filters.html#Filter-type-4-Paeth)
+
+ ## Example
+
+ iex> Imagineer.Image.PNG.Filter.Basic.Paeth.predictor(37, 84, 1)
+ 84
+
+ iex> Imagineer.Image.PNG.Filter.Basic.Paeth.predictor(118, 128, 125)
+ 118
+
+ iex> Imagineer.Image.PNG.Filter.Basic.Paeth.predictor(37, 84, 61)
+ 61
+ """
+ def predictor(left, above, upper_left) do
+ prediction = left + above - upper_left
+ nearest_to_prediction(prediction, left, above, upper_left)
+ end
+
+ defp nearest_to_prediction(prediction, left, above, upper_left)
+ when abs(prediction - left) <= abs(prediction - above)
+ and abs(prediction - left) <= abs(prediction - upper_left)
+ do
+ left
+ end
+
+ defp nearest_to_prediction(prediction, _left, above, upper_left)
+ when abs(prediction - above) <= abs(prediction - upper_left)
+ do
+ above
+ end
+
+ defp nearest_to_prediction(_prediction, _left, _above, upper_left) do
+ upper_left
+ end
+end
+
diff --git a/lib/imagineer/image/png/filter/basic/sub.ex b/lib/imagineer/image/png/filter/basic/sub.ex
new file mode 100644
index 0000000..ebd5e75
--- /dev/null
+++ b/lib/imagineer/image/png/filter/basic/sub.ex
@@ -0,0 +1,63 @@
+defmodule Imagineer.Image.PNG.Filter.Basic.Sub do
+ import Imagineer.Image.PNG.Helpers, only: [ null_binary: 1 ]
+ @moduledoc """
+ The Sub filter transmits the difference between each byte and the value of the
+ corresponding byte of the prior pixel.
+ """
+
+ @doc """
+ Takes in the uncompressed binary for a sub-filtered row of pixels plus the
+ number of bytes per pixel and returns the a binary of the row as
+ unfiltered pixel data.
+
+ For more information, see the PNG Filter [documentation for the Sub filter type
+ ](http://www.w3.org/TR/PNG-Filters.html#Filter-type-1-Sub).
+
+ ## Example
+
+ iex> filtered = <<127, 138, 255, 20, 21, 107>>
+ iex> Imagineer.Image.PNG.Filter.Basic.Sub.unfilter(filtered, 3)
+ <<127, 138, 255, 147, 159, 106>>
+
+ iex> filtered = <<1, 77, 16, 234, 234, 154>>
+ iex> Imagineer.Image.PNG.Filter.Basic.Sub.unfilter(filtered, 3)
+ <<1, 77, 16, 235, 55, 170>>
+
+ iex> filtered = <<1, 77, 16, 234, 234, 154>>
+ iex> Imagineer.Image.PNG.Filter.Basic.Sub.unfilter(filtered, 2)
+ <<1, 77, 17, 55, 250, 132>>
+ """
+ def unfilter(row, bytes_per_pixel) do
+ # the pixel data before the first pixel is assumed to be all bagels
+ ghost_prior_pixel = null_binary(bytes_per_pixel)
+ unfilter(row, ghost_prior_pixel, bytes_per_pixel, [])
+ |> Enum.join
+ end
+
+ # In the base case, we'll have a reversed list of lists, each of which
+ # contains a the unfiltered bytes for a pixel. Flatten them
+ defp unfilter(<<>>, _prior_pixel, _bytes_per_pixel, unfiltered_pixels) do
+ List.flatten Enum.reverse unfiltered_pixels
+ end
+
+ defp unfilter(row, prior_pixel, bytes_per_pixel, unfiltered_pixels) do
+ <> = row
+ unfiltered_pixel = unfilter_pixel(prior_pixel, pixel, [])
+ unfilter(rest, pixel, bytes_per_pixel, [ unfiltered_pixel | unfiltered_pixels])
+ end
+
+ # In the base case, we'll have a reversed list of a bunch of unfiltered bytes
+ defp unfilter_pixel(<<>>, <<>>, unfiltered_bytes) do
+ Enum.reverse(unfiltered_bytes)
+ end
+
+ # Adds the corresponding byte values of the current pixel and the previous one
+ defp unfilter_pixel(
+ <>,
+ <>,
+ unfiltered_bytes)
+ do
+ unfiltered_byte = <>
+ unfilter_pixel(rest_prior, rest_pixel, [unfiltered_byte | unfiltered_bytes])
+ end
+end
diff --git a/lib/imagineer/image/png/filter/basic/up.ex b/lib/imagineer/image/png/filter/basic/up.ex
new file mode 100644
index 0000000..dec17cc
--- /dev/null
+++ b/lib/imagineer/image/png/filter/basic/up.ex
@@ -0,0 +1,48 @@
+defmodule Imagineer.Image.PNG.Filter.Basic.Up do
+ @moduledoc """
+ The Up filter transmits the difference between each byte and the value of the
+ corresponding byte of the same pixel in the row above it.
+ """
+
+ @doc """
+ Takes in the uncompressed binary for an up-filtered row of pixels and the
+ unfiltered binary of the preceding row. It returns the a binary of the row
+ as unfiltered pixel data.
+
+ For more information, see the PNG Filter [documentation for the Up filter type
+ ](http://www.w3.org/TR/PNG-Filters.html#Filter-type-2-Up).
+
+ ## Example
+
+ iex> filtered_row = <<127, 138, 255, 20, 21, 107>>
+ iex> prior_unfiltered_row = <<1, 77, 16, 235, 55, 170>>
+ iex> Imagineer.Image.PNG.Filter.Basic.Up.unfilter(filtered_row, prior_unfiltered_row)
+ <<128, 215, 15, 255, 76, 21>>
+
+ When the row being unfiltered is the first row, we pass in a binary of equal
+ length that is all null bytes (`<<0>>`.)
+
+ iex> filtered = <<1, 77, 16, 234, 234, 154>>
+ iex> prior_unfiltered_row = <<0, 0, 0, 0, 0, 0>>
+ iex> Imagineer.Image.PNG.Filter.Basic.Up.unfilter(filtered, prior_unfiltered_row)
+ <<1, 77, 16, 234, 234, 154>>
+ """
+ def unfilter(row, prior_row) do
+ unfilter(row, prior_row, [])
+ |> Enum.join
+ end
+
+ # In the base case, we'll have a reversed list of a bunch of unfiltered bytes
+ defp unfilter(<<>>, <<>>, unfiltered_bytes) do
+ Enum.reverse(unfiltered_bytes)
+ end
+
+ defp unfilter(
+ <>,
+ <>,
+ unfiltered_pixels)
+ do
+ unfiltered_byte = <>
+ unfilter(row_rest, prior_row_rest, [ unfiltered_byte | unfiltered_pixels])
+ end
+end
diff --git a/lib/imagineer/image/png/helpers.ex b/lib/imagineer/image/png/helpers.ex
new file mode 100644
index 0000000..8791781
--- /dev/null
+++ b/lib/imagineer/image/png/helpers.ex
@@ -0,0 +1,98 @@
+defmodule Imagineer.Image.PNG.Helpers do
+
+ @doc """
+ Given a PNG's color type and bit depth, returns its color format
+ """
+ def color_format(0, 1) , do: :grayscale1
+ def color_format(0, 2) , do: :grayscale2
+ def color_format(0, 4) , do: :grayscale4
+ def color_format(0, 8) , do: :grayscale8
+ def color_format(0, 16), do: :grayscale16
+ def color_format(2, 8) , do: :rgb8
+ def color_format(2, 16), do: :rgb16
+ def color_format(3, 1) , do: :palette1
+ def color_format(3, 2) , do: :palette2
+ def color_format(3, 4) , do: :palette4
+ def color_format(3, 8) , do: :palette8
+ def color_format(4, 8) , do: :grayscale_alpha8
+ def color_format(4, 16), do: :grayscale_alpha16
+ def color_format(6, 8) , do: :rgb_alpha8
+ def color_format(6, 16), do: :rgb_alpha16
+
+ @doc """
+ Given a color format and the width of an image, tells us how many bytes are
+ are present per scanline (a row of pixels).
+ """
+ def bytes_per_row(:grayscale1, width), do: div(width, 8)
+ def bytes_per_row(:grayscale2, width), do: div(width, 4)
+ def bytes_per_row(:grayscale4, width), do: div(width, 2)
+ def bytes_per_row(:grayscale8, width), do: width
+ def bytes_per_row(:grayscale16, width), do: width * 2
+ def bytes_per_row(:rgb8, width), do: width * 3
+ def bytes_per_row(:rgb16, width), do: width * 6
+ def bytes_per_row(:palette1, width), do: div(width, 8)
+ def bytes_per_row(:palette2, width), do: div(width, 4)
+ def bytes_per_row(:palette4, width), do: div(width, 2)
+ def bytes_per_row(:palette8, width), do: width
+ def bytes_per_row(:grayscale_alpha8, width), do: width * 2
+ def bytes_per_row(:grayscale_alpha16, width), do: width * 4
+ def bytes_per_row(:rgb_alpha8, width), do: width * 4
+ def bytes_per_row(:rgb_alpha16, width), do: width * 8
+
+ @doc """
+ Given a color format, tells us how many bytes are needed to store a pixel
+ """
+ def bytes_per_pixel(:grayscale1), do: 1
+ def bytes_per_pixel(:grayscale2), do: 1
+ def bytes_per_pixel(:grayscale4), do: 1
+ def bytes_per_pixel(:grayscale8), do: 1
+ def bytes_per_pixel(:grayscale16), do: 2
+ def bytes_per_pixel(:rgb8), do: 3
+ def bytes_per_pixel(:rgb16), do: 6
+ def bytes_per_pixel(:palette1), do: 1
+ def bytes_per_pixel(:palette2), do: 1
+ def bytes_per_pixel(:palette4), do: 1
+ def bytes_per_pixel(:palette8), do: 1
+ def bytes_per_pixel(:grayscale_alpha8), do: 2
+ def bytes_per_pixel(:grayscale_alpha16), do: 4
+ def bytes_per_pixel(:rgb_alpha8), do: 4
+ def bytes_per_pixel(:rgb_alpha16), do: 8
+
+ @doc """
+ Returns the number of channels for a given `color_format`. For example,
+ `:rgb8` and `:rbg16` have 3 channels: one for Red, Green, and Blue.
+ `:rgb_alpha8` and `:rgb_alpha16` each have 4 channels: one for Red, Green,
+ Blue, and the alpha (transparency) channel.
+ """
+ def channels_per_pixel(:palette1), do: 1
+ def channels_per_pixel(:palette2), do: 1
+ def channels_per_pixel(:palette4), do: 1
+ def channels_per_pixel(:palette8), do: 1
+ def channels_per_pixel(:grayscale1), do: 1
+ def channels_per_pixel(:grayscale2), do: 1
+ def channels_per_pixel(:grayscale4), do: 1
+ def channels_per_pixel(:grayscale8), do: 1
+ def channels_per_pixel(:grayscale16), do: 1
+ def channels_per_pixel(:grayscale_alpha8), do: 2
+ def channels_per_pixel(:grayscale_alpha16), do: 2
+ def channels_per_pixel(:rgb8), do: 3
+ def channels_per_pixel(:rgb16), do: 3
+ def channels_per_pixel(:rgb_alpha8), do: 4
+ def channels_per_pixel(:rgb_alpha16), do: 4
+
+
+ @doc """
+ Returns a binary consisting of `length` null (`<<0>>`) bytes
+ """
+ def null_binary(length) when length >= 0 do
+ null_binary(<<>>, length)
+ end
+
+ defp null_binary(null_bytes, 0) do
+ null_bytes
+ end
+
+ defp null_binary(null_bytes, length) do
+ null_binary(null_bytes <> <<0>>, length - 1)
+ end
+end
diff --git a/lib/imagineer/image/png/pixels.ex b/lib/imagineer/image/png/pixels.ex
new file mode 100644
index 0000000..d7c2b42
--- /dev/null
+++ b/lib/imagineer/image/png/pixels.ex
@@ -0,0 +1,87 @@
+defmodule Imagineer.Image.PNG.Pixels do
+ alias Imagineer.Image.PNG
+ import Imagineer.Image.PNG.Helpers, only: [channels_per_pixel: 1]
+
+ def load_row do
+
+ end
+
+ @doc """
+ Extracts the pixels from all of the unfiltered rows. Sets the `pixels` field
+ on the image and returns it.
+
+ ## Example
+ iex> alias Imagineer.Image.PNG
+ iex> image = %PNG{
+ ...> color_format: :rgb8,
+ ...> bit_depth: 8,
+ ...> unfiltered_rows: [
+ ...> {0, <<127, 138, 255, 147, 159, 106>>},
+ ...> {1, <<233, 1, 77, 78, 191, 144>>},
+ ...> {2, <<234, 78, 93, 56, 169, 42>>},
+ ...> {3, <<184, 162, 144, 6, 26, 96>>},
+ ...> {4, <<32, 206, 231, 39, 117, 76>>}
+ ...> ]
+ ...> }
+ iex> PNG.Pixels.extract(image).pixels
+ [
+ [ {127, 138, 255}, {147, 159, 106} ],
+ [ {233, 1, 77}, {78, 191, 144} ],
+ [ {234, 78, 93}, {56, 169, 42} ],
+ [ {184, 162, 144}, {6, 26, 96} ],
+ [ {32, 206, 231}, {39, 117, 76} ]
+ ]
+
+ """
+ def extract(%PNG{
+ unfiltered_rows: unfiltered_rows,
+ color_format: color_format,
+ bit_depth: bit_depth
+ }=image)
+ do
+ pixels = extract_pixels(unfiltered_rows, channels_per_pixel(color_format), bit_depth, [])
+ Map.put(image, :pixels, pixels)
+ end
+
+ defp extract_pixels([], _channels_per_pixel, _bit_depth, pixel_rows) do
+ Enum.reverse(pixel_rows)
+ end
+
+ defp extract_pixels([{_row_index, row} | unfiltered_rows], channels_per_pixel, bit_depth, pixel_rows) do
+ pixel_row = extract_pixels_from_row(row, channels_per_pixel, bit_depth)
+ extract_pixels(unfiltered_rows, channels_per_pixel, bit_depth, [pixel_row | pixel_rows])
+ end
+
+ defp extract_pixels_from_row(row, channels_per_pixel, bit_depth) do
+ extract_pixels_from_row(row, channels_per_pixel, bit_depth, [])
+ end
+
+ # In the base case, we have pulled everything from the row and are left with
+ # a reversed list of pixels
+ defp extract_pixels_from_row(<<>>, _channels_per_pixel, _bit_depth, pixels) do
+ Enum.reverse pixels
+ end
+
+ defp extract_pixels_from_row(row, channels_per_pixel, bit_depth, pixels) do
+ pixel_size = channels_per_pixel * bit_depth
+ <> = row
+ pixel = extract_pixel(pixel_bits, bit_depth, channels_per_pixel)
+ extract_pixels_from_row(rest_of_row, channels_per_pixel, bit_depth, [pixel | pixels])
+ end
+
+ defp extract_pixel(pixel_bits, bit_depth, channels_per_pixel) do
+ extract_pixel(pixel_bits, bit_depth, [], channels_per_pixel)
+ end
+
+ # In the base case, we have no more channels to parse and we are done!
+ defp extract_pixel(<<>>, _bit_depth, channel_list, 0) do
+ List.to_tuple Enum.reverse channel_list
+ end
+
+ defp extract_pixel(pixel_bits, bit_depth, channel_list, channels) do
+ remaining_channels = channels - 1
+ rest_size = bit_depth * remaining_channels
+ <> = pixel_bits
+ extract_pixel(rest, bit_depth, [channel | channel_list], remaining_channels)
+ end
+end
diff --git a/mix.exs b/mix.exs
index 78989a5..6bcf674 100644
--- a/mix.exs
+++ b/mix.exs
@@ -2,29 +2,49 @@ defmodule Imagineer.Mixfile do
use Mix.Project
def project do
- [app: :imagineer,
- version: "0.0.1",
- elixir: "~> 1.0",
- deps: deps]
+ [
+ app: :imagineer,
+ version: "0.2.0-dev",
+ elixir: "~> 1.0",
+ deps: deps,
+ source_url: "https://github.com/SenecaSystems/imagineer",
+ description: description,
+ package: package
+ ]
end
- # Configuration for the OTP application
- #
- # Type `mix help compile.app` for more information
def application do
[applications: [:logger]]
end
- # Dependencies can be Hex packages:
- #
- # {:mydep, "~> 0.3.0"}
- #
- # Or git/path repositories:
- #
- # {:mydep, git: "https://github.com/elixir-lang/mydep.git", tag: "0.1.0"}
- #
- # Type `mix help deps` for more examples and options
defp deps do
[]
end
+
+ defp description do
+ """
+ Image processing in Elixir
+ """
+ end
+
+ defp package do
+ [
+ contributors: contributors,
+ links: links,
+ licenses: ["MIT"]
+ ]
+ end
+
+ defp contributors do
+ [
+ "Chris Maddox"
+ ]
+ end
+
+ defp links do
+ %{
+ github: "https://github.com/SenecaSystems/imagineer"
+ }
+ end
+
end
diff --git a/test/imagineer/image/png/compression/zlib_test.exs b/test/imagineer/image/png/compression/zlib_test.exs
new file mode 100644
index 0000000..28aea71
--- /dev/null
+++ b/test/imagineer/image/png/compression/zlib_test.exs
@@ -0,0 +1,4 @@
+defmodule Imagineer.Image.PNG.Compression.ZlibTest do
+ use ExUnit.Case, async: true
+ doctest Imagineer.Image.PNG.Compression.Zlib
+end
diff --git a/test/imagineer/image/png/data_content_test.exs b/test/imagineer/image/png/data_content_test.exs
new file mode 100644
index 0000000..cf40e1e
--- /dev/null
+++ b/test/imagineer/image/png/data_content_test.exs
@@ -0,0 +1,4 @@
+defmodule Imagineer.Image.PNG.DataContentTest do
+ use ExUnit.Case, async: true
+ doctest Imagineer.Image.PNG.DataContent
+end
diff --git a/test/imagineer/image/png/filter/basic/average_text.exs b/test/imagineer/image/png/filter/basic/average_text.exs
new file mode 100644
index 0000000..a3ff663
--- /dev/null
+++ b/test/imagineer/image/png/filter/basic/average_text.exs
@@ -0,0 +1,4 @@
+defmodule Imagineer.Image.PNG.Filter.Basic.AverageTest do
+ use ExUnit.Case, async: true
+ doctest Imagineer.Image.PNG.Filter.Basic.Average
+end
diff --git a/test/imagineer/image/png/filter/basic/paeth_test.exs b/test/imagineer/image/png/filter/basic/paeth_test.exs
new file mode 100644
index 0000000..f30b12d
--- /dev/null
+++ b/test/imagineer/image/png/filter/basic/paeth_test.exs
@@ -0,0 +1,4 @@
+defmodule Imagineer.Image.PNG.Filter.Basic.PaethTest do
+ use ExUnit.Case, async: true
+ doctest Imagineer.Image.PNG.Filter.Basic.Paeth
+end
diff --git a/test/imagineer/image/png/filter/basic/sub_test.exs b/test/imagineer/image/png/filter/basic/sub_test.exs
new file mode 100644
index 0000000..57bc3a1
--- /dev/null
+++ b/test/imagineer/image/png/filter/basic/sub_test.exs
@@ -0,0 +1,4 @@
+defmodule Imagineer.Image.PNG.Filter.Basic.SubTest do
+ use ExUnit.Case, async: true
+ doctest Imagineer.Image.PNG.Filter.Basic.Sub
+end
diff --git a/test/imagineer/image/png/filter/basic/up_test.exs b/test/imagineer/image/png/filter/basic/up_test.exs
new file mode 100644
index 0000000..4d11a79
--- /dev/null
+++ b/test/imagineer/image/png/filter/basic/up_test.exs
@@ -0,0 +1,4 @@
+defmodule Imagineer.Image.PNG.Filter.Basic.UpTest do
+ use ExUnit.Case, async: true
+ doctest Imagineer.Image.PNG.Filter.Basic.Up
+end
diff --git a/test/imagineer/image/png/filter/basic_test.exs b/test/imagineer/image/png/filter/basic_test.exs
new file mode 100644
index 0000000..a2a05dc
--- /dev/null
+++ b/test/imagineer/image/png/filter/basic_test.exs
@@ -0,0 +1,33 @@
+defmodule Imagineer.Image.PNG.Filter.BasicTest do
+ use ExUnit.Case, async: true
+ alias Imagineer.Image.PNG
+ alias PNG.Filter.Basic, as: BasicFilter
+
+ test "unfiltering with no filter" do
+ scanlines = [<<0, 127, 138, 255, 20, 21, 107>>, <<0, 1, 77, 16, 234, 234, 154>>]
+ unfiltered_lines = %PNG{scanlines: scanlines, color_format: :rgb8, width: 2}
+ |> BasicFilter.unfilter()
+ assert unfiltered_lines ==
+ [{0, <<127, 138, 255, 20, 21, 107>>}, {1, <<1, 77, 16, 234, 234, 154>>}]
+
+ end
+
+ test "unfiltering with all kinds of filters!" do
+ scanlines = [
+ <<1, 127, 138, 255, 20, 21, 107>>, # Sub filter
+ <<0, 233, 1, 77, 78, 191, 144>>, # None filter
+ <<2, 1, 77, 16, 234, 234, 154>>, # Up filter
+ <<3, 67, 123, 98, 142, 117, 3>>, # Average filter
+ <<4, 104, 44, 87, 33, 91, 188>> # Paeth filter
+ ]
+ unfiltered_lines = BasicFilter.unfilter(%PNG{scanlines: scanlines, color_format: :rgb8, width: 2})
+ assert unfiltered_lines ==
+ [
+ {0, <<127, 138, 255, 147, 159, 106>>},
+ {1, <<233, 1, 77, 78, 191, 144>>},
+ {2, <<234, 78, 93, 56, 169, 42>>},
+ {3, <<184, 162, 144, 6, 26, 96>>},
+ {4, <<32, 206, 231, 39, 117, 76>>}
+ ]
+ end
+end
diff --git a/test/imagineer/image/png/pixels_test.exs b/test/imagineer/image/png/pixels_test.exs
new file mode 100644
index 0000000..2791cd8
--- /dev/null
+++ b/test/imagineer/image/png/pixels_test.exs
@@ -0,0 +1,4 @@
+defmodule Imagineer.Image.PNG.PixelsTest do
+ use ExUnit.Case, async: true
+ doctest Imagineer.Image.PNG.Pixels
+end
diff --git a/test/imagineer/image_process_test.exs b/test/imagineer/image_process_test.exs
index 99c7dba..b3c4701 100644
--- a/test/imagineer/image_process_test.exs
+++ b/test/imagineer/image_process_test.exs
@@ -1,14 +1,11 @@
defmodule Imagineer.ImageProcessTest do
use ExUnit.Case, async: true
- alias Imagineer.Image
# Until ExUnit has contexts, we don't want to load all images on every `setup`
test "it parses the alpaca" do
- image = %Image{uri: "./test/support/images/alpaca.png"} |>
- Image.load() |>
- Image.process()
+ {:ok, image} = Imagineer.load("./test/support/images/png/alpaca.png")
- {:ok, raw_file} = File.read(image.uri)
+ {:ok, raw_file} = File.read("./test/support/images/png/alpaca.png")
assert raw_file == image.raw, "it should set the file's contents into `raw`"
assert image.format == :png, "it should set the image format"
@@ -20,26 +17,30 @@ defmodule Imagineer.ImageProcessTest do
# It should set the color format, color type, and bit_depth
assert image.bit_depth == 8, "it should set the bit depth"
assert image.color_format == :rgb8, "it should set the color format"
- assert image.attributes.color_type == 2, "it should set the color type"
+ assert image.color_type == 2, "it should set the color type"
assert image.attributes.pixel_dimensions == {5669, 5669, :meter},
"it should set the pixel dimensions"
+ # it should parse out rows of pixels equal to the height
+ assert length(image.pixels) == image.height
+
+ # each row of pixel should be equal to the width in length
+ Enum.each(image.pixels, fn (pixel_row) -> assert length(pixel_row) == image.width end)
+
xmp_text_chunk = "\n \n \n 96\n 96\n \n \n\n"
assert image.attributes[:"XML:com.adobe.xmp"] == xmp_text_chunk,
"it should pull arbitrary text chunks into attributes"
end
test "it parses the baby octopus" do
- image = %Image{uri: "./test/support/images/baby_octopus.png"} |>
- Image.load() |>
- Image.process()
+ {:ok, image} = Imagineer.load("./test/support/images/png/baby_octopus.png")
- {:ok, raw_file} = File.read(image.uri)
+ {:ok, raw_file} = File.read("./test/support/images/png/baby_octopus.png")
assert raw_file == image.raw, "it should set the file's contents into `raw`"
end
-# Image: test/support/images/black.jpg
+# Image: test/support/images/jpg/black.jpg
# Format: JPEG (Joint Photographic Experts Group JFIF format)
# Mime type: image/jpeg
# Class: DirectClass
@@ -92,7 +93,7 @@ defmodule Imagineer.ImageProcessTest do
# jpeg:sampling-factor: 2x1,1x1,1x1
# signature: 3ae28ca36ded757756bbc483c35afe741edcf15b9abaaa5695c6a80640a08702
# Artifacts:
-# filename: test/support/images/black.jpg
+# filename: test/support/images/jpg/black.jpg
# verbose: true
# Tainted: True
# Filesize: 1.24KB
@@ -103,9 +104,7 @@ defmodule Imagineer.ImageProcessTest do
# Version: ImageMagick 6.8.9-8 Q16 x86_64 2014-12-22 http://www.imagemagick.org
test "it can parse a jpg" do
- image = %Imagineer.Image{uri: "./test/support/images/black.jpg"} |>
- Imagineer.Image.load() |>
- Imagineer.Image.process()
+ {:ok, image} = Imagineer.load("./test/support/images/jpg/black.jpg")
assert image.format == :jpg
end
end
diff --git a/test/imagineer_test.exs b/test/imagineer_test.exs
index 702659b..7c0fe21 100644
--- a/test/imagineer_test.exs
+++ b/test/imagineer_test.exs
@@ -1,5 +1,15 @@
defmodule ImagineerTest do
use ExUnit.Case
+ test "loading a non-existent file" do
+ uri = "./404.png"
+ error_message = "Could not open #{uri} because the file does not exist."
+ {:error, ^error_message} = Imagineer.load(uri)
+ end
+ test "loading a file of unknown type" do
+ uri = "./test/support/images/gif/alpaca.gif"
+ error_message = "Unknown or unsupported image format."
+ {:error, ^error_message} = Imagineer.load(uri)
+ end
end
diff --git a/test/support/images/gif/alpaca.gif b/test/support/images/gif/alpaca.gif
new file mode 100644
index 0000000..fdabc28
Binary files /dev/null and b/test/support/images/gif/alpaca.gif differ
diff --git a/test/support/images/black.jpg b/test/support/images/jpg/black.jpg
similarity index 100%
rename from test/support/images/black.jpg
rename to test/support/images/jpg/black.jpg
diff --git a/test/support/images/drowning_girl.jpg b/test/support/images/jpg/drowning_girl.jpg
similarity index 100%
rename from test/support/images/drowning_girl.jpg
rename to test/support/images/jpg/drowning_girl.jpg
diff --git a/test/support/images/alpaca.png b/test/support/images/png/alpaca.png
similarity index 100%
rename from test/support/images/alpaca.png
rename to test/support/images/png/alpaca.png
diff --git a/test/support/images/baby_octopus.png b/test/support/images/png/baby_octopus.png
similarity index 100%
rename from test/support/images/baby_octopus.png
rename to test/support/images/png/baby_octopus.png
diff --git a/test/support/images/png/seneca-logo.png b/test/support/images/png/seneca-logo.png
new file mode 100644
index 0000000..cb75300
Binary files /dev/null and b/test/support/images/png/seneca-logo.png differ