use std::collections::HashMap;
use std::sync::Arc;
use rpdfium_codec::jpx::{Jpeg2000Info, decode_with_info as jpx_decode_with_info};
use rpdfium_codec::{DecodeFilter, FilterParams, apply_filter_chain};
use rpdfium_core::{Matrix, Name};
use rpdfium_graphics::ImageRef;
use rpdfium_parser::{Object, ObjectStore, Operand, resolve_filter_chain};
use rpdfium_render::RenderError;
use rpdfium_render::image::{DecodedImage, DecodedImageFormat, ImageDecoder};
pub(crate) struct PdfImageDecoder<'a> {
store: &'a ObjectStore<Arc<[u8]>>,
}
impl<'a> PdfImageDecoder<'a> {
pub fn new(store: &'a ObjectStore<Arc<[u8]>>) -> Self {
Self { store }
}
}
impl ImageDecoder for PdfImageDecoder<'_> {
fn decode_image(
&self,
image_ref: &ImageRef,
_matrix: &Matrix,
) -> Result<DecodedImage, RenderError> {
let obj = self.store.resolve(image_ref.object_id).map_err(|e| {
RenderError::ImageDecode(format!("resolve image {}: {e}", image_ref.object_id))
})?;
let dict = obj
.as_stream_dict()
.ok_or_else(|| RenderError::ImageDecode("image XObject is not a stream".into()))?;
let width = get_dict_int(dict, &Name::width(), self.store).unwrap_or(0) as u32;
let height = get_dict_int(dict, &Name::height(), self.store).unwrap_or(0) as u32;
let mut bpc =
get_dict_int(dict, &Name::bits_per_component(), self.store).unwrap_or(8) as u32;
if width == 0 || height == 0 {
return Err(RenderError::ImageDecode("image has zero dimensions".into()));
}
let is_image_mask = get_dict_bool(dict, &Name::image_mask(), self.store).unwrap_or(false);
let smask_in_data =
get_dict_int(dict, &Name::from("SMaskInData"), self.store).unwrap_or(0) as u32;
let has_color_space = dict.contains_key(&Name::color_space());
let (mut n_components, mut cs_type) = if is_image_mask {
(1u32, ColorSpaceType::DeviceGray)
} else {
resolve_image_color_space(dict, self.store)
};
let filters = resolve_filter_chain(dict);
let has_image_filter = filters.iter().any(|(f, _)| {
matches!(
f,
DecodeFilter::JBIG2 | DecodeFilter::DCT | DecodeFilter::JPX
)
});
let jpx_is_last = filters.last().is_some_and(|(f, _)| *f == DecodeFilter::JPX);
let (decoded, jpx_info) = if jpx_is_last && !is_image_mask {
let pre_jpx_filters = &filters[..filters.len() - 1];
let raw_bytes = self
.store
.raw_stream_bytes_for_object(obj, image_ref.object_id)
.map_err(|e| RenderError::ImageDecode(format!("raw stream bytes: {e}")))?;
let jpx_bytes = if pre_jpx_filters.is_empty() {
raw_bytes
} else {
apply_filter_chain(&raw_bytes, pre_jpx_filters)
.map_err(|e| RenderError::ImageDecode(format!("pre-JPX filter chain: {e}")))?
};
let (pixels, info) = jpx_decode_with_info(&jpx_bytes)
.map_err(|e| RenderError::ImageDecode(format!("JPX decode: {e}")))?;
(pixels, Some(info))
} else {
let decoded = self
.store
.decode_stream_for_object(obj, image_ref.object_id)
.map_err(|e| RenderError::ImageDecode(format!("decode image stream: {e}")))?;
(decoded, None)
};
let mut decoded = decoded;
if let Some(ref info) = jpx_info {
bpc = 8;
if !has_color_space {
let (inferred_n, inferred_cs) = infer_cs_from_jpx_info(info);
n_components = inferred_n;
cs_type = inferred_cs;
} else {
}
if info.has_alpha && smask_in_data == 1 {
decoded = strip_jpx_alpha(&decoded, width, height, n_components);
if n_components == 3 {
let pixel_count = (width * height) as usize;
let alpha_offset = pixel_count * 3;
if decoded.len() >= alpha_offset + pixel_count {
for i in 0..pixel_count {
let a = decoded[alpha_offset + i] as u32;
let na = 255 - a;
for c in 0..3 {
let idx = i * 3 + c;
let v = decoded[idx] as u32;
decoded[idx] = ((v * a + 255 * na) / 255) as u8;
}
}
}
}
} else if info.has_alpha && smask_in_data == 0 {
decoded = strip_jpx_alpha_discard(&decoded, width, height, n_components);
}
} else if has_image_filter && !is_image_mask {
let expected_8bpc = (width * height * n_components) as usize;
if decoded.len() == expected_8bpc {
bpc = 8;
} else {
let pixels = (width * height) as usize;
if pixels > 0 {
let inferred = decoded.len() / pixels;
if (1..=4).contains(&inferred) && decoded.len() == pixels * inferred {
bpc = 8;
n_components = inferred as u32;
cs_type = match inferred {
1 => ColorSpaceType::DeviceGray,
3 => ColorSpaceType::DeviceRGB,
4 => ColorSpaceType::DeviceCMYK,
_ => cs_type,
};
}
}
}
}
let decode_array = read_decode_array(dict, n_components, self.store);
let rgba = if jpx_info.as_ref().is_some_and(|info| info.has_alpha) && smask_in_data == 1 {
let pixel_count = (width * height) as usize;
let alpha_offset = pixel_count * n_components as usize;
let alpha_data = if decoded.len() >= alpha_offset + pixel_count {
&decoded[alpha_offset..]
} else {
&[] };
convert_to_rgba_with_alpha(
&decoded,
alpha_data,
width,
height,
bpc,
n_components,
&cs_type,
&decode_array,
)
} else {
convert_to_rgba(
&decoded,
width,
height,
bpc,
n_components,
&cs_type,
&decode_array,
is_image_mask,
)
};
Ok(DecodedImage {
width,
height,
data: rgba,
format: DecodedImageFormat::Rgba32,
})
}
fn decode_inline_image(
&self,
properties: &HashMap<Name, Operand>,
data: &[u8],
_matrix: &Matrix,
) -> Result<DecodedImage, RenderError> {
let width = get_inline_int(properties, "W", "Width").unwrap_or(0) as u32;
let height = get_inline_int(properties, "H", "Height").unwrap_or(0) as u32;
let bpc = get_inline_int(properties, "BPC", "BitsPerComponent").unwrap_or(8) as u32;
if width == 0 || height == 0 {
return Err(RenderError::ImageDecode(
"inline image has zero dimensions".into(),
));
}
let is_image_mask = get_inline_bool(properties, "IM", "ImageMask").unwrap_or(false);
let (n_components, cs_type) = if is_image_mask {
(1u32, ColorSpaceType::DeviceGray)
} else {
resolve_inline_color_space(properties)
};
let filters = resolve_inline_filters(properties);
let decoded = if filters.is_empty() {
data.to_vec()
} else {
apply_filter_chain(data, &filters)
.map_err(|e| RenderError::ImageDecode(format!("decode inline image: {e}")))?
};
let decode_array = read_inline_decode_array(properties, n_components);
let rgba = convert_to_rgba(
&decoded,
width,
height,
bpc,
n_components,
&cs_type,
&decode_array,
is_image_mask,
);
Ok(DecodedImage {
width,
height,
data: rgba,
format: DecodedImageFormat::Rgba32,
})
}
}
#[derive(Debug, Clone)]
pub(crate) enum ColorSpaceType {
DeviceGray,
DeviceRGB,
DeviceCMYK,
Indexed {
base: Box<ColorSpaceType>,
base_components: u32,
lookup: Vec<u8>,
},
}
pub(crate) fn get_dict_int(
dict: &HashMap<Name, Object>,
key: &Name,
store: &ObjectStore<Arc<[u8]>>,
) -> Option<i64> {
let obj = dict.get(key)?;
let resolved = store.deep_resolve(obj).ok()?;
resolved.as_i64()
}
fn get_dict_bool(
dict: &HashMap<Name, Object>,
key: &Name,
store: &ObjectStore<Arc<[u8]>>,
) -> Option<bool> {
let obj = dict.get(key)?;
let resolved = store.deep_resolve(obj).ok()?;
resolved.as_bool()
}
fn get_inline_int(props: &HashMap<Name, Operand>, abbr: &str, full: &str) -> Option<i64> {
props
.get(&Name::from(abbr))
.or_else(|| props.get(&Name::from(full)))
.and_then(|op| match op {
Operand::Integer(n) => Some(*n),
Operand::Real(f) => Some(*f as i64),
_ => None,
})
}
fn get_inline_bool(props: &HashMap<Name, Operand>, abbr: &str, full: &str) -> Option<bool> {
props
.get(&Name::from(abbr))
.or_else(|| props.get(&Name::from(full)))
.and_then(|op| match op {
Operand::Boolean(b) => Some(*b),
_ => None,
})
}
pub(crate) fn resolve_image_color_space(
dict: &HashMap<Name, Object>,
store: &ObjectStore<Arc<[u8]>>,
) -> (u32, ColorSpaceType) {
let cs_obj = match dict.get(&Name::color_space()) {
Some(obj) => obj,
None => return (3, ColorSpaceType::DeviceRGB), };
let resolved = match store.deep_resolve(cs_obj) {
Ok(r) => r,
Err(_) => return (3, ColorSpaceType::DeviceRGB),
};
if let Some(name) = resolved.as_name() {
return match_cs_name(name);
}
if let Some(arr) = resolved.as_array() {
if let Some(first) = arr.first() {
if let Ok(first_r) = store.deep_resolve(first) {
if let Some(name) = first_r.as_name() {
if *name == Name::from("Indexed") {
return resolve_indexed_cs(arr, store);
}
if *name == Name::from("ICCBased") {
return resolve_icc_cs(arr, store);
}
return match_cs_name(name);
}
}
}
}
(3, ColorSpaceType::DeviceRGB)
}
fn match_cs_name(name: &Name) -> (u32, ColorSpaceType) {
let s = name.as_str();
if s == "DeviceGray" || s == "G" || s == "CalGray" {
(1, ColorSpaceType::DeviceGray)
} else if s == "DeviceRGB" || s == "RGB" || s == "CalRGB" {
(3, ColorSpaceType::DeviceRGB)
} else if s == "DeviceCMYK" || s == "CMYK" {
(4, ColorSpaceType::DeviceCMYK)
} else {
(3, ColorSpaceType::DeviceRGB)
}
}
fn resolve_indexed_cs(arr: &[Object], store: &ObjectStore<Arc<[u8]>>) -> (u32, ColorSpaceType) {
if arr.len() < 4 {
return (1, ColorSpaceType::DeviceGray);
}
let (base_n, base_cs) = if let Ok(base_r) = store.deep_resolve(&arr[1]) {
if let Some(name) = base_r.as_name() {
match_cs_name(name)
} else if let Some(sub_arr) = base_r.as_array() {
if let Some(first) = sub_arr.first() {
if let Ok(fr) = store.deep_resolve(first) {
if let Some(n) = fr.as_name() {
match_cs_name(n)
} else {
(3, ColorSpaceType::DeviceRGB)
}
} else {
(3, ColorSpaceType::DeviceRGB)
}
} else {
(3, ColorSpaceType::DeviceRGB)
}
} else {
(3, ColorSpaceType::DeviceRGB)
}
} else {
(3, ColorSpaceType::DeviceRGB)
};
let lookup = if let Ok(lut_r) = store.deep_resolve(&arr[3]) {
if let Some(s) = lut_r.as_string() {
s.as_bytes().to_vec()
} else if lut_r.as_stream_dict().is_some() {
store.decode_stream(lut_r).unwrap_or_default()
} else {
Vec::new()
}
} else {
Vec::new()
};
(
1,
ColorSpaceType::Indexed {
base: Box::new(base_cs),
base_components: base_n,
lookup,
},
)
}
fn resolve_icc_cs(arr: &[Object], store: &ObjectStore<Arc<[u8]>>) -> (u32, ColorSpaceType) {
if arr.len() < 2 {
return (3, ColorSpaceType::DeviceRGB);
}
let icc_obj = match store.deep_resolve(&arr[1]) {
Ok(r) => r,
Err(_) => return (3, ColorSpaceType::DeviceRGB),
};
if let Some(dict) = icc_obj.as_stream_dict() {
let n = get_dict_int(dict, &Name::from("N"), store).unwrap_or(3) as u32;
if let Some(alt_obj) = dict.get(&Name::from("Alternate")) {
if let Ok(alt_r) = store.deep_resolve(alt_obj) {
if let Some(name) = alt_r.as_name() {
return match_cs_name(name);
}
}
}
match n {
1 => (1, ColorSpaceType::DeviceGray),
3 => (3, ColorSpaceType::DeviceRGB),
4 => (4, ColorSpaceType::DeviceCMYK),
_ => (n, ColorSpaceType::DeviceRGB),
}
} else {
(3, ColorSpaceType::DeviceRGB)
}
}
fn resolve_inline_color_space(props: &HashMap<Name, Operand>) -> (u32, ColorSpaceType) {
let cs = props
.get(&Name::from("CS"))
.or_else(|| props.get(&Name::from("ColorSpace")));
match cs {
Some(Operand::Name(name)) => match_cs_name(name),
_ => (3, ColorSpaceType::DeviceRGB),
}
}
pub(crate) fn read_decode_array(
dict: &HashMap<Name, Object>,
n_components: u32,
store: &ObjectStore<Arc<[u8]>>,
) -> Vec<[f32; 2]> {
if let Some(obj) = dict.get(&Name::decode()) {
if let Ok(resolved) = store.deep_resolve(obj) {
if let Some(arr) = resolved.as_array() {
return parse_decode_pairs(arr, n_components, |o| {
if let Some(n) = o.as_i64() {
Some(n as f32)
} else {
o.as_f64().map(|f| f as f32)
}
});
}
}
}
default_decode(n_components)
}
fn read_inline_decode_array(props: &HashMap<Name, Operand>, n_components: u32) -> Vec<[f32; 2]> {
let decode = props
.get(&Name::from("D"))
.or_else(|| props.get(&Name::from("Decode")));
if let Some(Operand::Array(arr)) = decode {
let mut result = Vec::with_capacity(n_components as usize);
for chunk in arr.chunks(2) {
if chunk.len() == 2 {
let lo = chunk[0].as_f32().unwrap_or(0.0);
let hi = chunk[1].as_f32().unwrap_or(1.0);
result.push([lo, hi]);
}
}
if result.len() == n_components as usize {
return result;
}
}
default_decode(n_components)
}
fn parse_decode_pairs<T, F>(arr: &[T], n: u32, to_f32: F) -> Vec<[f32; 2]>
where
F: Fn(&T) -> Option<f32>,
{
let mut result = Vec::with_capacity(n as usize);
for chunk in arr.chunks(2) {
if chunk.len() == 2 {
let lo = to_f32(&chunk[0]).unwrap_or(0.0);
let hi = to_f32(&chunk[1]).unwrap_or(1.0);
result.push([lo, hi]);
}
}
if result.len() == n as usize {
result
} else {
default_decode(n)
}
}
fn default_decode(n: u32) -> Vec<[f32; 2]> {
vec![[0.0, 1.0]; n as usize]
}
fn resolve_inline_filters(props: &HashMap<Name, Operand>) -> Vec<(DecodeFilter, FilterParams)> {
let filter = props
.get(&Name::from("F"))
.or_else(|| props.get(&Name::from("Filter")));
let filter_names = match filter {
Some(Operand::Name(name)) => vec![name.clone()],
Some(Operand::Array(arr)) => arr
.iter()
.filter_map(|op| {
if let Operand::Name(n) = op {
Some(n.clone())
} else {
None
}
})
.collect(),
_ => return Vec::new(),
};
filter_names
.iter()
.map(|name| {
let s = name.as_str();
let filter = if s == "AHx" || s == "ASCIIHexDecode" {
DecodeFilter::ASCIIHex
} else if s == "A85" || s == "ASCII85Decode" {
DecodeFilter::ASCII85
} else if s == "LZW" || s == "LZWDecode" {
DecodeFilter::LZW
} else if s == "Fl" || s == "FlateDecode" {
DecodeFilter::Flate
} else if s == "RL" || s == "RunLengthDecode" {
DecodeFilter::RunLength
} else if s == "CCF" || s == "CCITTFaxDecode" {
DecodeFilter::CCITTFax
} else if s == "DCT" || s == "DCTDecode" {
DecodeFilter::DCT
} else {
DecodeFilter::Flate };
(filter, FilterParams::default())
})
.collect()
}
#[allow(clippy::too_many_arguments)]
pub(crate) fn convert_to_rgba(
data: &[u8],
width: u32,
height: u32,
bpc: u32,
n_components: u32,
cs_type: &ColorSpaceType,
decode_array: &[[f32; 2]],
is_image_mask: bool,
) -> Vec<u8> {
let pixel_count = (width * height) as usize;
let mut rgba = vec![255u8; pixel_count * 4];
if is_image_mask {
let invert = decode_array.first().is_some_and(|d| d[0] > d[1]);
convert_stencil_mask(data, width, height, &mut rgba, invert);
return rgba;
}
match cs_type {
ColorSpaceType::Indexed {
base,
base_components,
lookup,
} => {
convert_indexed(
data,
width,
height,
bpc,
lookup,
*base_components,
base,
&mut rgba,
);
}
_ => {
let max_val = ((1u32 << bpc) - 1) as f32;
if max_val == 0.0 {
return rgba;
}
let row_bits = width * n_components * bpc;
let row_bytes = row_bits.div_ceil(8);
for y in 0..height {
let row_start = (y * row_bytes) as usize;
for x in 0..width {
let pixel_idx = (y * width + x) as usize;
let out_base = pixel_idx * 4;
let mut components = [0.0f32; 4];
for c in 0..n_components.min(4) {
let sample_idx = x * n_components + c;
let raw = extract_sample(data, row_start, sample_idx, bpc);
let normalized = raw as f32 / max_val;
let d = &decode_array[c as usize];
components[c as usize] = d[0] + normalized * (d[1] - d[0]);
}
match cs_type {
ColorSpaceType::DeviceGray => {
let v = (components[0].clamp(0.0, 1.0) * 255.0).round() as u8;
rgba[out_base] = v;
rgba[out_base + 1] = v;
rgba[out_base + 2] = v;
rgba[out_base + 3] = 255;
}
ColorSpaceType::DeviceRGB => {
rgba[out_base] = (components[0].clamp(0.0, 1.0) * 255.0).round() as u8;
rgba[out_base + 1] =
(components[1].clamp(0.0, 1.0) * 255.0).round() as u8;
rgba[out_base + 2] =
(components[2].clamp(0.0, 1.0) * 255.0).round() as u8;
rgba[out_base + 3] = 255;
}
ColorSpaceType::DeviceCMYK => {
let c_val = components[0].clamp(0.0, 1.0);
let m_val = components[1].clamp(0.0, 1.0);
let y_val = components[2].clamp(0.0, 1.0);
let k_val = components[3].clamp(0.0, 1.0);
let (r, g, b) =
rpdfium_render::cfx_cmyk_to_srgb::adobe_cmyk_f32_to_srgb(
c_val, m_val, y_val, k_val,
);
rgba[out_base] = r;
rgba[out_base + 1] = g;
rgba[out_base + 2] = b;
rgba[out_base + 3] = 255;
}
ColorSpaceType::Indexed { .. } => unreachable!(),
}
}
}
}
}
rgba
}
fn extract_sample(data: &[u8], row_start: usize, sample_idx: u32, bpc: u32) -> u32 {
match bpc {
8 => {
let idx = row_start + sample_idx as usize;
if idx < data.len() {
data[idx] as u32
} else {
0
}
}
16 => {
let idx = row_start + (sample_idx as usize) * 2;
if idx + 1 < data.len() {
data[idx] as u32
} else {
0
}
}
1 | 2 | 4 => {
let bit_offset = sample_idx * bpc;
let byte_idx = row_start + (bit_offset / 8) as usize;
if byte_idx >= data.len() {
return 0;
}
let bit_within_byte = bit_offset % 8;
let mask = (1u32 << bpc) - 1;
let shift = 8 - bpc - bit_within_byte;
(data[byte_idx] as u32 >> shift) & mask
}
_ => 0,
}
}
fn convert_stencil_mask(data: &[u8], width: u32, height: u32, rgba: &mut [u8], invert: bool) {
let row_bytes = width.div_ceil(8);
for y in 0..height {
let row_start = (y * row_bytes) as usize;
for x in 0..width {
let byte_idx = row_start + (x / 8) as usize;
let bit_idx = 7 - (x % 8);
let bit = if byte_idx < data.len() {
(data[byte_idx] >> bit_idx) & 1
} else {
0
};
let painted = if invert { bit == 0 } else { bit == 1 };
let pixel_idx = (y * width + x) as usize;
let base = pixel_idx * 4;
if painted {
rgba[base] = 0;
rgba[base + 1] = 0;
rgba[base + 2] = 0;
rgba[base + 3] = 255;
} else {
rgba[base] = 0;
rgba[base + 1] = 0;
rgba[base + 2] = 0;
rgba[base + 3] = 0;
}
}
}
}
#[allow(clippy::too_many_arguments)]
fn convert_indexed(
data: &[u8],
width: u32,
height: u32,
bpc: u32,
lookup: &[u8],
base_components: u32,
base_cs: &ColorSpaceType,
rgba: &mut [u8],
) {
let max_val = ((1u32 << bpc) - 1) as f32;
if max_val == 0.0 {
return;
}
let row_bits = width * bpc;
let row_bytes = row_bits.div_ceil(8);
let bc = base_components as usize;
for y in 0..height {
let row_start = (y * row_bytes) as usize;
for x in 0..width {
let pixel_idx = (y * width + x) as usize;
let out_base = pixel_idx * 4;
let index = extract_sample(data, row_start, x, bpc) as usize;
let lut_offset = index * bc;
if lut_offset + bc > lookup.len() {
continue; }
match base_cs {
ColorSpaceType::DeviceGray => {
let v = lookup[lut_offset];
rgba[out_base] = v;
rgba[out_base + 1] = v;
rgba[out_base + 2] = v;
rgba[out_base + 3] = 255;
}
ColorSpaceType::DeviceRGB => {
rgba[out_base] = lookup[lut_offset];
rgba[out_base + 1] = lookup[lut_offset + 1];
rgba[out_base + 2] = lookup[lut_offset + 2];
rgba[out_base + 3] = 255;
}
ColorSpaceType::DeviceCMYK => {
let c = lookup[lut_offset] as f32 / 255.0;
let m = lookup[lut_offset + 1] as f32 / 255.0;
let yy = lookup[lut_offset + 2] as f32 / 255.0;
let k = lookup[lut_offset + 3] as f32 / 255.0;
rgba[out_base] = ((1.0 - c) * (1.0 - k) * 255.0).round() as u8;
rgba[out_base + 1] = ((1.0 - m) * (1.0 - k) * 255.0).round() as u8;
rgba[out_base + 2] = ((1.0 - yy) * (1.0 - k) * 255.0).round() as u8;
rgba[out_base + 3] = 255;
}
ColorSpaceType::Indexed { .. } => {
let v = lookup[lut_offset];
rgba[out_base] = v;
rgba[out_base + 1] = v;
rgba[out_base + 2] = v;
rgba[out_base + 3] = 255;
}
}
}
}
}
fn infer_cs_from_jpx_info(info: &Jpeg2000Info) -> (u32, ColorSpaceType) {
match info.num_components {
1 => (1, ColorSpaceType::DeviceGray),
3 => (3, ColorSpaceType::DeviceRGB),
4 => (4, ColorSpaceType::DeviceCMYK),
_ => (info.num_components as u32, ColorSpaceType::DeviceRGB),
}
}
fn strip_jpx_alpha(data: &[u8], width: u32, height: u32, n_components: u32) -> Vec<u8> {
let pixel_count = (width * height) as usize;
let total_channels = n_components + 1; let expected_len = pixel_count * total_channels as usize;
if data.len() < expected_len {
return data.to_vec();
}
let color_size = pixel_count * n_components as usize;
let mut result = Vec::with_capacity(color_size + pixel_count);
for i in 0..pixel_count {
let src = i * total_channels as usize;
for c in 0..n_components as usize {
result.push(data[src + c]);
}
}
for i in 0..pixel_count {
let src = i * total_channels as usize + n_components as usize;
result.push(data[src]);
}
result
}
fn strip_jpx_alpha_discard(data: &[u8], width: u32, height: u32, n_components: u32) -> Vec<u8> {
let pixel_count = (width * height) as usize;
let total_channels = n_components + 1;
let expected_len = pixel_count * total_channels as usize;
if data.len() < expected_len {
return data.to_vec();
}
let color_size = pixel_count * n_components as usize;
let mut result = Vec::with_capacity(color_size);
for i in 0..pixel_count {
let src = i * total_channels as usize;
for c in 0..n_components as usize {
result.push(data[src + c]);
}
}
result
}
#[allow(clippy::too_many_arguments)]
fn convert_to_rgba_with_alpha(
data: &[u8],
alpha_data: &[u8],
width: u32,
height: u32,
bpc: u32,
n_components: u32,
cs_type: &ColorSpaceType,
decode_array: &[[f32; 2]],
) -> Vec<u8> {
let pixel_count = (width * height) as usize;
let mut rgba = vec![255u8; pixel_count * 4];
let max_val = ((1u32 << bpc) - 1) as f32;
if max_val == 0.0 {
return rgba;
}
let row_bits = width * n_components * bpc;
let row_bytes = row_bits.div_ceil(8);
for y in 0..height {
let row_start = (y * row_bytes) as usize;
for x in 0..width {
let pixel_idx = (y * width + x) as usize;
let out_base = pixel_idx * 4;
let mut components = [0.0f32; 4];
for c in 0..n_components.min(4) {
let sample_idx = x * n_components + c;
let raw = extract_sample(data, row_start, sample_idx, bpc);
let normalized = raw as f32 / max_val;
let d = &decode_array[c as usize];
components[c as usize] = d[0] + normalized * (d[1] - d[0]);
}
let alpha = if pixel_idx < alpha_data.len() {
alpha_data[pixel_idx]
} else {
255
};
match cs_type {
ColorSpaceType::DeviceGray => {
let v = (components[0].clamp(0.0, 1.0) * 255.0).round() as u8;
rgba[out_base] = v;
rgba[out_base + 1] = v;
rgba[out_base + 2] = v;
rgba[out_base + 3] = alpha;
}
ColorSpaceType::DeviceRGB => {
rgba[out_base] = (components[0].clamp(0.0, 1.0) * 255.0).round() as u8;
rgba[out_base + 1] = (components[1].clamp(0.0, 1.0) * 255.0).round() as u8;
rgba[out_base + 2] = (components[2].clamp(0.0, 1.0) * 255.0).round() as u8;
rgba[out_base + 3] = alpha;
}
ColorSpaceType::DeviceCMYK => {
let c_val = components[0].clamp(0.0, 1.0);
let m_val = components[1].clamp(0.0, 1.0);
let y_val = components[2].clamp(0.0, 1.0);
let k_val = components[3].clamp(0.0, 1.0);
let (r, g, b) = rpdfium_render::cfx_cmyk_to_srgb::adobe_cmyk_f32_to_srgb(
c_val, m_val, y_val, k_val,
);
rgba[out_base] = r;
rgba[out_base + 1] = g;
rgba[out_base + 2] = b;
rgba[out_base + 3] = alpha;
}
ColorSpaceType::Indexed { .. } => {
rgba[out_base + 3] = alpha;
}
}
}
}
rgba
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_extract_sample_8bpc() {
let data = [100, 200, 50];
assert_eq!(extract_sample(&data, 0, 0, 8), 100);
assert_eq!(extract_sample(&data, 0, 1, 8), 200);
assert_eq!(extract_sample(&data, 0, 2, 8), 50);
}
#[test]
fn test_extract_sample_1bpc() {
let data = [0b10110100]; assert_eq!(extract_sample(&data, 0, 0, 1), 1);
assert_eq!(extract_sample(&data, 0, 1, 1), 0);
assert_eq!(extract_sample(&data, 0, 2, 1), 1);
assert_eq!(extract_sample(&data, 0, 3, 1), 1);
assert_eq!(extract_sample(&data, 0, 4, 1), 0);
assert_eq!(extract_sample(&data, 0, 5, 1), 1);
assert_eq!(extract_sample(&data, 0, 6, 1), 0);
assert_eq!(extract_sample(&data, 0, 7, 1), 0);
}
#[test]
fn test_extract_sample_4bpc() {
let data = [0xAB, 0xCD];
assert_eq!(extract_sample(&data, 0, 0, 4), 0xA);
assert_eq!(extract_sample(&data, 0, 1, 4), 0xB);
assert_eq!(extract_sample(&data, 0, 2, 4), 0xC);
assert_eq!(extract_sample(&data, 0, 3, 4), 0xD);
}
#[test]
fn test_extract_sample_2bpc() {
let data = [0b11_10_01_00]; assert_eq!(extract_sample(&data, 0, 0, 2), 3);
assert_eq!(extract_sample(&data, 0, 1, 2), 2);
assert_eq!(extract_sample(&data, 0, 2, 2), 1);
assert_eq!(extract_sample(&data, 0, 3, 2), 0);
}
#[test]
fn test_convert_gray_image() {
let data = [0, 255, 128, 191];
let decode = vec![[0.0, 1.0]];
let rgba = convert_to_rgba(
&data,
2,
2,
8,
1,
&ColorSpaceType::DeviceGray,
&decode,
false,
);
assert_eq!(rgba.len(), 16);
assert_eq!(rgba[0], 0);
assert_eq!(rgba[1], 0);
assert_eq!(rgba[2], 0);
assert_eq!(rgba[3], 255);
assert_eq!(rgba[4], 255);
assert_eq!(rgba[5], 255);
assert_eq!(rgba[6], 255);
assert_eq!(rgba[7], 255);
}
#[test]
fn test_convert_rgb_image() {
let data = [255, 0, 0];
let decode = vec![[0.0, 1.0]; 3];
let rgba = convert_to_rgba(
&data,
1,
1,
8,
3,
&ColorSpaceType::DeviceRGB,
&decode,
false,
);
assert_eq!(rgba, [255, 0, 0, 255]);
}
#[test]
fn test_convert_indexed_image() {
let lookup = vec![255, 0, 0, 0, 255, 0]; let data = [0, 1]; let cs = ColorSpaceType::Indexed {
base: Box::new(ColorSpaceType::DeviceRGB),
base_components: 3,
lookup,
};
let decode = vec![[0.0, 1.0]];
let rgba = convert_to_rgba(&data, 2, 1, 8, 1, &cs, &decode, false);
assert_eq!(rgba[0..4], [255, 0, 0, 255]); assert_eq!(rgba[4..8], [0, 255, 0, 255]); }
#[test]
fn test_stencil_mask() {
let data = [0b10100000];
let mut rgba = vec![255u8; 32]; convert_stencil_mask(&data, 8, 1, &mut rgba, false);
assert_eq!(rgba[3], 255);
assert_eq!(rgba[7], 0);
assert_eq!(rgba[11], 255);
}
#[test]
fn test_default_decode_array() {
let d = default_decode(3);
assert_eq!(d.len(), 3);
assert_eq!(d[0], [0.0, 1.0]);
}
#[test]
fn test_match_cs_name_variants() {
assert!(matches!(
match_cs_name(&Name::from("DeviceGray")).1,
ColorSpaceType::DeviceGray
));
assert!(matches!(
match_cs_name(&Name::from("G")).1,
ColorSpaceType::DeviceGray
));
assert!(matches!(
match_cs_name(&Name::from("DeviceRGB")).1,
ColorSpaceType::DeviceRGB
));
assert!(matches!(
match_cs_name(&Name::from("DeviceCMYK")).1,
ColorSpaceType::DeviceCMYK
));
}
}