Packages
image
0.18.0
0.72.0
0.71.0
0.70.0
0.69.0
0.68.0
0.67.0
0.67.0-dev
retired
0.66.0
0.65.0
0.64.0
0.63.0
0.62.1
0.62.0
0.61.1
0.61.0
0.60.0
0.59.3
0.59.2
0.59.1
0.59.0
0.58.0
0.57.0
0.56.1
0.56.0
0.55.2
0.55.1
retired
0.55.0
0.54.4
0.54.3
0.54.2
0.54.1
0.54.0
0.53.0
0.52.3
0.52.2
0.52.1
0.52.0
retired
0.51.0
0.50.0
0.49.0
0.48.1
0.48.0
0.47.0
0.46.0
0.45.0
0.44.0
0.43.2
0.43.1
0.43.0
0.42.0
0.41.0
0.40.0
0.39.3
0.39.2
0.39.1
0.39.0
0.38.4
0.38.3
0.38.2
0.38.1
0.38.0
0.37.0
0.36.2
0.36.1
0.36.0
0.35.0
0.34.0
0.33.0
0.32.0
0.31.1
0.31.0
0.30.0
0.29.0
0.28.2
0.28.1
0.28.0
0.27.0
0.26.0
0.25.1
0.25.0
0.24.1
0.24.0
0.23.2
0.23.1
0.23.0
0.22.1
0.22.0
0.21.0
0.20.0
retired
0.19.0
0.18.1
0.18.0
0.17.0
0.16.0
0.15.0
0.14.4
0.14.2
0.14.1
0.14.0
retired
0.13.1
0.13.0
0.12.0
0.11.0
0.10.0
0.10.0-rc.0
retired
0.9.0
retired
0.8.0
0.7.0
0.6.0
0.5.0
0.4.0
0.3.0
0.2.0
0.1.0
An approachable image processing library primarily based upon Vix and libvips that is NIF-based, fast, multi-threaded, pipelined and has a low memory footprint.
Current section
Files
Jump to
Current section
Files
lib/image/shape.ex
defmodule Image.Shape do
@moduledoc """
Functions to render a shape as an image. The supported shapes match
those defined in [Scalable Vector Graphics](https://developer.mozilla.org/en-US/docs/Web/SVG)
including:
* Polygon
* Circle
* Ellipse
* Line
"""
alias Vix.Vips.Image, as: Vimage
alias Vix.Vips.Operation
@typedoc """
A point is a list of two integers
representing the `x` and `y` coordinates
"""
@type point :: [integer()]
@typedoc """
A path is a list of points representing
a path, open polygon or closed polygon.
"""
@type path :: String.t() | [point(), ...]
@default_width 500
@default_radius 100
@default_rotation 180
@default_star_points 5
@default_star_inner_radius 60
@default_star_outer_radius 150
@default_star_rotation 0
@doc """
Creates an image of a polygon as a single
band image on a transparent background.
### Arguments
* `points` defines the points of the polygon. The
origin is the top left of the image with a positive
`x` value moving from right to left and a positive
`y` value moving from top to bottom. The points can
be an [SVG point string](https://developer.mozilla.org/en-US/docs/Web/SVG/Attribute/points)
or a "list of lists" of the form
`[[x1, y1], [x2, y2], ...]` where `x1` and `y1`
are integers. `points` can also be a positive
integer >= 3 which indicates that an `n` sided
polygon will be generated. In this case the options
`:rotation` and `:radius` are also applicable.
* `options` is a `t:Keyword.t/0` list of options.
### Options
* `:width` is the width of the canvas onto which the
polygon is drawn. The default is `500` pixels.
* `:height` is the width of the canvas onto which the
polygon is drawn. The default is `500` pixels.
* `:fill_color` is the color used to fill in the
polygon. The default is `:none`.
* `:stroke_color` is the color used for the outline
of the polygon. The default is `:black`
* `:opacity` is the opacity as a float between
`0.0` and `1.0` where `0.0` is completely transparent
and `1.0` is completely opaque. The default is `0.7`.
* `:rotation` is the number of degrees to rotate the
axis of a generated n-sided polygon. This option is
only valid if `points` is an integer >= 3.
The default is `#{@default_rotation}`.
* `:radius` indicates the radius in pixels of a generated
n-sided polygon. The default is `#{@default_radius}`.
### Notes
* The polygon points are scaled to fit the canvas size
defined by `:width` and `:height` This means that the
resulting image will fill the canvas. This is useful
for composing images. Define the canvas to be the size
intended to be composed into a base image and the
polygon will be scaled to fit.
* Colors may be any valid
[CSS color name](https://www.w3.org/wiki/CSS/Properties/color/keywords) or
a six hexadecimal digit string prefixed with `#`. For example
`#FF00FF` for the color "Fuchsia".
### Returns
* `{:ok, image}` or
* `{:error, reason}`
### Examples
"""
@spec polygon(points :: path(), options :: Keyword.t()) ::
{:ok, Vimage.t()} | {:error, Image.error_message()}
def polygon(points, options \\ [])
def polygon(points, options) when is_binary(points) do
points
|> points_to_path()
|> polygon(options)
end
def polygon(points, options) when is_list(points) and is_list(options) do
with {:ok, options} <- Image.Options.Shape.validate_polygon_options(options) do
polygon(points, options)
end
end
def polygon(points, %{} = options) when is_list(points) do
{width, height} = dimensions_from(points, options[:width], options[:height])
points =
points
|> rescale(0, width, 0, height)
|> format_points()
svg = """
<svg width="#{width}px" height="#{height}px">
<style type="text/css">
svg polygon {
fill: #{options.fill_color};
stroke: #{options.stroke_color};
stroke-width: #{options.stroke_width};
opacity: #{options.opacity};
}
</style>
<polygon points="#{points}" />
</svg>
"""
case Operation.svgload_buffer(svg) do
{:ok, {polygon, _flags}} -> {:ok, polygon}
{:error, reason} -> {:error, reason}
end
end
@spec polygon(sides :: pos_integer(), options :: Keyword.t()) ::
{:ok, Vimage.t()} | {:error, Image.error_message()}
def polygon(sides, options) when is_integer(sides) and sides > 2 do
{radius, options} = Keyword.pop(options, :radius, @default_radius)
{rotation, options} = Keyword.pop(options, :rotation, @default_rotation)
segment = :math.pi() * 2 / sides
rotation = rotation * :math.pi() / 180
for side <- 1..sides do
[
:math.sin(segment * side + rotation) * radius,
:math.cos(segment * side + rotation) * radius
]
end
|> polygon(options)
end
defp dimensions_from(points, nil, nil) do
aspect_ratio = aspect_ratio(points)
{@default_width, round(@default_width / aspect_ratio)}
end
defp dimensions_from(_points, width, height) when is_integer(width) and is_integer(height) do
{width, height}
end
defp dimensions_from(points, width, nil) when is_integer(width) do
aspect_ratio = aspect_ratio(points)
{width, round(width / aspect_ratio)}
end
defp dimensions_from(points, nil, height) when is_integer(height) do
aspect_ratio = aspect_ratio(points)
{round(height * aspect_ratio), height}
end
@doc """
Creates an image of a polygon as a single
band image on a transparent background.
### Arguments
* `points` defines the points of the polygon. The
origin is the top left of the image with a positive
`x` value moving from right to left and a positive
`y` value moving from top to bottom. The points can
be an [SVG point string](https://developer.mozilla.org/en-US/docs/Web/SVG/Attribute/points)
or a "list of lists" of the form
`[[x1, y1], [x2, y2], ...]` where `x1` and `y1`
are integers.
* `options` is a `t:Keyword.t/0` list of options.
### Options
* `:width` is the width of the canvas onto which the
polygon is drawn. The default is `500` pixels.
* `:height` is the width of the canvas onto which the
polygon is drawn. The default is `500` pixels.
* `:fill_color` is the color used to fill in the
polygon. The default is `:none`.
* `:stroke_color` is the color used for the outline
of the polygon. The default is `:black`
* `:opacity` is the opacity as a float between
`0.0` and `1.0` where `0.0` is completely transparent
and `1.0` is completely opaque. The default is `0.7`.
### Notes
* The polygon points are scaled to fit the canvas size
defined by `:width` and `:height` This means that the
resulting image will fill the canvas. This is useful
for composing images. Define the canvas to be the size
intended to be composed into a base image and the
polygon will be scaled to fit.
* Colors may be any valid
[CSS color name](https://www.w3.org/wiki/CSS/Properties/color/keywords) or
a six hexadecimal digit string prefixed with `#`. For example
`#FF00FF` for the color "Fuchsia".
### Returns
* `image` or
* raises an exception
### Examples
"""
@spec polygon!(points :: path(), options :: Keyword.t()) ::
Vimage.t() | no_return()
def polygon!(points, options \\ []) do
case polygon(points, options) do
{:ok, image} -> image
{:error, reason} -> raise Image.Error, reason
end
end
@doc """
Returns an image of an n-pointed star that
can be composed over other images.
### Arguments
* `points` is an integer number of points
on the star. `points` must be >= 3. The default
is `#{@default_star_points}`.
* `options` is a `t:Keyword.t/0` list of options.
### Options
* `:inner_radius` is the size of the inner
radius. The default is `#{@default_star_inner_radius}`.
* `:outer_radius` is the size of the outer
radius. The default is `#{@default_star_outer_radius}`.
* `:rotation` is the angle in degrees of rotation
applied to the points. The default is `#{@default_star_rotation}`.
* Any remaining options are passed to `Image.Shape.polygon/2`.
### Returns
* `{:ok, image}` or
* `{:error, reason}`
### Examples
#=> {:ok, star} = Image.Shape.star
#=> {:ok, star} = Image.Shape.star 5, rotation: 90, fill_color: :red, stroke_color: :green
"""
@spec star(points :: pos_integer(), options :: Keyword.t()) ::
{:ok, Vimage.t()} | {:error, Image.error_message()}
def star(points \\ @default_star_points, options \\ []) when points > 3 do
{inner_radius, options} = Keyword.pop(options, :inner_radius, @default_star_inner_radius)
{outer_radius, options} = Keyword.pop(options, :outer_radius, @default_star_outer_radius)
{rotation, options} = Keyword.pop(options, :rotation, @default_star_rotation)
rotation = rotation * :math.pi() / 180
Enum.reduce(1..points, [], fn point, polygon ->
inner_angle = 2 * :math.pi() * point / points
outer_angle = inner_angle + :math.pi() / points
inner_angle = inner_angle + rotation
outer_angle = outer_angle + rotation
inner = [
inner_radius * :math.cos(inner_angle),
inner_radius * :math.sin(inner_angle)
]
outer = [
outer_radius * :math.cos(outer_angle),
outer_radius * :math.sin(outer_angle)
]
[outer, inner | polygon]
end)
|> polygon(options)
end
@doc """
Returns an image of an n-pointed star that
can be composed over other images.
### Arguments
* `points` is an integer number of points
on the star. `points` must be >= 3. The default
is `#{@default_star_points}`.
* `options` is a `t:Keyword.t/0` list of options.
### Options
* `:inner_radius` is the size of the inner
radius. The default is `#{@default_star_inner_radius}`.
* `:outer_radius` is the size of the outer
radius. The default is `#{@default_star_inner_radius}`.
* `:rotation` is the angle in degrees of rotation
applied to the points. The default is `#{@default_star_rotation}`.
* Any remaining options are passed to `Image.Shape.polygon/2`.
### Returns
* `image` or
* raises an exception
### Examples
#=> star = Image.Shape.star!
#=> star = Image.Shape.star! 5, rotation: 90, fill_color: :red, stroke_color: :green
"""
@spec star!(points :: pos_integer(), options :: Keyword.t()) :: Vimage.t() | no_return()
def star!(points \\ @default_star_points, options \\ []) do
case star(points, options) do
{:ok, image} -> image
{:error, reason} -> raise Image.Error, reason
end
end
### Helpers
defp format_points(points) do
points
|> List.flatten()
|> Enum.join(" ")
end
@doc false
def rescale(unscaled, from_min, from_max, to_min, to_max) when is_number(unscaled) do
round((to_max - to_min) * (unscaled - from_min) / (from_max - from_min) + to_min)
end
@doc false
def rescale(polygon, x_min, x_max, y_min, y_max, scale \\ nil) when is_list(polygon) do
{from_x_min, from_x_max, from_y_min, from_y_max} = scale || polygon_scale(polygon)
for [x, y] <- polygon do
[
rescale(x, from_x_min, from_x_max, x_min, x_max),
rescale(y, from_y_min, from_y_max, y_min, y_max)
]
end
end
@doc false
def rescale(polygon, %Vimage{} = image) when is_list(polygon) do
{from_x_min, from_x_max, from_y_min, from_y_max} = polygon_scale(polygon)
aspect_ratio = (from_x_max - from_x_min) / (from_y_max - from_y_min)
width = Image.width(image)
height = round(width * aspect_ratio)
min = 0
rescale(polygon, min, width, min, height, {from_x_min, from_x_max, from_y_min, from_y_max})
end
@doc false
def aspect_ratio(%Vimage{} = image) do
Image.width(image) / Image.height(image)
end
def aspect_ratio(polygon) when is_list(polygon) do
{from_x_min, from_x_max, from_y_min, from_y_max} = polygon_scale(polygon)
(from_x_max - from_x_min) / (from_y_max - from_y_min)
end
defp polygon_scale(polygon) do
Enum.reduce(polygon, {10_000_000, -10_000_000, 10_000_000, -10_000_000}, fn
[x, y], {x_min, x_max, y_min, y_max} ->
x_min = min(x, x_min)
x_max = max(x, x_max)
y_min = min(y, y_min)
y_max = max(y, y_max)
{x_min, x_max, y_min, y_max}
end)
end
defp points_to_path(points) when is_binary(points) do
points
|> String.split([",", " ", "\n"], trim: true)
|> Enum.map(&String.to_integer/1)
|> Enum.chunk_every(2)
end
end