Skip to main content

justpdf_render/
shading.rs

1use justpdf_core::function::PdfFunction;
2use justpdf_core::object::{PdfDict, PdfObject};
3use tiny_skia::{
4    Color, FillRule, GradientStop, LinearGradient, Mask, Paint, PathBuilder, Pixmap,
5    RadialGradient, SpreadMode, Transform,
6};
7
8use crate::graphics_state::Matrix;
9
10/// Render a shading pattern into the device.
11/// `stream_data` is the decoded binary stream for mesh shadings (Type 4/5/6/7).
12pub fn render_shading(
13    pixmap: &mut Pixmap,
14    shading_dict: &PdfDict,
15    ctm: &Matrix,
16    page_transform: &Matrix,
17    clip_mask: Option<&Mask>,
18    stream_data: Option<&[u8]>,
19) {
20    let shading_type = shading_dict.get_i64(b"ShadingType").unwrap_or(0);
21
22    match shading_type {
23        1 => render_function_based(pixmap, shading_dict, ctm, page_transform, clip_mask),
24        2 => render_axial(pixmap, shading_dict, ctm, page_transform, clip_mask),
25        3 => render_radial(pixmap, shading_dict, ctm, page_transform, clip_mask),
26        4 | 5 => render_gouraud_mesh(pixmap, shading_dict, ctm, page_transform, clip_mask, stream_data),
27        6 | 7 => render_patch_mesh(pixmap, shading_dict, ctm, page_transform, clip_mask, stream_data),
28        _ => {} // unsupported
29    }
30}
31
32// ---------------------------------------------------------------------------
33// Type 1: Function-based shading
34// ---------------------------------------------------------------------------
35
36fn render_function_based(
37    pixmap: &mut Pixmap,
38    dict: &PdfDict,
39    ctm: &Matrix,
40    page_transform: &Matrix,
41    clip: Option<&Mask>,
42) {
43    // Domain defaults to [0 1 0 1]
44    let domain = dict.get_array(b"Domain");
45    let x0 = domain
46        .and_then(|a| a.first())
47        .and_then(|o| o.as_f64())
48        .unwrap_or(0.0);
49    let x1 = domain
50        .and_then(|a| a.get(1))
51        .and_then(|o| o.as_f64())
52        .unwrap_or(1.0);
53    let y0 = domain
54        .and_then(|a| a.get(2))
55        .and_then(|o| o.as_f64())
56        .unwrap_or(0.0);
57    let y1 = domain
58        .and_then(|a| a.get(3))
59        .and_then(|o| o.as_f64())
60        .unwrap_or(1.0);
61
62    let cs_name = dict
63        .get(b"ColorSpace")
64        .and_then(|o| o.as_name())
65        .unwrap_or(b"DeviceRGB");
66
67    // Matrix from function domain to shading space
68    let shading_matrix = if let Some(matrix_arr) = dict.get_array(b"Matrix") {
69        if matrix_arr.len() >= 6 {
70            Matrix {
71                a: matrix_arr[0].as_f64().unwrap_or(1.0),
72                b: matrix_arr[1].as_f64().unwrap_or(0.0),
73                c: matrix_arr[2].as_f64().unwrap_or(0.0),
74                d: matrix_arr[3].as_f64().unwrap_or(1.0),
75                e: matrix_arr[4].as_f64().unwrap_or(0.0),
76                f: matrix_arr[5].as_f64().unwrap_or(0.0),
77            }
78        } else {
79            Matrix::identity()
80        }
81    } else {
82        Matrix::identity()
83    };
84
85    // Try to resolve the function
86    let func = dict.get(b"Function").and_then(PdfFunction::parse);
87
88    let transform = shading_matrix.concat(ctm).concat(page_transform).to_skia();
89
90    // Sample the function on a grid and render as colored rectangles
91    let samples = 64; // Grid resolution
92    let dx = (x1 - x0) / samples as f64;
93    let dy = (y1 - y0) / samples as f64;
94
95    for iy in 0..samples {
96        for ix in 0..samples {
97            let sx = x0 + (ix as f64 + 0.5) * dx;
98            let sy = y0 + (iy as f64 + 0.5) * dy;
99
100            let color = if let Some(ref f) = func {
101                let result = f.evaluate(&[sx, sy]);
102                components_to_color(&result, cs_name)
103            } else {
104                // No function — gray fallback
105                components_to_color(&[0.5, 0.5, 0.5], cs_name)
106            };
107
108            let mut paint = Paint::default();
109            paint.set_color(color);
110
111            let rx = x0 + ix as f64 * dx;
112            let ry = y0 + iy as f64 * dy;
113
114            let mut pb = PathBuilder::new();
115            pb.move_to(rx as f32, ry as f32);
116            pb.line_to((rx + dx) as f32, ry as f32);
117            pb.line_to((rx + dx) as f32, (ry + dy) as f32);
118            pb.line_to(rx as f32, (ry + dy) as f32);
119            pb.close();
120
121            if let Some(path) = pb.finish() {
122                pixmap.fill_path(&path, &paint, FillRule::Winding, transform, clip);
123            }
124        }
125    }
126}
127
128// ---------------------------------------------------------------------------
129// Type 2: Axial (linear) gradient
130// ---------------------------------------------------------------------------
131
132fn render_axial(
133    pixmap: &mut Pixmap,
134    dict: &PdfDict,
135    ctm: &Matrix,
136    page_transform: &Matrix,
137    clip: Option<&Mask>,
138) {
139    let coords = match dict.get_array(b"Coords") {
140        Some(arr) if arr.len() >= 4 => arr,
141        _ => return,
142    };
143
144    let x0 = coords[0].as_f64().unwrap_or(0.0) as f32;
145    let y0 = coords[1].as_f64().unwrap_or(0.0) as f32;
146    let x1 = coords[2].as_f64().unwrap_or(0.0) as f32;
147    let y1 = coords[3].as_f64().unwrap_or(0.0) as f32;
148
149    let stops = extract_color_stops(dict);
150    if stops.is_empty() {
151        return;
152    }
153
154    let gradient = match LinearGradient::new(
155        tiny_skia::Point::from_xy(x0, y0),
156        tiny_skia::Point::from_xy(x1, y1),
157        stops,
158        SpreadMode::Pad,
159        Transform::identity(),
160    ) {
161        Some(g) => g,
162        None => return,
163    };
164
165    let mut paint = Paint::default();
166    paint.shader = gradient;
167    paint.anti_alias = true;
168
169    let transform = ctm.concat(page_transform).to_skia();
170
171    let mut pb = PathBuilder::new();
172    pb.move_to(-10000.0, -10000.0);
173    pb.line_to(20000.0, -10000.0);
174    pb.line_to(20000.0, 20000.0);
175    pb.line_to(-10000.0, 20000.0);
176    pb.close();
177
178    if let Some(path) = pb.finish() {
179        pixmap.fill_path(&path, &paint, FillRule::Winding, transform, clip);
180    }
181}
182
183// ---------------------------------------------------------------------------
184// Type 3: Radial gradient
185// ---------------------------------------------------------------------------
186
187fn render_radial(
188    pixmap: &mut Pixmap,
189    dict: &PdfDict,
190    ctm: &Matrix,
191    page_transform: &Matrix,
192    clip: Option<&Mask>,
193) {
194    let coords = match dict.get_array(b"Coords") {
195        Some(arr) if arr.len() >= 6 => arr,
196        _ => return,
197    };
198
199    let x0 = coords[0].as_f64().unwrap_or(0.0) as f32;
200    let y0 = coords[1].as_f64().unwrap_or(0.0) as f32;
201    let _r0 = coords[2].as_f64().unwrap_or(0.0) as f32;
202    let x1 = coords[3].as_f64().unwrap_or(0.0) as f32;
203    let y1 = coords[4].as_f64().unwrap_or(0.0) as f32;
204    let r1 = coords[5].as_f64().unwrap_or(0.0) as f32;
205
206    let stops = extract_color_stops(dict);
207    if stops.is_empty() {
208        return;
209    }
210
211    let gradient = match RadialGradient::new(
212        tiny_skia::Point::from_xy(x0, y0),
213        tiny_skia::Point::from_xy(x1, y1),
214        r1,
215        stops,
216        SpreadMode::Pad,
217        Transform::identity(),
218    ) {
219        Some(g) => g,
220        None => return,
221    };
222
223    let mut paint = Paint::default();
224    paint.shader = gradient;
225    paint.anti_alias = true;
226
227    let transform = ctm.concat(page_transform).to_skia();
228
229    let mut pb = PathBuilder::new();
230    pb.move_to(-10000.0, -10000.0);
231    pb.line_to(20000.0, -10000.0);
232    pb.line_to(20000.0, 20000.0);
233    pb.line_to(-10000.0, 20000.0);
234    pb.close();
235
236    if let Some(path) = pb.finish() {
237        pixmap.fill_path(&path, &paint, FillRule::Winding, transform, clip);
238    }
239}
240
241// ---------------------------------------------------------------------------
242// Type 4/5: Free-form / Lattice-form Gouraud-shaded triangle mesh
243// ---------------------------------------------------------------------------
244
245/// A vertex with position and color.
246#[derive(Debug, Clone, Copy)]
247#[allow(dead_code)]
248pub struct Vertex {
249    x: f64,
250    y: f64,
251    r: u8,
252    g: u8,
253    b: u8,
254}
255
256fn render_gouraud_mesh(
257    pixmap: &mut Pixmap,
258    dict: &PdfDict,
259    ctm: &Matrix,
260    page_transform: &Matrix,
261    clip: Option<&Mask>,
262    stream_data: Option<&[u8]>,
263) {
264    let shading_type = dict.get_i64(b"ShadingType").unwrap_or(4);
265
266    let cs_name = dict
267        .get(b"ColorSpace")
268        .and_then(|o| o.as_name())
269        .unwrap_or(b"DeviceRGB");
270    let n_comps = color_space_components(cs_name);
271
272    let bits_per_coordinate = dict.get_i64(b"BitsPerCoordinate").unwrap_or(8) as u32;
273    let bits_per_component = dict.get_i64(b"BitsPerComponent").unwrap_or(8) as u32;
274    let bits_per_flag = if shading_type == 4 {
275        dict.get_i64(b"BitsPerFlag").unwrap_or(8) as u32
276    } else {
277        0
278    };
279
280    let decode = dict
281        .get_array(b"Decode")
282        .map(|a| a.iter().filter_map(|o| o.as_f64()).collect::<Vec<_>>())
283        .unwrap_or_default();
284
285    let data = match stream_data {
286        Some(d) if !d.is_empty() => d,
287        _ => {
288            // No stream data — fallback
289            render_mesh_fallback(pixmap, ctm, page_transform, clip, cs_name, &decode);
290            return;
291        }
292    };
293
294    let transform = ctm.concat(page_transform).to_skia();
295
296    if shading_type == 4 {
297        // Type 4: Free-form Gouraud triangle mesh
298        let triangles = parse_gouraud_triangles(
299            data,
300            bits_per_flag,
301            bits_per_coordinate,
302            bits_per_component,
303            n_comps,
304            &decode,
305            cs_name,
306        );
307
308        for tri in &triangles {
309            rasterize_triangle(
310                pixmap,
311                (tri[0].x as f32, tri[0].y as f32, [tri[0].r, tri[0].g, tri[0].b, 255]),
312                (tri[1].x as f32, tri[1].y as f32, [tri[1].r, tri[1].g, tri[1].b, 255]),
313                (tri[2].x as f32, tri[2].y as f32, [tri[2].r, tri[2].g, tri[2].b, 255]),
314                transform,
315                clip,
316            );
317        }
318    } else {
319        // Type 5: Lattice-form Gouraud mesh
320        let vertices_per_row = dict.get_i64(b"VerticesPerRow").unwrap_or(2) as usize;
321        if vertices_per_row < 2 {
322            return;
323        }
324
325        let vertices = parse_lattice_vertices(
326            data,
327            bits_per_coordinate,
328            bits_per_component,
329            n_comps,
330            &decode,
331            cs_name,
332        );
333
334        // Build triangles from lattice grid
335        let n_rows = vertices.len() / vertices_per_row;
336        for row in 0..n_rows.saturating_sub(1) {
337            for col in 0..vertices_per_row - 1 {
338                let i0 = row * vertices_per_row + col;
339                let i1 = i0 + 1;
340                let i2 = i0 + vertices_per_row;
341                let i3 = i2 + 1;
342
343                if i3 < vertices.len() {
344                    let v0 = &vertices[i0];
345                    let v1 = &vertices[i1];
346                    let v2 = &vertices[i2];
347                    let v3 = &vertices[i3];
348
349                    // Two triangles per quad
350                    rasterize_triangle(
351                        pixmap,
352                        (v0.x as f32, v0.y as f32, [v0.r, v0.g, v0.b, 255]),
353                        (v1.x as f32, v1.y as f32, [v1.r, v1.g, v1.b, 255]),
354                        (v2.x as f32, v2.y as f32, [v2.r, v2.g, v2.b, 255]),
355                        transform,
356                        clip,
357                    );
358                    rasterize_triangle(
359                        pixmap,
360                        (v1.x as f32, v1.y as f32, [v1.r, v1.g, v1.b, 255]),
361                        (v3.x as f32, v3.y as f32, [v3.r, v3.g, v3.b, 255]),
362                        (v2.x as f32, v2.y as f32, [v2.r, v2.g, v2.b, 255]),
363                        transform,
364                        clip,
365                    );
366                }
367            }
368        }
369    }
370}
371
372// ---------------------------------------------------------------------------
373// Type 6/7: Coons / Tensor-product patch mesh
374// ---------------------------------------------------------------------------
375
376fn render_patch_mesh(
377    pixmap: &mut Pixmap,
378    dict: &PdfDict,
379    ctm: &Matrix,
380    page_transform: &Matrix,
381    clip: Option<&Mask>,
382    stream_data: Option<&[u8]>,
383) {
384    let shading_type = dict.get_i64(b"ShadingType").unwrap_or(6);
385
386    let cs_name = dict
387        .get(b"ColorSpace")
388        .and_then(|o| o.as_name())
389        .unwrap_or(b"DeviceRGB");
390    let n_comps = color_space_components(cs_name);
391
392    let bits_per_coordinate = dict.get_i64(b"BitsPerCoordinate").unwrap_or(8) as u32;
393    let bits_per_component = dict.get_i64(b"BitsPerComponent").unwrap_or(8) as u32;
394    let bits_per_flag = dict.get_i64(b"BitsPerFlag").unwrap_or(8) as u32;
395
396    let decode = dict
397        .get_array(b"Decode")
398        .map(|a| a.iter().filter_map(|o| o.as_f64()).collect::<Vec<_>>())
399        .unwrap_or_default();
400
401    let data = match stream_data {
402        Some(d) if !d.is_empty() => d,
403        _ => {
404            render_mesh_fallback(pixmap, ctm, page_transform, clip, cs_name, &decode);
405            return;
406        }
407    };
408
409    let transform = ctm.concat(page_transform).to_skia();
410
411    // Number of control points per patch: 12 for Coons (Type 6), 16 for Tensor (Type 7)
412    let points_per_patch: usize = if shading_type == 7 { 16 } else { 12 };
413
414    let patches = parse_patch_mesh(
415        data,
416        bits_per_flag,
417        bits_per_coordinate,
418        bits_per_component,
419        n_comps,
420        points_per_patch,
421        &decode,
422        cs_name,
423    );
424
425    // Subdivide each patch into triangles and rasterize
426    for patch in &patches {
427        subdivide_and_rasterize_patch(pixmap, patch, transform, clip, 3);
428    }
429}
430
431/// Fallback renderer for mesh shadings when we don't have stream data.
432/// Renders the decode-range bounding box with averaged color.
433fn render_mesh_fallback(
434    pixmap: &mut Pixmap,
435    ctm: &Matrix,
436    page_transform: &Matrix,
437    clip: Option<&Mask>,
438    cs_name: &[u8],
439    decode: &[f64],
440) {
441    let x_min = decode.first().copied().unwrap_or(0.0) as f32;
442    let x_max = decode.get(1).copied().unwrap_or(1.0) as f32;
443    let y_min = decode.get(2).copied().unwrap_or(0.0) as f32;
444    let y_max = decode.get(3).copied().unwrap_or(1.0) as f32;
445
446    // Use mid-range colors as fallback
447    let n_comps = color_space_components(cs_name);
448    let mut mid_comps = Vec::new();
449    for i in 0..n_comps {
450        let idx = 4 + i * 2;
451        let cmin = decode.get(idx).copied().unwrap_or(0.0);
452        let cmax = decode.get(idx + 1).copied().unwrap_or(1.0);
453        mid_comps.push((cmin + cmax) / 2.0);
454    }
455
456    let color = components_to_color(&mid_comps, cs_name);
457    let transform = ctm.concat(page_transform).to_skia();
458
459    let mut paint = Paint::default();
460    paint.set_color(color);
461    paint.anti_alias = true;
462
463    let mut pb = PathBuilder::new();
464    pb.move_to(x_min, y_min);
465    pb.line_to(x_max, y_min);
466    pb.line_to(x_max, y_max);
467    pb.line_to(x_min, y_max);
468    pb.close();
469
470    if let Some(path) = pb.finish() {
471        pixmap.fill_path(&path, &paint, FillRule::Winding, transform, clip);
472    }
473}
474
475/// Rasterize a single triangle with per-vertex colors using barycentric interpolation.
476/// This is the core routine for mesh shading.
477pub fn rasterize_triangle(
478    pixmap: &mut Pixmap,
479    v0: (f32, f32, [u8; 4]),
480    v1: (f32, f32, [u8; 4]),
481    v2: (f32, f32, [u8; 4]),
482    transform: Transform,
483    clip: Option<&Mask>,
484) {
485    // Transform vertices to pixel coordinates
486    let p0 = transform_point(transform, v0.0, v0.1);
487    let p1 = transform_point(transform, v1.0, v1.1);
488    let p2 = transform_point(transform, v2.0, v2.1);
489
490    // Bounding box
491    let min_x = p0.0.min(p1.0).min(p2.0).floor().max(0.0) as i32;
492    let max_x = p0.0.max(p1.0).max(p2.0).ceil().min(pixmap.width() as f32) as i32;
493    let min_y = p0.1.min(p1.1).min(p2.1).floor().max(0.0) as i32;
494    let max_y = p0.1.max(p1.1).max(p2.1).ceil().min(pixmap.height() as f32) as i32;
495
496    let area = edge_function(p0, p1, p2);
497    if area.abs() < 0.001 {
498        return; // degenerate triangle
499    }
500    let inv_area = 1.0 / area;
501
502    let width = pixmap.width() as i32;
503    let pixels = pixmap.pixels_mut();
504
505    for y in min_y..max_y {
506        for x in min_x..max_x {
507            let px = x as f32 + 0.5;
508            let py = y as f32 + 0.5;
509            let p = (px, py);
510
511            let w0 = edge_function(p1, p2, p) * inv_area;
512            let w1 = edge_function(p2, p0, p) * inv_area;
513            let w2 = edge_function(p0, p1, p) * inv_area;
514
515            if w0 >= 0.0 && w1 >= 0.0 && w2 >= 0.0 {
516                // Check clip mask
517                // Note: clip mask check for mesh triangles is not implemented
518                let _ = clip;
519
520                let r = (w0 * v0.2[0] as f32 + w1 * v1.2[0] as f32 + w2 * v2.2[0] as f32) as u8;
521                let g = (w0 * v0.2[1] as f32 + w1 * v1.2[1] as f32 + w2 * v2.2[1] as f32) as u8;
522                let b = (w0 * v0.2[2] as f32 + w1 * v1.2[2] as f32 + w2 * v2.2[2] as f32) as u8;
523                let a = (w0 * v0.2[3] as f32 + w1 * v1.2[3] as f32 + w2 * v2.2[3] as f32) as u8;
524
525                let idx = (y * width + x) as usize;
526                if idx < pixels.len() {
527                    // Premultiply alpha for tiny-skia
528                    if let Some(color) =
529                        tiny_skia::PremultipliedColorU8::from_rgba(r, g, b, a)
530                    {
531                        pixels[idx] = color;
532                    }
533                }
534            }
535        }
536    }
537}
538
539fn edge_function(a: (f32, f32), b: (f32, f32), c: (f32, f32)) -> f32 {
540    (c.0 - a.0) * (b.1 - a.1) - (c.1 - a.1) * (b.0 - a.0)
541}
542
543fn transform_point(t: Transform, x: f32, y: f32) -> (f32, f32) {
544    (
545        t.sx * x + t.kx * y + t.tx,
546        t.ky * x + t.sy * y + t.ty,
547    )
548}
549
550// ---------------------------------------------------------------------------
551// Mesh stream parser (for when stream data is available)
552// ---------------------------------------------------------------------------
553
554/// Parse vertices from a Type 4 (free-form Gouraud) mesh stream.
555pub fn parse_gouraud_triangles(
556    data: &[u8],
557    bits_per_flag: u32,
558    bits_per_coordinate: u32,
559    bits_per_component: u32,
560    n_components: usize,
561    decode: &[f64],
562    cs_name: &[u8],
563) -> Vec<[Vertex; 3]> {
564    let mut reader = BitReader::new(data);
565    let mut vertices: Vec<Vertex> = Vec::new();
566    let mut triangles: Vec<[Vertex; 3]> = Vec::new();
567
568    let (x_min, x_max) = (
569        decode.first().copied().unwrap_or(0.0),
570        decode.get(1).copied().unwrap_or(1.0),
571    );
572    let (y_min, y_max) = (
573        decode.get(2).copied().unwrap_or(0.0),
574        decode.get(3).copied().unwrap_or(1.0),
575    );
576
577    let coord_max = ((1u64 << bits_per_coordinate) - 1) as f64;
578    let comp_max = ((1u64 << bits_per_component) - 1) as f64;
579
580    loop {
581        // Read flag
582        let flag = if bits_per_flag > 0 {
583            match reader.read_bits(bits_per_flag) {
584                Some(f) => f as u8,
585                None => break,
586            }
587        } else {
588            0
589        };
590
591        // Read coordinates
592        let raw_x = match reader.read_bits(bits_per_coordinate) {
593            Some(v) => v,
594            None => break,
595        };
596        let raw_y = match reader.read_bits(bits_per_coordinate) {
597            Some(v) => v,
598            None => break,
599        };
600
601        let x = x_min + (raw_x as f64 / coord_max) * (x_max - x_min);
602        let y = y_min + (raw_y as f64 / coord_max) * (y_max - y_min);
603
604        // Read color components
605        let mut comps = Vec::with_capacity(n_components);
606        for i in 0..n_components {
607            let raw_c = match reader.read_bits(bits_per_component) {
608                Some(v) => v,
609                None => break,
610            };
611            let c_min = decode.get(4 + i * 2).copied().unwrap_or(0.0);
612            let c_max = decode.get(4 + i * 2 + 1).copied().unwrap_or(1.0);
613            comps.push(c_min + (raw_c as f64 / comp_max) * (c_max - c_min));
614        }
615        if comps.len() != n_components {
616            break;
617        }
618
619        let color = components_to_color(&comps, cs_name);
620        let [r, g, b, _] = color_to_rgba8(color);
621
622        let vertex = Vertex { x, y, r, g, b };
623
624        match flag {
625            0 => {
626                // New triangle: need 3 vertices
627                vertices.clear();
628                vertices.push(vertex);
629            }
630            1 => {
631                // Continue from last 2 vertices
632                if vertices.len() >= 2 {
633                    let v_prev1 = vertices[vertices.len() - 2];
634                    let v_prev2 = vertices[vertices.len() - 1];
635                    vertices.push(vertex);
636                    triangles.push([v_prev2, vertices.last().copied().unwrap(), v_prev1]);
637                } else {
638                    vertices.push(vertex);
639                }
640            }
641            2 => {
642                // Continue from first and last vertex
643                if vertices.len() >= 2 {
644                    let v_first = vertices[0];
645                    let v_last = vertices[vertices.len() - 1];
646                    vertices.push(vertex);
647                    triangles.push([v_last, vertices.last().copied().unwrap(), v_first]);
648                } else {
649                    vertices.push(vertex);
650                }
651            }
652            _ => {
653                vertices.push(vertex);
654            }
655        }
656
657        // When we have 3 vertices with flag=0, form a triangle
658        if flag == 0 && vertices.len() == 3 {
659            triangles.push([vertices[0], vertices[1], vertices[2]]);
660        } else if flag == 0 && vertices.len() < 3 {
661            continue; // need more vertices
662        }
663    }
664
665    triangles
666}
667
668/// Parse vertices from a Type 5 (lattice-form Gouraud) mesh stream.
669/// No flag bits — vertices are read in row-major order.
670pub fn parse_lattice_vertices(
671    data: &[u8],
672    bits_per_coordinate: u32,
673    bits_per_component: u32,
674    n_components: usize,
675    decode: &[f64],
676    cs_name: &[u8],
677) -> Vec<Vertex> {
678    let mut reader = BitReader::new(data);
679    let mut vertices: Vec<Vertex> = Vec::new();
680
681    let (x_min, x_max) = (
682        decode.first().copied().unwrap_or(0.0),
683        decode.get(1).copied().unwrap_or(1.0),
684    );
685    let (y_min, y_max) = (
686        decode.get(2).copied().unwrap_or(0.0),
687        decode.get(3).copied().unwrap_or(1.0),
688    );
689
690    let coord_max = ((1u64 << bits_per_coordinate) - 1) as f64;
691    let comp_max = ((1u64 << bits_per_component) - 1) as f64;
692
693    loop {
694        let raw_x = match reader.read_bits(bits_per_coordinate) {
695            Some(v) => v,
696            None => break,
697        };
698        let raw_y = match reader.read_bits(bits_per_coordinate) {
699            Some(v) => v,
700            None => break,
701        };
702
703        let x = x_min + (raw_x as f64 / coord_max) * (x_max - x_min);
704        let y = y_min + (raw_y as f64 / coord_max) * (y_max - y_min);
705
706        let mut comps = Vec::with_capacity(n_components);
707        let mut ok = true;
708        for i in 0..n_components {
709            match reader.read_bits(bits_per_component) {
710                Some(raw_c) => {
711                    let c_min = decode.get(4 + i * 2).copied().unwrap_or(0.0);
712                    let c_max = decode.get(4 + i * 2 + 1).copied().unwrap_or(1.0);
713                    comps.push(c_min + (raw_c as f64 / comp_max) * (c_max - c_min));
714                }
715                None => {
716                    ok = false;
717                    break;
718                }
719            }
720        }
721        if !ok {
722            break;
723        }
724
725        let color = components_to_color(&comps, cs_name);
726        let [r, g, b, _] = color_to_rgba8(color);
727        vertices.push(Vertex { x, y, r, g, b });
728    }
729
730    vertices
731}
732
733/// A patch with 4 corner positions and colors (subdivided from Coons/Tensor control points).
734#[derive(Debug, Clone)]
735pub struct Patch {
736    /// Corner positions: [p00, p01, p10, p11] (or more control points)
737    pub corners: [(f64, f64); 4],
738    /// Corner colors: [c00, c01, c10, c11]
739    pub colors: [[u8; 4]; 4],
740}
741
742/// Parse patches from a Type 6/7 mesh stream.
743pub fn parse_patch_mesh(
744    data: &[u8],
745    bits_per_flag: u32,
746    bits_per_coordinate: u32,
747    bits_per_component: u32,
748    n_components: usize,
749    points_per_patch: usize,
750    decode: &[f64],
751    cs_name: &[u8],
752) -> Vec<Patch> {
753    let mut reader = BitReader::new(data);
754    let mut patches: Vec<Patch> = Vec::new();
755
756    let (x_min, x_max) = (
757        decode.first().copied().unwrap_or(0.0),
758        decode.get(1).copied().unwrap_or(1.0),
759    );
760    let (y_min, y_max) = (
761        decode.get(2).copied().unwrap_or(0.0),
762        decode.get(3).copied().unwrap_or(1.0),
763    );
764
765    let coord_max = ((1u64 << bits_per_coordinate) - 1).max(1) as f64;
766    let comp_max = ((1u64 << bits_per_component) - 1).max(1) as f64;
767
768    let mut prev_points: Vec<(f64, f64)> = Vec::new();
769    let mut prev_colors: Vec<[u8; 4]> = Vec::new();
770
771    loop {
772        let flag = match reader.read_bits(bits_per_flag) {
773            Some(f) => f as u8,
774            None => break,
775        };
776
777        // Determine how many new points/colors to read based on flag
778        let (n_points, n_colors) = if flag == 0 {
779            (points_per_patch, 4) // Full patch
780        } else {
781            // Continuation: reuse some points from previous patch
782            // Type 6: flag 1/2/3 reuse 4 points from previous edge, read 8 new + 2 colors
783            // Type 7: flag 1/2/3 reuse 4 points, read 12 new + 2 colors
784            if points_per_patch == 16 {
785                (12, 2)
786            } else {
787                (8, 2)
788            }
789        };
790
791        // Read control points
792        let mut points: Vec<(f64, f64)> = Vec::with_capacity(n_points);
793        let mut ok = true;
794        for _ in 0..n_points {
795            let raw_x = match reader.read_bits(bits_per_coordinate) {
796                Some(v) => v,
797                None => { ok = false; break; }
798            };
799            let raw_y = match reader.read_bits(bits_per_coordinate) {
800                Some(v) => v,
801                None => { ok = false; break; }
802            };
803            let x = x_min + (raw_x as f64 / coord_max) * (x_max - x_min);
804            let y = y_min + (raw_y as f64 / coord_max) * (y_max - y_min);
805            points.push((x, y));
806        }
807        if !ok { break; }
808
809        // Read colors
810        let mut colors: Vec<[u8; 4]> = Vec::with_capacity(n_colors);
811        for _ in 0..n_colors {
812            let mut comps = Vec::with_capacity(n_components);
813            for i in 0..n_components {
814                match reader.read_bits(bits_per_component) {
815                    Some(raw_c) => {
816                        let c_min = decode.get(4 + i * 2).copied().unwrap_or(0.0);
817                        let c_max = decode.get(4 + i * 2 + 1).copied().unwrap_or(1.0);
818                        comps.push(c_min + (raw_c as f64 / comp_max) * (c_max - c_min));
819                    }
820                    None => { ok = false; break; }
821                }
822            }
823            if !ok { break; }
824            let color = components_to_color(&comps, cs_name);
825            let [r, g, b, a] = color_to_rgba8(color);
826            colors.push([r, g, b, a]);
827        }
828        if !ok { break; }
829
830        // For simplicity, extract the 4 corner positions and colors
831        // (ignoring intermediate bezier control points — approximation)
832        let (corners, corner_colors) = if flag == 0 && points.len() >= 4 && colors.len() >= 4 {
833            // Full patch: corners are at indices 0, 3, 6, 9 for Type 6 (12 points)
834            // or 0, 3, 8, 11 for Type 7 (16 points)
835            let c0 = points[0];
836            let c1 = if points_per_patch == 16 { points[3] } else { points[3] };
837            let c2 = if points_per_patch == 16 { points[12] } else { points[9] };
838            let c3 = if points_per_patch == 16 {
839                points.get(15).copied().unwrap_or(c2)
840            } else {
841                points.get(6).copied().unwrap_or(c0)
842            };
843            (
844                [c0, c1, c2, c3],
845                [colors[0], colors[1], colors[2], colors[3]],
846            )
847        } else if !prev_points.is_empty() && points.len() >= 4 && colors.len() >= 2 {
848            // Continuation: approximate with new points
849            let c0 = points[0];
850            let c1 = points.get(3).copied().unwrap_or(c0);
851            let c2 = points.last().copied().unwrap_or(c0);
852            let c3 = points.get(points.len() / 2).copied().unwrap_or(c0);
853            let pc = if prev_colors.len() >= 4 {
854                [prev_colors[1], prev_colors[2]]
855            } else {
856                [[128, 128, 128, 255]; 2]
857            };
858            (
859                [c0, c1, c2, c3],
860                [pc[0], colors[0], pc[1], colors[1]],
861            )
862        } else {
863            continue;
864        };
865
866        prev_points = points;
867        prev_colors = Vec::from(corner_colors);
868
869        patches.push(Patch {
870            corners,
871            colors: corner_colors,
872        });
873    }
874
875    patches
876}
877
878/// Subdivide a patch into triangles and rasterize them.
879/// Uses bilinear interpolation across the 4 corners.
880fn subdivide_and_rasterize_patch(
881    pixmap: &mut Pixmap,
882    patch: &Patch,
883    transform: Transform,
884    clip: Option<&Mask>,
885    subdivisions: usize,
886) {
887    let n = subdivisions.max(1);
888    let step = 1.0 / n as f64;
889
890    for i in 0..n {
891        for j in 0..n {
892            let u0 = i as f64 * step;
893            let v0 = j as f64 * step;
894            let u1 = u0 + step;
895            let v1 = v0 + step;
896
897            let p00 = bilinear_point(&patch.corners, u0, v0);
898            let p10 = bilinear_point(&patch.corners, u1, v0);
899            let p01 = bilinear_point(&patch.corners, u0, v1);
900            let p11 = bilinear_point(&patch.corners, u1, v1);
901
902            let c00 = bilinear_color(&patch.colors, u0, v0);
903            let c10 = bilinear_color(&patch.colors, u1, v0);
904            let c01 = bilinear_color(&patch.colors, u0, v1);
905            let c11 = bilinear_color(&patch.colors, u1, v1);
906
907            // Two triangles per quad
908            rasterize_triangle(
909                pixmap,
910                (p00.0 as f32, p00.1 as f32, c00),
911                (p10.0 as f32, p10.1 as f32, c10),
912                (p01.0 as f32, p01.1 as f32, c01),
913                transform,
914                clip,
915            );
916            rasterize_triangle(
917                pixmap,
918                (p10.0 as f32, p10.1 as f32, c10),
919                (p11.0 as f32, p11.1 as f32, c11),
920                (p01.0 as f32, p01.1 as f32, c01),
921                transform,
922                clip,
923            );
924        }
925    }
926}
927
928/// Bilinear interpolation of a point on a quad defined by 4 corners.
929fn bilinear_point(corners: &[(f64, f64); 4], u: f64, v: f64) -> (f64, f64) {
930    let x = corners[0].0 * (1.0 - u) * (1.0 - v)
931        + corners[1].0 * u * (1.0 - v)
932        + corners[2].0 * (1.0 - u) * v
933        + corners[3].0 * u * v;
934    let y = corners[0].1 * (1.0 - u) * (1.0 - v)
935        + corners[1].1 * u * (1.0 - v)
936        + corners[2].1 * (1.0 - u) * v
937        + corners[3].1 * u * v;
938    (x, y)
939}
940
941/// Bilinear interpolation of color on a quad.
942fn bilinear_color(colors: &[[u8; 4]; 4], u: f64, v: f64) -> [u8; 4] {
943    let mut result = [0u8; 4];
944    for ch in 0..4 {
945        let c = colors[0][ch] as f64 * (1.0 - u) * (1.0 - v)
946            + colors[1][ch] as f64 * u * (1.0 - v)
947            + colors[2][ch] as f64 * (1.0 - u) * v
948            + colors[3][ch] as f64 * u * v;
949        result[ch] = c.clamp(0.0, 255.0) as u8;
950    }
951    result
952}
953
954fn color_to_rgba8(color: Color) -> [u8; 4] {
955    [
956        (color.red() * 255.0) as u8,
957        (color.green() * 255.0) as u8,
958        (color.blue() * 255.0) as u8,
959        (color.alpha() * 255.0) as u8,
960    ]
961}
962
963/// Simple bit reader for parsing mesh stream data.
964struct BitReader<'a> {
965    data: &'a [u8],
966    byte_pos: usize,
967    bit_pos: u8, // 0-7, MSB first
968}
969
970impl<'a> BitReader<'a> {
971    fn new(data: &'a [u8]) -> Self {
972        Self {
973            data,
974            byte_pos: 0,
975            bit_pos: 0,
976        }
977    }
978
979    fn read_bits(&mut self, n: u32) -> Option<u64> {
980        if n == 0 || n > 64 {
981            return Some(0);
982        }
983
984        let mut result: u64 = 0;
985        let mut remaining = n;
986
987        while remaining > 0 {
988            if self.byte_pos >= self.data.len() {
989                return None;
990            }
991
992            let available = 8 - self.bit_pos as u32;
993            let to_read = remaining.min(available);
994
995            let byte = self.data[self.byte_pos];
996            let shift = available - to_read;
997            let mask = ((1u16 << to_read) - 1) as u8;
998            let bits = (byte >> shift) & mask;
999
1000            result = (result << to_read) | bits as u64;
1001            remaining -= to_read;
1002
1003            self.bit_pos += to_read as u8;
1004            if self.bit_pos >= 8 {
1005                self.bit_pos = 0;
1006                self.byte_pos += 1;
1007            }
1008        }
1009
1010        Some(result)
1011    }
1012}
1013
1014// ---------------------------------------------------------------------------
1015// Color stop extraction (for Type 2/3 shadings)
1016// ---------------------------------------------------------------------------
1017
1018fn extract_color_stops(dict: &PdfDict) -> Vec<GradientStop> {
1019    let cs_name = dict
1020        .get(b"ColorSpace")
1021        .and_then(|o| o.as_name())
1022        .unwrap_or(b"DeviceRGB");
1023
1024    if let Some(func_obj) = dict.get(b"Function") {
1025        if let PdfObject::Dict(func) = func_obj {
1026            return extract_stops_from_function(func, cs_name);
1027        }
1028        if let PdfObject::Array(funcs) = func_obj {
1029            if let Some(PdfObject::Dict(func)) = funcs.first() {
1030                return extract_stops_from_function(func, cs_name);
1031            }
1032        }
1033    }
1034
1035    vec![
1036        GradientStop::new(0.0, Color::BLACK),
1037        GradientStop::new(1.0, Color::WHITE),
1038    ]
1039}
1040
1041fn extract_stops_from_function(func: &PdfDict, cs_name: &[u8]) -> Vec<GradientStop> {
1042    let func_type = func.get_i64(b"FunctionType").unwrap_or(0);
1043
1044    if func_type == 2 {
1045        let c0 = func
1046            .get_array(b"C0")
1047            .map(|a| a.iter().map(|o| o.as_f64().unwrap_or(0.0)).collect::<Vec<_>>())
1048            .unwrap_or_else(|| vec![0.0]);
1049        let c1 = func
1050            .get_array(b"C1")
1051            .map(|a| a.iter().map(|o| o.as_f64().unwrap_or(0.0)).collect::<Vec<_>>())
1052            .unwrap_or_else(|| vec![1.0]);
1053
1054        vec![
1055            GradientStop::new(0.0, components_to_color(&c0, cs_name)),
1056            GradientStop::new(1.0, components_to_color(&c1, cs_name)),
1057        ]
1058    } else if func_type == 3 {
1059        let bounds = func
1060            .get_array(b"Bounds")
1061            .map(|a| a.iter().filter_map(|o| o.as_f64()).collect::<Vec<_>>())
1062            .unwrap_or_default();
1063        let functions = func.get_array(b"Functions").unwrap_or(&[]);
1064
1065        let mut color_stops: Vec<(f32, Color)> = Vec::new();
1066
1067        for (i, sub_func) in functions.iter().enumerate() {
1068            if let PdfObject::Dict(sub) = sub_func {
1069                let sub_colors = extract_colors_from_function(sub, cs_name);
1070                let t_start = if i == 0 {
1071                    0.0
1072                } else {
1073                    bounds.get(i - 1).copied().unwrap_or(0.0) as f32
1074                };
1075                let t_end = if i < bounds.len() {
1076                    bounds[i] as f32
1077                } else {
1078                    1.0
1079                };
1080
1081                if let Some(c) = sub_colors.first() {
1082                    color_stops.push((t_start.clamp(0.0, 1.0), *c));
1083                }
1084                if let Some(c) = sub_colors.last() {
1085                    color_stops.push((t_end.clamp(0.0, 1.0), *c));
1086                }
1087            }
1088        }
1089
1090        color_stops.dedup_by(|a, b| (a.0 - b.0).abs() < 0.001);
1091
1092        if color_stops.len() < 2 {
1093            color_stops = vec![(0.0, Color::BLACK), (1.0, Color::WHITE)];
1094        }
1095
1096        color_stops
1097            .into_iter()
1098            .map(|(pos, color)| GradientStop::new(pos, color))
1099            .collect()
1100    } else {
1101        vec![
1102            GradientStop::new(0.0, Color::BLACK),
1103            GradientStop::new(1.0, Color::WHITE),
1104        ]
1105    }
1106}
1107
1108fn extract_colors_from_function(func: &PdfDict, cs_name: &[u8]) -> Vec<Color> {
1109    let c0 = func
1110        .get_array(b"C0")
1111        .map(|a| a.iter().map(|o| o.as_f64().unwrap_or(0.0)).collect::<Vec<_>>())
1112        .unwrap_or_else(|| vec![0.0]);
1113    let c1 = func
1114        .get_array(b"C1")
1115        .map(|a| a.iter().map(|o| o.as_f64().unwrap_or(0.0)).collect::<Vec<_>>())
1116        .unwrap_or_else(|| vec![1.0]);
1117
1118    vec![
1119        components_to_color(&c0, cs_name),
1120        components_to_color(&c1, cs_name),
1121    ]
1122}
1123
1124// ---------------------------------------------------------------------------
1125// Helpers
1126// ---------------------------------------------------------------------------
1127
1128fn components_to_color(comps: &[f64], cs_name: &[u8]) -> Color {
1129    match cs_name {
1130        b"DeviceGray" | b"CalGray" | b"G" => {
1131            let g = (comps.first().copied().unwrap_or(0.0).clamp(0.0, 1.0) * 255.0) as u8;
1132            Color::from_rgba8(g, g, g, 255)
1133        }
1134        b"DeviceCMYK" | b"CMYK" => {
1135            let c = comps.first().copied().unwrap_or(0.0);
1136            let m = comps.get(1).copied().unwrap_or(0.0);
1137            let y = comps.get(2).copied().unwrap_or(0.0);
1138            let k = comps.get(3).copied().unwrap_or(0.0);
1139            let r = ((1.0 - c) * (1.0 - k) * 255.0) as u8;
1140            let g = ((1.0 - m) * (1.0 - k) * 255.0) as u8;
1141            let b = ((1.0 - y) * (1.0 - k) * 255.0) as u8;
1142            Color::from_rgba8(r, g, b, 255)
1143        }
1144        _ => {
1145            let r = (comps.first().copied().unwrap_or(0.0).clamp(0.0, 1.0) * 255.0) as u8;
1146            let g = (comps.get(1).copied().unwrap_or(0.0).clamp(0.0, 1.0) * 255.0) as u8;
1147            let b = (comps.get(2).copied().unwrap_or(0.0).clamp(0.0, 1.0) * 255.0) as u8;
1148            Color::from_rgba8(r, g, b, 255)
1149        }
1150    }
1151}
1152
1153fn color_space_components(cs_name: &[u8]) -> usize {
1154    match cs_name {
1155        b"DeviceGray" | b"CalGray" | b"G" => 1,
1156        b"DeviceRGB" | b"CalRGB" | b"RGB" => 3,
1157        b"DeviceCMYK" | b"CMYK" => 4,
1158        _ => 3,
1159    }
1160}
1161
1162#[cfg(test)]
1163mod tests {
1164    use super::*;
1165
1166    #[test]
1167    fn test_bit_reader_8bit() {
1168        let data = [0xAB, 0xCD];
1169        let mut r = BitReader::new(&data);
1170        assert_eq!(r.read_bits(8), Some(0xAB));
1171        assert_eq!(r.read_bits(8), Some(0xCD));
1172        assert_eq!(r.read_bits(8), None);
1173    }
1174
1175    #[test]
1176    fn test_bit_reader_4bit() {
1177        let data = [0xAB]; // 1010 1011
1178        let mut r = BitReader::new(&data);
1179        assert_eq!(r.read_bits(4), Some(0xA)); // 1010
1180        assert_eq!(r.read_bits(4), Some(0xB)); // 1011
1181    }
1182
1183    #[test]
1184    fn test_bit_reader_mixed() {
1185        let data = [0b11001010, 0b01010101];
1186        let mut r = BitReader::new(&data);
1187        assert_eq!(r.read_bits(2), Some(0b11));
1188        assert_eq!(r.read_bits(3), Some(0b001));
1189        assert_eq!(r.read_bits(3), Some(0b010));
1190        // 8 bits read, next byte: 01010101
1191        assert_eq!(r.read_bits(8), Some(0b01010101));
1192    }
1193
1194    #[test]
1195    fn test_bit_reader_16bit() {
1196        let data = [0x12, 0x34];
1197        let mut r = BitReader::new(&data);
1198        assert_eq!(r.read_bits(16), Some(0x1234));
1199    }
1200
1201    #[test]
1202    fn test_components_to_color_rgb() {
1203        let c = components_to_color(&[1.0, 0.0, 0.5], b"DeviceRGB");
1204        assert_eq!(c.red(), 1.0);
1205        assert_eq!(c.green(), 0.0);
1206        assert!((c.blue() - 0.5).abs() < 0.01);
1207    }
1208
1209    #[test]
1210    fn test_components_to_color_gray() {
1211        let c = components_to_color(&[0.5], b"DeviceGray");
1212        assert!((c.red() - c.green()).abs() < 0.01);
1213        assert!((c.green() - c.blue()).abs() < 0.01);
1214    }
1215
1216    #[test]
1217    fn test_rasterize_triangle_basic() {
1218        let mut pixmap = Pixmap::new(10, 10).unwrap();
1219        rasterize_triangle(
1220            &mut pixmap,
1221            (1.0, 1.0, [255, 0, 0, 255]),
1222            (9.0, 1.0, [0, 255, 0, 255]),
1223            (5.0, 9.0, [0, 0, 255, 255]),
1224            Transform::identity(),
1225            None,
1226        );
1227
1228        // Check that some pixels were filled (not all transparent)
1229        let has_colored = pixmap.pixels().iter().any(|p| p.alpha() > 0);
1230        assert!(has_colored, "triangle should have colored some pixels");
1231    }
1232
1233    #[test]
1234    fn test_edge_function() {
1235        let a = (0.0, 0.0);
1236        let b = (10.0, 0.0);
1237        let c = (5.0, 5.0);
1238        let area = edge_function(a, b, c);
1239        // The sign depends on winding order; the important thing is it's non-zero
1240        assert!(area.abs() > 0.0, "degenerate triangle should not have zero area");
1241    }
1242}