Skip to content
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
12 changes: 6 additions & 6 deletions src/ansel.ex
Original file line number Diff line number Diff line change
Expand Up @@ -5,17 +5,17 @@ defmodule Ansel do
Path.expand(path) |> Image.new_from_file()
end

def write_to_file(img, path) do
save_path = Path.expand(path)
def write_to_file(img, path, {:format_components, extension, options}) do
save_path = Path.expand(path <> extension)

case Image.write_to_file(img, save_path) do
:ok -> {:ok, nil}
case Image.write_to_file(img, save_path <> options) do
:ok -> {:ok, save_path}
{:error, reason} -> {:error, reason}
end
end

def to_bit_array(image, format) do
Image.write_to_stream(image, format) |> Enum.into(<<>>)
def to_bit_array(image, {:format_components, extension, options}) do
Image.write_to_stream(image, extension <> options) |> Enum.into(<<>>)
end

def to_rgb_list(image) do
Expand Down
79 changes: 50 additions & 29 deletions src/ansel/image.gleam
Original file line number Diff line number Diff line change
Expand Up @@ -42,9 +42,11 @@ import snag
/// own vips binary on the host system to. See the package readme for details.
///
/// The `Custom` constructor allows for advanced vips save options to be
/// used, like `ansel.Custom(".png[compression=90,squash=true]"), refer to the
/// [Vix package documentation](https://hexdocs.pm/vix/Vix.Vips.Image.html#write_to_file/2)
/// for details.
/// used as a comma separated list, like `ansel.Custom(".png", "compression=90,squash=true")`,
/// which is equivalent to the `.png[compression=90,squash=true]` syntax used in vips.
///
/// You can use the libvips CLI to print a list of all supported options for each image format,
/// e.g. `vips pngsave`, `vips jpegsave`.
pub type ImageFormat {
JPEG(quality: Int, keep_metadata: Bool)
JPEG2000(quality: Int, keep_metadata: Bool)
Expand All @@ -65,38 +67,43 @@ pub type ImageFormat {
Analyze
NIfTI
DeepZoom
Custom(format: String)
Custom(extension: String, format: String)
}

type FormatComponents {
FormatComponents(extension: String, options: String)
}

fn image_format_to_string(format: ImageFormat) -> String {
fn image_format_to_string(format: ImageFormat) -> FormatComponents {
case format {
JPEG(quality:, keep_metadata:) ->
".jpeg" <> format_common_options(quality, keep_metadata)
FormatComponents(".jpeg", format_common_options(quality, keep_metadata))
JPEG2000(quality:, keep_metadata:) ->
".jp2" <> format_common_options(quality, keep_metadata)
FormatComponents(".jp2", format_common_options(quality, keep_metadata))
JPEGXL(quality:, keep_metadata:) ->
".jxl" <> format_common_options(quality, keep_metadata)
PNG -> ".png"
FormatComponents(".jxl", format_common_options(quality, keep_metadata))
PNG -> FormatComponents(".png", "")
WebP(quality:, keep_metadata:) ->
".webp" <> format_common_options(quality, keep_metadata)
FormatComponents(".webp", format_common_options(quality, keep_metadata))
AVIF(quality:, keep_metadata:) ->
".avif" <> format_common_options(quality, keep_metadata)
FormatComponents(".avif", format_common_options(quality, keep_metadata))
TIFF(quality:, keep_metadata:) ->
".tiff" <> format_common_options(quality, keep_metadata)
FormatComponents(".tiff", format_common_options(quality, keep_metadata))
HEIC(quality:, keep_metadata:) ->
".heic" <> format_common_options(quality, keep_metadata)
FITS -> ".fits"
Matlab -> ".mat"
PDF -> ".pdf"
SVG -> ".svg"
HDR -> ".hdr"
PPM -> ".ppm"
CSV -> ".csv"
GIF -> ".gif"
Analyze -> ".analyze"
NIfTI -> ".nii"
DeepZoom -> ".dzi"
Custom(format:) -> format
FormatComponents(".heic", format_common_options(quality, keep_metadata))
FITS -> FormatComponents(".fits", "")
Matlab -> FormatComponents(".mat", "")
PDF -> FormatComponents(".pdf", "")
SVG -> FormatComponents(".svg", "")
HDR -> FormatComponents(".hdr", "")
PPM -> FormatComponents(".ppm", "")
CSV -> FormatComponents(".csv", "")
GIF -> FormatComponents(".gif", "")
Analyze -> FormatComponents(".analyze", "")
NIfTI -> FormatComponents(".nii", "")
DeepZoom -> FormatComponents(".dzi", "")
Custom(extension:, format:) ->
FormatComponents(extension, "[" <> format <> "]")
}
}

Expand Down Expand Up @@ -173,7 +180,7 @@ pub fn to_bit_array(img: ansel.Image, format: ImageFormat) -> BitArray {
}

@external(erlang, "Elixir.Ansel", "to_bit_array")
fn to_bit_array_ffi(img: ansel.Image, format: String) -> BitArray
fn to_bit_array_ffi(img: ansel.Image, format: FormatComponents) -> BitArray

/// Converts a vips image into a matrix of pixel values.
///
Expand Down Expand Up @@ -491,18 +498,32 @@ pub fn scale(
fn resize_ffi(img: ansel.Image, scale: Float) -> Result(ansel.Image, String)

/// Writes an image to the specified path in the specified format.
/// A filename extension is added automatically to the path, based on the format,
/// and therefore should not be provided by the user.
/// Returns the absolute path to the image as it was written to disk, including the extension.
///
/// ## Example
/// ```gleam
/// image.new(6, 6, color.Olive)
/// |> image.write(to: "wobble", in: image.PNG)
/// // -> Ok("Users/lucy/myproject/wobble.png")
/// ```
pub fn write(
image img: ansel.Image,
to path: String,
in format: ImageFormat,
) -> Result(Nil, snag.Snag) {
write_ffi(img, path <> image_format_to_string(format))
) -> Result(String, snag.Snag) {
write_ffi(img, path, image_format_to_string(format))
|> result.map_error(snag.new)
|> snag.context("Unable to write image to file")
}

@external(erlang, "Elixir.Ansel", "write_to_file")
fn write_ffi(img: ansel.Image, to path: String) -> Result(Nil, String)
fn write_ffi(
img: ansel.Image,
to path: String,
using format: FormatComponents,
) -> Result(String, String)

/// Reads an image from the specified path.
pub fn read(from path: String) -> Result(ansel.Image, snag.Snag) {
Expand Down
32 changes: 32 additions & 0 deletions test/image_test.gleam
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import ansel/color
import ansel/image
import gleam/list
import gleam/result
import gleam/string
import gleeunit
import gleeunit/should
import simplifile
Expand All @@ -23,6 +24,37 @@ pub fn read_test() {
|> should.equal(6)
}

pub fn write_test() {
let path = "test/tmp_write_test_asset"

let output_path =
image.new(5, 5, color.GleamLucy)
|> should.be_ok
|> image.write(path, image.JPEG(quality: 100, keep_metadata: True))
|> should.be_ok()

string.ends_with(output_path, "jpeg")
|> should.be_true()

image.read(output_path)
|> should.be_ok
|> image.get_width
|> should.equal(5)

let assert Ok(_) = simplifile.delete(output_path)
}

pub fn custom_options_test() {
let assert Ok(reference_image) = image.new(6, 6, color.Grey)
let output =
reference_image
|> image.to_bit_array(image.JPEG(quality: 50, keep_metadata: True))

reference_image
|> image.to_bit_array(image.Custom(".jpeg", "Q=50,strip=false"))
|> should.equal(output)
}

pub fn new_solid_grey_test() {
let assert Ok(bin) =
simplifile.read_bits("test/resources/solid_grey_6x6.avif")
Expand Down