1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
//! Encoder input normalization policy (#694).
//!
//! Ingest converts archival image files into the internal RGBA [`Pixmap`] used
//! by segmentation and encoding. See `docs/encoder-ingestion.md` in the repo
//! root for the supported input matrix.
/// How semi-transparent pixels are handled when an ingest path must produce
/// opaque RGBA (CLI: `djvu encode --background <COLOR>`).
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub enum AlphaCompositing {
/// Keep the alpha channel unchanged. Downstream segmentation/encoding sees
/// partial transparency as-is; no background colour is applied silently.
#[default]
Preserve,
/// Composite onto a solid background at decode time.
CompositeOnBackground { red: u8, green: u8, blue: u8 },
}
impl AlphaCompositing {
/// Apply this policy to decoded RGBA pixels in place.
///
/// [`Preserve`](Self::Preserve) is a no-op. `CompositeOnBackground`
/// blends every non-opaque pixel onto the solid background with
/// deterministic integer rounding and sets its alpha to 255:
/// `out = (c·a + bg·(255 − a) + 127) / 255`.
pub fn apply(&self, rgba: &mut [u8]) {
let Self::CompositeOnBackground { red, green, blue } = *self else {
return;
};
for px in rgba.chunks_exact_mut(4) {
let a = u32::from(px[3]);
if a == 255 {
continue;
}
for (c, bg) in px.iter_mut().zip([red, green, blue]) {
*c = ((u32::from(*c) * a + u32::from(bg) * (255 - a) + 127) / 255) as u8;
}
px[3] = 255;
}
}
}
/// What to do when an input file embeds an ICC colour profile
/// (CLI: `djvu encode --icc <MODE>`).
///
/// DjVu has no container for ICC profiles and ingest applies no colour
/// management, so a profile can never survive into the output. The policy
/// makes that explicit instead of silent: `Ignore` decodes the pixel bytes
/// as-is (the long-standing default), `Reject` refuses profiled input with
/// an error naming the profile. A future `Transform` mode would need a
/// colour-management engine and is out of scope (#694).
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub enum IccHandling {
/// Decode without colour management; the embedded profile is dropped.
#[default]
Ignore,
/// Fail with an explicit error when the input embeds an ICC profile.
Reject,
}
/// Deterministic reduction of >8-bit samples to 8-bit channels.
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub enum DepthDownconversion {
/// Keep the high 8 bits of each big-endian 16-bit sample (`sample >> 8`).
/// No dithering; repeatable across platforms.
#[default]
TruncateHighByte,
}
/// Policy knobs applied while decoding raster inputs into [`Pixmap`].
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub struct IngestPolicy {
pub alpha: AlphaCompositing,
pub depth_downconversion: DepthDownconversion,
pub icc: IccHandling,
}
impl IngestPolicy {
/// Reduce one big-endian 16-bit channel sample to 8 bits.
#[inline]
pub fn downsample_u16_be(&self, hi: u8, lo: u8) -> u8 {
let _ = (self.depth_downconversion, lo);
hi
}
/// Reduce one native-endian 16-bit channel sample to 8 bits.
#[inline]
pub fn downsample_u16(&self, sample: u16) -> u8 {
let _ = self.depth_downconversion;
(sample >> 8) as u8
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn default_policy_preserves_alpha_and_truncates_depth() {
let policy = IngestPolicy::default();
assert_eq!(policy.alpha, AlphaCompositing::Preserve);
assert_eq!(policy.icc, IccHandling::Ignore);
assert_eq!(
policy.depth_downconversion,
DepthDownconversion::TruncateHighByte
);
assert_eq!(policy.downsample_u16_be(0x12, 0x34), 0x12);
}
#[test]
fn preserve_leaves_pixels_untouched() {
let mut rgba = [200, 100, 0, 128, 10, 20, 30, 0];
AlphaCompositing::Preserve.apply(&mut rgba);
assert_eq!(rgba, [200, 100, 0, 128, 10, 20, 30, 0]);
}
#[test]
fn composite_blends_and_makes_opaque() {
let white = AlphaCompositing::CompositeOnBackground {
red: 255,
green: 255,
blue: 255,
};
// (c·a + bg·(255 − a) + 127) / 255 for a = 128 on white.
let mut rgba = [200, 100, 0, 128, 10, 20, 30, 255];
white.apply(&mut rgba);
assert_eq!(rgba, [227, 177, 127, 255, 10, 20, 30, 255]);
// Fully transparent becomes the background colour exactly.
let blue = AlphaCompositing::CompositeOnBackground {
red: 0,
green: 0,
blue: 255,
};
let mut rgba = [200, 100, 0, 0];
blue.apply(&mut rgba);
assert_eq!(rgba, [0, 0, 255, 255]);
}
}