Skip to content

Instantly share code, notes, and snippets.

@vlad-ivanov-name
Last active December 14, 2024 08:39
Show Gist options
  • Save vlad-ivanov-name/9dd7298b9e6c55d865dcd6cf76ce483b to your computer and use it in GitHub Desktop.
Save vlad-ivanov-name/9dd7298b9e6c55d865dcd6cf76ce483b to your computer and use it in GitHub Desktop.
Radiance HDR to HEIF/HEIC converter

Radiance HDR to HEIF/HEIC converter

Simple Rust program to convert Radiance HDR files to HEIC files.

Usage: heif-converter [OPTIONS] <INPUT> [OUTPUT]

Arguments:
  <INPUT>   
  [OUTPUT]  

Options:
      --cll <CLL>  

CLL is Content Light Level and defaults to zero. When set to zero, some image viewers will attempt to estimate it, which can lead to images being too dark or too bright. You can override it with a number instead.

Code

At the time of writing this program, libheif-rs bindings were missing features required to make HDR work, so this program calls libheif-sys directly.

Note that no clean-up of resources is performed so you will want to add Drop wrappers if you want to use this code

License

Note that libheif is licensed as LGPL. While libheif-sys is technically declaring its license as MIT, it's embedding libheif sources so I'm not sure what the license situation there is. Either way, you can choose LGPL or MIT for this snippet - at your own risk.

[package]
name = "heif-converter"
version = "0.1.0"
edition = "2021"
[dependencies]
anyhow = "1.0.93"
clap = { version = "4.5.21", features = ["derive"] }
image = { version = "0.25.5", default-features = false, features = ["hdr"] }
itertools = "0.13.0"
libc = "0.2.167"
[dependencies.libheif-sys]
git = "https://github.com/Cykooz/libheif-sys.git"
rev = "323d58d5638e1453c1792aca45a519219aca7c3c"
use image::codecs::hdr::HdrDecoder;
use image::{DynamicImage, Rgb32FImage};
use itertools::Itertools;
use libc::c_char;
use libheif_sys::{
heif_channel_heif_channel_interleaved, heif_chroma_heif_chroma_interleaved_RRGGBB_BE,
heif_color_primaries_heif_color_primaries_ITU_R_BT_2020_2_and_2100_0,
heif_colorspace_heif_colorspace_RGB, heif_compression_format_heif_compression_HEVC,
heif_content_light_level, heif_context_alloc, heif_context_encode_image,
heif_context_get_encoder, heif_context_write_to_file, heif_encoder, heif_encoder_descriptor,
heif_encoder_set_lossy_quality, heif_error, heif_get_encoder_descriptors, heif_image,
heif_image_add_plane, heif_image_create, heif_image_get_plane, heif_image_handle,
heif_image_set_content_light_level, heif_image_set_nclx_color_profile,
heif_matrix_coefficients_heif_matrix_coefficients_unspecified, heif_nclx_color_profile_alloc,
heif_transfer_characteristics_heif_transfer_characteristic_ITU_R_BT_2100_0_PQ,
};
use std::ffi::c_int;
use std::path::{Path, PathBuf};
use std::ptr::{null, null_mut};
use clap::Parser;
fn check(value: heif_error) -> anyhow::Result<()> {
if value.code == 0 {
Ok(())
} else {
Err(anyhow::anyhow!("Error: {:?}", value))
}
}
fn check_alloc<T>(value: *mut T) -> anyhow::Result<*mut T> {
if value.is_null() {
Err(anyhow::anyhow!("Allocation failed"))
} else {
Ok(value)
}
}
fn read_source(path: &Path) -> anyhow::Result<Rgb32FImage> {
let mut reader = std::io::BufReader::new(std::fs::File::open(path)?);
let decoder = HdrDecoder::new(&mut reader)?;
let image = DynamicImage::from_decoder(decoder)?;
let image = match image {
DynamicImage::ImageRgb32F(image) => image,
_ => {
return Err(anyhow::anyhow!("Unsupported image format"));
}
};
Ok(image)
}
// Constants for PQ (SMPTE ST 2084)
const M1: f32 = 2610.0 / 16384.0;
const M2: f32 = 2523.0 / 32.0;
const C1: f32 = 3424.0 / 4096.0;
const C2: f32 = 2413.0 / 128.0;
const C3: f32 = 2392.0 / 128.0;
// Linear RGB to BT.2020
fn linear_rgb_to_bt2020(r: f32, g: f32, b: f32) -> (f32, f32, f32) {
let r_bt2020 = 0.627402 * r + 0.329292 * g + 0.043306 * b;
let g_bt2020 = 0.069095 * r + 0.919544 * g + 0.011361 * b;
let b_bt2020 = 0.016394 * r + 0.088028 * g + 0.895578 * b;
(r_bt2020, g_bt2020, b_bt2020)
}
// Apply PQ transfer function
fn apply_pq(value: f32) -> f32 {
let y = value.powf(M1);
((C1 + C2 * y) / (1.0 + C3 * y)).powf(M2)
}
// Convert HDR pixel to 10-bit PQ
fn hdr_to_pq_10bit(r: f32, g: f32, b: f32, max: f32) -> (u16, u16, u16) {
let r_norm = (r / max).min(1.0);
let g_norm = (g / max).min(1.0);
let b_norm = (b / max).min(1.0);
// Convert to BT.2020
let (r_bt2020, g_bt2020, b_bt2020) = linear_rgb_to_bt2020(r_norm, g_norm, b_norm);
// Apply PQ and scale to 10-bit range
let r_pq = (apply_pq(r_bt2020) * 1023.0).round() as u16;
let g_pq = (apply_pq(g_bt2020) * 1023.0).round() as u16;
let b_pq = (apply_pq(b_bt2020) * 1023.0).round() as u16;
// Clamp values to 10-bit range
(r_pq.min(1023), g_pq.min(1023), b_pq.min(1023))
}
fn encode_f32_to_u10be(source: u16) -> (u8, u8) {
let byte1 = ((source >> 8) & 0xFF) as u8;
let byte2 = (source & 0xFF) as u8;
(byte1, byte2)
}
unsafe fn encode_output(source: Rgb32FImage, cll: u16, path: &Path) -> anyhow::Result<()> {
let mut image: *mut heif_image = null_mut();
check(heif_image_create(
source.width() as c_int,
source.height() as c_int,
heif_colorspace_heif_colorspace_RGB,
heif_chroma_heif_chroma_interleaved_RRGGBB_BE,
&raw mut image,
))?;
let profile = check_alloc(heif_nclx_color_profile_alloc())?;
(*profile).color_primaries =
heif_color_primaries_heif_color_primaries_ITU_R_BT_2020_2_and_2100_0;
(*profile).transfer_characteristics =
heif_transfer_characteristics_heif_transfer_characteristic_ITU_R_BT_2100_0_PQ;
(*profile).matrix_coefficients = heif_matrix_coefficients_heif_matrix_coefficients_unspecified;
(*profile).full_range_flag = 1;
check(heif_image_set_nclx_color_profile(image, profile))?;
let light_level = heif_content_light_level {
max_content_light_level: cll,
max_pic_average_light_level: 0,
};
heif_image_set_content_light_level(image, &light_level);
check(heif_image_add_plane(
image,
heif_channel_heif_channel_interleaved,
source.width() as c_int,
source.height() as c_int,
10,
))?;
let mut stride_raw: c_int = 0;
let plane_data_raw = check_alloc(heif_image_get_plane(
image,
heif_channel_heif_channel_interleaved,
&raw mut stride_raw,
))?;
let byte_length = (stride_raw * (source.height() as c_int)) as usize;
eprintln!("stride_raw: {}, byte_length: {}", stride_raw, byte_length);
let plane_data = std::slice::from_raw_parts_mut(plane_data_raw, byte_length);
let source_max = source.iter().cloned().reduce(f32::max).unwrap() * 8f32;
let source_min = source.iter().cloned().reduce(f32::min).unwrap();
eprintln!("source_max: {}, source_min: {}", source_max, source_min);
plane_data
.iter_mut()
.enumerate()
.chunks(stride_raw as usize)
.into_iter()
.for_each(|stride| {
stride
.chunks(6)
.into_iter()
.for_each(|chunk| {
let mut chunk = chunk.collect::<Vec<_>>();
if chunk.len() != 6 {
return;
}
let byte_offset = chunk[0].0;
let x = (byte_offset % stride_raw as usize) / 6;
let y = byte_offset / stride_raw as usize;
if x >= source.width() as usize || y >= source.height() as usize {
return;
}
let source_pixel = source.get_pixel(x as u32, y as u32);
let (source_r, source_g, source_b) = hdr_to_pq_10bit(
source_pixel.0[0],
source_pixel.0[1],
source_pixel.0[2],
source_max,
);
let (r0, r1) = encode_f32_to_u10be(source_r);
let (g0, g1) = encode_f32_to_u10be(source_g);
let (b0, b1) = encode_f32_to_u10be(source_b);
*(chunk[0].1) = r0;
*(chunk[1].1) = r1;
*(chunk[2].1) = g0;
*(chunk[3].1) = g1;
*(chunk[4].1) = b0;
*(chunk[5].1) = b1;
});
});
let context = check_alloc(heif_context_alloc())?;
let mut encoder_descriptor: *const heif_encoder_descriptor = null_mut();
let n_encoder_descriptor = heif_get_encoder_descriptors(
heif_compression_format_heif_compression_HEVC,
null(),
&raw mut encoder_descriptor,
c_int::MAX,
);
if n_encoder_descriptor < 1 {
return Err(anyhow::anyhow!("No encoders found"));
}
let mut encoder: *mut heif_encoder = null_mut();
check(heif_context_get_encoder(
context,
encoder_descriptor,
&raw mut encoder,
))?;
check(heif_encoder_set_lossy_quality(encoder, 90))?;
let mut image_handle: *mut heif_image_handle = null_mut();
check(heif_context_encode_image(
context,
image,
encoder,
null_mut(),
&raw mut image_handle,
))?;
check(heif_context_write_to_file(
context,
path.display().to_string().as_ptr() as *const c_char,
))?;
Ok(())
}
#[derive(Parser, Debug)]
#[command()]
struct Args {
#[arg()]
input: PathBuf,
#[arg()]
output: Option<PathBuf>,
#[arg(long)]
cll: Option<u16>,
}
fn main() -> anyhow::Result<()> {
let args = Args::parse();
let source = read_source(&args.input)?;
let output = args.input.with_extension("heic");
unsafe {
encode_output(source, args.cll.unwrap_or(0), &output)?;
}
Ok(())
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment