//! Safe, bounded access to the system `OpenJPEG` codec.
//!
//! All foreign calls and callback pointer handling are contained here. The
//! build requires `OpenJPEG` 2.5.4 or newer, excluding releases affected by
//! CVE-2025-54874 and earlier fixed decoder defects.

#![allow(unsafe_code)]

use std::ffi::{CStr, c_void};
use std::marker::PhantomData;
use std::mem::MaybeUninit;
use std::ptr::NonNull;

const MAX_PIXELS: usize = 4096 * 4096;

#[allow(
    dead_code,
    non_camel_case_types,
    non_snake_case,
    non_upper_case_globals,
    rustdoc::bare_urls,
    rustdoc::broken_intra_doc_links
)]
mod ffi;

/// Codec/container selection.
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub enum Format {
    J2k,
    Jp2,
}

/// `OpenJPEG` rate-control selection.
#[derive(Clone, Copy, Debug, PartialEq)]
pub enum Compression {
    Lossless,
    Lossy { compression_ratio: f32 },
}

/// Decoder controls passed directly to `OpenJPEG`.
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub struct DecodeOptions {
    pub discard_levels: u32,
    pub quality_layers: u32,
    pub strict: bool,
    pub max_pixels: usize,
}

/// One decoded component with original integer precision.
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct Component {
    pub width: u32,
    pub height: u32,
    pub precision: u8,
    pub signed: bool,
    pub alpha: bool,
    pub samples: Vec<i32>,
}

/// Decoded `OpenJPEG` image.
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct Image {
    pub width: u32,
    pub height: u32,
    pub components: Vec<Component>,
}

/// Input image component for encoding.
#[derive(Clone, Copy, Debug)]
pub struct ComponentRef<'a> {
    pub precision: u8,
    pub signed: bool,
    pub alpha: bool,
    pub samples: &'a [i32],
}

/// Stable error categories; `OpenJPEG` diagnostics never cross the safe API.
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub enum Error {
    InvalidInput,
    LimitExceeded,
    Allocation,
    Codec,
}

/// Decodes J2K or JP2 after validating header dimensions and components and
/// before allowing `OpenJPEG` to allocate decoded sample planes.
///
/// # Errors
///
/// Returns a stable validation, limit, allocation, or codec error without
/// exposing native handles or diagnostics.
pub fn decode(bytes: &[u8], format: Format, options: DecodeOptions) -> Result<Image, Error> {
    ensure_supported_version()?;
    if bytes.is_empty() || options.max_pixels == 0 {
        return Err(Error::InvalidInput);
    }
    validate_encoded_header(bytes, format, options.max_pixels)?;
    let stream = InputStream::new(bytes)?;
    let codec = Codec::decoder(format)?;
    let mut parameters = MaybeUninit::<ffi::opj_dparameters_t>::zeroed();
    unsafe { ffi::opj_set_default_decoder_parameters(parameters.as_mut_ptr()) };
    let mut parameters = unsafe { parameters.assume_init() };
    parameters.cp_reduce = options.discard_levels;
    parameters.cp_layer = options.quality_layers;
    if unsafe { ffi::opj_setup_decoder(codec.raw.as_ptr(), &raw mut parameters) } != 1
        || unsafe {
            ffi::opj_decoder_set_strict_mode(codec.raw.as_ptr(), i32::from(options.strict))
        } != 1
    {
        return Err(Error::Codec);
    }

    // Initializing this pointer is security-significant: OpenJPEG <=2.5.3 had
    // an out-of-bounds write on short JP2 input when it was uninitialized.
    let mut raw_image = std::ptr::null_mut();
    if unsafe { ffi::opj_read_header(stream.raw.as_ptr(), codec.raw.as_ptr(), &raw mut raw_image) }
        != 1
    {
        return Err(Error::InvalidInput);
    }
    let mut image = RawImage::new(raw_image)?;
    validate_header(image.raw.as_ptr(), options.max_pixels)?;
    if unsafe { ffi::opj_decode(codec.raw.as_ptr(), stream.raw.as_ptr(), image.raw.as_ptr()) } != 1
        || unsafe { ffi::opj_end_decompress(codec.raw.as_ptr(), stream.raw.as_ptr()) } != 1
    {
        return Err(Error::InvalidInput);
    }
    image.copy(options.max_pixels)
}

/// Encodes one through five equal-sized component planes to memory.
///
/// # Errors
///
/// Returns a stable validation, limit, allocation, or codec error. Dimensions,
/// sample ranges, and output capacity are checked before native encoding.
pub fn encode(
    width: u32,
    height: u32,
    components: &[ComponentRef<'_>],
    format: Format,
    compression: Compression,
    max_encoded_bytes: usize,
) -> Result<Vec<u8>, Error> {
    ensure_supported_version()?;
    let pixels = checked_pixels(width, height, MAX_PIXELS)?;
    if components.is_empty()
        || components.len() > 5
        || max_encoded_bytes == 0
        || components.iter().any(|component| {
            !(1..=31).contains(&component.precision) || component.samples.len() != pixels
        })
    {
        return Err(Error::InvalidInput);
    }
    if let Compression::Lossy { compression_ratio } = compression {
        if !compression_ratio.is_finite() || compression_ratio < 1.0 {
            return Err(Error::InvalidInput);
        }
    }

    let image = RawImage::from_components(width, height, components)?;
    let codec = Codec::encoder(format)?;
    let stream = OutputStream::new(max_encoded_bytes)?;
    let mut parameters = MaybeUninit::<ffi::opj_cparameters_t>::zeroed();
    unsafe { ffi::opj_set_default_encoder_parameters(parameters.as_mut_ptr()) };
    let mut parameters = unsafe { parameters.assume_init() };
    parameters.tcp_numlayers = 1;
    parameters.cp_disto_alloc = 1;
    parameters.tcp_mct = (components.len() >= 3).into();
    parameters.numresolution = maximum_resolution_levels(width, height);
    parameters.max_cs_size = i32::try_from(max_encoded_bytes).unwrap_or(i32::MAX);
    match compression {
        Compression::Lossless => {
            parameters.tcp_rates[0] = 0.0;
            parameters.irreversible = 0;
        }
        Compression::Lossy { compression_ratio } => {
            parameters.tcp_rates[0] = compression_ratio;
            parameters.irreversible = 1;
        }
    }
    if unsafe {
        ffi::opj_setup_encoder(codec.raw.as_ptr(), &raw mut parameters, image.raw.as_ptr())
    } != 1
        || unsafe {
            ffi::opj_start_compress(codec.raw.as_ptr(), image.raw.as_ptr(), stream.raw.as_ptr())
        } != 1
        || unsafe { ffi::opj_encode(codec.raw.as_ptr(), stream.raw.as_ptr()) } != 1
        || unsafe { ffi::opj_end_compress(codec.raw.as_ptr(), stream.raw.as_ptr()) } != 1
    {
        return Err(if stream.state.limit_exceeded {
            Error::LimitExceeded
        } else {
            Error::Codec
        });
    }
    stream.into_bytes()
}

struct Codec {
    raw: NonNull<ffi::opj_codec_t>,
}

impl Codec {
    fn decoder(format: Format) -> Result<Self, Error> {
        let raw = unsafe { ffi::opj_create_decompress(codec_format(format)) };
        NonNull::new(raw)
            .map(|raw| Self { raw })
            .ok_or(Error::Codec)
    }

    fn encoder(format: Format) -> Result<Self, Error> {
        let raw = unsafe { ffi::opj_create_compress(codec_format(format)) };
        NonNull::new(raw)
            .map(|raw| Self { raw })
            .ok_or(Error::Codec)
    }
}

impl Drop for Codec {
    fn drop(&mut self) {
        unsafe { ffi::opj_destroy_codec(self.raw.as_ptr()) };
    }
}

struct RawImage {
    raw: NonNull<ffi::opj_image_t>,
}

impl RawImage {
    fn new(raw: *mut ffi::opj_image_t) -> Result<Self, Error> {
        NonNull::new(raw)
            .map(|raw| Self { raw })
            .ok_or(Error::Codec)
    }

    fn from_components(
        width: u32,
        height: u32,
        components: &[ComponentRef<'_>],
    ) -> Result<Self, Error> {
        let mut parameters: Vec<_> = components
            .iter()
            .map(|component| ffi::opj_image_cmptparm_t {
                dx: 1,
                dy: 1,
                w: width,
                h: height,
                x0: 0,
                y0: 0,
                prec: u32::from(component.precision),
                bpp: u32::from(component.precision),
                sgnd: u32::from(component.signed),
            })
            .collect();
        let color_space = if components.len() >= 3 {
            ffi::OPJ_COLOR_SPACE::OPJ_CLRSPC_SRGB
        } else {
            ffi::OPJ_COLOR_SPACE::OPJ_CLRSPC_GRAY
        };
        let raw = unsafe {
            ffi::opj_image_create(
                u32::try_from(parameters.len()).map_err(|_| Error::InvalidInput)?,
                parameters.as_mut_ptr(),
                color_space,
            )
        };
        let mut image = Self::new(raw)?;
        unsafe {
            let raw = image.raw.as_mut();
            raw.x0 = 0;
            raw.y0 = 0;
            raw.x1 = width;
            raw.y1 = height;
            let output = std::slice::from_raw_parts_mut(raw.comps, raw.numcomps as usize);
            for (output, input) in output.iter_mut().zip(components) {
                if output.data.is_null() {
                    return Err(Error::Allocation);
                }
                output.alpha = u16::from(input.alpha);
                let destination = std::slice::from_raw_parts_mut(output.data, input.samples.len());
                destination.copy_from_slice(input.samples);
            }
        }
        Ok(image)
    }

    fn copy(&mut self, max_pixels: usize) -> Result<Image, Error> {
        let raw = unsafe { self.raw.as_ref() };
        let width = raw.x1.checked_sub(raw.x0).ok_or(Error::InvalidInput)?;
        let height = raw.y1.checked_sub(raw.y0).ok_or(Error::InvalidInput)?;
        checked_pixels(width, height, max_pixels)?;
        let components = unsafe { std::slice::from_raw_parts(raw.comps, raw.numcomps as usize) };
        if !(1..=5).contains(&components.len()) {
            return Err(Error::InvalidInput);
        }
        let first = components.first().ok_or(Error::InvalidInput)?;
        let mut copied = Vec::new();
        copied
            .try_reserve_exact(components.len())
            .map_err(|_| Error::Allocation)?;
        for component in components {
            if component.w != first.w
                || component.h != first.h
                || component.data.is_null()
                || !(1..=31).contains(&component.prec)
            {
                return Err(Error::InvalidInput);
            }
            checked_pixels(component.w, component.h, max_pixels)?;
            let length = checked_pixels(component.w, component.h, max_pixels)?;
            let source = unsafe { std::slice::from_raw_parts(component.data, length) };
            let mut samples = Vec::new();
            samples
                .try_reserve_exact(length)
                .map_err(|_| Error::Allocation)?;
            samples.extend_from_slice(source);
            copied.push(Component {
                width: component.w,
                height: component.h,
                precision: u8::try_from(component.prec).map_err(|_| Error::InvalidInput)?,
                signed: component.sgnd != 0,
                alpha: component.alpha != 0,
                samples,
            });
        }
        Ok(Image {
            width: first.w,
            height: first.h,
            components: copied,
        })
    }
}

impl Drop for RawImage {
    fn drop(&mut self) {
        unsafe { ffi::opj_image_destroy(self.raw.as_ptr()) };
    }
}

struct InputState<'a> {
    bytes: &'a [u8],
    position: usize,
}

struct InputStream<'a> {
    raw: NonNull<ffi::opj_stream_t>,
    state: Box<InputState<'a>>,
    _lifetime: PhantomData<&'a [u8]>,
}

impl<'a> InputStream<'a> {
    fn new(bytes: &'a [u8]) -> Result<Self, Error> {
        let raw = unsafe { ffi::opj_stream_default_create(1) };
        let raw = NonNull::new(raw).ok_or(Error::Allocation)?;
        let mut state = Box::new(InputState { bytes, position: 0 });
        unsafe {
            ffi::opj_stream_set_read_function(raw.as_ptr(), Some(input_read));
            ffi::opj_stream_set_skip_function(raw.as_ptr(), Some(input_skip));
            ffi::opj_stream_set_seek_function(raw.as_ptr(), Some(input_seek));
            ffi::opj_stream_set_user_data(
                raw.as_ptr(),
                std::ptr::from_mut(state.as_mut()).cast(),
                None,
            );
            ffi::opj_stream_set_user_data_length(raw.as_ptr(), bytes.len() as u64);
        }
        Ok(Self {
            raw,
            state,
            _lifetime: PhantomData,
        })
    }
}

impl Drop for InputStream<'_> {
    fn drop(&mut self) {
        unsafe { ffi::opj_stream_destroy(self.raw.as_ptr()) };
        // Keep the callback state observably alive until after stream destroy.
        let _ = &self.state;
    }
}

struct OutputState {
    bytes: Vec<u8>,
    position: usize,
    limit: usize,
    limit_exceeded: bool,
}

struct OutputStream {
    raw: NonNull<ffi::opj_stream_t>,
    state: Box<OutputState>,
}

impl OutputStream {
    fn new(limit: usize) -> Result<Self, Error> {
        let raw = unsafe { ffi::opj_stream_default_create(0) };
        let raw = NonNull::new(raw).ok_or(Error::Allocation)?;
        let mut state = Box::new(OutputState {
            bytes: Vec::new(),
            position: 0,
            limit,
            limit_exceeded: false,
        });
        unsafe {
            ffi::opj_stream_set_write_function(raw.as_ptr(), Some(output_write));
            ffi::opj_stream_set_skip_function(raw.as_ptr(), Some(output_skip));
            ffi::opj_stream_set_seek_function(raw.as_ptr(), Some(output_seek));
            ffi::opj_stream_set_user_data(
                raw.as_ptr(),
                std::ptr::from_mut(state.as_mut()).cast(),
                None,
            );
        }
        Ok(Self { raw, state })
    }

    fn into_bytes(mut self) -> Result<Vec<u8>, Error> {
        unsafe { ffi::opj_stream_destroy(self.raw.as_ptr()) };
        self.raw = NonNull::dangling();
        if self.state.limit_exceeded || self.state.bytes.is_empty() {
            return Err(if self.state.limit_exceeded {
                Error::LimitExceeded
            } else {
                Error::Codec
            });
        }
        Ok(std::mem::take(&mut self.state.bytes))
    }
}

impl Drop for OutputStream {
    fn drop(&mut self) {
        if self.raw != NonNull::dangling() {
            unsafe { ffi::opj_stream_destroy(self.raw.as_ptr()) };
        }
    }
}

unsafe extern "C" fn input_read(
    output: *mut c_void,
    length: usize,
    user_data: *mut c_void,
) -> usize {
    if output.is_null() || user_data.is_null() || length == 0 {
        return usize::MAX;
    }
    let state = unsafe { &mut *user_data.cast::<InputState<'_>>() };
    let remaining = state.bytes.len().saturating_sub(state.position);
    if remaining == 0 {
        return usize::MAX;
    }
    let count = remaining.min(length);
    unsafe {
        std::ptr::copy_nonoverlapping(
            state.bytes.as_ptr().add(state.position),
            output.cast(),
            count,
        );
    }
    state.position += count;
    count
}

unsafe extern "C" fn input_skip(offset: i64, user_data: *mut c_void) -> i64 {
    if user_data.is_null() || offset < 0 {
        return -1;
    }
    let state = unsafe { &mut *user_data.cast::<InputState<'_>>() };
    let Ok(offset) = usize::try_from(offset) else {
        return -1;
    };
    let Some(position) = state.position.checked_add(offset) else {
        return -1;
    };
    if position > state.bytes.len() {
        return -1;
    }
    state.position = position;
    i64::try_from(offset).unwrap_or(-1)
}

unsafe extern "C" fn input_seek(position: i64, user_data: *mut c_void) -> i32 {
    if user_data.is_null() || position < 0 {
        return 0;
    }
    let state = unsafe { &mut *user_data.cast::<InputState<'_>>() };
    let Ok(position) = usize::try_from(position) else {
        return 0;
    };
    if position > state.bytes.len() {
        return 0;
    }
    state.position = position;
    1
}

unsafe extern "C" fn output_write(
    input: *mut c_void,
    length: usize,
    user_data: *mut c_void,
) -> usize {
    if input.is_null() || user_data.is_null() {
        return usize::MAX;
    }
    let state = unsafe { &mut *user_data.cast::<OutputState>() };
    let Some(end) = state.position.checked_add(length) else {
        state.limit_exceeded = true;
        return usize::MAX;
    };
    if end > state.limit {
        state.limit_exceeded = true;
        return usize::MAX;
    }
    if end > state.bytes.len() {
        if state.bytes.try_reserve(end - state.bytes.len()).is_err() {
            return usize::MAX;
        }
        state.bytes.resize(end, 0);
    }
    let source = unsafe { std::slice::from_raw_parts(input.cast::<u8>(), length) };
    state.bytes[state.position..end].copy_from_slice(source);
    state.position = end;
    length
}

unsafe extern "C" fn output_skip(offset: i64, user_data: *mut c_void) -> i64 {
    if user_data.is_null() {
        return -1;
    }
    let state = unsafe { &mut *user_data.cast::<OutputState>() };
    let Ok(offset_isize) = isize::try_from(offset) else {
        return -1;
    };
    let Some(position) = state.position.checked_add_signed(offset_isize) else {
        return -1;
    };
    if position > state.limit {
        state.limit_exceeded = true;
        return -1;
    }
    state.position = position;
    offset
}

unsafe extern "C" fn output_seek(position: i64, user_data: *mut c_void) -> i32 {
    if user_data.is_null() || position < 0 {
        return 0;
    }
    let state = unsafe { &mut *user_data.cast::<OutputState>() };
    let Ok(position) = usize::try_from(position) else {
        return 0;
    };
    if position > state.limit {
        state.limit_exceeded = true;
        return 0;
    }
    state.position = position;
    1
}

fn validate_header(raw: *mut ffi::opj_image_t, max_pixels: usize) -> Result<(), Error> {
    let raw = unsafe { raw.as_ref() }.ok_or(Error::InvalidInput)?;
    checked_pixels(
        raw.x1.checked_sub(raw.x0).ok_or(Error::InvalidInput)?,
        raw.y1.checked_sub(raw.y0).ok_or(Error::InvalidInput)?,
        max_pixels,
    )?;
    let count = usize::try_from(raw.numcomps).map_err(|_| Error::InvalidInput)?;
    if !(1..=5).contains(&count) || raw.comps.is_null() {
        return Err(Error::InvalidInput);
    }
    let components = unsafe { std::slice::from_raw_parts(raw.comps, count) };
    for component in components {
        checked_pixels(component.w, component.h, max_pixels)?;
        if !(1..=31).contains(&component.prec) {
            return Err(Error::InvalidInput);
        }
    }
    Ok(())
}

fn checked_pixels(width: u32, height: u32, maximum: usize) -> Result<usize, Error> {
    if width == 0 || height == 0 {
        return Err(Error::InvalidInput);
    }
    usize::try_from(width)
        .ok()
        .and_then(|width| {
            usize::try_from(height)
                .ok()
                .and_then(|height| width.checked_mul(height))
        })
        .filter(|pixels| *pixels <= maximum)
        .ok_or(Error::LimitExceeded)
}

fn validate_encoded_header(bytes: &[u8], format: Format, max_pixels: usize) -> Result<(), Error> {
    match format {
        Format::J2k => validate_siz_marker(bytes, max_pixels),
        Format::Jp2 => {
            let mut position = 0_usize;
            while bytes.len().saturating_sub(position) >= 8 {
                let box_length = read_u32(bytes, position)?;
                let box_type = bytes
                    .get(position + 4..position + 8)
                    .ok_or(Error::InvalidInput)?;
                let (header_length, length) = if box_length == 1 {
                    let extended = usize::try_from(read_u64(bytes, position + 8)?)
                        .map_err(|_| Error::LimitExceeded)?;
                    (16_usize, extended)
                } else if box_length == 0 {
                    (8_usize, bytes.len() - position)
                } else {
                    (
                        8_usize,
                        usize::try_from(box_length).map_err(|_| Error::LimitExceeded)?,
                    )
                };
                if length < header_length {
                    return Err(Error::InvalidInput);
                }
                let body = position
                    .checked_add(header_length)
                    .ok_or(Error::LimitExceeded)?;
                let end = position.checked_add(length).ok_or(Error::LimitExceeded)?;
                if end > bytes.len() {
                    return Err(Error::InvalidInput);
                }
                if box_type == b"jp2c" {
                    return validate_siz_marker(&bytes[body..end], max_pixels);
                }
                position = end;
            }
            Err(Error::InvalidInput)
        }
    }
}

fn validate_siz_marker(codestream: &[u8], max_pixels: usize) -> Result<(), Error> {
    if codestream.get(..4) != Some(&[0xff, 0x4f, 0xff, 0x51]) {
        return Err(Error::InvalidInput);
    }
    let length = usize::from(read_u16(codestream, 4)?);
    let end = 4_usize.checked_add(length).ok_or(Error::LimitExceeded)?;
    if length < 38 || end > codestream.len() {
        return Err(Error::InvalidInput);
    }
    let x1 = read_u32(codestream, 8)?;
    let y1 = read_u32(codestream, 12)?;
    let x0 = read_u32(codestream, 16)?;
    let y0 = read_u32(codestream, 20)?;
    if read_u32(codestream, 24)? == 0 || read_u32(codestream, 28)? == 0 {
        return Err(Error::InvalidInput);
    }
    let width = x1.checked_sub(x0).ok_or(Error::InvalidInput)?;
    let height = y1.checked_sub(y0).ok_or(Error::InvalidInput)?;
    checked_pixels(width, height, max_pixels)?;

    let component_count = usize::from(read_u16(codestream, 40)?);
    if !(1..=5).contains(&component_count)
        || length != 38 + component_count.checked_mul(3).ok_or(Error::LimitExceeded)?
    {
        return Err(Error::InvalidInput);
    }
    for component in 0..component_count {
        let offset = 42 + component * 3;
        let precision = codestream.get(offset).copied().ok_or(Error::InvalidInput)? & 0x7f;
        let dx = u32::from(
            codestream
                .get(offset + 1)
                .copied()
                .ok_or(Error::InvalidInput)?,
        );
        let dy = u32::from(
            codestream
                .get(offset + 2)
                .copied()
                .ok_or(Error::InvalidInput)?,
        );
        if precision >= 31 || dx == 0 || dy == 0 {
            return Err(Error::InvalidInput);
        }
        let component_width = ceiling_div(x1, dx)?
            .checked_sub(ceiling_div(x0, dx)?)
            .ok_or(Error::InvalidInput)?;
        let component_height = ceiling_div(y1, dy)?
            .checked_sub(ceiling_div(y0, dy)?)
            .ok_or(Error::InvalidInput)?;
        checked_pixels(component_width, component_height, max_pixels)?;
    }
    Ok(())
}

fn ceiling_div(value: u32, divisor: u32) -> Result<u32, Error> {
    value
        .checked_add(divisor - 1)
        .map(|value| value / divisor)
        .ok_or(Error::LimitExceeded)
}

fn read_u16(bytes: &[u8], position: usize) -> Result<u16, Error> {
    let bytes: [u8; 2] = bytes
        .get(position..position.checked_add(2).ok_or(Error::LimitExceeded)?)
        .ok_or(Error::InvalidInput)?
        .try_into()
        .map_err(|_| Error::InvalidInput)?;
    Ok(u16::from_be_bytes(bytes))
}

fn read_u32(bytes: &[u8], position: usize) -> Result<u32, Error> {
    let bytes: [u8; 4] = bytes
        .get(position..position.checked_add(4).ok_or(Error::LimitExceeded)?)
        .ok_or(Error::InvalidInput)?
        .try_into()
        .map_err(|_| Error::InvalidInput)?;
    Ok(u32::from_be_bytes(bytes))
}

fn read_u64(bytes: &[u8], position: usize) -> Result<u64, Error> {
    let bytes: [u8; 8] = bytes
        .get(position..position.checked_add(8).ok_or(Error::LimitExceeded)?)
        .ok_or(Error::InvalidInput)?
        .try_into()
        .map_err(|_| Error::InvalidInput)?;
    Ok(u64::from_be_bytes(bytes))
}

fn maximum_resolution_levels(width: u32, height: u32) -> i32 {
    let minimum = width.min(height);
    let levels = u32::BITS - minimum.leading_zeros();
    i32::try_from(levels.min(6)).unwrap_or(1).max(1)
}

const fn codec_format(format: Format) -> ffi::OPJ_CODEC_FORMAT {
    match format {
        Format::J2k => ffi::OPJ_CODEC_FORMAT::OPJ_CODEC_J2K,
        Format::Jp2 => ffi::OPJ_CODEC_FORMAT::OPJ_CODEC_JP2,
    }
}

fn ensure_supported_version() -> Result<(), Error> {
    let raw = unsafe { ffi::opj_version() };
    if raw.is_null() {
        return Err(Error::Codec);
    }
    let version = unsafe { CStr::from_ptr(raw) }
        .to_str()
        .map_err(|_| Error::Codec)?;
    let mut components = version.split('.').map(|component| {
        component
            .bytes()
            .take_while(u8::is_ascii_digit)
            .try_fold(0_u32, |value, digit| {
                value.checked_mul(10)?.checked_add(u32::from(digit - b'0'))
            })
    });
    let parsed = (
        components.next().flatten().ok_or(Error::Codec)?,
        components.next().flatten().ok_or(Error::Codec)?,
        components.next().flatten().ok_or(Error::Codec)?,
    );
    if parsed < (2, 5, 4) {
        return Err(Error::Codec);
    }
    Ok(())
}
