use crate::color::Argb;
#[derive(Debug, Clone)]
pub struct TransferFunc<'a> {
inner: &'a pdfrum_page::TransferFunc,
}
impl<'a> TransferFunc<'a> {
#[must_use]
pub fn new(inner: &'a pdfrum_page::TransferFunc) -> Self {
Self { inner }
}
#[must_use]
pub fn is_identity(&self) -> bool {
self.inner.identity
}
#[must_use]
pub fn samples(&self) -> &[[u8; pdfrum_page::CHANNEL_SAMPLES]; 3] {
&self.inner.samples
}
#[must_use]
pub fn translate(&self, c: Argb) -> Argb {
Argb {
a: c.a,
r: self.inner.apply(0, c.r),
g: self.inner.apply(1, c.g),
b: self.inner.apply(2, c.b),
}
}
}
#[cfg(test)]
mod tests {
use std::sync::Arc;
use pdfrum_common::{Diagnostics, Limits};
use pdfrum_object::{Array, ByteSpan, Dict, Name, NoResolve, Object, Stream};
use pdfrum_page::FunctionCache;
use super::*;
fn nums(values: &[f32]) -> Object {
Object::Array(Array::of(values.iter().copied().map(Object::Real)))
}
fn constant(v: f32) -> Object {
Object::Dict(Dict::from_pairs([
(Name::from("FunctionType"), Object::Int(2)),
(Name::from("Domain"), nums(&[0.0, 1.0])),
(Name::from("N"), Object::Int(1)),
(Name::from("C0"), nums(&[v])),
(Name::from("C1"), nums(&[v])),
]))
}
fn load(obj: &Object) -> Option<pdfrum_page::TransferFunc> {
let mut cache = FunctionCache::new();
let mut diags = Diagnostics::default();
pdfrum_page::TransferFunc::load(obj, &NoResolve, &mut cache, &Limits::default(), &mut diags)
}
fn stream(dict: Dict, data: &[u8]) -> Object {
let file: Arc<[u8]> = Arc::from(data);
let span = ByteSpan::new(Arc::clone(&file), 0..file.len()).expect("in range");
Object::Stream(Box::new(Stream::new(dict, span)))
}
fn oracle_type0() -> Object {
let dict = Dict::from_pairs([
(Name::from("FunctionType"), Object::Int(0)),
(Name::from("BitsPerSample"), Object::Int(8)),
(Name::from("Domain"), nums(&[0.0, 1.0])),
(Name::from("Range"), nums(&[0.0, 0.5])),
(
Name::from("Size"),
Object::Array(Array::of([Object::Int(4)])),
),
]);
stream(dict, b"1234\0")
}
fn oracle_type2() -> Object {
Object::Dict(Dict::from_pairs([
(Name::from("FunctionType"), Object::Int(2)),
(Name::from("N"), Object::Int(1)),
(Name::from("Domain"), nums(&[0.0, 1.0])),
(Name::from("C0"), nums(&[0.1, 0.2, 0.8])),
(Name::from("C1"), nums(&[0.05, 0.01, 0.4])),
]))
}
fn oracle_type4() -> Object {
let dict = Dict::from_pairs([
(Name::from("FunctionType"), Object::Int(4)),
(Name::from("Domain"), nums(&[0.0, 1.0])),
(Name::from("Range"), nums(&[-1.0, 1.0])),
]);
stream(dict, b"{ 360 mul sin 2 div }")
}
#[test]
fn the_first_array_element_drives_red() {
let array = Object::Array(Array::of([
constant(10.0 / 255.0),
constant(100.0 / 255.0),
constant(200.0 / 255.0),
]));
let parsed = load(&array).expect("should load");
let out = TransferFunc::new(&parsed).translate(Argb::opaque(10, 20, 30));
assert_eq!(out.r, 10, "array[0] must drive red");
assert_eq!(out.g, 100, "array[1] must drive green");
assert_eq!(out.b, 200, "array[2] must drive blue");
}
#[test]
fn the_oracle_fixtures_ten_translate_colour_pairs() {
let array = Object::Array(Array::of([oracle_type0(), oracle_type2(), oracle_type4()]));
let parsed = load(&array).expect("the oracle's array should load");
let tf = TransferFunc::new(&parsed);
assert!(!tf.is_identity());
for (input, expected) in [
((0xff, 0xff, 0xff), (0x1a, 0x0d, 0x00)),
((0x00, 0x00, 0xff), (0x19, 0x1a, 0x00)),
((0x00, 0xff, 0x00), (0x19, 0x0d, 0x00)),
((0xff, 0x00, 0x00), (0x1a, 0x1a, 0x00)),
((0xcc, 0xcc, 0xcc), (0x1a, 0x0f, 0x00)),
((0x56, 0x34, 0x12), (0x19, 0x17, 0x37)),
] {
let (r, g, b) = input;
let out = TransferFunc::new(&parsed).translate(Argb::opaque(r, g, b));
assert_eq!(
(out.r, out.g, out.b),
expected,
"translating ({r:#04x}, {g:#04x}, {b:#04x})"
);
}
}
#[test]
fn alpha_survives_the_transfer() {
let parsed = load(&constant(0.0)).expect("should load");
let tf = TransferFunc::new(&parsed);
let out = tf.translate(Argb {
a: 77,
r: 255,
g: 255,
b: 255,
});
assert_eq!(out.a, 77);
assert_eq!((out.r, out.g, out.b), (0, 0, 0));
}
#[test]
fn identity_is_detected() {
let identity = Object::Dict(Dict::from_pairs([
(Name::from("FunctionType"), Object::Int(2)),
(Name::from("Domain"), nums(&[0.0, 1.0])),
(Name::from("N"), Object::Int(1)),
(Name::from("C0"), nums(&[0.0])),
(Name::from("C1"), nums(&[1.0])),
]));
let parsed = load(&identity).expect("should load");
assert!(TransferFunc::new(&parsed).is_identity());
}
#[test]
fn bad_transfer_functions_drop_the_whole_thing() {
assert!(load(&Object::Array(Array::of([constant(0.0), constant(0.0)]))).is_none());
assert!(
load(&Object::Array(Array::of([
constant(0.0),
Object::Int(7),
constant(0.0)
])))
.is_none()
);
assert!(load(&Object::Name(Name::from("Identity"))).is_none());
}
}