diff --git a/README.md b/README.md index de347bf..280fa61 100644 --- a/README.md +++ b/README.md @@ -28,6 +28,13 @@ The following QR formats are supported: - base64 PNG (`:png64`) - base64 JPG (`:jpg64`) +### Options + +- width: width in pixels (default: 200) +- height: height in pixels (default: 200) +- size: shorthand for width and height (default: 200) +- ec: [error correction level](https://docs.rs/qrcode/0.6.0/qrcode/types/enum.EcLevel.html#variants) `:l`, `:m`, `:q`, `:h` (default: :m) + ### SVG ```elixir @@ -68,6 +75,12 @@ File.write("./assets/qr.jpg", binary) | PNG64 | [ sample ](assets/base65.html) | | JPG64 | -- | +Error correction: + +| L | M | Q | H | +| -------------------------- | -------------------------- | -------------------------- | -------------------------- | +| ![ l ](assets/qr_ec_l.jpg) | ![ m ](assets/qr_ec_m.jpg) | ![ q ](assets/qr_ec_q.jpg) | ![ h ](assets/qr_ec_h.jpg) | + ## Benchmarks Benchmarks have been included to compare Qrusty (Rust based) to [EQRCode](https://github.com/SiliconJungles/eqrcode) (Elixir based), as it's the defacto Elixir QR Code library. @@ -106,11 +119,8 @@ You will need the following installed: - Rust - [Cross](https://github.com/cross-rs/cross) -### Compiling QRusty - -``` -mix compile.local -``` +1. Increment the version in mix.exs so that the build on github can no longer be found +2. Compile locally: `mix compile.local` or `QRUSTY_BUILD=true iex -S mix` ## Contributions diff --git a/assets/qr_ec_h.jpg b/assets/qr_ec_h.jpg new file mode 100644 index 0000000..befe6ae Binary files /dev/null and b/assets/qr_ec_h.jpg differ diff --git a/assets/qr_ec_l.jpg b/assets/qr_ec_l.jpg new file mode 100644 index 0000000..e82b3d4 Binary files /dev/null and b/assets/qr_ec_l.jpg differ diff --git a/assets/qr_ec_m.jpg b/assets/qr_ec_m.jpg new file mode 100644 index 0000000..7b6ac96 Binary files /dev/null and b/assets/qr_ec_m.jpg differ diff --git a/assets/qr_ec_q.jpg b/assets/qr_ec_q.jpg new file mode 100644 index 0000000..1b0e183 Binary files /dev/null and b/assets/qr_ec_q.jpg differ diff --git a/lib/qrusty.ex b/lib/qrusty.ex index 85e778a..ee4e12d 100644 --- a/lib/qrusty.ex +++ b/lib/qrusty.ex @@ -16,10 +16,16 @@ defmodule Qrusty do - base64 PNG (`:png64`) - base64 JPG (`:jpg64`) + ### Options + - *width*: width in pixels (default: 200) + - *height*: height in pixels (default: 200) + - *size*: shorthand for width and height (default: 200) + - *ec*: [error correction level](https://docs.rs/qrcode/0.6.0/qrcode/types/enum.EcLevel.html#variants) `:l`, `:m`, `:q`, `:h` (default: :m) + ### SVG ```elixir - > {:ok, %Qrusty.QR{encoded_data: svg}} = Qrusty.qr("https://elixir-lang.org/", :svg, size: 200) + > {:ok, %Qrusty.QR{encoded_data: svg}} = Qrusty.qr("https://elixir-lang.org/", :svg, size: 200, ec: :h) File.write("./assets/qr.svg", svg) ``` @@ -48,18 +54,25 @@ defmodule Qrusty do ``` - | Format | Sample | - | ------ | ----------------------------------------------------------------------- | - | SVG | ![ svg ](assets/qr.svg) | - | PNG | ![ png ](assets/qr.png) | - | JPG | ![ jpg ](assets/qr.jpg) | - | PNG64 | [ sample ](assets/base64.html) | - | JPG64 | -- | + | Format | Sample | + | ------ | --------------------------------- | + | SVG | ![ svg ](assets/qr.svg) | + | PNG | ![ png ](assets/qr.png) | + | JPG | ![ jpg ](assets/qr.jpg) | + | PNG64 | [ sample ](assets/base64.html) | + | JPG64 | -- | + + Error correction: + + | L | M | Q | H | + | -------------------------- |--------------------------- | -------------------------- | -------------------------- | + | ![ l ](assets/qr_ec_l.jpg) | ![ m ](assets/qr_ec_m.jpg) | ![ q ](assets/qr_ec_q.jpg) | ![ h ](assets/qr_ec_h.jpg) | """ alias Qrusty.QR @default_size 200 + @default_ec :m import Keyword, only: [get: 2, get: 3] @@ -73,19 +86,46 @@ defmodule Qrusty do ] | [] + defmodule Error do + defexception message: "a error has occured" + + @impl true + def exception(reason) when is_atom(reason), do: exception(Atom.to_string(reason)) + def exception(reason), do: %__MODULE__{message: reason} + end + @doc """ Generate a QR code. ## Example - iex> Qrusty.qr("https://elixir-lang.org/", size: 100); + iex> Qrusty.qr("https://elixir-lang.org/", :svg, size: 100); {:ok, %QR{}} - iex> Qrusty.qr("https://elixir-lang.org/", width: 100, height: 100); + iex> Qrusty.qr("https://elixir-lang.org/", :png, width: 100, height: 100); {:ok, %QR{}} """ @spec qr(data, format, opts) :: {:ok, %QR{}} | {:error, any()} def qr(data, format, opts \\ []) do - QR.new(data, format, width(opts), height(opts)) + QR.new(data, format, width(opts), height(opts), error_correction(opts)) + end + + @doc """ + Generate a QR code. + + ## Example + iex> Qrusty.qr!("https://elixir-lang.org/", :svg, size: 100); + "..." + + Raises `Qrusty.Error` if the input is invalid + + """ + @spec qr!(data, format, opts) :: binary() + def qr!(data, format, opts \\ []) do + QR.new(data, format, width(opts), height(opts), error_correction(opts)) + |> case do + {:ok, %{encoded_data: qr}} -> qr + {:error, reason} -> raise Error, reason + end end defp width(opts) do @@ -95,4 +135,6 @@ defmodule Qrusty do defp height(opts) do get(opts, :height) || get(opts, :size, @default_size) end + + defp error_correction(opts), do: get(opts, :ec, @default_ec) end diff --git a/lib/qrusty/native.ex b/lib/qrusty/native.ex index b874e02..e6675ed 100644 --- a/lib/qrusty/native.ex +++ b/lib/qrusty/native.ex @@ -1,3 +1,10 @@ +defmodule Qrusty.Native.Options do + defstruct format: :svg, + width: 200, + height: 200, + error_correction: :m +end + defmodule Qrusty.Native do @moduledoc """ Generates QR Codes by executing a Rust NIF. @@ -15,29 +22,30 @@ defmodule Qrusty.Native do force_build: System.get_env("QRUSTY_BUILD") in ["1", "true"], version: version - @doc false - def generate(data, :svg, w, h), do: svg_nif(data, w, h) - - def generate(data, :png, w, h), do: png_nif(data, w, h) - - def generate(data, :png64, w, h), do: png64_nif(data, w, h) - - def generate(data, f, w, h) when f in ~w(jpg jpeg)a, do: jpg_nif(data, w, h) - - def generate(data, f, w, h) when f in ~w(jpg64 jpeg64)a, do: jpg64_nif(data, w, h) + alias Qrusty.Native.Options @doc false - def svg_nif(_data, _w, _h), do: :erlang.nif_error(:nif_not_loaded) + def generate(data, :svg, w, h, ec) do + opts = %Options{format: :svg, width: w, height: h, error_correction: ec} + svg_nif(data, opts) + end - @doc false - def png_nif(_data, _w, _h), do: :erlang.nif_error(:nif_not_loaded) + def generate(data, f, w, h, ec) when f in ~w(png64 jpg64 jpeg64)a do + opts = %Options{format: f, width: w, height: h, error_correction: ec} + image_base64_nif(data, opts) + end + + def generate(data, f, w, h, ec) when f in ~w(png jpg jpeg)a do + opts = %Options{format: f, width: w, height: h, error_correction: ec} + image_binary_nif(data, opts) + end @doc false - def png64_nif(_data, _w, _h), do: :erlang.nif_error(:nif_not_loaded) + def svg_nif(_data, _opts), do: :erlang.nif_error(:nif_not_loaded) @doc false - def jpg_nif(_data, _w, _h), do: :erlang.nif_error(:nif_not_loaded) + def image_binary_nif(_data, _opts), do: :erlang.nif_error(:nif_not_loaded) @doc false - def jpg64_nif(_data, _width, _height), do: :erlang.nif_error(:nif_not_loaded) + def image_base64_nif(_data, _opts), do: :erlang.nif_error(:nif_not_loaded) end diff --git a/lib/qrusty/qr.ex b/lib/qrusty/qr.ex index 5f73f87..5cc57ed 100644 --- a/lib/qrusty/qr.ex +++ b/lib/qrusty/qr.ex @@ -28,17 +28,25 @@ defmodule Qrusty.QR do @doc """ Validates input arguements and generates a QR if valid. """ - @spec new(data :: binary(), format :: atom(), width :: integer(), height: integer()) :: + @spec new( + data :: binary(), + format :: atom(), + width :: integer(), + height :: integer(), + error_correction :: integer() + ) :: {:ok, %__MODULE__{}} | {:error, :invalid_dimensions} | {:error, :invalid_format} | {:error, :invalid_data} - def new(data, format, width, height) do + | {:error, :invalid_error_correction} + def new(data, format, width, height, ec) do with :ok <- validate_data(data), :ok <- validate_format(format), :ok <- validate_dimension(width), :ok <- validate_dimension(height), - {:ok, encoded_data} <- Native.generate(data, format, width, height) do + :ok <- validate_error_correction(ec), + {:ok, encoded_data} <- Native.generate(data, format, width, height, ec) do {:ok, %__MODULE__{ data: data, @@ -58,4 +66,7 @@ defmodule Qrusty.QR do defp validate_data(d) when is_binary(d), do: :ok defp validate_data(_d), do: {:error, :invalid_data} + + defp validate_error_correction(ec) when ec in ~w(l m q h)a, do: :ok + defp validate_error_correction(_ec), do: {:error, :invalid_error_correction} end diff --git a/mix.exs b/mix.exs index 9b39e12..2f90f89 100644 --- a/mix.exs +++ b/mix.exs @@ -1,7 +1,7 @@ defmodule Qrusty.MixProject do use Mix.Project - @version "0.1.3" + @version "0.1.4" @source_url "https://github.com/nbw/qrusty" def project do diff --git a/native/qrusty/Cargo.lock b/native/qrusty/Cargo.lock index 3275d99..50ca3cb 100644 --- a/native/qrusty/Cargo.lock +++ b/native/qrusty/Cargo.lock @@ -469,7 +469,7 @@ dependencies = [ [[package]] name = "qrusty" -version = "0.1.0" +version = "0.1.4" dependencies = [ "base64", "image", diff --git a/native/qrusty/Cargo.toml b/native/qrusty/Cargo.toml index 536a470..26c8484 100644 --- a/native/qrusty/Cargo.toml +++ b/native/qrusty/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "qrusty" -version = "0.1.0" +version = "0.1.4" edition = "2021" [lib] diff --git a/native/qrusty/src/lib.rs b/native/qrusty/src/lib.rs index 6a5625e..b6a5e6b 100644 --- a/native/qrusty/src/lib.rs +++ b/native/qrusty/src/lib.rs @@ -1,56 +1,50 @@ -use qrcode::QrCode; +use qrcode::{QrCode, EcLevel}; use qrcode::render::svg; +use image::ImageOutputFormat::{ Png, Jpeg }; use image::Luma; use base64; use std::io::Cursor; use rustler::types::atom::ok; -use rustler::{Atom, Binary, Env, Error, OwnedBinary}; -use image::ImageOutputFormat::{ - Png, - Jpeg -}; +use rustler::{Atom, Binary, Env, Error, NifUnitEnum, NifStruct, OwnedBinary}; + +#[derive(NifStruct)] +#[module = "Qrusty.Native.Options"] +struct Options { + pub width: u32, + pub height: u32, + pub error_correction: ECL, + pub format: Format, +} #[rustler::nif] -fn svg_nif(data: &str, width: u32, height: u32) -> Result<(Atom, String), Error> { - let code = QrCode::new(data.as_bytes()).unwrap(); +fn svg_nif(data: &str, opts: Options) -> Result<(Atom, String), Error> { + let code = QrCode::with_error_correction_level(data.as_bytes(), opts.error_correction.t()).unwrap(); let svg = code.render::() - .min_dimensions(width, height) + .min_dimensions(opts.width, opts.height) .build(); Ok((ok(), svg)) } #[rustler::nif] -fn png_nif<'a>(env: Env<'a>, data: &str, width: u32, height: u32) -> Result<(Atom, Binary<'a>), Error> { - let bytes = create_qr_image(data, width, height, Png); - Ok((ok(), bytes_to_binary(env, bytes))) -} - -#[rustler::nif] -fn png64_nif(data: &str, width: u32, height: u32) -> Result<(Atom, String), Error> { - let bytes = create_qr_image(data, width, height, Png); - Ok((ok(), base64::encode(&bytes))) -} - -#[rustler::nif] -fn jpg_nif<'a>(env: Env<'a>, data: &str, width: u32, height: u32) -> Result<(Atom, Binary<'a>), Error> { - let bytes = create_qr_image(data, width, height, Jpeg(100)); +fn image_binary_nif<'a>(env: Env<'a>, data: &str, opts: Options) -> Result<(Atom, Binary<'a>), Error> { + let bytes = create_qr_image(data, opts); Ok((ok(), bytes_to_binary(env, bytes))) } #[rustler::nif] -fn jpg64_nif(data: &str, width: u32, height: u32) -> Result<(Atom, String), Error> { - let bytes = create_qr_image(data, width, height, Jpeg(100)); +fn image_base64_nif(data: &str, opts: Options) -> Result<(Atom, String), Error> { + let bytes = create_qr_image(data, opts); Ok((ok(), base64::encode(&bytes))) } -fn create_qr_image(data: &str, width: u32, height: u32, format : image::ImageOutputFormat ) -> Vec { - let code = QrCode::new(data.as_bytes()).unwrap(); +fn create_qr_image(data: &str, opts: Options) -> Vec { + let code = QrCode::with_error_correction_level(data.as_bytes(), opts.error_correction.t()).unwrap(); let img = code.render::>() - .min_dimensions(width, height) + .min_dimensions(opts.width, opts.height) .build(); let mut bytes: Vec = Vec::new(); - img.write_to(&mut Cursor::new(&mut bytes), format).unwrap(); + img.write_to(&mut Cursor::new(&mut bytes), opts.format.t().unwrap()).unwrap(); bytes } @@ -60,4 +54,50 @@ fn bytes_to_binary<'a>(env: Env<'a>, bytes: Vec) -> Binary { bin.release(env) } -rustler::init!("Elixir.Qrusty.Native", [svg_nif, png_nif, png64_nif, jpg_nif, jpg64_nif]); \ No newline at end of file +#[derive(NifUnitEnum)] +pub enum ECL { + L, + M, + Q, + H +} + +// wrap qrcode's EcLevel enum +impl ECL { + fn t(&self) -> EcLevel { + match self { + ECL::L => EcLevel::L, + ECL::M => EcLevel::M, + ECL::Q => EcLevel::Q, + ECL::H => EcLevel::H, + } + } +} + +#[derive(NifUnitEnum)] +pub enum Format { + JPG, + JPEG, + PNG, + JPG64, + JPEG64, + PNG64, + SVG +} + +// wrap image's ImageOutputFormat enum +impl Format { + fn t(&self) -> Result { + match self { + Format::JPG => Ok(Jpeg(100)), + Format::JPEG => Ok(Jpeg(100)), + Format::PNG => Ok(Png), + Format::PNG64 => Ok(Png), + Format::JPG64 => Ok(Jpeg(100)), + Format::JPEG64 => Ok(Jpeg(100)), + _ => Err("Not Implemented") + } + } +} + +rustler::init!("Elixir.Qrusty.Native", [svg_nif, image_binary_nif, image_base64_nif]); \ No newline at end of file diff --git a/test/qrusty/native_test.exs b/test/qrusty/native_test.exs index 29caa33..8768a1c 100644 --- a/test/qrusty/native_test.exs +++ b/test/qrusty/native_test.exs @@ -4,40 +4,54 @@ defmodule Qrusty.NativeTest do alias Qrusty.Native @valid_input "https://elixir-lang.org/" + @valid_size 200 + @valid_ec :m describe "generate/3" do test "svg" do - assert {:ok, svg} = Native.generate(@valid_input, :svg, 200, 200) + assert {:ok, svg} = Native.generate(@valid_input, :svg, @valid_size, @valid_size, @valid_ec) assert svg == load_asset("qr.svg") end test "png" do - assert {:ok, binary} = Native.generate(@valid_input, :png, 200, 200) + assert {:ok, binary} = + Native.generate(@valid_input, :png, @valid_size, @valid_size, @valid_ec) + assert binary == load_asset("qr.png") end test "jpg/jpeg" do jpg_asset = load_asset("qr.jpg") - assert {:ok, binary} = Native.generate(@valid_input, :jpg, 200, 200) + assert {:ok, binary} = + Native.generate(@valid_input, :jpg, @valid_size, @valid_size, @valid_ec) + assert binary == jpg_asset - assert {:ok, binary} = Native.generate(@valid_input, :jpeg, 200, 200) + assert {:ok, binary} = + Native.generate(@valid_input, :jpeg, @valid_size, @valid_size, @valid_ec) + assert binary == jpg_asset end test "jpg64/jpeg64" do jpg_asset = load_asset("qr_jpg64.txt") - assert {:ok, binary64} = Native.generate(@valid_input, :jpg64, 200, 200) + assert {:ok, binary64} = + Native.generate(@valid_input, :jpg64, @valid_size, @valid_size, @valid_ec) + assert binary64 == jpg_asset - assert {:ok, binary64} = Native.generate(@valid_input, :jpeg64, 200, 200) + assert {:ok, binary64} = + Native.generate(@valid_input, :jpeg64, @valid_size, @valid_size, @valid_ec) + assert binary64 == jpg_asset end test "png64" do - assert {:ok, binary64} = Native.generate(@valid_input, :png64, 200, 200) + assert {:ok, binary64} = + Native.generate(@valid_input, :png64, @valid_size, @valid_size, @valid_ec) + assert binary64 == load_asset("qr_png64.txt") end end diff --git a/test/qrusty/qr_test.exs b/test/qrusty/qr_test.exs index 739780f..9a2e912 100644 --- a/test/qrusty/qr_test.exs +++ b/test/qrusty/qr_test.exs @@ -4,24 +4,39 @@ defmodule Qrusty.QrTest do alias Qrusty.QR @valid_input "https://elixir-lang.org/" + @valid_size 100 + @valid_ec :m describe "new/3" do test "valid dimensions" do - assert {:ok, %QR{}} = QR.new(@valid_input, :svg, 100, 100) - assert {:error, :invalid_dimensions} == QR.new(@valid_input, :svg, -100, 0) + assert {:ok, %QR{}} = QR.new(@valid_input, :svg, @valid_size, @valid_size, @valid_ec) + assert {:error, :invalid_dimensions} == QR.new(@valid_input, :svg, -100, 0, @valid_ec) end test "valid format" do Enum.each(~w(svg png jpg jpeg png64 jpeg64 jpg64)a, fn format -> - assert {:ok, %QR{}} = QR.new(@valid_input, format, 100, 100) + assert {:ok, %QR{}} = QR.new(@valid_input, format, @valid_size, @valid_size, @valid_ec) end) - assert {:error, :invalid_format} = QR.new(@valid_input, :mp3, 101, 100) + assert {:error, :invalid_format} = + QR.new(@valid_input, :mp3, @valid_size, @valid_size, @valid_ec) end test "valid data" do - assert {:ok, %QR{}} = QR.new(@valid_input, :svg, 100, 100) - assert {:error, :invalid_data} == QR.new(%{hello: "世界"}, :svg, 100, 100) + assert {:ok, %QR{}} = QR.new(@valid_input, :svg, @valid_size, @valid_size, @valid_ec) + + assert {:error, :invalid_data} == + QR.new(%{hello: "世界"}, :svg, @valid_size, @valid_size, @valid_ec) + end + + test "valid error correction" do + assert {:ok, %QR{}} = QR.new(@valid_input, :svg, @valid_size, @valid_size, :l) + assert {:ok, %QR{}} = QR.new(@valid_input, :svg, @valid_size, @valid_size, :m) + assert {:ok, %QR{}} = QR.new(@valid_input, :svg, @valid_size, @valid_size, :q) + assert {:ok, %QR{}} = QR.new(@valid_input, :svg, @valid_size, @valid_size, :h) + + assert {:error, :invalid_error_correction} == + QR.new(@valid_input, :svg, @valid_size, @valid_size, :x) end end end diff --git a/test/qrusty_test.exs b/test/qrusty_test.exs index c0b02d8..4259b8b 100644 --- a/test/qrusty_test.exs +++ b/test/qrusty_test.exs @@ -22,4 +22,18 @@ defmodule QrustyTest do assert {:ok, %QR{width: 100, height: 100}} = Qrusty.qr(@valid_input, :svg, size: 100) end end + + describe "qr!/3" do + test "returns encoded data" do + {:ok, %QR{encoded_data: data}} = Qrusty.qr(@valid_input, :svg) + + assert Qrusty.qr!(@valid_input, :svg) == data + end + + test "raise error if invalid input" do + assert_raise Qrusty.Error, fn -> + Qrusty.qr!(@valid_input, :svg, width: -100) + end + end + end end