├── .circleci └── config.yml ├── .formatter.exs ├── .gitignore ├── .tool-versions ├── LICENSE ├── README.md ├── bundlex.exs ├── c_src └── turbojpeg │ ├── _generated │ └── .gitignore │ ├── turbojpeg_native.c │ ├── turbojpeg_native.h │ └── turbojpeg_native.spec.exs ├── dialyzer.ignore-warnings.exs ├── fixture ├── ff0000_i444.jpg └── i420.yuv ├── lib ├── turbojpeg.ex └── turbojpeg │ ├── filter.ex │ ├── jpeg_header.ex │ ├── native.ex │ └── sink.ex ├── mix.exs ├── mix.lock └── test ├── test_helper.exs ├── turbojpeg ├── filter_test.exs └── sink_test.exs └── turbojpeg_test.exs /.circleci/config.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | 3 | jobs: 4 | build: 5 | parallelism: 1 6 | docker: 7 | - image: binarynoggin/elixir-release-builder:elixir-1.10 8 | environment: 9 | MIX_ENV: test 10 | 11 | working_directory: ~/apps 12 | 13 | steps: 14 | - checkout 15 | 16 | - run: mix local.hex --force 17 | - run: mix local.rebar --force 18 | - run: apt-get update 19 | - run: apt-get install -y imagemagick git libturbojpeg libturbojpeg0-dev pkg-config 20 | 21 | - restore_cache: # restores saved mix cache 22 | # Read about caching dependencies: https://circleci.com/docs/2.0/caching/ 23 | keys: # list of cache keys, in decreasing specificity 24 | - v1-mix-cache-{{ .Branch }}-{{ checksum "mix.lock" }} 25 | - v1-mix-cache-{{ .Branch }} 26 | - v1-mix-cache 27 | - restore_cache: # restores saved build cache 28 | keys: 29 | - v1-build-cache-{{ .Branch }} 30 | - v1-build-cache 31 | - run: mix do deps.get, compile # get updated dependencies & compile them 32 | - save_cache: # generate and store mix cache 33 | key: v1-mix-cache-{{ .Branch }}-{{ checksum "mix.lock" }} 34 | paths: "deps" 35 | - save_cache: # make another, less specific cache 36 | key: v1-mix-cache-{{ .Branch }} 37 | paths: "deps" 38 | - save_cache: # you should really save one more cache (just in case) 39 | key: v1-mix-cache 40 | paths: "deps" 41 | - save_cache: # don't forget to save a *build* cache, too 42 | key: v1-build-cache-{{ .Branch }} 43 | paths: "_build" 44 | - save_cache: # and one more build cache for good measure 45 | key: v1-build-cache 46 | paths: "_build" 47 | 48 | - run: mix test # run all tests in project 49 | -------------------------------------------------------------------------------- /.formatter.exs: -------------------------------------------------------------------------------- 1 | # Used by "mix format" 2 | [ 3 | inputs: ["{mix,.formatter,bundlex}.exs", "{config,lib,test}/**/*.{ex,exs}"], 4 | import_deps: [:membrane_core] 5 | ] 6 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # The directory Mix will write compiled artifacts to. 2 | /_build/ 3 | 4 | # If you run "mix test --cover", coverage assets end up here. 5 | /cover/ 6 | 7 | # The directory Mix downloads your dependencies sources to. 8 | /deps/ 9 | 10 | # Where third-party dependencies like ExDoc output generated docs. 11 | /doc/ 12 | 13 | # Ignore .fetch files in case you like to edit your project deps locally. 14 | /.fetch 15 | 16 | # Ignore tmp dir used in tests via :tmp_dir tag 17 | /tmp/ 18 | 19 | # If the VM crashes, it generates a dump, let's ignore it too. 20 | erl_crash.dump 21 | 22 | # Also ignore archive artifacts (built via "mix archive.build"). 23 | *.ez 24 | 25 | # Ignore package tarball (built via "mix hex.build"). 26 | turbojpeg-*.tar 27 | 28 | -------------------------------------------------------------------------------- /.tool-versions: -------------------------------------------------------------------------------- 1 | elixir 1.15.7-otp-26 2 | erlang 26.1.2 3 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright 2020 BinaryNoggin 2 | 3 | Licensed under the Apache License, Version 2.0 (the "License"); 4 | you may not use this file except in compliance with the License. 5 | You may obtain a copy of the License at 6 | 7 | http://www.apache.org/licenses/LICENSE-2.0 8 | 9 | Unless required by applicable law or agreed to in writing, software 10 | distributed under the License is distributed on an "AS IS" BASIS, 11 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | See the License for the specific language governing permissions and 13 | limitations under the License. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # TurboJPEG 2 | 3 | Fast JPEG encoding from raw YUV data using [libjpeg-turbo](https://libjpeg-turbo.org/) 4 | 5 | [![CircleCI](https://circleci.com/gh/BinaryNoggin/elixir-turbojpeg/tree/master.svg?style=svg)](https://circleci.com/gh/BinaryNoggin/elixir-turbojpeg/tree/master) 6 | 7 | ## Installation 8 | 9 | This library requires libjpeg-turbo to be installed 10 | 11 | ### Arch linux 12 | 13 | sudo pacman -S libjpeg-turbo 14 | 15 | ### Ubuntu/Debian 16 | 17 | sudo apt-get install libturbojpeg libturbojpeg0-dev 18 | 19 | ### OSX 20 | 21 | brew install libjpeg-turbo 22 | 23 | ### Develement Dependencies 24 | 25 | ### Arch linux 26 | 27 | sudo pacman -S imagemagick 28 | 29 | ### Ubuntu/Debian 30 | 31 | sudo apt-get install imagemagick 32 | 33 | ### OSX 34 | 35 | brew install imagemagick 36 | 37 | If [available in Hex](https://hex.pm/packages/turbojpeg), the package can be installed 38 | by adding `turbojpeg` to your list of dependencies in `mix.exs`: 39 | 40 | ```elixir 41 | def deps do 42 | [ 43 | {:turbojpeg, "~> 0.4"} 44 | ] 45 | end 46 | ``` 47 | 48 | ## Basic Usage 49 | 50 | ```elixir 51 | iex(1)> frame = File.read!("fixture/i420.yuv") 52 | <<0, 0, 0, 0, 0, 1, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 53 | 0, 0, 0, 2, 0, 0, 1, 1, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, ...>> 54 | iex(2)> {:ok, jpeg} = Turbojpeg.yuv_to_jpeg(frame, 1920, 1080, 90, :I420) 55 | {:ok, <<255, 216, 255, 224, 0, 16, 74, 70, 73, 70, 0, 1, 1, 0, 0, 1, 0, 1, 0, 0, 255, 56 | 219, 0, 67, 0, 3, 2, 2, 3, 2, 2, 3, 3, 3, 3, 4, 3, 3, 4, 5, 8, 5, 5, 4, 4, 5, 57 | 10, 7, 7, 6, ...>>} 58 | iex(3)> File.write!("test.jpg", jpeg) 59 | :ok 60 | ``` 61 | 62 | ## Membrane Sink Usage 63 | 64 | In this example we'll read an H264 encoded frame and save it as a JPEG image 65 | 66 | ```elixir 67 | defmodule Your.Module.Pipeline do 68 | use Membrane.Pipeline 69 | 70 | alias Membrane.{File, H264} 71 | 72 | @impl true 73 | def handle_init(_ctx, _opts) do 74 | children = [ 75 | child(:source, %File.Source{location: "input.h264"}) 76 | |> child(:parser, H264.Parser) 77 | |> child(:decoder, H264.FFmpeg.Decoder) 78 | |> child(:sink, %Turbojpeg.Sink{filename: "/tmp/frame.jpeg", quality: 100}) 79 | ] 80 | 81 | {[spec: spec], %{}} 82 | end 83 | 84 | @impl true 85 | def handle_element_end_of_stream(:sink, _ctx, state) do 86 | {[terminate: :normal], state} 87 | end 88 | end 89 | ``` 90 | 91 | # Copyright and License 92 | 93 | Copyright 2023, Binary Noggin 94 | -------------------------------------------------------------------------------- /bundlex.exs: -------------------------------------------------------------------------------- 1 | defmodule Turbojpeg.BundlexProject do 2 | use Bundlex.Project 3 | 4 | def project() do 5 | [ 6 | natives: natives(Bundlex.platform()) 7 | ] 8 | end 9 | 10 | def natives(_platform) do 11 | [ 12 | turbojpeg_native: [ 13 | interface: :nif, 14 | preprocessor: Unifex, 15 | sources: ["turbojpeg_native.c"], 16 | os_deps: [ 17 | turbojpeg: [ 18 | {:pkg_config, ["libturbojpeg"]} 19 | ] 20 | ] 21 | ] 22 | ] 23 | end 24 | end 25 | -------------------------------------------------------------------------------- /c_src/turbojpeg/_generated/.gitignore: -------------------------------------------------------------------------------- 1 | **/*.h 2 | **/*.c 3 | **/*.cpp 4 | -------------------------------------------------------------------------------- /c_src/turbojpeg/turbojpeg_native.c: -------------------------------------------------------------------------------- 1 | #include "turbojpeg_native.h" 2 | 3 | /** 4 | * Supported pixel formats: :I420 | :I422 | :I444 5 | * Unsupported pixel formats: :RGB | :BGRA | :RGBA | :NV12 | :NV21 | :YV12 | :AYUV 6 | */ 7 | int format_to_tjsamp(char* format) { 8 | if(strcmp(format, "I420") == 0) { 9 | return TJSAMP_420; 10 | } else if(strcmp(format, "I422") == 0) { 11 | return TJSAMP_422; 12 | } else if(strcmp(format, "I444") == 0) { 13 | return TJSAMP_444; 14 | } else if(strcmp(format, "GRAY") == 0) { 15 | return TJSAMP_GRAY; 16 | } else { 17 | return -1; 18 | } 19 | } 20 | 21 | /** 22 | * Supported pixel formats: :I420 | :I422 | :I444 23 | * Unsupported pixel formats: :RGB | :BGRA | :RGBA | :NV12 | :NV21 | :YV12 | :AYUV 24 | */ 25 | const char* tjsamp_to_format(enum TJSAMP tjsamp) { 26 | switch(tjsamp) { 27 | case(TJSAMP_420): 28 | return("I420"); 29 | case(TJSAMP_422): 30 | return("I422"); 31 | case(TJSAMP_444): 32 | return("I444"); 33 | case(TJSAMP_GRAY): 34 | return("GRAY"); 35 | default: 36 | return("unknown_format"); 37 | } 38 | } 39 | 40 | /** 41 | * Returns %{height: int, width: int, format: atom} 42 | */ 43 | UNIFEX_TERM get_jpeg_header(UnifexEnv* env, UnifexPayload *payload) { 44 | tjhandle tjh; 45 | enum TJSAMP tjsamp; 46 | enum TJCS cspace; 47 | int res, width, height; 48 | UNIFEX_TERM ret; 49 | 50 | tjh = tjInitDecompress(); 51 | if(!tjh) 52 | return get_jpeg_header_result_error(env, tjGetErrorStr()); 53 | 54 | res = tjDecompressHeader3( 55 | tjh, 56 | payload->data, 57 | payload->size, 58 | &width, &height, 59 | (int*)&tjsamp, (int*)&cspace 60 | ); 61 | if(res < 0) { 62 | ret = get_jpeg_header_result_error(env, tjGetErrorStr2(tjh)); 63 | goto cleanup; 64 | } 65 | 66 | jpeg_header header = {(char*)tjsamp_to_format(tjsamp), width, height}; 67 | ret = get_jpeg_header_result_ok(env, header); 68 | 69 | cleanup: 70 | if(tjh) tjDestroy(tjh); 71 | return ret; 72 | } 73 | 74 | /** 75 | * Convert a yuv binary payload into a jpeg encoded payload 76 | */ 77 | UNIFEX_TERM yuv_to_jpeg(UnifexEnv* env, UnifexPayload *payload, int width, int height, int quality, char* format) { 78 | tjhandle tjh = NULL; 79 | enum TJSAMP tjsamp; 80 | unsigned char *jpegBuf = NULL; 81 | unsigned long jpegSize; 82 | int res; 83 | UnifexPayload *jpegFrame = NULL; 84 | UNIFEX_TERM ret; 85 | 86 | res = format_to_tjsamp(format); 87 | if(res < 0) { 88 | return(yuv_to_jpeg_result_error(env, "unsupported_format")); 89 | } else { 90 | tjsamp = (enum TJSAMP)res; 91 | } 92 | 93 | tjh = tjInitCompress(); 94 | if(!tjh) 95 | return yuv_to_jpeg_result_error(env, tjGetErrorStr()); 96 | 97 | res = tjCompressFromYUV( 98 | tjh, 99 | payload->data, 100 | width, 4, height, tjsamp, 101 | &jpegBuf, &jpegSize, 102 | quality, 0 103 | ); 104 | 105 | if(res < 0) { 106 | ret = yuv_to_jpeg_result_error(env, tjGetErrorStr2(tjh)); 107 | goto cleanup; 108 | } 109 | 110 | jpegFrame = unifex_alloc(sizeof(*jpegFrame)); 111 | unifex_payload_alloc(env, UNIFEX_PAYLOAD_BINARY, jpegSize, jpegFrame); 112 | memcpy(jpegFrame->data, jpegBuf, jpegSize); 113 | ret = yuv_to_jpeg_result_ok(env, jpegFrame); 114 | 115 | cleanup: 116 | if(jpegFrame != NULL) { 117 | unifex_payload_release(jpegFrame); 118 | unifex_free(jpegFrame); 119 | } 120 | 121 | if(jpegBuf) tjFree(jpegBuf); 122 | if(tjh) tjDestroy(tjh); 123 | return ret; 124 | } 125 | 126 | /** 127 | * Convert a binary jpeg payload into a yuv encoded payload 128 | */ 129 | UNIFEX_TERM jpeg_to_yuv(UnifexEnv* env, UnifexPayload *payload) { 130 | tjhandle tjh; 131 | enum TJSAMP tjsamp; 132 | enum TJCS cspace; 133 | unsigned long yuvBufSize; 134 | UnifexPayload *yuvFrame = NULL; 135 | int res, width, height; 136 | UNIFEX_TERM ret; 137 | 138 | tjh = tjInitDecompress(); 139 | if(!tjh) 140 | return jpeg_to_yuv_result_error(env, tjGetErrorStr()); 141 | 142 | res = tjDecompressHeader3( 143 | tjh, 144 | payload->data, 145 | payload->size, 146 | &width, &height, 147 | (int*)&tjsamp, (int*)&cspace 148 | ); 149 | 150 | if(res < 0) { 151 | ret = jpeg_to_yuv_result_error(env, tjGetErrorStr2(tjh)); 152 | goto cleanup; 153 | } 154 | 155 | yuvBufSize = tjBufSizeYUV2(width, 4, height, tjsamp); 156 | yuvFrame = unifex_alloc(sizeof(*yuvFrame)); 157 | unifex_payload_alloc(env, UNIFEX_PAYLOAD_BINARY, yuvBufSize, yuvFrame); 158 | 159 | res = tjDecompressToYUV2( 160 | tjh, 161 | payload->data, 162 | payload->size, 163 | yuvFrame->data, 164 | width, 4, height, 165 | 0 166 | ); 167 | 168 | if(res < 0) { 169 | ret = jpeg_to_yuv_result_error(env, tjGetErrorStr2(tjh)); 170 | goto cleanup; 171 | } 172 | 173 | ret = jpeg_to_yuv_result_ok(env, yuvFrame); 174 | 175 | cleanup: 176 | if(yuvFrame != NULL) { 177 | unifex_payload_release(yuvFrame); 178 | unifex_free(yuvFrame); 179 | } 180 | 181 | if(tjh) tjDestroy(tjh); 182 | return ret; 183 | } 184 | 185 | void handle_destroy_state(UnifexEnv* env, State* state) { 186 | UNIFEX_UNUSED(env); 187 | UNIFEX_UNUSED(state); 188 | } -------------------------------------------------------------------------------- /c_src/turbojpeg/turbojpeg_native.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include 4 | 5 | /** NIF State */ 6 | typedef struct _turbojpeg_native_state { 7 | } State; 8 | 9 | #include "_generated/turbojpeg_native.h" -------------------------------------------------------------------------------- /c_src/turbojpeg/turbojpeg_native.spec.exs: -------------------------------------------------------------------------------- 1 | module Turbojpeg.Native 2 | 3 | type jpeg_header :: %Turbojpeg.JpegHeader{ 4 | format: atom, 5 | width: int, 6 | height: int 7 | } 8 | 9 | spec yuv_to_jpeg(payload, width::int, height::int, quality::int, format::atom) :: {:ok :: label, payload} | {:error :: label, reason :: string} 10 | spec jpeg_to_yuv(payload) :: {:ok :: label, payload} | {:error :: label, reason :: string} 11 | spec get_jpeg_header(payload) :: {:ok :: label, jpeg_header} | {:error::label, reason :: string} 12 | 13 | dirty :cpu, yuv_to_jpeg: 5, jpeg_to_yuv: 1, get_jpeg_header: 1 14 | -------------------------------------------------------------------------------- /dialyzer.ignore-warnings.exs: -------------------------------------------------------------------------------- 1 | [ 2 | {"lib/turbojpeg/native.ex"} 3 | ] 4 | -------------------------------------------------------------------------------- /fixture/ff0000_i444.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/BinaryNoggin/elixir-turbojpeg/c1b302c91c63c27ac705b92cf2366ce92e8dda5b/fixture/ff0000_i444.jpg -------------------------------------------------------------------------------- /fixture/i420.yuv: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/BinaryNoggin/elixir-turbojpeg/c1b302c91c63c27ac705b92cf2366ce92e8dda5b/fixture/i420.yuv -------------------------------------------------------------------------------- /lib/turbojpeg.ex: -------------------------------------------------------------------------------- 1 | defmodule Turbojpeg do 2 | @moduledoc File.read!("README.md") 3 | 4 | alias Turbojpeg.{JpegHeader, Native} 5 | 6 | @type width :: dimension 7 | @type height :: dimension 8 | @type dimension :: pos_integer() 9 | @type quality :: 0..100 10 | @type format :: 11 | :I420 12 | | :I422 13 | | :I444 14 | | :GRAY 15 | @type error :: {:error, atom()} | {:error, struct} 16 | 17 | @doc """ 18 | Converts yuv to jpeg images 19 | 20 | 21 | iex> {:ok, jpeg} = Turbojpeg.yuv_to_jpeg(frame, 1920, 1080, 90, :I420) 22 | {:ok, <<....>>} 23 | """ 24 | @spec yuv_to_jpeg(binary(), width, height, quality, format) :: 25 | {:ok, binary()} | error() 26 | def yuv_to_jpeg(yuv, width, height, quality, format) do 27 | Native.yuv_to_jpeg(yuv, width, height, quality, format) 28 | end 29 | 30 | @doc """ 31 | Converts jpeg to yuv 32 | 33 | 34 | iex> {:ok, yuv} = Turbojpeg.jpeg_to_yuv(jpeg) 35 | {:ok,<<..>>} 36 | """ 37 | @spec jpeg_to_yuv(binary()) :: {:ok, binary()} | error() 38 | def jpeg_to_yuv(jpeg) do 39 | Native.jpeg_to_yuv(jpeg) 40 | end 41 | 42 | @doc """ 43 | Gets the header from a jpeg binary 44 | 45 | ## Examples 46 | 47 | iex> {:ok, header} = Turbojpeg.get_jpeg_header(jpeg) 48 | {:ok, 49 | %Turbojpeg.JpegHeader{ 50 | format: :I422, 51 | width: 192, 52 | height: 192 53 | } 54 | } 55 | 56 | iex> Turbojpeg.get_jpeg_header(<<45, 48, 44, 41, 11>>) 57 | {:error, "Not a JPEG file: starts with 0x2d 0x30"} 58 | """ 59 | @spec get_jpeg_header(binary()) :: {:ok, JpegHeader.t()} | error() 60 | def get_jpeg_header(jpeg) do 61 | Native.get_jpeg_header(jpeg) 62 | end 63 | end 64 | -------------------------------------------------------------------------------- /lib/turbojpeg/filter.ex: -------------------------------------------------------------------------------- 1 | defmodule Turbojpeg.Filter do 2 | @moduledoc """ 3 | Membrane filter converting raw video frames to JPEG. 4 | """ 5 | use Membrane.Filter 6 | 7 | alias Membrane.Buffer 8 | alias Membrane.RawVideo 9 | alias Membrane.RemoteStream 10 | 11 | def_input_pad :input, 12 | flow_control: :auto, 13 | accepted_format: %RawVideo{pixel_format: pix_fmt} when pix_fmt in [:I420, :I422, :I444] 14 | 15 | # TODO: implement JPEG stream format 16 | def_output_pad :output, flow_control: :auto, accepted_format: RemoteStream 17 | 18 | def_options quality: [ 19 | spec: Turbojpeg.quality(), 20 | default: 75, 21 | description: "Jpeg encoding quality" 22 | ] 23 | 24 | @impl true 25 | def handle_init(_ctx, options) do 26 | {[], Map.from_struct(options)} 27 | end 28 | 29 | @impl true 30 | def handle_stream_format(:input, _stream_format, _ctx, state) do 31 | {[stream_format: {:output, %RemoteStream{type: :bytestream}}], state} 32 | end 33 | 34 | @impl true 35 | def handle_buffer(:input, %Buffer{payload: payload} = buffer, ctx, state) do 36 | %{stream_format: stream_format} = ctx.pads.input 37 | %{width: width, height: height, pixel_format: pix_fmt} = stream_format 38 | 39 | case Turbojpeg.yuv_to_jpeg(payload, width, height, state.quality, pix_fmt) do 40 | {:ok, jpeg} -> 41 | {[buffer: {:output, %Buffer{buffer | payload: jpeg}}], state} 42 | 43 | error -> 44 | raise """ 45 | could not create JPEG image 46 | #{inspect(error)} 47 | """ 48 | end 49 | end 50 | end 51 | -------------------------------------------------------------------------------- /lib/turbojpeg/jpeg_header.ex: -------------------------------------------------------------------------------- 1 | defmodule Turbojpeg.JpegHeader do 2 | @moduledoc """ 3 | Structure representing JPEG image header information. 4 | """ 5 | 6 | @typedoc """ 7 | The header contains the following information: 8 | * `width` - The width of the image. 9 | * `height` - The height of the image. 10 | * `format` - The pixel format of the image. 11 | """ 12 | @type t :: %__MODULE__{ 13 | width: Turbojpeg.dimension(), 14 | height: Turbojpeg.dimension(), 15 | format: Turbojpeg.format() 16 | } 17 | 18 | defstruct [:width, :height, :format] 19 | end 20 | -------------------------------------------------------------------------------- /lib/turbojpeg/native.ex: -------------------------------------------------------------------------------- 1 | defmodule Turbojpeg.Native do 2 | use Unifex.Loader 3 | end 4 | -------------------------------------------------------------------------------- /lib/turbojpeg/sink.ex: -------------------------------------------------------------------------------- 1 | defmodule Turbojpeg.Sink do 2 | @moduledoc """ 3 | Element responsible for converting YUV binary data to jpeg image format using `turbojpeg`. 4 | """ 5 | 6 | use Membrane.Sink 7 | 8 | alias Membrane.{Buffer, RawVideo} 9 | 10 | def_input_pad :input, 11 | flow_control: :auto, 12 | accepted_format: %RawVideo{pixel_format: pix_fmt} when pix_fmt in [:I420, :I422, :I444] 13 | 14 | def_options filename: [ 15 | spec: binary(), 16 | description: "File to write the jpeg data" 17 | ], 18 | quality: [ 19 | spec: non_neg_integer(), 20 | default: 75, 21 | description: "Jpeg encoding quality" 22 | ] 23 | 24 | @impl true 25 | def handle_init(_ctx, options) do 26 | state = 27 | options 28 | |> Map.from_struct() 29 | |> Map.merge(%{ 30 | height: nil, 31 | width: nil, 32 | format: nil 33 | }) 34 | 35 | {[], state} 36 | end 37 | 38 | @impl true 39 | def handle_stream_format( 40 | :input, 41 | %RawVideo{width: width, height: height, pixel_format: pix_fmt}, 42 | _ctx, 43 | state 44 | ) do 45 | {[], %{state | width: width, height: height, format: pix_fmt}} 46 | end 47 | 48 | @impl true 49 | def handle_buffer(:input, %Buffer{payload: payload}, _ctx, state) do 50 | with {:ok, data} <- 51 | Turbojpeg.yuv_to_jpeg(payload, state.width, state.height, state.quality, state.format), 52 | :ok <- File.write(state.filename, data) do 53 | {[], state} 54 | else 55 | error -> 56 | raise """ 57 | could not create a JPEG file 58 | #{inspect(error)} 59 | """ 60 | end 61 | end 62 | end 63 | -------------------------------------------------------------------------------- /mix.exs: -------------------------------------------------------------------------------- 1 | defmodule Turbojpeg.MixProject do 2 | use Mix.Project 3 | 4 | @version "0.4.0" 5 | @github_link "https://github.com/binarynoggin/elixir-turbojpeg" 6 | 7 | def project do 8 | [ 9 | compilers: [:unifex, :bundlex] ++ Mix.compilers(), 10 | app: :turbojpeg, 11 | version: @version, 12 | elixir: "~> 1.12", 13 | start_permanent: Mix.env() == :prod, 14 | description: "Elixir bindings for libjpeg-turbo", 15 | source_url: @github_link, 16 | homepage_url: @github_link, 17 | package: package(), 18 | docs: docs(), 19 | deps: deps(), 20 | dialyzer: [ 21 | ignore_warnings: "dialyzer.ignore-warnings.exs" 22 | ] 23 | ] 24 | end 25 | 26 | # Run "mix help compile.app" to learn about applications. 27 | def application do 28 | [ 29 | extra_applications: [:logger] 30 | ] 31 | end 32 | 33 | # Run "mix help deps" to learn about dependencies. 34 | defp deps do 35 | [ 36 | {:bundlex, "~> 1.4.0"}, 37 | {:unifex, "~> 1.1.0"}, 38 | {:membrane_core, "~> 1.0"}, 39 | {:membrane_raw_video_format, "~> 0.3.0"}, 40 | {:ex_doc, "~> 0.30", only: :dev, runtime: false}, 41 | {:propcheck, "~> 1.4.0", only: [:test]}, 42 | {:mogrify, "~> 0.9.0", only: [:test, :dev]}, 43 | {:dialyxir, "~> 1.3", only: [:dev], runtime: false} 44 | ] 45 | end 46 | 47 | defp package do 48 | [ 49 | licenses: ["Apache 2.0"], 50 | files: ["lib", "c_src", "mix.exs", "README*", "LICENSE*", ".formatter.exs", "bundlex.exs"], 51 | links: %{ 52 | "GitHub" => @github_link, 53 | "Binary Noggin" => "https://binarynoggin.com/" 54 | } 55 | ] 56 | end 57 | 58 | defp docs do 59 | [ 60 | main: "readme", 61 | extras: ["README.md"], 62 | source_ref: "v#{@version}" 63 | ] 64 | end 65 | end 66 | -------------------------------------------------------------------------------- /mix.lock: -------------------------------------------------------------------------------- 1 | %{ 2 | "bunch": {:hex, :bunch, "1.6.1", "5393d827a64d5f846092703441ea50e65bc09f37fd8e320878f13e63d410aec7", [:mix], [], "hexpm", "286cc3add551628b30605efbe2fca4e38cc1bea89bcd0a1a7226920b3364fe4a"}, 3 | "bunch_native": {:hex, :bunch_native, "0.5.0", "8ac1536789a597599c10b652e0b526d8833348c19e4739a0759a2bedfd924e63", [:mix], [{:bundlex, "~> 1.0", [hex: :bundlex, repo: "hexpm", optional: false]}], "hexpm", "24190c760e32b23b36edeb2dc4852515c7c5b3b8675b1a864e0715bdd1c8f80d"}, 4 | "bundlex": {:hex, :bundlex, "1.4.0", "73d4ce6e1773a263f6faeb0bca0703bea3597bc73cb286e6781251983497ef31", [:mix], [{:bunch, "~> 1.0", [hex: :bunch, repo: "hexpm", optional: false]}, {:elixir_uuid, "~> 1.2", [hex: :elixir_uuid, repo: "hexpm", optional: false]}, {:qex, "~> 0.5", [hex: :qex, repo: "hexpm", optional: false]}, {:req, "~> 0.4.0", [hex: :req, repo: "hexpm", optional: false]}, {:zarex, "~> 1.0", [hex: :zarex, repo: "hexpm", optional: false]}], "hexpm", "98c07a8c6b4168877fb5ea0f09293b8a40bbd1f420e9b0fdbccf2155c781c4a5"}, 5 | "castore": {:hex, :castore, "1.0.4", "ff4d0fb2e6411c0479b1d965a814ea6d00e51eb2f58697446e9c41a97d940b28", [:mix], [], "hexpm", "9418c1b8144e11656f0be99943db4caf04612e3eaecefb5dae9a2a87565584f8"}, 6 | "coerce": {:hex, :coerce, "1.0.1", "211c27386315dc2894ac11bc1f413a0e38505d808153367bd5c6e75a4003d096", [:mix], [], "hexpm", "b44a691700f7a1a15b4b7e2ff1fa30bebd669929ac8aa43cffe9e2f8bf051cf1"}, 7 | "dialyxir": {:hex, :dialyxir, "1.3.0", "fd1672f0922b7648ff9ce7b1b26fcf0ef56dda964a459892ad15f6b4410b5284", [:mix], [{:erlex, ">= 0.2.6", [hex: :erlex, repo: "hexpm", optional: false]}], "hexpm", "00b2a4bcd6aa8db9dcb0b38c1225b7277dca9bc370b6438715667071a304696f"}, 8 | "earmark_parser": {:hex, :earmark_parser, "1.4.39", "424642f8335b05bb9eb611aa1564c148a8ee35c9c8a8bba6e129d51a3e3c6769", [:mix], [], "hexpm", "06553a88d1f1846da9ef066b87b57c6f605552cfbe40d20bd8d59cc6bde41944"}, 9 | "elixir_uuid": {:hex, :elixir_uuid, "1.2.1", "dce506597acb7e6b0daeaff52ff6a9043f5919a4c3315abb4143f0b00378c097", [:mix], [], "hexpm", "f7eba2ea6c3555cea09706492716b0d87397b88946e6380898c2889d68585752"}, 10 | "erlex": {:hex, :erlex, "0.2.6", "c7987d15e899c7a2f34f5420d2a2ea0d659682c06ac607572df55a43753aa12e", [:mix], [], "hexpm", "2ed2e25711feb44d52b17d2780eabf998452f6efda104877a3881c2f8c0c0c75"}, 11 | "ex_doc": {:hex, :ex_doc, "0.30.9", "d691453495c47434c0f2052b08dd91cc32bc4e1a218f86884563448ee2502dd2", [:mix], [{:earmark_parser, "~> 1.4.31", [hex: :earmark_parser, repo: "hexpm", optional: false]}, {:makeup_elixir, "~> 0.14", [hex: :makeup_elixir, repo: "hexpm", optional: false]}, {:makeup_erlang, "~> 0.1", [hex: :makeup_erlang, repo: "hexpm", optional: false]}], "hexpm", "d7aaaf21e95dc5cddabf89063327e96867d00013963eadf2c6ad135506a8bc10"}, 12 | "finch": {:hex, :finch, "0.16.0", "40733f02c89f94a112518071c0a91fe86069560f5dbdb39f9150042f44dcfb1a", [:mix], [{:castore, "~> 0.1 or ~> 1.0", [hex: :castore, repo: "hexpm", optional: false]}, {:mime, "~> 1.0 or ~> 2.0", [hex: :mime, repo: "hexpm", optional: false]}, {:mint, "~> 1.3", [hex: :mint, repo: "hexpm", optional: false]}, {:nimble_options, "~> 0.4 or ~> 1.0", [hex: :nimble_options, repo: "hexpm", optional: false]}, {:nimble_pool, "~> 0.2.6 or ~> 1.0", [hex: :nimble_pool, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "f660174c4d519e5fec629016054d60edd822cdfe2b7270836739ac2f97735ec5"}, 13 | "hpax": {:hex, :hpax, "0.1.2", "09a75600d9d8bbd064cdd741f21fc06fc1f4cf3d0fcc335e5aa19be1a7235c84", [:mix], [], "hexpm", "2c87843d5a23f5f16748ebe77969880e29809580efdaccd615cd3bed628a8c13"}, 14 | "jason": {:hex, :jason, "1.4.1", "af1504e35f629ddcdd6addb3513c3853991f694921b1b9368b0bd32beb9f1b63", [:mix], [{:decimal, "~> 1.0 or ~> 2.0", [hex: :decimal, repo: "hexpm", optional: true]}], "hexpm", "fbb01ecdfd565b56261302f7e1fcc27c4fb8f32d56eab74db621fc154604a7a1"}, 15 | "libgraph": {:hex, :libgraph, "0.16.0", "3936f3eca6ef826e08880230f806bfea13193e49bf153f93edcf0239d4fd1d07", [:mix], [], "hexpm", "41ca92240e8a4138c30a7e06466acc709b0cbb795c643e9e17174a178982d6bf"}, 16 | "makeup": {:hex, :makeup, "1.1.1", "fa0bc768698053b2b3869fa8a62616501ff9d11a562f3ce39580d60860c3a55e", [:mix], [{:nimble_parsec, "~> 1.2.2 or ~> 1.3", [hex: :nimble_parsec, repo: "hexpm", optional: false]}], "hexpm", "5dc62fbdd0de44de194898b6710692490be74baa02d9d108bc29f007783b0b48"}, 17 | "makeup_elixir": {:hex, :makeup_elixir, "0.16.1", "cc9e3ca312f1cfeccc572b37a09980287e243648108384b97ff2b76e505c3555", [:mix], [{:makeup, "~> 1.0", [hex: :makeup, repo: "hexpm", optional: false]}, {:nimble_parsec, "~> 1.2.3 or ~> 1.3", [hex: :nimble_parsec, repo: "hexpm", optional: false]}], "hexpm", "e127a341ad1b209bd80f7bd1620a15693a9908ed780c3b763bccf7d200c767c6"}, 18 | "makeup_erlang": {:hex, :makeup_erlang, "0.1.3", "d684f4bac8690e70b06eb52dad65d26de2eefa44cd19d64a8095e1417df7c8fd", [:mix], [{:makeup, "~> 1.0", [hex: :makeup, repo: "hexpm", optional: false]}], "hexpm", "b78dc853d2e670ff6390b605d807263bf606da3c82be37f9d7f68635bd886fc9"}, 19 | "membrane_core": {:hex, :membrane_core, "1.0.0", "1b543aefd952283be1f2a215a1db213aa4d91222722ba03cd35280622f1905ee", [:mix], [{:bunch, "~> 1.6", [hex: :bunch, repo: "hexpm", optional: false]}, {:qex, "~> 0.3", [hex: :qex, repo: "hexpm", optional: false]}, {:ratio, "~> 3.0", [hex: :ratio, repo: "hexpm", optional: false]}, {:telemetry, "~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "352c90fd0a29942143c4bf7a727cc05c632e323f50a1a4e99321b1e8982f1533"}, 20 | "membrane_raw_video_format": {:hex, :membrane_raw_video_format, "0.3.0", "ba10f475e0814a6fe79602a74536b796047577c7ef5b0e33def27cd344229699", [:mix], [], "hexpm", "2f08760061c8a5386ecf04273480f10e48d25a1a40aa99476302b0bcd34ccb1c"}, 21 | "mime": {:hex, :mime, "2.0.5", "dc34c8efd439abe6ae0343edbb8556f4d63f178594894720607772a041b04b02", [:mix], [], "hexpm", "da0d64a365c45bc9935cc5c8a7fc5e49a0e0f9932a761c55d6c52b142780a05c"}, 22 | "mint": {:hex, :mint, "1.5.1", "8db5239e56738552d85af398798c80648db0e90f343c8469f6c6d8898944fb6f", [:mix], [{:castore, "~> 0.1.0 or ~> 1.0", [hex: :castore, repo: "hexpm", optional: true]}, {:hpax, "~> 0.1.1", [hex: :hpax, repo: "hexpm", optional: false]}], "hexpm", "4a63e1e76a7c3956abd2c72f370a0d0aecddc3976dea5c27eccbecfa5e7d5b1e"}, 23 | "mogrify": {:hex, :mogrify, "0.9.3", "238c782f00271dace01369ad35ae2e9dd020feee3443b9299ea5ea6bed559841", [:mix], [], "hexpm", "0189b1e1de27455f2b9ae8cf88239cefd23d38de9276eb5add7159aea51731e6"}, 24 | "nimble_options": {:hex, :nimble_options, "1.0.2", "92098a74df0072ff37d0c12ace58574d26880e522c22801437151a159392270e", [:mix], [], "hexpm", "fd12a8db2021036ce12a309f26f564ec367373265b53e25403f0ee697380f1b8"}, 25 | "nimble_parsec": {:hex, :nimble_parsec, "1.4.0", "51f9b613ea62cfa97b25ccc2c1b4216e81df970acd8e16e8d1bdc58fef21370d", [:mix], [], "hexpm", "9c565862810fb383e9838c1dd2d7d2c437b3d13b267414ba6af33e50d2d1cf28"}, 26 | "nimble_pool": {:hex, :nimble_pool, "1.0.0", "5eb82705d138f4dd4423f69ceb19ac667b3b492ae570c9f5c900bb3d2f50a847", [:mix], [], "hexpm", "80be3b882d2d351882256087078e1b1952a28bf98d0a287be87e4a24a710b67a"}, 27 | "numbers": {:hex, :numbers, "5.2.4", "f123d5bb7f6acc366f8f445e10a32bd403c8469bdbce8ce049e1f0972b607080", [:mix], [{:coerce, "~> 1.0", [hex: :coerce, repo: "hexpm", optional: false]}, {:decimal, "~> 1.9 or ~> 2.0", [hex: :decimal, repo: "hexpm", optional: true]}], "hexpm", "eeccf5c61d5f4922198395bf87a465b6f980b8b862dd22d28198c5e6fab38582"}, 28 | "propcheck": {:hex, :propcheck, "1.4.1", "c12908dbe6f572032928548089b34ff9d40672d5d70f1562e3a9e9058d226cc9", [:mix], [{:libgraph, "~> 0.13", [hex: :libgraph, repo: "hexpm", optional: false]}, {:proper, "~> 1.4", [hex: :proper, repo: "hexpm", optional: false]}], "hexpm", "e1b088f574785c3c7e864da16f39082d5599b3aaf89086d3f9be6adb54464b19"}, 29 | "proper": {:hex, :proper, "1.4.0", "89a44b8c39d28bb9b4be8e4d715d534905b325470f2e0ec5e004d12484a79434", [:rebar3], [], "hexpm", "18285842185bd33efbda97d134a5cb5a0884384db36119fee0e3cfa488568cbb"}, 30 | "qex": {:hex, :qex, "0.5.1", "0d82c0f008551d24fffb99d97f8299afcb8ea9cf99582b770bd004ed5af63fd6", [:mix], [], "hexpm", "935a39fdaf2445834b95951456559e9dc2063d0a055742c558a99987b38d6bab"}, 31 | "ratio": {:hex, :ratio, "3.0.2", "60a5976872a4dc3d873ecc57eed1738589e99d1094834b9c935b118231297cfb", [:mix], [{:decimal, "~> 1.6 or ~> 2.0", [hex: :decimal, repo: "hexpm", optional: true]}, {:numbers, "~> 5.2.0", [hex: :numbers, repo: "hexpm", optional: false]}], "hexpm", "3a13ed5a30ad0bfd7e4a86bf86d93d2b5a06f5904417d38d3f3ea6406cdfc7bb"}, 32 | "req": {:hex, :req, "0.4.5", "2071bbedd280f107b9e33e1ddff2beb3991ec1ae06caa2cca2ab756393d8aca5", [:mix], [{:brotli, "~> 0.3.1", [hex: :brotli, repo: "hexpm", optional: true]}, {:ezstd, "~> 1.0", [hex: :ezstd, repo: "hexpm", optional: true]}, {:finch, "~> 0.9", [hex: :finch, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: false]}, {:mime, "~> 1.6 or ~> 2.0", [hex: :mime, repo: "hexpm", optional: false]}, {:nimble_csv, "~> 1.0", [hex: :nimble_csv, repo: "hexpm", optional: true]}, {:plug, "~> 1.0", [hex: :plug, repo: "hexpm", optional: true]}], "hexpm", "dd23e9c7303ddeb2dee09ff11ad8102cca019e38394456f265fb7b9655c64dd8"}, 33 | "secure_random": {:hex, :secure_random, "0.5.1", "c5532b37c89d175c328f5196a0c2a5680b15ebce3e654da37129a9fe40ebf51b", [:mix], [], "hexpm", "1b9754f15e3940a143baafd19da12293f100044df69ea12db5d72878312ae6ab"}, 34 | "shmex": {:hex, :shmex, "0.5.0", "7dc4fb1a8bd851085a652605d690bdd070628717864b442f53d3447326bcd3e8", [:mix], [{:bunch_native, "~> 0.5.0", [hex: :bunch_native, repo: "hexpm", optional: false]}, {:bundlex, "~> 1.0", [hex: :bundlex, repo: "hexpm", optional: false]}], "hexpm", "b67bb1e22734758397c84458dbb746519e28eac210423c267c7248e59fc97bdc"}, 35 | "telemetry": {:hex, :telemetry, "1.2.1", "68fdfe8d8f05a8428483a97d7aab2f268aaff24b49e0f599faa091f1d4e7f61c", [:rebar3], [], "hexpm", "dad9ce9d8effc621708f99eac538ef1cbe05d6a874dd741de2e689c47feafed5"}, 36 | "unifex": {:hex, :unifex, "1.1.0", "26b1bcb6c3b3454e1ea15f85b2e570aaa5b5c609566aa9f5c2e0a8b213379d6b", [:mix], [{:bunch, "~> 1.0", [hex: :bunch, repo: "hexpm", optional: false]}, {:bundlex, "~> 1.0", [hex: :bundlex, repo: "hexpm", optional: false]}, {:shmex, "~> 0.5.0", [hex: :shmex, repo: "hexpm", optional: false]}], "hexpm", "d8f47e9e3240301f5b20eec5792d1d4341e1a3a268d94f7204703b48da4aaa06"}, 37 | "zarex": {:hex, :zarex, "1.0.3", "a9e9527a1c31df7f39499819bd76ccb15b0b4e479eed5a4a40db9df7ad7db25c", [:mix], [], "hexpm", "4400a7d33bbf222383ce9a3d5ec9411798eb2b12e86c65ad8e6ac08d8116ca8b"}, 38 | } 39 | -------------------------------------------------------------------------------- /test/test_helper.exs: -------------------------------------------------------------------------------- 1 | ExUnit.start(capture_log: true) 2 | -------------------------------------------------------------------------------- /test/turbojpeg/filter_test.exs: -------------------------------------------------------------------------------- 1 | defmodule Turbojpeg.FilterTest do 2 | use ExUnit.Case 3 | 4 | import Membrane.ChildrenSpec 5 | import Membrane.Testing.Assertions 6 | 7 | alias Membrane.Testing 8 | 9 | @stream_format %Membrane.RawVideo{ 10 | width: 64, 11 | height: 64, 12 | pixel_format: :I444, 13 | framerate: nil, 14 | aligned: true 15 | } 16 | 17 | defp start_pipeline(jpeg, repeat) do 18 | {:ok, yuv} = Turbojpeg.jpeg_to_yuv(jpeg) 19 | data = List.duplicate(yuv, repeat) 20 | 21 | spec = [ 22 | child(:source, %Testing.Source{output: data, stream_format: @stream_format}) 23 | |> child(:filter, %Turbojpeg.Filter{quality: 100}) 24 | |> child(:sink, Testing.Sink) 25 | ] 26 | 27 | Testing.Pipeline.start_link_supervised!(spec: spec) 28 | end 29 | 30 | test "integration test" do 31 | jpeg = File.read!("fixture/ff0000_i444.jpg") 32 | repeat = 5 33 | 34 | pid = start_pipeline(jpeg, repeat) 35 | assert_sink_playing(pid, :sink) 36 | 37 | 1..repeat 38 | |> Enum.each(fn _ -> 39 | assert_sink_buffer(pid, :sink, buffer) 40 | assert buffer.payload == jpeg 41 | end) 42 | 43 | assert_end_of_stream(pid, :sink) 44 | Testing.Pipeline.terminate(pid) 45 | end 46 | end 47 | -------------------------------------------------------------------------------- /test/turbojpeg/sink_test.exs: -------------------------------------------------------------------------------- 1 | defmodule Turbojpeg.SinkTest do 2 | @moduledoc false 3 | 4 | use ExUnit.Case 5 | 6 | alias Turbojpeg.Sink 7 | 8 | @moduletag :tmp_dir 9 | 10 | @stream_format %Membrane.RawVideo{ 11 | width: 1920, 12 | height: 1080, 13 | aligned: true, 14 | pixel_format: :I420, 15 | framerate: nil 16 | } 17 | 18 | @in_path "fixture/i420.yuv" 19 | @ctx %{pads: %{input: %{stream_format: @stream_format}}} 20 | 21 | setup %{tmp_dir: tmp_dir} do 22 | %{out_path: Path.join(tmp_dir, "image.jpeg")} 23 | end 24 | 25 | test "write yuv data to a jpeg file", %{out_path: out_path} do 26 | yuv = File.read!(@in_path) 27 | {:ok, jpeg} = Turbojpeg.yuv_to_jpeg(yuv, 1920, 1080, 56, :I420) 28 | 29 | assert {[], state} = Sink.handle_init(@ctx, %Sink{filename: out_path, quality: 56}) 30 | 31 | assert {[], %{width: 1920, height: 1080, format: :I420} = state} = 32 | Sink.handle_stream_format(:input, @stream_format, @ctx, state) 33 | 34 | assert {[], _state} = Sink.handle_buffer(:input, %Membrane.Buffer{payload: yuv}, @ctx, state) 35 | 36 | assert File.exists?(out_path) 37 | assert File.read!(out_path) == jpeg 38 | end 39 | end 40 | -------------------------------------------------------------------------------- /test/turbojpeg_test.exs: -------------------------------------------------------------------------------- 1 | defmodule TurbojpegTest do 2 | @moduledoc false 3 | 4 | use ExUnit.Case 5 | use PropCheck, numtests: 10 6 | use Mogrify.Options 7 | 8 | alias Turbojpeg.JpegHeader 9 | 10 | @jpeg_header <<255, 216, 255>> 11 | @i420_fixture "fixture/i420.yuv" 12 | @ff0000_fixture "fixture/ff0000_i444.jpg" 13 | 14 | test "Converts an i420 frame into a jpeg" do 15 | frame = File.read!(@i420_fixture) 16 | {:ok, jpeg} = Turbojpeg.yuv_to_jpeg(frame, 1920, 1080, 100, :I420) 17 | assert match?(@jpeg_header <> _, jpeg) 18 | end 19 | 20 | test "extracts i444 frame from jpeg" do 21 | jpeg = File.read!(@ff0000_fixture) 22 | {:ok, yuv} = Turbojpeg.jpeg_to_yuv(jpeg) 23 | {:ok, new_jpeg} = Turbojpeg.yuv_to_jpeg(yuv, 64, 64, 100, :I444) 24 | assert jpeg == new_jpeg 25 | end 26 | 27 | test "get jpeg header" do 28 | jpeg = File.read!(@ff0000_fixture) 29 | {:ok, %JpegHeader{} = result} = Turbojpeg.get_jpeg_header(jpeg) 30 | assert result.width == 64 31 | assert result.height == 64 32 | assert result.format == :I444 33 | end 34 | 35 | property "solid color jpeg complementary" do 36 | forall [width, height, seed, {r, g, b}, {sampling_factor, _format}] <- [ 37 | width(), 38 | height(), 39 | seed(), 40 | rgb(), 41 | format() 42 | ] do 43 | color = :io_lib.format(~c"#~2.16.0B~2.16.0B~2.16.0B", [r, g, b]) 44 | 45 | jpeg = 46 | %Mogrify.Image{} 47 | |> Mogrify.custom("size", "#{width}x#{height}") 48 | |> Mogrify.custom("seed", seed) 49 | |> Mogrify.custom("canvas", to_string(color)) 50 | |> Mogrify.custom("sampling-factor", sampling_factor) 51 | |> Mogrify.custom("stdout", "jpg:-") 52 | |> Mogrify.create(buffer: true) 53 | 54 | {:ok, yuv} = Turbojpeg.jpeg_to_yuv(jpeg.buffer) 55 | 56 | {:ok, original_header} = Turbojpeg.get_jpeg_header(jpeg.buffer) 57 | 58 | {:ok, new_jpeg} = Turbojpeg.yuv_to_jpeg(yuv, width, height, 100, original_header.format) 59 | 60 | {:ok, new_header} = Turbojpeg.get_jpeg_header(new_jpeg) 61 | 62 | assert original_header == new_header 63 | end 64 | end 65 | 66 | property "jpeg and yuv conversion are complementary after running through the tool once" do 67 | forall [width, height, seed, {sampling_factor, format}, quality] <- [ 68 | width(), 69 | height(), 70 | seed(), 71 | format(), 72 | integer(0, 100) 73 | ] do 74 | jpeg = 75 | %Mogrify.Image{} 76 | |> Mogrify.custom("size", "#{width}x#{height}") 77 | |> Mogrify.custom("seed", seed) 78 | |> Mogrify.custom("plasma", "fractal") 79 | |> Mogrify.custom("sampling-factor", sampling_factor) 80 | |> Mogrify.custom("stdout", "jpg:-") 81 | |> Mogrify.create(buffer: true) 82 | 83 | {:ok, yuv} = Turbojpeg.jpeg_to_yuv(jpeg.buffer) 84 | {:ok, new_jpeg} = Turbojpeg.yuv_to_jpeg(yuv, width, height, quality, format) 85 | {:ok, original_header} = Turbojpeg.get_jpeg_header(jpeg.buffer) 86 | {:ok, new_header} = Turbojpeg.get_jpeg_header(new_jpeg) 87 | assert original_header == new_header 88 | end 89 | end 90 | 91 | def to_range(size, n) do 92 | base = div(n, size) 93 | {base * size, (base + 1) * size} 94 | end 95 | 96 | def as_bytes({min, max}, size) do 97 | {div(min, size), div(max, size)} 98 | end 99 | 100 | def width(multiplier \\ 10) do 101 | dimension(multiplier) 102 | end 103 | 104 | def height(multiplier \\ 10) do 105 | dimension(multiplier) 106 | end 107 | 108 | def dimension(multiplier) do 109 | sized(s, resize(s * multiplier, pos_integer())) 110 | end 111 | 112 | def seed do 113 | pos_integer() 114 | end 115 | 116 | def format do 117 | oneof([{"4:2:0", :I420}, {"4:4:4", :I444}, {"4:2:2", :I422}]) 118 | end 119 | 120 | def rgb do 121 | {color(), color(), color()} 122 | end 123 | 124 | def color(), do: integer(0, 255) 125 | end 126 | --------------------------------------------------------------------------------