├── .gitattributes ├── .github └── workflows │ ├── autoupdate.yml │ └── ci.yml ├── .gitignore ├── .gitmodules ├── Cargo.toml ├── LICENSE ├── README.md ├── kagamijxl ├── Cargo.toml ├── README.md ├── src │ ├── contiguous_buffer.rs │ ├── coupled_bufread.rs │ ├── decode.rs │ ├── encode.rs │ └── lib.rs └── tests │ ├── README.md │ ├── decode.rs │ ├── encode.rs │ ├── resources │ ├── needmoreinput.jxl │ ├── sample.jpg │ ├── sample.jxl │ └── spinfox.jxl │ └── signature.rs ├── libjxl-src ├── Cargo.toml ├── README.md ├── build.rs └── src │ ├── build.rs │ └── lib.rs └── libjxl-sys ├── Cargo.toml ├── README.md ├── build.rs ├── src └── lib.rs ├── tests ├── README.md ├── decode.rs ├── encode.rs └── sample.jxl └── wrapper.h /.gitattributes: -------------------------------------------------------------------------------- 1 | # Auto detect text files and perform LF normalization 2 | * text=auto 3 | -------------------------------------------------------------------------------- /.github/workflows/autoupdate.yml: -------------------------------------------------------------------------------- 1 | name: Update libjxl 2 | on: 3 | workflow_dispatch: null 4 | schedule: 5 | # https://crontab.guru/#0_0_1_*_* 6 | - cron: "0 0 1 * *" 7 | 8 | # Conditional runs 9 | # https://stackoverflow.com/a/61832535/2460034 10 | jobs: 11 | update-libjxl: 12 | runs-on: ubuntu-latest 13 | steps: 14 | - uses: actions/checkout@v2 15 | with: 16 | submodules: recursive 17 | 18 | - run: | 19 | git fetch && git checkout origin/main~0 20 | git submodule update --recursive 21 | echo "LIBJXL_COMMIT_HASH=$(git rev-parse HEAD)" >> $GITHUB_ENV 22 | working-directory: ./libjxl-src/submodules/libjxl 23 | 24 | - run: | 25 | git config --local user.email "action@github.com" 26 | git config --local user.name "GitHub Action" 27 | git add -A 28 | git commit -m "Update libjxl to ${{ env.LIBJXL_COMMIT_HASH }}" 29 | 30 | - name: Install stable toolchain 31 | uses: actions-rs/toolchain@v1 32 | with: 33 | profile: minimal 34 | toolchain: stable 35 | override: true 36 | 37 | - name: Run cargo test 38 | uses: actions-rs/cargo@v1 39 | with: 40 | command: test 41 | 42 | - uses: peter-evans/create-pull-request@v3 43 | with: 44 | title: "Update libjxl to ${{ env.LIBJXL_COMMIT_HASH }}" 45 | branch: update-libjxl 46 | token: ${{ secrets.GH_TOKEN }} 47 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | # https://github.com/actions-rs/example/blob/master/.github/workflows/quickstart.yml 2 | 3 | on: [push, pull_request] 4 | 5 | name: CI 6 | 7 | jobs: 8 | test-linux: 9 | name: Test on Linux 10 | runs-on: ubuntu-latest 11 | steps: 12 | - name: Checkout sources 13 | uses: actions/checkout@v2 14 | with: 15 | submodules: recursive 16 | 17 | - name: Install stable toolchain 18 | uses: actions-rs/toolchain@v1 19 | with: 20 | profile: minimal 21 | toolchain: stable 22 | override: true 23 | 24 | - name: Run cargo test 25 | uses: actions-rs/cargo@v1 26 | with: 27 | command: test 28 | 29 | test-windows: 30 | name: Test on Windows 31 | runs-on: windows-latest 32 | steps: 33 | - name: Checkout sources 34 | uses: actions/checkout@v2 35 | with: 36 | submodules: recursive 37 | 38 | - name: Install stable toolchain 39 | uses: actions-rs/toolchain@v1 40 | with: 41 | profile: minimal 42 | toolchain: stable 43 | override: true 44 | 45 | - name: Install Ninja 46 | uses: seanmiddleditch/gha-setup-ninja@master 47 | 48 | - name: Cache LLVM and Clang 49 | id: cache-llvm 50 | uses: actions/cache@v2 51 | with: 52 | path: ${{ runner.temp }}/llvm 53 | key: llvm-13.0 54 | - name: Install LLVM and Clang 55 | uses: KyleMayes/install-llvm-action@v1 56 | with: 57 | version: "13.0" 58 | directory: ${{ runner.temp }}/llvm 59 | cached: ${{ steps.cache-llvm.outputs.cache-hit }} 60 | 61 | # See: 62 | # https://github.com/rust-lang/rust-bindgen/issues/1797 63 | # https://github.com/KyleMayes/clang-sys/issues/121 64 | - name: Set LIBCLANG_PATH 65 | run: echo "LIBCLANG_PATH=$((gcm clang).source -replace "clang.exe")" >> $env:GITHUB_ENV 66 | 67 | - name: Run cargo test 68 | uses: actions-rs/cargo@v1 69 | with: 70 | command: test 71 | 72 | lints: 73 | name: Lints 74 | runs-on: ubuntu-latest 75 | steps: 76 | - name: Checkout sources 77 | uses: actions/checkout@v2 78 | with: 79 | submodules: recursive 80 | 81 | - name: Install stable toolchain 82 | uses: actions-rs/toolchain@v1 83 | with: 84 | profile: minimal 85 | toolchain: stable 86 | override: true 87 | components: rustfmt, clippy 88 | 89 | - name: Run cargo fmt 90 | uses: actions-rs/cargo@v1 91 | with: 92 | command: fmt 93 | args: --all -- --check 94 | 95 | - name: Run cargo clippy 96 | uses: actions-rs/cargo@v1 97 | with: 98 | command: clippy 99 | args: -- -D warnings 100 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Generated by Cargo 2 | # will have compiled files and executables 3 | /target/ 4 | 5 | # Remove Cargo.lock from gitignore if creating an executable, leave it for libraries 6 | # More information here https://doc.rust-lang.org/cargo/guide/cargo-toml-vs-cargo-lock.html 7 | Cargo.lock 8 | 9 | # These are backup files generated by rustfmt 10 | **/*.rs.bk 11 | -------------------------------------------------------------------------------- /.gitmodules: -------------------------------------------------------------------------------- 1 | [submodule "libjxl-src/submodules/libjxl"] 2 | path = libjxl-src/submodules/libjxl 3 | url = https://github.com/libjxl/libjxl.git 4 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [workspace] 2 | members = [ 3 | "libjxl-src", 4 | "libjxl-sys", 5 | "kagamijxl", 6 | ] 7 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | ISC License 2 | 3 | Copyright (c) 2021, Kagami Sascha Rosylight 4 | 5 | Permission to use, copy, modify, and/or distribute this software for any 6 | purpose with or without fee is hereby granted, provided that the above 7 | copyright notice and this permission notice appear in all copies. 8 | 9 | THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES 10 | WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF 11 | MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR 12 | ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES 13 | WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN 14 | ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF 15 | OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. 16 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # jxl-rs workspace 2 | 3 | Play with [JPEG XL](https://gitlab.com/wg1/jpeg-xl) in Rust environment. 4 | -------------------------------------------------------------------------------- /kagamijxl/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "kagamijxl" 3 | version = "0.3.3" 4 | authors = ["Kagami Sascha Rosylight "] 5 | edition = "2018" 6 | description = "Opinionated JPEG XL decoder/encoder library." 7 | license = "ISC" 8 | readme = "README.md" 9 | repository = "https://github.com/saschanaz/jxl-rs/tree/main/kagamijxl" 10 | keywords = ["libjxl", "jxl", "jpegxl"] 11 | categories = ["multimedia::images"] 12 | 13 | # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html 14 | 15 | [dependencies] 16 | libjxl-sys = { version = "0.7.1", path = "../libjxl-sys" } 17 | -------------------------------------------------------------------------------- /kagamijxl/README.md: -------------------------------------------------------------------------------- 1 | # kagamijxl 2 | 3 | Opinionated JPEG XL decoder/encoder library. 4 | 5 | ## API 6 | 7 | See the [documentation](https://docs.rs/kagamijxl). 8 | 9 | ### Easiest use 10 | 11 | ```rust 12 | let result = kagamijxl::decode_memory(vec); 13 | result.frames[0].data 14 | ``` 15 | 16 | ### Advanced 17 | 18 | ```rust 19 | let mut decoder = kagamijxl::Decoder::default(); 20 | decoder.need_color_profile = true; 21 | let result = decoder.decode(vec); 22 | (result.color_profile, result.frames[0].data) 23 | ``` 24 | -------------------------------------------------------------------------------- /kagamijxl/src/contiguous_buffer.rs: -------------------------------------------------------------------------------- 1 | use std::io::BufRead; 2 | 3 | use crate::coupled_bufread::CoupledBufRead; 4 | 5 | pub struct ContiguousBuffer { 6 | contiguous: Vec, 7 | buffer: CoupledBufRead, 8 | position: usize, 9 | } 10 | 11 | impl ContiguousBuffer 12 | where 13 | T: BufRead, 14 | { 15 | pub fn new(unread: Vec, buffer: T) -> Self { 16 | ContiguousBuffer { 17 | contiguous: unread, 18 | buffer: CoupledBufRead::new(buffer), 19 | position: 0, 20 | } 21 | } 22 | 23 | fn vec(&self) -> &[u8] { 24 | if self.contiguous.is_empty() { 25 | self.buffer.data() 26 | } else { 27 | &self.contiguous 28 | } 29 | } 30 | 31 | fn copy_unread(&mut self) { 32 | if self.contiguous.is_empty() { 33 | // copy before getting more buffer 34 | // if self.contiguous is non-empty it means it's already copied 35 | // if it's fully consumed the [self.position..] is empty so no-op 36 | self.contiguous.extend(&self.buffer.data()[self.position..]); 37 | self.position = 0; 38 | } 39 | self.buffer.consume_all(); 40 | } 41 | 42 | pub fn more_buf(&mut self) -> Result<(), std::io::Error> { 43 | self.copy_unread(); 44 | let data = self.buffer.fill_buf()?; 45 | if data.is_empty() { 46 | return Err(std::io::Error::new( 47 | std::io::ErrorKind::UnexpectedEof, 48 | "No more buffer", 49 | )); 50 | } 51 | 52 | if !self.contiguous.is_empty() { 53 | // we have unconsumed data, so copy it to make it contiguous 54 | self.contiguous.extend(data); 55 | } 56 | 57 | Ok(()) 58 | } 59 | 60 | pub fn consume(&mut self, amount: usize) { 61 | let new_position = self.position + amount; 62 | let vec = self.vec(); 63 | assert!(vec.len() >= new_position); 64 | if vec.len() == new_position && !self.contiguous.is_empty() { 65 | self.contiguous.clear(); 66 | self.position = self.buffer.data().len(); 67 | } else { 68 | self.position = new_position; 69 | } 70 | } 71 | 72 | pub fn as_slice(&self) -> &[u8] { 73 | &self.vec()[self.position..] 74 | } 75 | 76 | pub fn as_ptr(&self) -> *const u8 { 77 | self.as_slice().as_ptr() 78 | } 79 | 80 | pub fn len(&self) -> usize { 81 | self.as_slice().len() 82 | } 83 | 84 | pub fn take_unread(mut self) -> Vec { 85 | self.copy_unread(); 86 | self.contiguous 87 | } 88 | } 89 | 90 | #[cfg(test)] 91 | mod tests { 92 | use std::io::BufReader; 93 | 94 | use super::ContiguousBuffer; 95 | 96 | #[test] 97 | fn consume_all() { 98 | let vec = vec![1, 2, 3]; 99 | let mut buffer = ContiguousBuffer::new(Vec::new(), &vec[..]); 100 | buffer.more_buf().unwrap(); 101 | assert_eq!(buffer.as_slice(), [1, 2, 3]); 102 | 103 | buffer.consume(1); 104 | assert_eq!(buffer.as_slice(), [2, 3]); 105 | 106 | buffer.consume(2); 107 | assert_eq!(buffer.as_slice(), []); 108 | } 109 | 110 | #[test] 111 | fn consume_and_more() { 112 | let vec = vec![1, 2, 3, 4]; 113 | let reader = BufReader::with_capacity(2, &vec[..]); 114 | let mut buffer = ContiguousBuffer::new(Vec::new(), reader); 115 | buffer.more_buf().unwrap(); 116 | assert_eq!(buffer.as_slice(), [1, 2]); 117 | 118 | buffer.consume(2); 119 | assert_eq!(buffer.as_slice(), []); 120 | 121 | buffer.more_buf().unwrap(); 122 | assert_eq!(buffer.as_slice(), [3, 4]); 123 | } 124 | 125 | #[test] 126 | fn partial_consume() { 127 | let vec = vec![1, 2, 3, 4, 5]; 128 | let reader = BufReader::with_capacity(2, &vec[..]); 129 | let mut buffer = ContiguousBuffer::new(Vec::new(), reader); 130 | buffer.more_buf().unwrap(); 131 | assert_eq!(buffer.as_slice(), [1, 2]); 132 | 133 | buffer.consume(1); 134 | assert_eq!(buffer.as_slice(), [2]); 135 | 136 | buffer.more_buf().unwrap(); 137 | assert_eq!(buffer.as_slice(), [2, 3, 4]); 138 | 139 | buffer.more_buf().unwrap(); 140 | assert_eq!(buffer.as_slice(), [2, 3, 4, 5]); 141 | } 142 | 143 | #[test] 144 | fn partial_consume_and_more() { 145 | let vec = vec![1, 2, 3, 4, 5]; 146 | let reader = BufReader::with_capacity(2, &vec[..]); 147 | let mut buffer = ContiguousBuffer::new(Vec::new(), reader); 148 | buffer.more_buf().unwrap(); 149 | assert_eq!(buffer.as_slice(), [1, 2]); 150 | 151 | buffer.consume(1); 152 | assert_eq!(buffer.as_slice(), [2]); 153 | 154 | buffer.more_buf().unwrap(); 155 | assert_eq!(buffer.as_slice(), [2, 3, 4]); 156 | 157 | buffer.consume(3); 158 | assert_eq!(buffer.as_slice(), []); 159 | 160 | buffer.more_buf().unwrap(); 161 | assert_eq!(buffer.as_slice(), [5]); 162 | } 163 | } 164 | -------------------------------------------------------------------------------- /kagamijxl/src/coupled_bufread.rs: -------------------------------------------------------------------------------- 1 | use std::io::BufRead; 2 | 3 | pub struct CoupledBufRead { 4 | buf_read: T, 5 | data: Box<[u8]>, 6 | } 7 | 8 | impl CoupledBufRead 9 | where 10 | T: BufRead, 11 | { 12 | pub fn new(buf_read: T) -> CoupledBufRead { 13 | CoupledBufRead { 14 | buf_read, 15 | data: Box::from([]), 16 | } 17 | } 18 | 19 | pub fn fill_buf(&mut self) -> Result<&[u8], std::io::Error> { 20 | self.data = Box::from(self.buf_read.fill_buf()?); 21 | Ok(self.data.as_ref()) 22 | } 23 | 24 | pub fn consume_all(&mut self) { 25 | self.buf_read.consume(self.data.len()); 26 | } 27 | 28 | pub fn data(&self) -> &[u8] { 29 | self.data.as_ref() 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /kagamijxl/src/decode.rs: -------------------------------------------------------------------------------- 1 | use std::{ 2 | ffi::c_void, 3 | fmt::Debug, 4 | fs::File, 5 | io::{BufRead, BufReader}, 6 | }; 7 | 8 | use crate::{contiguous_buffer::ContiguousBuffer, BasicInfo}; 9 | use libjxl_sys::*; 10 | 11 | #[derive(Debug)] 12 | pub enum JxlDecodeError { 13 | AllocationFailed, 14 | InputNotComplete, 15 | AlreadyFinished, 16 | General, 17 | } 18 | 19 | macro_rules! try_dec_fatal { 20 | ($left:expr) => {{ 21 | if unsafe { $left } != JXL_DEC_SUCCESS { 22 | panic!("A fatal error occurred in kagamijxl::Decoder"); 23 | } 24 | }}; 25 | } 26 | 27 | fn read_basic_info( 28 | dec: *mut JxlDecoderStruct, 29 | result: &mut DecodeProgress, 30 | ) -> Result<(), JxlDecodeError> { 31 | // Get the basic info 32 | try_dec_fatal!(JxlDecoderGetBasicInfo(dec, &mut result.basic_info)); 33 | Ok(()) 34 | } 35 | 36 | fn read_color_encoding( 37 | dec: *mut JxlDecoderStruct, 38 | result: &mut DecodeProgress, 39 | pixel_format: &JxlPixelFormat, 40 | ) -> Result<(), JxlDecodeError> { 41 | // Get the ICC color profile of the pixel data 42 | let mut icc_size = 0usize; 43 | try_dec_fatal!(JxlDecoderGetICCProfileSize( 44 | dec, 45 | pixel_format, 46 | JXL_COLOR_PROFILE_TARGET_DATA, 47 | &mut icc_size, 48 | )); 49 | result.color_profile.resize(icc_size, 0); 50 | try_dec_fatal!(JxlDecoderGetColorAsICCProfile( 51 | dec, 52 | pixel_format, 53 | JXL_COLOR_PROFILE_TARGET_DATA, 54 | result.color_profile.as_mut_ptr(), 55 | icc_size, 56 | )); 57 | Ok(()) 58 | } 59 | 60 | fn prepare_frame( 61 | dec: *mut JxlDecoderStruct, 62 | result: &mut DecodeProgress, 63 | ) -> Result<(), JxlDecodeError> { 64 | let mut header = JxlFrameHeader::default(); 65 | try_dec_fatal!(JxlDecoderGetFrameHeader(dec, &mut header)); 66 | 67 | let mut name_vec: Vec = Vec::new(); 68 | name_vec.resize((header.name_length + 1) as usize, 0); 69 | try_dec_fatal!(JxlDecoderGetFrameName( 70 | dec, 71 | name_vec.as_mut_ptr() as *mut _, 72 | name_vec.len() 73 | )); 74 | 75 | name_vec.pop(); // The string ends with null which is redundant in Rust 76 | 77 | let frame = Frame { 78 | name: String::from_utf8_lossy(&name_vec[..]).to_string(), 79 | duration: header.duration, 80 | timecode: header.timecode, 81 | is_last: header.is_last != 0, 82 | ..Default::default() 83 | }; 84 | result.frames.push(frame); 85 | Ok(()) 86 | } 87 | 88 | fn prepare_preview_out_buffer( 89 | dec: *mut JxlDecoderStruct, 90 | result: &mut DecodeProgress, 91 | pixel_format: &JxlPixelFormat, 92 | ) -> Result<(), JxlDecodeError> { 93 | let mut buffer_size = 0usize; 94 | try_dec_fatal!(JxlDecoderPreviewOutBufferSize( 95 | dec, 96 | pixel_format, 97 | &mut buffer_size 98 | )); 99 | 100 | assert_eq!( 101 | buffer_size, 102 | (result.basic_info.xsize * result.basic_info.ysize * 4) as usize 103 | ); 104 | 105 | let buffer = &mut result.preview; 106 | 107 | buffer.resize(buffer_size as usize, 0); 108 | try_dec_fatal!(JxlDecoderSetPreviewOutBuffer( 109 | dec, 110 | pixel_format, 111 | buffer.as_mut_ptr() as *mut _, 112 | buffer_size, 113 | )); 114 | Ok(()) 115 | } 116 | 117 | fn prepare_image_out_buffer( 118 | dec: *mut JxlDecoderStruct, 119 | result: &mut DecodeProgress, 120 | pixel_format: &JxlPixelFormat, 121 | ) -> Result<(), JxlDecodeError> { 122 | let mut buffer_size = 0usize; 123 | try_dec_fatal!(JxlDecoderImageOutBufferSize( 124 | dec, 125 | pixel_format, 126 | &mut buffer_size 127 | )); 128 | 129 | assert_eq!( 130 | buffer_size, 131 | (result.basic_info.xsize * result.basic_info.ysize * 4) as usize 132 | ); 133 | 134 | let buffer = &mut result 135 | .frames 136 | .last_mut() 137 | .expect("Frames vector is unexpectedly empty") 138 | .data; 139 | 140 | buffer.resize(buffer_size as usize, 0); 141 | try_dec_fatal!(JxlDecoderSetImageOutBuffer( 142 | dec, 143 | pixel_format, 144 | buffer.as_mut_ptr() as *mut _, 145 | buffer_size, 146 | )); 147 | Ok(()) 148 | } 149 | 150 | fn decode_loop( 151 | progress: &mut DecodeProgress, 152 | data: impl BufRead, 153 | pixel_format: &JxlPixelFormat, 154 | stop_on_frame: bool, 155 | allow_partial: bool, 156 | ) -> Result<(), JxlDecodeError> { 157 | let dec = progress.raw.decoder; 158 | 159 | let mut buffer = ContiguousBuffer::new(progress.unread_buffer.take().unwrap_or_default(), data); 160 | 161 | try_dec_fatal!(JxlDecoderSetInput(dec, buffer.as_ptr(), buffer.len())); 162 | 163 | loop { 164 | let status = unsafe { JxlDecoderProcessInput(dec) }; 165 | 166 | match status { 167 | JXL_DEC_NEED_MORE_INPUT => { 168 | let remaining = unsafe { JxlDecoderReleaseInput(dec) }; 169 | let consumed = buffer.len() - remaining; 170 | buffer.consume(consumed); 171 | 172 | if buffer.more_buf().is_err() { 173 | if allow_partial { 174 | break; 175 | } else { 176 | return Err(JxlDecodeError::InputNotComplete); 177 | } 178 | } 179 | 180 | try_dec_fatal!(JxlDecoderSetInput(dec, buffer.as_ptr(), buffer.len())); 181 | } 182 | 183 | JXL_DEC_BASIC_INFO => read_basic_info(dec, progress)?, 184 | 185 | JXL_DEC_COLOR_ENCODING => read_color_encoding(dec, progress, pixel_format)?, 186 | 187 | JXL_DEC_FRAME => prepare_frame(dec, progress)?, 188 | 189 | JXL_DEC_NEED_PREVIEW_OUT_BUFFER => { 190 | prepare_preview_out_buffer(dec, progress, pixel_format)? 191 | } 192 | 193 | // Get the output buffer 194 | JXL_DEC_NEED_IMAGE_OUT_BUFFER => prepare_image_out_buffer(dec, progress, pixel_format)?, 195 | 196 | JXL_DEC_FULL_IMAGE => { 197 | if stop_on_frame && !progress.frames.last().unwrap().is_last { 198 | let remaining = unsafe { JxlDecoderReleaseInput(dec) }; 199 | let consumed = buffer.len() - remaining; 200 | buffer.consume(consumed); 201 | break; 202 | } 203 | } 204 | JXL_DEC_SUCCESS => { 205 | // All decoding successfully finished. 206 | progress.is_partial = false; 207 | break; 208 | } 209 | 210 | JXL_DEC_ERROR => return Err(JxlDecodeError::General), 211 | _ => panic!("Unexpected JXL decoding status found: {}", status), 212 | } 213 | } 214 | 215 | progress.unread_buffer = Some(buffer.take_unread()); 216 | 217 | Ok(()) 218 | } 219 | 220 | fn get_event_subscription_flags(dec: &Decoder) -> JxlDecoderStatus { 221 | let mut flags: JxlDecoderStatus = JXL_DEC_BASIC_INFO; 222 | if dec.need_color_profile { 223 | flags |= JXL_DEC_COLOR_ENCODING; 224 | } 225 | if dec.need_optional_preview { 226 | flags |= JXL_DEC_PREVIEW_IMAGE; 227 | } 228 | if !dec.no_full_frame { 229 | flags |= JXL_DEC_FRAME; 230 | } 231 | if !dec.no_full_image && !dec.no_full_frame { 232 | flags |= JXL_DEC_FULL_IMAGE; 233 | } 234 | flags 235 | } 236 | 237 | fn prepare_decoder( 238 | keep_orientation: Option, 239 | dec_raw: *mut JxlDecoderStruct, 240 | runner: *mut c_void, 241 | ) -> Result<(), JxlDecodeError> { 242 | if let Some(keep_orientation) = keep_orientation { 243 | try_dec_fatal!(JxlDecoderSetKeepOrientation( 244 | dec_raw, 245 | keep_orientation as i32 246 | )); 247 | } 248 | try_dec_fatal!(JxlDecoderSetParallelRunner( 249 | dec_raw, 250 | Some(JxlThreadParallelRunner), 251 | runner 252 | )); 253 | Ok(()) 254 | } 255 | 256 | pub fn decode_oneshot(data: impl BufRead, dec: &Decoder) -> Result { 257 | let mut progress = DecodeProgress::new(dec.keep_orientation)?; 258 | 259 | let event_flags = get_event_subscription_flags(dec); 260 | try_dec_fatal!(JxlDecoderSubscribeEvents( 261 | progress.raw.decoder, 262 | event_flags as i32 263 | )); 264 | 265 | // TODO: Support different pixel format 266 | // Not sure how to type the output vector properly 267 | let pixel_format = JxlPixelFormat { 268 | num_channels: 4, 269 | data_type: JXL_TYPE_UINT8, 270 | endianness: JXL_NATIVE_ENDIAN, 271 | align: 0, 272 | }; 273 | decode_loop( 274 | &mut progress, 275 | data, 276 | &pixel_format, 277 | dec.stop_on_frame, 278 | dec.allow_partial, 279 | )?; 280 | 281 | Ok(progress) 282 | } 283 | 284 | #[derive(Default)] 285 | pub struct Decoder { 286 | pub keep_orientation: Option, 287 | 288 | // pub pixel_format: Option, 289 | /** Reads color profile into `DecodeProgres::color_profile` when set to true */ 290 | pub need_color_profile: bool, 291 | /** Tries reading preview image into `DecodeProgress::preview` when set to true */ 292 | pub need_optional_preview: bool, 293 | /** Prevents reading frames at all when set to true, which means the length of DecodeProgress::frames becomes 0 */ 294 | pub no_full_frame: bool, 295 | /** Reads frame header without pixels when set to true */ 296 | pub no_full_image: bool, 297 | 298 | /** Specify if you want to stop on the first frame decode */ 299 | pub stop_on_frame: bool, 300 | /** Specify when partial input is expected */ 301 | pub allow_partial: bool, 302 | } 303 | 304 | impl Decoder { 305 | #[inline] 306 | pub fn new() -> Self { 307 | Self::default() 308 | } 309 | 310 | pub fn decode(&self, data: &[u8]) -> Result { 311 | // Just a helpful alias of decode_buffer for Vec which doesn't implement BufRead by itself 312 | self.decode_buffer(data) 313 | } 314 | 315 | pub fn decode_file(&self, file: &File) -> Result { 316 | self.decode_buffer(BufReader::new(file)) 317 | } 318 | 319 | pub fn decode_buffer(&self, buffer: impl BufRead) -> Result { 320 | decode_oneshot(buffer, self) 321 | } 322 | } 323 | 324 | struct DecodeRaw { 325 | decoder: *mut JxlDecoderStruct, 326 | parallel_runner: *mut c_void, 327 | } 328 | 329 | impl Drop for DecodeRaw { 330 | fn drop(&mut self) { 331 | unsafe { 332 | JxlThreadParallelRunnerDestroy(self.parallel_runner); 333 | JxlDecoderDestroy(self.decoder); 334 | } 335 | } 336 | } 337 | 338 | pub struct DecodeProgress { 339 | raw: DecodeRaw, 340 | unread_buffer: Option>, 341 | 342 | is_partial: bool, 343 | 344 | pub basic_info: BasicInfo, 345 | /** Can be empty unless `need_color_profile` is specified */ 346 | pub color_profile: Vec, 347 | /** Can be empty unless `need_optional_preview` is specified */ 348 | pub preview: Vec, 349 | /** Can be empty if neither of `need_frame_header` nor `need_frame` is specified */ 350 | pub frames: Vec, 351 | } 352 | 353 | impl Debug for DecodeProgress { 354 | fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { 355 | f.debug_struct("DecodeProgress") 356 | .field("is_partial", &self.is_partial) 357 | .field("basic_info", &self.basic_info) 358 | .finish() 359 | } 360 | } 361 | 362 | impl DecodeProgress { 363 | pub fn new(keep_orientation: Option) -> Result { 364 | let decoder = unsafe { JxlDecoderCreate(std::ptr::null()) }; 365 | let parallel_runner = unsafe { 366 | JxlThreadParallelRunnerCreate( 367 | std::ptr::null(), 368 | JxlThreadParallelRunnerDefaultNumWorkerThreads(), 369 | ) 370 | }; 371 | 372 | prepare_decoder(keep_orientation, decoder, parallel_runner)?; 373 | 374 | Ok(DecodeProgress { 375 | raw: DecodeRaw { 376 | decoder, 377 | parallel_runner, 378 | }, 379 | unread_buffer: None, 380 | 381 | is_partial: true, 382 | 383 | basic_info: BasicInfo::default(), 384 | color_profile: Vec::new(), 385 | preview: Vec::new(), 386 | frames: Vec::new(), 387 | }) 388 | } 389 | 390 | pub fn is_partial(&self) -> bool { 391 | self.is_partial 392 | } 393 | 394 | pub fn proceed( 395 | &mut self, 396 | data: impl BufRead, 397 | allow_partial: bool, 398 | stop_on_frame: bool, 399 | ) -> Result<(), JxlDecodeError> { 400 | if !self.is_partial { 401 | return Err(JxlDecodeError::AlreadyFinished); 402 | } 403 | 404 | // TODO: Support different pixel format 405 | // Not sure how to type the output vector properly 406 | let pixel_format = JxlPixelFormat { 407 | num_channels: 4, 408 | data_type: JXL_TYPE_UINT8, 409 | endianness: JXL_NATIVE_ENDIAN, 410 | align: 0, 411 | }; 412 | decode_loop(self, data, &pixel_format, stop_on_frame, allow_partial)?; 413 | Ok(()) 414 | } 415 | 416 | pub fn flush(&mut self) { 417 | unsafe { JxlDecoderFlushImage(self.raw.decoder) }; 418 | } 419 | } 420 | 421 | #[derive(Default)] 422 | pub struct Frame { 423 | pub name: String, 424 | pub duration: u32, 425 | pub timecode: u32, 426 | pub is_last: bool, 427 | 428 | /** Can be empty when `no_full_frame` is specified */ 429 | pub data: Vec, 430 | } 431 | -------------------------------------------------------------------------------- /kagamijxl/src/encode.rs: -------------------------------------------------------------------------------- 1 | use std::{ffi::c_void, os::raw::c_int}; 2 | 3 | use libjxl_sys::*; 4 | 5 | macro_rules! try_enc { 6 | ($left:expr, $right:expr) => {{ 7 | if unsafe { $left } != JXL_ENC_SUCCESS { 8 | return Err($right); 9 | } 10 | }}; 11 | } 12 | 13 | macro_rules! try_enc_fatal { 14 | ($left:expr) => {{ 15 | if unsafe { $left } != JXL_ENC_SUCCESS { 16 | panic!("A fatal error occurred in kagamijxl::Encoder"); 17 | } 18 | }}; 19 | } 20 | 21 | #[derive(Debug)] 22 | pub enum JxlEncodeError { 23 | UnsupportedValue(String), 24 | } 25 | 26 | unsafe fn encode_loop(enc: *mut JxlEncoderStruct) -> Vec { 27 | let mut compressed: Vec = Vec::new(); 28 | compressed.resize(64, 0); 29 | let mut next_out = compressed.as_mut_ptr(); 30 | let mut avail_out = compressed.len(); 31 | loop { 32 | let process_result = JxlEncoderProcessOutput(enc, &mut next_out, &mut avail_out); 33 | match process_result { 34 | JXL_ENC_NEED_MORE_OUTPUT => { 35 | let offset = next_out.offset_from(compressed.as_ptr()); 36 | compressed.resize(compressed.len() * 2, 0); 37 | next_out = compressed.as_mut_ptr().offset(offset); 38 | avail_out = compressed.len() - offset as usize; 39 | } 40 | JXL_ENC_SUCCESS => { 41 | compressed.resize(compressed.len() - avail_out, 0); 42 | return compressed; 43 | } 44 | 45 | JXL_ENC_ERROR => panic!("Encoder reported an unexpected error during processing"), 46 | _ => panic!("Unknown JXL encoding status found: {}", process_result), 47 | } 48 | } 49 | } 50 | 51 | fn prepare_encoder( 52 | enc: &Encoder, 53 | enc_raw: *mut JxlEncoderStruct, 54 | basic_info: &JxlBasicInfo, 55 | runner: *mut c_void, 56 | frame: &dyn InputFrame, 57 | ) -> Result<(), JxlEncodeError> { 58 | try_enc_fatal!(JxlEncoderSetParallelRunner( 59 | enc_raw, 60 | Some(JxlThreadParallelRunner), 61 | runner 62 | )); 63 | 64 | try_enc_fatal!(JxlEncoderSetBasicInfo(enc_raw, basic_info)); 65 | 66 | let mut color_encoding = JxlColorEncoding::default(); 67 | unsafe { JxlColorEncodingSetToSRGB(&mut color_encoding, 0) }; 68 | try_enc_fatal!(JxlEncoderSetColorEncoding(enc_raw, &color_encoding)); 69 | 70 | let options = enc.create_options(enc_raw)?; 71 | 72 | match frame.get_type() { 73 | FrameType::Bitmap => { 74 | let pixel_format = JxlPixelFormat { 75 | num_channels: 4, 76 | data_type: JXL_TYPE_UINT8, 77 | endianness: JXL_NATIVE_ENDIAN, 78 | align: 0, 79 | }; 80 | try_enc_fatal!(JxlEncoderAddImageFrame( 81 | options, 82 | &pixel_format, 83 | frame.get_data().as_ptr() as *mut std::ffi::c_void, 84 | frame.get_data().len(), 85 | )); 86 | } 87 | FrameType::Jpeg => { 88 | try_enc_fatal!(JxlEncoderStoreJPEGMetadata(enc_raw, 1)); 89 | try_enc_fatal!(JxlEncoderAddJPEGFrame( 90 | options, 91 | frame.get_data().as_ptr(), 92 | frame.get_data().len(), 93 | )) 94 | } 95 | } 96 | 97 | unsafe { JxlEncoderCloseInput(enc_raw) }; 98 | 99 | Ok(()) 100 | } 101 | 102 | pub unsafe fn encode_oneshot( 103 | frame: &dyn InputFrame, 104 | enc: &Encoder, 105 | ) -> Result, JxlEncodeError> { 106 | let runner = JxlThreadParallelRunnerCreate( 107 | std::ptr::null(), 108 | JxlThreadParallelRunnerDefaultNumWorkerThreads(), 109 | ); 110 | 111 | let enc_raw = JxlEncoderCreate(std::ptr::null()); 112 | 113 | prepare_encoder(enc, enc_raw, &enc.basic_info, runner, frame)?; 114 | 115 | let result = encode_loop(enc_raw); 116 | 117 | JxlThreadParallelRunnerDestroy(runner); 118 | JxlEncoderDestroy(enc_raw); 119 | 120 | Ok(result) 121 | } 122 | 123 | pub enum FrameType { 124 | Bitmap, 125 | Jpeg, 126 | } 127 | 128 | pub trait InputFrame<'a> { 129 | fn get_type(&self) -> FrameType; 130 | fn get_data(&self) -> &'a [u8]; 131 | } 132 | 133 | pub struct BitmapFrame<'a> { 134 | pub data: &'a [u8], 135 | } 136 | 137 | impl<'a> InputFrame<'a> for BitmapFrame<'a> { 138 | fn get_type(&self) -> FrameType { 139 | FrameType::Bitmap 140 | } 141 | 142 | fn get_data(&self) -> &'a [u8] { 143 | self.data 144 | } 145 | } 146 | 147 | pub struct JpegFrame<'a> { 148 | pub data: &'a [u8], 149 | } 150 | 151 | impl<'a> InputFrame<'a> for JpegFrame<'a> { 152 | fn get_type(&self) -> FrameType { 153 | FrameType::Jpeg 154 | } 155 | 156 | fn get_data(&self) -> &'a [u8] { 157 | self.data 158 | } 159 | } 160 | 161 | pub struct Encoder { 162 | pub lossless: Option, 163 | pub effort: Option, 164 | pub distance: Option, 165 | pub basic_info: JxlBasicInfo, 166 | } 167 | 168 | impl Encoder { 169 | #[inline] 170 | pub fn new() -> Self { 171 | Self::default() 172 | } 173 | 174 | fn create_options( 175 | &self, 176 | enc_raw: *mut JxlEncoderStruct, 177 | ) -> Result<*mut JxlEncoderFrameSettings, JxlEncodeError> { 178 | let options = unsafe { JxlEncoderOptionsCreate(enc_raw, std::ptr::null()) }; 179 | 180 | if let Some(lossless) = self.lossless { 181 | try_enc_fatal!(JxlEncoderOptionsSetLossless(options, lossless as i32)); 182 | } 183 | if let Some(effort) = self.effort { 184 | try_enc!( 185 | JxlEncoderOptionsSetEffort(options, effort as c_int), 186 | JxlEncodeError::UnsupportedValue(format!("Effort value {} is unsupported", effort)) 187 | ); 188 | } 189 | if let Some(distance) = self.distance { 190 | try_enc!( 191 | JxlEncoderOptionsSetDistance(options, distance), 192 | JxlEncodeError::UnsupportedValue(format!( 193 | "Distance value {} is unsupported", 194 | distance 195 | )) 196 | ); 197 | } 198 | 199 | Ok(options) 200 | } 201 | 202 | pub fn encode(&self, data: &[u8]) -> Result, JxlEncodeError> { 203 | let frame = BitmapFrame { data }; 204 | self.encode_frame(&frame) 205 | } 206 | 207 | pub fn encode_frame(&self, frame: &dyn InputFrame) -> Result, JxlEncodeError> { 208 | unsafe { encode_oneshot(frame, self) } 209 | } 210 | } 211 | 212 | impl Default for Encoder { 213 | fn default() -> Self { 214 | let mut basic_info = JxlBasicInfo::default(); 215 | unsafe { 216 | JxlEncoderInitBasicInfo(&mut basic_info); 217 | } 218 | basic_info.alpha_bits = 8; 219 | basic_info.num_extra_channels = 1; 220 | basic_info.uses_original_profile = true as _; 221 | 222 | Self { 223 | lossless: None, 224 | effort: None, 225 | distance: None, 226 | basic_info, 227 | } 228 | } 229 | } 230 | -------------------------------------------------------------------------------- /kagamijxl/src/lib.rs: -------------------------------------------------------------------------------- 1 | mod contiguous_buffer; 2 | mod coupled_bufread; 3 | mod decode; 4 | mod encode; 5 | pub use decode::{DecodeProgress, Decoder, Frame, JxlDecodeError}; 6 | pub use encode::{BitmapFrame, Encoder, JpegFrame, JxlEncodeError}; 7 | pub use libjxl_sys::JxlBasicInfo as BasicInfo; 8 | 9 | pub fn decode_memory(data: &[u8]) -> Result { 10 | let decoder = Decoder::default(); 11 | decoder.decode(data) 12 | } 13 | 14 | pub fn check_signature(data: &[u8]) -> libjxl_sys::JxlSignature { 15 | unsafe { libjxl_sys::JxlSignatureCheck(data.as_ptr(), data.len()) } 16 | } 17 | 18 | pub fn encode_memory(data: &[u8], xsize: usize, ysize: usize) -> Result, JxlEncodeError> { 19 | let mut encoder = Encoder::default(); 20 | encoder.basic_info.xsize = xsize as u32; 21 | encoder.basic_info.ysize = ysize as u32; 22 | encoder.encode(data) 23 | } 24 | -------------------------------------------------------------------------------- /kagamijxl/tests/README.md: -------------------------------------------------------------------------------- 1 | * `sample.jxl` and `sample.jpg` is from [Wikimedia (user: Albert duce)](https://commons.wikimedia.org/wiki/File:Abandoned_Packard_Automobile_Factory_Detroit_200.jpg). 2 | -------------------------------------------------------------------------------- /kagamijxl/tests/decode.rs: -------------------------------------------------------------------------------- 1 | use std::{fs::File, io::BufReader, path::PathBuf}; 2 | 3 | use kagamijxl::{decode_memory, Decoder, JxlDecodeError}; 4 | use libjxl_sys::JXL_ORIENT_IDENTITY; 5 | 6 | const MANIFEST_DIR: &str = env!("CARGO_MANIFEST_DIR"); 7 | 8 | fn sample_image_path() -> PathBuf { 9 | // Resolve path manually or it will fail when running each test 10 | PathBuf::from(MANIFEST_DIR).join("tests/resources/sample.jxl") 11 | } 12 | 13 | fn get_sample_image() -> Vec { 14 | std::fs::read(sample_image_path()).expect("Failed to read the sample image") 15 | } 16 | 17 | fn get_sample_image_file() -> File { 18 | File::open(sample_image_path()).expect("Failed to read the sample image") 19 | } 20 | 21 | fn get_sample_animation() -> Vec { 22 | // Resolve path manually or it will fail when running each test 23 | let sample_path = PathBuf::from(MANIFEST_DIR).join("tests/resources/spinfox.jxl"); 24 | std::fs::read(sample_path).expect("Failed to read the sample image") 25 | } 26 | 27 | #[test] 28 | fn test_decode_memory() { 29 | let data = get_sample_image(); 30 | 31 | let result = decode_memory(&data).expect("Failed to decode the sample image"); 32 | let basic_info = &result.basic_info; 33 | 34 | assert_eq!(basic_info.xsize, 1404); 35 | assert_eq!(basic_info.ysize, 936); 36 | assert_eq!(basic_info.have_container, 0); 37 | assert_eq!(basic_info.orientation, JXL_ORIENT_IDENTITY); 38 | assert_eq!(result.preview.len(), 0); 39 | assert_eq!(result.color_profile.len(), 0); 40 | assert_eq!(result.frames.len(), 1); 41 | assert_eq!(result.frames[0].name, ""); 42 | assert_ne!(result.frames[0].data.len(), 0); 43 | } 44 | 45 | #[test] 46 | fn test_decode_default() { 47 | let data = get_sample_image(); 48 | 49 | let result = Decoder::default() 50 | .decode(&data) 51 | .expect("Failed to decode the sample image"); 52 | let basic_info = &result.basic_info; 53 | 54 | assert!(!result.is_partial()); 55 | assert_eq!(basic_info.xsize, 1404); 56 | assert_eq!(basic_info.ysize, 936); 57 | } 58 | 59 | #[test] 60 | fn test_decode_new() { 61 | let data = get_sample_image(); 62 | 63 | let result = Decoder::new() 64 | .decode(&data) 65 | .expect("Failed to decode the sample image"); 66 | let basic_info = &result.basic_info; 67 | 68 | assert_eq!(basic_info.xsize, 1404); 69 | assert_eq!(basic_info.ysize, 936); 70 | } 71 | 72 | #[test] 73 | fn test_decode_no_frame() { 74 | let data = get_sample_image(); 75 | 76 | let mut decoder = Decoder::default(); 77 | decoder.no_full_frame = true; 78 | 79 | let result = decoder 80 | .decode(&data) 81 | .expect("Failed to decode the sample image"); 82 | assert_eq!(result.frames.len(), 0); 83 | } 84 | 85 | #[test] 86 | fn test_decode_no_full_image() { 87 | let data = get_sample_animation(); 88 | 89 | let mut decoder = Decoder::default(); 90 | decoder.no_full_image = true; 91 | 92 | let result = decoder 93 | .decode(&data) 94 | .expect("Failed to decode the sample image"); 95 | assert_eq!(result.frames.len(), 25); 96 | assert_eq!(result.frames[0].data.len(), 0); 97 | } 98 | 99 | #[test] 100 | fn test_decode_color_profile() { 101 | let data = get_sample_image(); 102 | 103 | let mut decoder = Decoder::default(); 104 | decoder.need_color_profile = true; 105 | 106 | let result = decoder 107 | .decode(&data) 108 | .expect("Failed to decode the sample image"); 109 | assert_ne!(result.color_profile.len(), 0); 110 | } 111 | 112 | #[test] 113 | fn test_decode_file() { 114 | let file = get_sample_image_file(); 115 | 116 | let result = Decoder::default() 117 | .decode_file(&file) 118 | .expect("Failed to decode the sample image"); 119 | let basic_info = &result.basic_info; 120 | 121 | assert_eq!(basic_info.xsize, 1404); 122 | assert_eq!(basic_info.ysize, 936); 123 | } 124 | 125 | #[test] 126 | fn test_decode_need_more_input() { 127 | let path = PathBuf::from(MANIFEST_DIR).join("tests/resources/needmoreinput.jxl"); 128 | let file = File::open(path).expect("Failed to open the sample image"); 129 | 130 | let result = Decoder::default() 131 | .decode_file(&file) 132 | .expect("Failed to decode the sample image"); 133 | let basic_info = &result.basic_info; 134 | 135 | assert_eq!(basic_info.xsize, 3264); 136 | assert_eq!(basic_info.ysize, 1836); 137 | } 138 | 139 | #[test] 140 | fn test_decode_animation() { 141 | let data = get_sample_animation(); 142 | 143 | let result = decode_memory(&data).expect("Failed to decode the sample image"); 144 | assert_eq!(result.frames.len(), 25); 145 | for frame in result.frames { 146 | assert_ne!(frame.data.len(), 0); 147 | } 148 | } 149 | 150 | #[test] 151 | fn test_decode_animation_first() { 152 | let data = get_sample_animation(); 153 | 154 | let mut decoder = Decoder::default(); 155 | decoder.stop_on_frame = true; 156 | 157 | let result = decoder 158 | .decode(&data) 159 | .expect("Failed to decode the sample image"); 160 | 161 | assert_eq!(result.frames.len(), 1); 162 | assert_ne!(result.frames[0].data.len(), 0); 163 | } 164 | 165 | #[test] 166 | fn test_decode_animation_step() { 167 | let data = get_sample_animation(); 168 | 169 | let mut buffer = BufReader::new(&data[..]); 170 | 171 | let mut decoder = Decoder::default(); 172 | decoder.stop_on_frame = true; 173 | 174 | let mut result = decoder 175 | .decode_buffer(&mut buffer) 176 | .expect("Failed to decode the sample image"); 177 | 178 | assert_eq!(result.frames.len(), 1); 179 | assert_ne!(result.frames[0].data.len(), 0); 180 | 181 | for i in 2..=25 { 182 | result 183 | .proceed(&mut buffer, false, true) 184 | .expect(format!("Should be able to proceed: frame {}", i).as_str()); 185 | assert_eq!(result.frames.len(), i); 186 | if i == 25 { 187 | assert!(!result.is_partial()); 188 | } else { 189 | assert!(result.is_partial()); 190 | } 191 | } 192 | } 193 | 194 | #[test] 195 | fn test_decode_partial() { 196 | let data = get_sample_image(); 197 | 198 | let mut decoder = Decoder::default(); 199 | decoder.allow_partial = true; 200 | 201 | let mut result = decoder 202 | .decode(&data[..3]) 203 | .expect("Failed to decode the sample image"); 204 | 205 | assert!(result.is_partial()); 206 | assert_eq!(result.frames.len(), 0); 207 | 208 | result 209 | .proceed(&data[3..40960], true, false) 210 | .expect("Should be able to proceed"); 211 | 212 | assert!(result.is_partial()); 213 | assert_eq!(result.frames.len(), 1); 214 | assert_ne!(result.frames[0].data.len(), 0); 215 | 216 | result 217 | .proceed(&data[40960..], true, false) 218 | .expect("Should be able to proceed"); 219 | assert!(!result.is_partial()); 220 | 221 | let err = result.proceed(&[0xff][..], true, false).unwrap_err(); 222 | assert!(matches!(err, JxlDecodeError::AlreadyFinished)); 223 | } 224 | 225 | #[test] 226 | fn test_decode_partial_flush() { 227 | let data = get_sample_image(); 228 | 229 | let mut decoder = Decoder::default(); 230 | decoder.allow_partial = true; 231 | 232 | let mut result = decoder 233 | .decode(&data[..40960]) 234 | .expect("Failed to decode the sample image"); 235 | 236 | assert!(result.is_partial()); 237 | assert_eq!(result.frames.len(), 1); 238 | 239 | { 240 | let first_frame_data = &result.frames[0].data; 241 | assert_ne!(first_frame_data.len(), 0); 242 | assert_eq!(first_frame_data[first_frame_data.len() - 10..], [0; 10]); 243 | } 244 | 245 | result.flush(); 246 | { 247 | let first_frame_data = &result.frames[0].data; 248 | assert_ne!(first_frame_data[first_frame_data.len() - 10..], [0; 10]); 249 | } 250 | } 251 | 252 | #[test] 253 | fn test_decode_partial_buffer() { 254 | let data = get_sample_image(); 255 | 256 | let mut buffer = BufReader::new(&data[..40960]); 257 | 258 | let mut decoder = Decoder::default(); 259 | decoder.allow_partial = true; 260 | 261 | let result = decoder 262 | .decode_buffer(&mut buffer) 263 | .expect("Failed to decode the sample image"); 264 | 265 | assert!(result.is_partial()); 266 | assert_eq!(result.frames.len(), 1); 267 | assert_ne!(result.frames[0].data.len(), 0); 268 | assert_eq!(buffer.buffer().len(), 0, "Buffer should be consumed"); 269 | } 270 | 271 | #[test] 272 | fn test_decode_partial_fail() { 273 | let data = get_sample_image(); 274 | 275 | let err = decode_memory(&data[..40960]).unwrap_err(); 276 | assert!(matches!(err, JxlDecodeError::InputNotComplete)); 277 | } 278 | 279 | #[test] 280 | fn test_decode_partial_fail_buffer() { 281 | let err = decode_memory(&[0xff, 0x0a]).unwrap_err(); 282 | assert!(matches!(err, JxlDecodeError::InputNotComplete)); 283 | } 284 | -------------------------------------------------------------------------------- /kagamijxl/tests/encode.rs: -------------------------------------------------------------------------------- 1 | use kagamijxl::{decode_memory, encode_memory, BitmapFrame, Encoder, JpegFrame, JxlEncodeError}; 2 | use std::path::PathBuf; 3 | 4 | #[rustfmt::skip] 5 | const RGBA_DATA: [u8; 36] = [ 6 | 0x25, 0xae, 0x8e, 0x05, 0xa2, 0xad, 0x9c, 0x6c, 0xb0, 0xc1, 0xd7, 0x7c, 7 | 0xf3, 0xa6, 0x34, 0xed, 0xb7, 0x8c, 0xda, 0x80, 0xd0, 0x2d, 0x7e, 0xda, 8 | 0x48, 0x5a, 0xf7, 0x62, 0xce, 0xd8, 0x38, 0x35, 0x24, 0xd1, 0x33, 0xe9, 9 | ]; 10 | 11 | const MANIFEST_DIR: &str = env!("CARGO_MANIFEST_DIR"); 12 | 13 | fn sample_jpeg_path() -> PathBuf { 14 | // Resolve path manually or it will fail when running each test 15 | PathBuf::from(MANIFEST_DIR).join("tests/resources/sample.jpg") 16 | } 17 | 18 | fn get_sample_jpeg() -> Vec { 19 | std::fs::read(sample_jpeg_path()).expect("Failed to read the sample image") 20 | } 21 | 22 | #[test] 23 | fn test_encode_memory() { 24 | let encoded = encode_memory(&RGBA_DATA, 3, 3).expect("Failed to encode"); 25 | 26 | let result = decode_memory(&encoded).expect("Failed to decode again"); 27 | let basic_info = &result.basic_info; 28 | 29 | assert_eq!(basic_info.xsize, 3); 30 | assert_eq!(basic_info.ysize, 3); 31 | } 32 | 33 | #[test] 34 | fn test_encode_default() { 35 | let mut encoder = Encoder::default(); 36 | encoder.basic_info.xsize = 3; 37 | encoder.basic_info.ysize = 3; 38 | 39 | let encoded = encoder.encode(&RGBA_DATA).expect("Failed to encode"); 40 | 41 | let result = decode_memory(&encoded).expect("Failed to decode again"); 42 | let basic_info = &result.basic_info; 43 | 44 | assert_eq!(basic_info.xsize, 3); 45 | assert_eq!(basic_info.ysize, 3); 46 | } 47 | 48 | #[test] 49 | fn test_encode_new() { 50 | let encoded = encode_memory(&RGBA_DATA, 3, 3).expect("Failed to encode"); 51 | 52 | let result = decode_memory(&encoded).expect("Failed to decode again"); 53 | let basic_info = &result.basic_info; 54 | 55 | assert_eq!(basic_info.xsize, 3); 56 | assert_eq!(basic_info.ysize, 3); 57 | } 58 | 59 | #[test] 60 | fn test_encode_lossless() { 61 | let mut encoder = Encoder::default(); 62 | encoder.lossless = Some(true); 63 | encoder.basic_info.xsize = 3; 64 | encoder.basic_info.ysize = 3; 65 | 66 | let encoded = encoder.encode(&RGBA_DATA).expect("Failed to encode"); 67 | 68 | let result = decode_memory(&encoded).expect("Failed to decode again"); 69 | let basic_info = &result.basic_info; 70 | 71 | assert_eq!(basic_info.xsize, 3); 72 | assert_eq!(basic_info.ysize, 3); 73 | assert_eq!(result.frames[0].data[..], RGBA_DATA); 74 | } 75 | 76 | #[test] 77 | fn test_encode_frame() { 78 | let mut encoder = Encoder::default(); 79 | encoder.lossless = Some(true); 80 | encoder.basic_info.xsize = 3; 81 | encoder.basic_info.ysize = 3; 82 | 83 | let frame = BitmapFrame { data: &RGBA_DATA }; 84 | let encoded = encoder.encode_frame(&frame).expect("Failed to encode"); 85 | 86 | let result = decode_memory(&encoded).expect("Failed to decode again"); 87 | let basic_info = &result.basic_info; 88 | 89 | assert_eq!(basic_info.xsize, 3); 90 | assert_eq!(basic_info.ysize, 3); 91 | assert_eq!(result.frames.len(), 1); 92 | assert_eq!(result.frames[0].data[..], RGBA_DATA); 93 | } 94 | 95 | #[test] 96 | fn test_encode_jpeg_frame() { 97 | let mut encoder = Encoder::default(); 98 | encoder.basic_info.xsize = 800; 99 | encoder.basic_info.ysize = 533; 100 | encoder.basic_info.alpha_bits = 0; // TODO: this must be implied 101 | encoder.basic_info.num_extra_channels = 0; // TODO: this must be implied 102 | 103 | let frame = JpegFrame { 104 | data: &get_sample_jpeg()[..], 105 | }; 106 | let encoded = encoder.encode_frame(&frame).expect("Failed to encode"); 107 | 108 | let result = decode_memory(&encoded).expect("Failed to decode again"); 109 | let basic_info = &result.basic_info; 110 | 111 | assert_eq!(basic_info.xsize, 800); 112 | assert_eq!(basic_info.ysize, 533); 113 | assert_eq!(result.frames.len(), 1); 114 | assert_eq!(result.frames[0].data[0], 57); 115 | } 116 | 117 | #[test] 118 | fn test_encode_unsupported_values() { 119 | let mut encoder = Encoder::default(); 120 | encoder.distance = Some(99f32); 121 | encoder.basic_info.xsize = 3; 122 | encoder.basic_info.ysize = 3; 123 | 124 | let err = encoder.encode(&RGBA_DATA).unwrap_err(); 125 | assert!(matches!(err, JxlEncodeError::UnsupportedValue(_))); 126 | 127 | encoder = Encoder::default(); 128 | encoder.distance = Some(99f32); 129 | encoder.basic_info.xsize = 3; 130 | encoder.basic_info.ysize = 3; 131 | 132 | let err = encoder.encode(&RGBA_DATA).unwrap_err(); 133 | assert!(matches!(err, JxlEncodeError::UnsupportedValue(_))); 134 | } 135 | -------------------------------------------------------------------------------- /kagamijxl/tests/resources/needmoreinput.jxl: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/saschanaz/jxl-rs/1abbf055e9a1e184e1f6e5e49307d271c39244cd/kagamijxl/tests/resources/needmoreinput.jxl -------------------------------------------------------------------------------- /kagamijxl/tests/resources/sample.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/saschanaz/jxl-rs/1abbf055e9a1e184e1f6e5e49307d271c39244cd/kagamijxl/tests/resources/sample.jpg -------------------------------------------------------------------------------- /kagamijxl/tests/resources/sample.jxl: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/saschanaz/jxl-rs/1abbf055e9a1e184e1f6e5e49307d271c39244cd/kagamijxl/tests/resources/sample.jxl -------------------------------------------------------------------------------- /kagamijxl/tests/resources/spinfox.jxl: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/saschanaz/jxl-rs/1abbf055e9a1e184e1f6e5e49307d271c39244cd/kagamijxl/tests/resources/spinfox.jxl -------------------------------------------------------------------------------- /kagamijxl/tests/signature.rs: -------------------------------------------------------------------------------- 1 | use kagamijxl::check_signature; 2 | use libjxl_sys::JXL_SIG_CODESTREAM; 3 | 4 | #[test] 5 | fn test_signature_check() { 6 | // Resolve path manually or it will fail when running each test 7 | let sample_path = 8 | std::path::PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("tests/resources/sample.jxl"); 9 | 10 | let data = std::fs::read(sample_path).expect("Failed to read the sample image"); 11 | let result = check_signature(&data); 12 | 13 | assert_eq!(result, JXL_SIG_CODESTREAM); 14 | } 15 | -------------------------------------------------------------------------------- /libjxl-src/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "libjxl-src" 3 | version = "0.7.6" 4 | authors = ["Kagami Sascha Rosylight "] 5 | license = "ISC" 6 | keywords = ["libjxl", "jxl", "jpegxl"] 7 | categories = ["multimedia::images"] 8 | description = "Provides a vendored libjxl." 9 | repository = "https://github.com/saschanaz/jxl-rs/tree/main/libjxl-src" 10 | edition = "2018" 11 | build = "build.rs" 12 | exclude = [ 13 | "submodules/libjxl/third_party/brotli/csharp", 14 | "submodules/libjxl/third_party/brotli/docs", 15 | "submodules/libjxl/third_party/brotli/fetch-spec", 16 | "submodules/libjxl/third_party/brotli/go", 17 | "submodules/libjxl/third_party/brotli/java", 18 | "submodules/libjxl/third_party/brotli/js", 19 | "submodules/libjxl/third_party/brotli/python", 20 | "submodules/libjxl/third_party/brotli/research", 21 | "submodules/libjxl/third_party/brotli/tests", 22 | "submodules/libjxl/third_party/difftest_ng", 23 | "submodules/libjxl/third_party/googletest", 24 | "submodules/libjxl/third_party/highway/g3doc", 25 | "submodules/libjxl/third_party/IQA-optimization", 26 | "submodules/libjxl/third_party/lcms", 27 | "submodules/libjxl/third_party/skcms/profiles", 28 | "submodules/libjxl/third_party/sjpeg/tests", 29 | "submodules/libjxl/third_party/testdata", 30 | "submodules/libjxl/third_party/vmaf", 31 | ] 32 | 33 | # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html 34 | 35 | [dependencies] 36 | cmake = "0.1.48" 37 | 38 | [build-dependencies] 39 | cmake = "0.1.48" 40 | 41 | [features] 42 | default = ["instant-build"] 43 | instant-build = [] 44 | -------------------------------------------------------------------------------- /libjxl-src/README.md: -------------------------------------------------------------------------------- 1 | # libjxl-src 2 | 3 | Builds the bundled [JPEG XL reference library](https://gitlab.com/wg1/jpeg-xl) 0.2 (version 945ad0ce, 2021-01-22). 4 | 5 | Build requires GCC/Clang and CMake, while Windows additionally requires MSVC, Clang, and Ninja. 6 | 7 | ## Note 8 | 9 | The crate builds instantly by default, but a build dependency only builds in dev profile. To do a release build, use `default-features = false` in Cargo.toml and call `libjxl_src::build()` in your `build.rs`. 10 | -------------------------------------------------------------------------------- /libjxl-src/build.rs: -------------------------------------------------------------------------------- 1 | fn main() { 2 | #[cfg(feature = "instant-build")] 3 | { 4 | #[path = "src/build.rs"] 5 | mod build; 6 | build::build() 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /libjxl-src/src/build.rs: -------------------------------------------------------------------------------- 1 | use cmake::Config; 2 | use std::env; 3 | use std::path::Path; 4 | 5 | pub fn build() { 6 | let path = Path::new(env!("CARGO_MANIFEST_DIR")).join("submodules/libjxl"); 7 | let mut config = Config::new(path); 8 | config.define("JPEGXL_ENABLE_OPENEXR", "OFF"); 9 | config.define("JPEGXL_ENABLE_BENCHMARK", "OFF"); 10 | 11 | let target = env::var("TARGET").unwrap(); 12 | if target.contains("msvc") { 13 | config 14 | // MSVC is not supported, force clang 15 | .define("CMAKE_C_COMPILER", "clang-cl") 16 | .define("CMAKE_CXX_COMPILER", "clang-cl") 17 | // Force Ninja or VS will ignore CMAKE_*_COMPILER 18 | .generator("Ninja"); 19 | } 20 | 21 | config 22 | .define("JPEGXL_STATIC", "ON") 23 | .define("BUILD_TESTING", "OFF") 24 | .define("JPEGXL_ENABLE_EXAMPLES", "OFF") 25 | .define("JPEGXL_ENABLE_TOOLS", "OFF") 26 | .build(); 27 | } 28 | -------------------------------------------------------------------------------- /libjxl-src/src/lib.rs: -------------------------------------------------------------------------------- 1 | mod build; 2 | pub use build::build; 3 | 4 | pub fn out_dir() -> &'static str { 5 | std::env!("OUT_DIR") 6 | } 7 | 8 | pub fn print_cargo_link() { 9 | print_cargo_link_from(out_dir()) 10 | } 11 | 12 | /** 13 | * @param dst Pass OUT_DIR environment variable value. 14 | */ 15 | pub fn print_cargo_link_from(dst: &str) { 16 | #[cfg(all(windows, debug_assertions))] 17 | // Prevents "undefined symbol _CrtDbgReport" linker error 18 | println!("cargo:rustc-link-lib=dylib=msvcrtd"); 19 | 20 | println!("cargo:rustc-link-search=native={}/lib", dst); 21 | println!("cargo:rustc-link-search=native={}/build/third_party", dst); 22 | println!( 23 | "cargo:rustc-link-search=native={}/build/third_party/brotli", 24 | dst 25 | ); 26 | println!( 27 | "cargo:rustc-link-search=native={}/build/third_party/highway", 28 | dst 29 | ); 30 | 31 | if cfg!(windows) { 32 | println!("cargo:rustc-link-lib=static=jxl-static"); 33 | println!("cargo:rustc-link-lib=static=jxl_threads-static"); 34 | } else { 35 | println!("cargo:rustc-link-lib=static=jxl"); 36 | println!("cargo:rustc-link-lib=static=jxl_threads"); 37 | } 38 | println!("cargo:rustc-link-lib=static=brotlicommon-static"); 39 | println!("cargo:rustc-link-lib=static=brotlidec-static"); 40 | println!("cargo:rustc-link-lib=static=brotlienc-static"); 41 | println!("cargo:rustc-link-lib=static=hwy"); 42 | 43 | #[cfg(not(windows))] 44 | // The order matters; this should be after other libs or the linker fails 45 | println!("cargo:rustc-link-lib=dylib=stdc++"); 46 | } 47 | 48 | #[cfg(test)] 49 | mod tests { 50 | #[test] 51 | fn test_print() { 52 | super::print_cargo_link(); 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /libjxl-sys/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | authors = ["Kagami Sascha Rosylight "] 3 | description = "Rust bindings for libjxl, the JPEG XL reference library." 4 | edition = "2018" 5 | license = "ISC" 6 | keywords = ["libjxl", "jxl", "jpegxl"] 7 | categories = ["multimedia::images"] 8 | name = "libjxl-sys" 9 | readme = "README.md" 10 | repository = "https://github.com/saschanaz/jxl-rs/tree/main/libjxl-sys" 11 | version = "0.7.2" 12 | links = "jxl" 13 | 14 | # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html 15 | 16 | [build-dependencies] 17 | bindgen = "0.60.1" 18 | libjxl-src = { version = "0.7.6", path = "../libjxl-src", default-features = false } 19 | -------------------------------------------------------------------------------- /libjxl-sys/README.md: -------------------------------------------------------------------------------- 1 | # libjxl-sys 2 | 3 | `libjxl-sys` is a wrapper over [JPEG XL reference library](https://github.com/libjxl/libjxl) aka libjxl. 4 | 5 | Build requires GCC/Clang and CMake, while Windows additionally requires MSVC, Clang, and Ninja. 6 | -------------------------------------------------------------------------------- /libjxl-sys/build.rs: -------------------------------------------------------------------------------- 1 | extern crate bindgen; 2 | 3 | use std::env; 4 | use std::path::PathBuf; 5 | 6 | fn main() { 7 | libjxl_src::build(); 8 | 9 | let out_dir = std::env::var("OUT_DIR").unwrap(); 10 | 11 | println!("cargo:rerun-if-changed=wrapper.h"); 12 | libjxl_src::print_cargo_link_from(&out_dir); 13 | 14 | let include_dir = format!("{}/include", out_dir); 15 | println!("cargo:include={}", include_dir); 16 | 17 | // The bindgen::Builder is the main entry point 18 | // to bindgen, and lets you build up options for 19 | // the resulting bindings. 20 | let bindings = bindgen::Builder::default() 21 | // The input header we would like to generate 22 | // bindings for. 23 | .header("wrapper.h") 24 | // Tell where to find the jxl/ headers. 25 | .clang_arg("-I") 26 | .clang_arg(include_dir) 27 | // Reduce noise from system libs. 28 | .allowlist_function("Jxl.*") 29 | // #[derive(Default)] for struct initialization. 30 | .derive_default(true) 31 | // `size_t` is `usize` almost everywhere 32 | // https://github.com/rust-lang/rust-bindgen/issues/1901 33 | .size_t_is_usize(true) 34 | // libjxl already adds appropriate prefixes 35 | .prepend_enum_name(false) 36 | // Finish the builder and generate the bindings. 37 | .generate() 38 | // Unwrap the Result and panic on failure. 39 | .expect("Unable to generate bindings"); 40 | 41 | // Write the bindings to the $OUT_DIR/bindings.rs file. 42 | let out_path = PathBuf::from(env::var("OUT_DIR").unwrap()); 43 | bindings 44 | .write_to_file(out_path.join("bindings.rs")) 45 | .expect("Couldn't write bindings!"); 46 | } 47 | -------------------------------------------------------------------------------- /libjxl-sys/src/lib.rs: -------------------------------------------------------------------------------- 1 | #![allow(non_camel_case_types)] 2 | #![allow(non_snake_case)] 3 | #![allow(non_upper_case_globals)] 4 | 5 | include!(concat!(env!("OUT_DIR"), "/bindings.rs")); 6 | -------------------------------------------------------------------------------- /libjxl-sys/tests/README.md: -------------------------------------------------------------------------------- 1 | `sample.jxl` is from [Wikimedia (user: Albert duce)](https://commons.wikimedia.org/wiki/File:Abandoned_Packard_Automobile_Factory_Detroit_200.jpg). 2 | -------------------------------------------------------------------------------- /libjxl-sys/tests/decode.rs: -------------------------------------------------------------------------------- 1 | use std::usize; 2 | 3 | use libjxl_sys::*; 4 | 5 | #[test] 6 | fn test_version() { 7 | unsafe { 8 | assert_eq!(JxlDecoderVersion(), 7000); 9 | } 10 | } 11 | 12 | macro_rules! try_dec { 13 | ($left:expr) => {{ 14 | if $left != JXL_DEC_SUCCESS { 15 | return Err("Decoder failed"); 16 | } 17 | }}; 18 | } 19 | 20 | // Ported version of https://gitlab.com/wg1/jpeg-xl/-/blob/v0.2/examples/decode_oneshot.cc 21 | 22 | // Copyright (c) the JPEG XL Project 23 | // 24 | // Licensed under the Apache License, Version 2.0 (the "License"); 25 | // you may not use this file except in compliance with the License. 26 | // You may obtain a copy of the License at 27 | // 28 | // http://www.apache.org/licenses/LICENSE-2.0 29 | // 30 | // Unless required by applicable law or agreed to in writing, software 31 | // distributed under the License is distributed on an "AS IS" BASIS, 32 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 33 | // See the License for the specific language governing permissions and 34 | // limitations under the License. 35 | 36 | unsafe fn decode_loop( 37 | dec: *mut JxlDecoderStruct, 38 | data: Vec, 39 | ) -> Result<(JxlBasicInfo, Vec, Vec), &'static str> { 40 | let pixel_format = JxlPixelFormat { 41 | num_channels: 4, 42 | data_type: JXL_TYPE_UINT8, 43 | endianness: JXL_NATIVE_ENDIAN, 44 | align: 0, 45 | }; 46 | 47 | let mut basic_info = JxlBasicInfo::default(); 48 | let mut pixels_buffer: Vec = Vec::new(); 49 | let mut icc_profile: Vec = Vec::new(); 50 | 51 | try_dec!(JxlDecoderSubscribeEvents( 52 | dec, 53 | (JXL_DEC_BASIC_INFO | JXL_DEC_COLOR_ENCODING | JXL_DEC_FULL_IMAGE) as i32 54 | )); 55 | 56 | JxlDecoderSetInput(dec, data.as_ptr(), data.len()); 57 | 58 | loop { 59 | let status = JxlDecoderProcessInput(dec); 60 | 61 | match status { 62 | JXL_DEC_ERROR => return Err("Decoder error"), 63 | JXL_DEC_NEED_MORE_INPUT => return Err("Error, already provided all input"), 64 | 65 | // Get the basic info 66 | JXL_DEC_BASIC_INFO => { 67 | try_dec!(JxlDecoderGetBasicInfo(dec, &mut basic_info)); 68 | } 69 | 70 | JXL_DEC_COLOR_ENCODING => { 71 | // Get the ICC color profile of the pixel data 72 | let mut icc_size = 0usize; 73 | try_dec!(JxlDecoderGetICCProfileSize( 74 | dec, 75 | &pixel_format, 76 | JXL_COLOR_PROFILE_TARGET_DATA, 77 | &mut icc_size 78 | )); 79 | icc_profile.resize(icc_size, 0); 80 | try_dec!(JxlDecoderGetColorAsICCProfile( 81 | dec, 82 | &pixel_format, 83 | JXL_COLOR_PROFILE_TARGET_DATA, 84 | icc_profile.as_mut_ptr(), 85 | icc_size 86 | )); 87 | } 88 | 89 | // Get the output buffer 90 | JXL_DEC_NEED_IMAGE_OUT_BUFFER => { 91 | let mut buffer_size = 0usize; 92 | try_dec!(JxlDecoderImageOutBufferSize( 93 | dec, 94 | &pixel_format, 95 | &mut buffer_size 96 | )); 97 | 98 | if buffer_size != (basic_info.xsize * basic_info.ysize * 4) as usize { 99 | return Err("Invalid out buffer size"); 100 | } 101 | 102 | pixels_buffer.resize(buffer_size as usize, 0); 103 | try_dec!(JxlDecoderSetImageOutBuffer( 104 | dec, 105 | &pixel_format, 106 | pixels_buffer.as_mut_ptr() as *mut std::ffi::c_void, 107 | buffer_size, 108 | )); 109 | } 110 | 111 | JXL_DEC_FULL_IMAGE => { 112 | // Nothing to do. Do not yet return. If the image is an animation, more 113 | // full frames may be decoded. This example only keeps the last one. 114 | continue; 115 | } 116 | JXL_DEC_SUCCESS => { 117 | // All decoding successfully finished. 118 | return Ok((basic_info, pixels_buffer, icc_profile)); 119 | } 120 | _ => return Err("Unknown decoder status"), 121 | } 122 | } 123 | } 124 | 125 | pub unsafe fn decode_oneshot( 126 | data: Vec, 127 | ) -> Result<(JxlBasicInfo, Vec, Vec), &'static str> { 128 | // Multi-threaded parallel runner. 129 | let runner = JxlThreadParallelRunnerCreate( 130 | std::ptr::null(), 131 | JxlThreadParallelRunnerDefaultNumWorkerThreads(), 132 | ); 133 | 134 | let dec = JxlDecoderCreate(std::ptr::null()); 135 | 136 | if JXL_DEC_SUCCESS != JxlDecoderSetParallelRunner(dec, Some(JxlThreadParallelRunner), runner) { 137 | JxlThreadParallelRunnerDestroy(runner); 138 | JxlDecoderDestroy(dec); 139 | return Err("JxlDecoderSubscribeEvents failed"); 140 | } 141 | 142 | let result = decode_loop(dec, data); 143 | 144 | JxlThreadParallelRunnerDestroy(runner); 145 | JxlDecoderDestroy(dec); 146 | 147 | result 148 | } 149 | 150 | #[test] 151 | fn test_decode_oneshot() { 152 | // Resolve path manually or it will fail when running each test 153 | let sample_path = std::path::PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("tests/sample.jxl"); 154 | 155 | let data = std::fs::read(sample_path).expect("Failed to read the sample image"); 156 | let (basic_info, _, _) = 157 | unsafe { decode_oneshot(data).expect("Failed to decode the sample image") }; 158 | 159 | assert_eq!(basic_info.xsize, 1404); 160 | assert_eq!(basic_info.ysize, 936); 161 | assert_eq!(basic_info.have_container, 0); 162 | assert_eq!(basic_info.orientation, JXL_ORIENT_IDENTITY); 163 | } 164 | -------------------------------------------------------------------------------- /libjxl-sys/tests/encode.rs: -------------------------------------------------------------------------------- 1 | use libjxl_sys::*; 2 | mod decode; 3 | 4 | #[test] 5 | fn test_version() { 6 | unsafe { 7 | assert_eq!(JxlEncoderVersion(), 7000); 8 | } 9 | } 10 | 11 | // Ported version of https://gitlab.com/wg1/jpeg-xl/-/blob/v0.2/examples/encode_oneshot.cc 12 | 13 | // Copyright (c) the JPEG XL Project 14 | // 15 | // Licensed under the Apache License, Version 2.0 (the "License"); 16 | // you may not use this file except in compliance with the License. 17 | // You may obtain a copy of the License at 18 | // 19 | // http://www.apache.org/licenses/LICENSE-2.0 20 | // 21 | // Unless required by applicable law or agreed to in writing, software 22 | // distributed under the License is distributed on an "AS IS" BASIS, 23 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 24 | // See the License for the specific language governing permissions and 25 | // limitations under the License. 26 | 27 | unsafe fn encode_loop(enc: *mut JxlEncoderStruct) -> Result, &'static str> { 28 | let mut compressed: Vec = Vec::new(); 29 | compressed.resize(64, 0); 30 | let mut next_out = compressed.as_mut_ptr(); 31 | let mut avail_out = compressed.len(); 32 | loop { 33 | let process_result = JxlEncoderProcessOutput(enc, &mut next_out, &mut avail_out); 34 | match process_result { 35 | JXL_ENC_NEED_MORE_OUTPUT => { 36 | let offset = next_out.offset_from(compressed.as_ptr()); 37 | compressed.resize(compressed.len() * 2, 0); 38 | next_out = compressed.as_mut_ptr().offset(offset); 39 | avail_out = compressed.len() - offset as usize; 40 | } 41 | JXL_ENC_SUCCESS => { 42 | compressed.resize(compressed.len() - avail_out, 0); 43 | return Ok(compressed); 44 | } 45 | _ => return Err("JxlEncoderProcessOutput failed"), 46 | } 47 | } 48 | } 49 | 50 | unsafe fn encode_oneshot( 51 | data: &Vec, 52 | xsize: usize, 53 | ysize: usize, 54 | ) -> Result, &'static str> { 55 | let enc = JxlEncoderCreate(std::ptr::null()); 56 | 57 | let runner = JxlThreadParallelRunnerCreate( 58 | std::ptr::null(), 59 | JxlThreadParallelRunnerDefaultNumWorkerThreads(), 60 | ); 61 | 62 | if JXL_ENC_SUCCESS != JxlEncoderSetParallelRunner(enc, Some(JxlThreadParallelRunner), runner) { 63 | JxlThreadParallelRunnerDestroy(runner); 64 | JxlEncoderDestroy(enc); 65 | return Err("JxlEncoderSetParallelRunner failed"); 66 | } 67 | 68 | let basic_info = { 69 | let mut info = JxlBasicInfo::default(); 70 | JxlEncoderInitBasicInfo(&mut info); 71 | info.xsize = xsize as _; 72 | info.ysize = ysize as _; 73 | info.alpha_bits = 8; 74 | info.num_extra_channels = 1; 75 | info.uses_original_profile = true as _; 76 | info 77 | }; 78 | 79 | if JXL_ENC_SUCCESS != JxlEncoderSetBasicInfo(enc, &basic_info) { 80 | JxlThreadParallelRunnerDestroy(runner); 81 | JxlEncoderDestroy(enc); 82 | return Err("JxlEncoderSetDimensions failed"); 83 | } 84 | 85 | let pixel_format = JxlPixelFormat { 86 | num_channels: 4, 87 | data_type: JXL_TYPE_UINT8, 88 | endianness: JXL_NATIVE_ENDIAN, 89 | align: 0, 90 | }; 91 | 92 | let options = JxlEncoderOptionsCreate(enc, std::ptr::null()); 93 | JxlEncoderOptionsSetLossless(options, 1); 94 | 95 | let mut color_encoding = JxlColorEncoding::default(); 96 | JxlColorEncodingSetToSRGB(&mut color_encoding, 0); 97 | JxlEncoderSetColorEncoding(enc, &color_encoding); 98 | 99 | if JXL_ENC_SUCCESS 100 | != JxlEncoderAddImageFrame( 101 | options, // moving ownership, no need to destroy later 102 | &pixel_format, 103 | data.as_ptr() as *mut std::ffi::c_void, 104 | data.len(), 105 | ) 106 | { 107 | JxlThreadParallelRunnerDestroy(runner); 108 | JxlEncoderDestroy(enc); 109 | return Err("JxlEncoderAddImageFrame failed"); 110 | } 111 | 112 | JxlEncoderCloseInput(enc); 113 | 114 | let result = encode_loop(enc); 115 | 116 | JxlThreadParallelRunnerDestroy(runner); 117 | JxlEncoderDestroy(enc); 118 | 119 | result 120 | } 121 | 122 | #[test] 123 | fn test_encode_oneshot() { 124 | #[rustfmt::skip] 125 | let data = vec![ 126 | 0x25, 0xae, 0x8e, 0x05, 0xa2, 0xad, 0x9c, 0x6c, 0xb0, 0xc1, 0xd7, 0x7c, 127 | 0xf3, 0xa6, 0x34, 0xed, 0xb7, 0x8c, 0xda, 0x80, 0xd0, 0x2d, 0x7e, 0xda, 128 | 0x48, 0x5a, 0xf7, 0x62, 0xce, 0xd8, 0x38, 0x35, 0x24, 0xd1, 0x33, 0xe9, 129 | ]; 130 | 131 | let encoded = unsafe { encode_oneshot(&data, 3, 3).expect("Failed to encode") }; 132 | 133 | let (basic_info, decoded, _) = 134 | unsafe { decode::decode_oneshot(encoded).expect("Failed to decode again") }; 135 | 136 | assert_eq!(basic_info.xsize, 3); 137 | assert_eq!(basic_info.ysize, 3); 138 | assert_eq!(data, decoded); 139 | } 140 | -------------------------------------------------------------------------------- /libjxl-sys/tests/sample.jxl: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/saschanaz/jxl-rs/1abbf055e9a1e184e1f6e5e49307d271c39244cd/libjxl-sys/tests/sample.jxl -------------------------------------------------------------------------------- /libjxl-sys/wrapper.h: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | #include 4 | #include 5 | --------------------------------------------------------------------------------