Skip to main content

libbitsub_core/pgs/
rle.rs

1//! Run-length encoding decoder for PGS subtitle bitmaps.
2//!
3//! PGS uses a variant of RLE encoding where:
4//! - Non-zero bytes are literal palette indices
5//! - Zero byte starts a control sequence:
6//!   - 0x00 0x00 = end of line
7//!   - 0x00 0xNN = repeat color 0, NN times (NN in 1-63)
8//!   - 0x00 0x4N 0xNN = repeat color 0, N*256+NN times
9//!   - 0x00 0x8N 0xCC = repeat color CC, N times (N in 1-63)
10//!   - 0x00 0xCN 0xNN 0xCC = repeat color CC, N*256+NN times
11
12/// Decode RLE-encoded data to indexed pixel values (palette indices).
13/// This is optimized for high-frequency subtitle animations where we want to
14/// decode once and apply different palettes quickly.
15///
16/// Returns the number of decoded pixels.
17#[inline]
18pub fn decode_rle_to_indexed(data: &[u8], target: &mut [u8]) -> usize {
19    let mut idx = 0;
20    let mut pos = 0;
21    let len = data.len();
22    let target_len = target.len();
23
24    while pos < len && idx < target_len {
25        let byte1 = data[pos];
26        pos += 1;
27
28        // Most common case: literal palette index
29        if byte1 != 0 {
30            target[idx] = byte1;
31            idx += 1;
32            continue;
33        }
34
35        // Zero byte - start of control sequence
36        if pos >= len {
37            break;
38        }
39
40        let byte2 = data[pos];
41        pos += 1;
42
43        // End of line marker
44        if byte2 == 0 {
45            continue;
46        }
47
48        // Parse run-length encoding
49        let (count, value) = if byte2 & 0xC0 == 0xC0 {
50            // Extended length with color: 0x00 0xCN 0xNN 0xCC
51            let high = (byte2 & 0x3F) as usize;
52            let low = data.get(pos).copied().unwrap_or(0) as usize;
53            pos += 1;
54            let color = data.get(pos).copied().unwrap_or(0);
55            pos += 1;
56            ((high << 8) | low, color)
57        } else if byte2 & 0x80 != 0 {
58            // Short length with color: 0x00 0x8N 0xCC
59            let count = (byte2 & 0x3F) as usize;
60            let color = data.get(pos).copied().unwrap_or(0);
61            pos += 1;
62            (count, color)
63        } else if byte2 & 0x40 != 0 {
64            // Extended length, transparent: 0x00 0x4N 0xNN
65            let high = (byte2 & 0x3F) as usize;
66            let low = data.get(pos).copied().unwrap_or(0) as usize;
67            pos += 1;
68            ((high << 8) | low, 0)
69        } else {
70            // Short length, transparent: 0x00 0xNN
71            ((byte2 & 0x3F) as usize, 0)
72        };
73
74        // Fill with the value
75        let end = (idx + count).min(target_len);
76        if count > 8 {
77            target[idx..end].fill(value);
78        } else {
79            // Unrolled loop for small runs
80            while idx < end {
81                target[idx] = value;
82                idx += 1;
83            }
84        }
85        idx = end;
86    }
87
88    idx
89}
90
91/// Decode RLE-encoded data directly to RGBA pixels using a palette lookup.
92/// This is the fast path when palette doesn't change between frames.
93///
94/// Returns the number of decoded pixels.
95#[inline]
96pub fn decode_rle_to_rgba(data: &[u8], palette: &[u32], target: &mut [u32]) -> usize {
97    let mut idx = 0;
98    let mut pos = 0;
99    let len = data.len();
100    let target_len = target.len();
101    let palette_len = palette.len();
102
103    // Cache transparent color
104    let transparent = if palette_len > 0 { palette[0] } else { 0 };
105
106    while pos < len && idx < target_len {
107        let byte1 = data[pos];
108        pos += 1;
109
110        // Most common case: literal palette index
111        if byte1 != 0 {
112            let color = if (byte1 as usize) < palette_len {
113                palette[byte1 as usize]
114            } else {
115                0
116            };
117            target[idx] = color;
118            idx += 1;
119            continue;
120        }
121
122        // Zero byte - start of control sequence
123        if pos >= len {
124            break;
125        }
126
127        let byte2 = data[pos];
128        pos += 1;
129
130        // End of line marker
131        if byte2 == 0 {
132            continue;
133        }
134
135        // Parse run-length encoding
136        let (count, color) = if byte2 & 0xC0 == 0xC0 {
137            // Extended length with color: 0x00 0xCN 0xNN 0xCC
138            let high = (byte2 & 0x3F) as usize;
139            let low = data.get(pos).copied().unwrap_or(0) as usize;
140            pos += 1;
141            let color_idx = data.get(pos).copied().unwrap_or(0) as usize;
142            pos += 1;
143            let color = if color_idx < palette_len {
144                palette[color_idx]
145            } else {
146                0
147            };
148            ((high << 8) | low, color)
149        } else if byte2 & 0x80 != 0 {
150            // Short length with color: 0x00 0x8N 0xCC
151            let count = (byte2 & 0x3F) as usize;
152            let color_idx = data.get(pos).copied().unwrap_or(0) as usize;
153            pos += 1;
154            let color = if color_idx < palette_len {
155                palette[color_idx]
156            } else {
157                0
158            };
159            (count, color)
160        } else if byte2 & 0x40 != 0 {
161            // Extended length, transparent: 0x00 0x4N 0xNN
162            let high = (byte2 & 0x3F) as usize;
163            let low = data.get(pos).copied().unwrap_or(0) as usize;
164            pos += 1;
165            ((high << 8) | low, transparent)
166        } else {
167            // Short length, transparent: 0x00 0xNN
168            ((byte2 & 0x3F) as usize, transparent)
169        };
170
171        // Fill with the color
172        let end = (idx + count).min(target_len);
173        if count > 8 {
174            target[idx..end].fill(color);
175        } else {
176            while idx < end {
177                target[idx] = color;
178                idx += 1;
179            }
180        }
181        idx = end;
182    }
183
184    idx
185}
186
187/// Apply a palette to indexed pixel data, producing RGBA output.
188/// This is used when we've cached the indexed pixels and need to apply a new palette.
189#[inline]
190pub fn apply_palette(indexed: &[u8], palette: &[u32], target: &mut [u32]) {
191    let len = indexed.len().min(target.len());
192
193    // PGS palettes are always 256 entries
194    if palette.len() >= 256 {
195        // Fast path: no bounds checking needed for palette lookup
196        // Process in chunks of 8 for better cache utilization and SIMD potential
197        let chunks = len / 8;
198        let remainder = len % 8;
199
200        for chunk in 0..chunks {
201            let base = chunk * 8;
202            // Unroll 8 iterations
203            target[base] = palette[indexed[base] as usize];
204            target[base + 1] = palette[indexed[base + 1] as usize];
205            target[base + 2] = palette[indexed[base + 2] as usize];
206            target[base + 3] = palette[indexed[base + 3] as usize];
207            target[base + 4] = palette[indexed[base + 4] as usize];
208            target[base + 5] = palette[indexed[base + 5] as usize];
209            target[base + 6] = palette[indexed[base + 6] as usize];
210            target[base + 7] = palette[indexed[base + 7] as usize];
211        }
212
213        // Handle remainder
214        let base = chunks * 8;
215        for i in 0..remainder {
216            target[base + i] = palette[indexed[base + i] as usize];
217        }
218    } else {
219        // Fallback with bounds checking
220        let palette_len = palette.len();
221        for i in 0..len {
222            let idx = indexed[i] as usize;
223            target[i] = if idx < palette_len { palette[idx] } else { 0 };
224        }
225    }
226}
227
228/// Apply a palette to indexed pixel data, producing RGBA bytes.
229#[inline]
230pub fn apply_palette_rgba_bytes(indexed: &[u8], palette: &[u32], target: &mut [u8]) {
231    let len = indexed.len().min(target.len() / 4);
232
233    if palette.len() >= 256 {
234        for (pixel_index, &palette_index) in indexed.iter().take(len).enumerate() {
235            let start = pixel_index * 4;
236            target[start..start + 4]
237                .copy_from_slice(&palette[palette_index as usize].to_le_bytes());
238        }
239    } else {
240        let palette_len = palette.len();
241        for (pixel_index, &palette_index) in indexed.iter().take(len).enumerate() {
242            let start = pixel_index * 4;
243            let rgba = if (palette_index as usize) < palette_len {
244                palette[palette_index as usize]
245            } else {
246                0
247            };
248            target[start..start + 4].copy_from_slice(&rgba.to_le_bytes());
249        }
250    }
251}
252
253#[cfg(test)]
254mod tests {
255    use super::*;
256
257    #[test]
258    fn test_decode_literal() {
259        // Simple literal values
260        let data = [1, 2, 3, 4, 5];
261        let mut target = vec![0u8; 10];
262        let count = decode_rle_to_indexed(&data, &mut target);
263        assert_eq!(count, 5);
264        assert_eq!(&target[..5], &[1, 2, 3, 4, 5]);
265    }
266
267    #[test]
268    fn test_decode_short_run_transparent() {
269        // 0x00 0x05 = 5 transparent pixels
270        let data = [0x00, 0x05];
271        let mut target = vec![0xFFu8; 10];
272        let count = decode_rle_to_indexed(&data, &mut target);
273        assert_eq!(count, 5);
274        assert_eq!(&target[..5], &[0, 0, 0, 0, 0]);
275    }
276
277    #[test]
278    fn test_decode_short_run_color() {
279        // 0x00 0x85 0x07 = 5 pixels of color 7
280        let data = [0x00, 0x85, 0x07];
281        let mut target = vec![0u8; 10];
282        let count = decode_rle_to_indexed(&data, &mut target);
283        assert_eq!(count, 5);
284        assert_eq!(&target[..5], &[7, 7, 7, 7, 7]);
285    }
286
287    #[test]
288    fn test_decode_end_of_line() {
289        // 1, EOL, 2
290        let data = [0x01, 0x00, 0x00, 0x02];
291        let mut target = vec![0u8; 10];
292        let count = decode_rle_to_indexed(&data, &mut target);
293        assert_eq!(count, 2);
294        assert_eq!(&target[..2], &[1, 2]);
295    }
296}