1use super::{
2 DebugOverlay, MollweideScale, blit_grid_to_sink, draw_debug_overlay_raster,
3 draw_figure_labels_png, fill_grid_background, percentile, render_projection_to_grid,
4};
5use crate::colorbar::{format_tick_label_with_units, render_colorbar_gradient};
6use crate::healpix::is_seen;
7use crate::layout::{MollweideLayout, compute_mollweide_layout};
8use crate::params::MollweideParams;
9use crate::render::pdf::{draw_colorbar_pdf, draw_projection_border_pdf};
10use crate::render::raster::RasterGrid;
11use crate::rotation::CoordSystem;
12use crate::scale::{
13 HistogramRange, Scale, build_histogram_scale, generate_colorbar_ticks, unsafe_float_cmp,
14};
15use crate::{PixelSink, PngSink};
16use ab_glyph::{Font, FontRef, PxScale};
17use cairo::{Context, Format, ImageSurface, PdfSurface};
18use image::{Rgba, RgbaImage};
19use imageproc::drawing::draw_text_mut;
20use std::path::Path;
21
22pub fn compute_mollweide_scale(
23 map: &[f64],
24 minv: Option<f64>,
25 maxv: Option<f64>,
26 gamma: f64,
27 scale: Scale,
28) -> MollweideScale {
29 let mut values: Vec<f64> = map.iter().filter(|v| is_seen(**v)).copied().collect();
30
31 if values.is_empty() {
32 panic!("Map contains no valid HEALPix values");
33 }
34
35 values.sort_unstable_by(unsafe_float_cmp);
36
37 let data_min = *values.first().unwrap();
38 let data_max = *values.last().unwrap();
39
40 let (minv, maxv) = match scale {
41 Scale::Histogram => match (minv, maxv) {
43 (Some(lo), Some(hi)) => (lo, hi),
44 _ => (data_min, data_max),
45 },
46
47 _ => match (minv, maxv) {
49 (Some(lo), Some(hi)) => (lo, hi),
50 _ => (percentile(&values, 5.0), percentile(&values, 95.0)),
51 },
52 };
53
54 if gamma <= 0.0 {
55 panic!("Gamma must be > 0");
56 }
57
58 if minv > maxv {
59 panic!("Invalid color scale: {minv} > {maxv}");
60 }
61
62 MollweideScale { minv, maxv }
63}
64
65pub fn render_mollweide_pixels(
66 params: crate::params::RenderMollweideParams,
67 layout: MollweideLayout,
68 sink: &mut dyn PixelSink,
69 debug_overlay: Option<DebugOverlay>,
70) {
71 use crate::mollweide::MollweideProjection;
72 let proj = MollweideProjection;
73
74 let mut grid = RasterGrid::new(layout.map_w as u32, layout.map_h as u32);
75
76 if let Some(overlay) = debug_overlay
77 && overlay.show_background
78 {
79 fill_grid_background(&mut grid);
80 }
81
82 render_projection_to_grid(
83 crate::params::RenderGridParams {
84 map: params.map,
85 proj: &proj,
86 scale: params.scale,
87 cmap: params.cmap,
88 scale_type: params.scale_type,
89 neg_mode: params.neg_mode,
90 gamma: params.gamma,
91 bad_color: params.bad_color,
92 meta: params.meta,
93 hist_scale: params.hist_scale,
94 view: params.view,
95 mask: params.mask,
96 scale_cache: params.scale_cache,
97 underflow: (255, 0, 0),
98 overflow: (0, 0, 255),
99 },
100 &mut grid,
101 );
102
103 if let Some(overlay) = debug_overlay {
105 draw_debug_overlay_raster(&mut grid, overlay);
106 }
107
108 blit_grid_to_sink(&grid, sink, 0, 0);
109}
110
111pub fn plot_mollweide_pdf(params: MollweideParams) {
112 _plot_mollweide_pdf_impl(params, render_mollweide_pixels);
113}
114
115pub fn _plot_mollweide_pdf_impl<F>(params: MollweideParams, pixel_renderer: F)
116where
117 F: Fn(
118 crate::params::RenderMollweideParams,
119 MollweideLayout,
120 &mut dyn PixelSink,
121 Option<DebugOverlay>,
122 ),
123{
124 let map = params.plot.map;
125 let width = params.plot.width;
126 let filename = params.plot.filename;
127 let minv = params.scale.minv;
128 let maxv = params.scale.maxv;
129 let cmap = params.color.cmap;
130 let show_colorbar = params.display.show_colorbar;
131 let transparent = params.display.transparent;
132 let draw_border = params.display.draw_border;
133 let gamma = params.scale.gamma;
134 let scale = params.scale.scale;
135 let neg_mode = params.scale.neg_mode;
136 let bad_color = params.color.bad_color;
137 let meta = params.meta;
138 let latex_rendering = params.display.latex_rendering;
139 let units = params.display.units.as_deref();
140 let view = params.view;
141 let show_graticule = params.graticule.show_graticule;
142 let grat_coord = params.graticule.grat_coord;
143 let grat_overlay = params.graticule.grat_overlay;
144 let overlay_color = params.graticule.overlay_color;
145 let dpar_deg = params.graticule.dpar_deg;
146 let dmer_deg = params.graticule.dmer_deg;
147 let mask = params.display.mask.as_ref();
148
149 let (layout, cb_layout) = compute_mollweide_layout(
150 width as f64,
151 show_colorbar,
152 params.display.tick_direction.clone(),
153 );
154
155 let mut values: Vec<f64> = map.iter().filter(|&v| is_seen(*v)).copied().collect();
156
157 if values.is_empty() {
158 let n_maps = map.len();
160 let n_finite = map.iter().filter(|v| v.is_finite()).count();
161 let n_gt_neg1e30 = map.iter().filter(|v| **v > -1e30).count();
162 let n_inf = map.iter().filter(|v| v.is_infinite()).count();
163 let n_nan = map.iter().filter(|v| v.is_nan()).count();
164
165 eprintln!("\n=== DEBUG: No valid HEALPix values found ===");
166 eprintln!("Total pixels in map: {}", n_maps);
167 eprintln!("Finite values: {}", n_finite);
168 eprintln!("Values > -1e30: {}", n_gt_neg1e30);
169 eprintln!("Infinite values: {}", n_inf);
170 eprintln!("NaN values: {}", n_nan);
171 if !map.is_empty() {
172 eprintln!(
173 "Min: {:.6e}, Max: {:.6e}",
174 map.iter().copied().fold(f64::INFINITY, f64::min),
175 map.iter().copied().fold(f64::NEG_INFINITY, f64::max)
176 );
177 eprintln!("First 5 values: {:?}", &map[..5.min(map.len())]);
178 }
179 panic!("Map contains no valid HEALPix values");
180 }
181
182 values.sort_unstable_by(unsafe_float_cmp);
183
184 let surface_pdf = PdfSurface::new(layout.width, layout.height, filename)
185 .expect("Failed to create PDF surface");
186
187 let cr_pdf = Context::new(&surface_pdf).unwrap();
188
189 if transparent {
190 cr_pdf.set_operator(cairo::Operator::Source);
191 cr_pdf.set_source_rgba(0.0, 0.0, 0.0, 0.0);
192 cr_pdf.paint().unwrap();
193 }
194
195 let surface_img = ImageSurface::create(
197 Format::ARgb32,
198 (layout.map_w + 2.0 * layout.map_pad) as i32,
199 (layout.map_h + 2.0 * layout.map_pad) as i32,
200 )
201 .expect("Failed to create image surface");
202
203 let cr_img = Context::new(&surface_img).unwrap();
204
205 if transparent {
207 cr_img.set_source_rgba(0.0, 0.0, 0.0, 0.0);
208 } else {
209 cr_img.set_source_rgb(1.0, 1.0, 1.0);
210 }
211 cr_img.paint().unwrap();
212
213 let scale_params = compute_mollweide_scale(map, minv, maxv, gamma, scale);
214
215 let hist_scale_opt = if scale == Scale::Histogram {
216 let range = match (minv, maxv) {
217 (Some(minv), Some(maxv)) => HistogramRange::Explicit {
218 min: minv,
219 max: maxv,
220 },
221 _ => HistogramRange::Full,
222 };
223
224 Some(build_histogram_scale(
225 map, range, 1024, ))
227 } else {
228 None
229 };
230
231 let scale_cache = crate::scale::ScaleCache::new(scale_params.minv, scale_params.maxv, scale);
233
234 let map_w_int = (layout.map_w + 2.0 * layout.map_pad) as u32;
238 let map_h_int = (layout.map_h + 2.0 * layout.map_pad) as u32;
239
240 let mut pixel_buffer = crate::render::create_image_buffer_uninitialized(map_w_int, map_h_int);
244
245 let bg_color = if transparent {
247 image::Rgba([0, 0, 0, 0])
248 } else {
249 image::Rgba([255, 255, 255, 255])
250 };
251 for pixel in pixel_buffer.pixels_mut() {
252 *pixel = bg_color;
253 }
254
255 let mut sink = PngSink {
257 img: &mut pixel_buffer,
258 x0: 0,
259 y0: 0,
260 };
261
262 let debug_overlay = if cfg!(feature = "debug_overlay") {
263 Some(DebugOverlay::grid_only())
264 } else {
265 None
266 };
267
268 pixel_renderer(
269 crate::params::RenderMollweideParams {
270 map,
271 scale: &scale_params,
272 cmap,
273 gamma,
274 scale_type: scale,
275 neg_mode,
276 bad_color,
277 meta,
278 hist_scale: hist_scale_opt.as_ref(),
279 view,
280 mask,
281 scale_cache: Some(&scale_cache),
282 },
283 layout,
284 &mut sink,
285 debug_overlay,
286 );
287
288 let mut argb_buffer = Vec::with_capacity(pixel_buffer.len() * 4);
293 for pixel in pixel_buffer.pixels() {
294 argb_buffer.push(pixel[2]); argb_buffer.push(pixel[1]); argb_buffer.push(pixel[0]); argb_buffer.push(pixel[3]); }
300
301 if let Ok(pixel_surface) = cairo::ImageSurface::create_for_data(
302 argb_buffer,
303 cairo::Format::ARgb32,
304 map_w_int as i32,
305 map_h_int as i32,
306 map_w_int as i32 * 4,
307 ) {
308 let _ = cr_pdf.set_source_surface(&pixel_surface, layout.map_x, layout.map_y);
309 cr_pdf.paint().unwrap();
310 }
311
312 if show_graticule {
317 use crate::graticule::{
318 render_graticule_cairo, render_graticule_cairo_with_color,
319 render_graticule_mollweide_vectorized,
320 };
321
322 let grat_coord_sys = grat_coord.unwrap_or(CoordSystem::E);
323
324 let graticule = render_graticule_mollweide_vectorized(
325 view,
326 dpar_deg,
327 dmer_deg,
328 grat_coord_sys,
329 view.input_coord,
330 );
331
332 render_graticule_cairo(
334 &graticule,
335 &cr_pdf,
336 layout.map_x,
337 layout.map_y,
338 layout.map_w,
339 layout.map_h,
340 );
341
342 if let Some(overlay_sys) = grat_overlay {
344 let overlay_graticule = render_graticule_mollweide_vectorized(
345 view,
346 dpar_deg,
347 dmer_deg,
348 overlay_sys,
349 view.input_coord,
350 );
351
352 let r = overlay_color[0] as f64 / 255.0;
354 let g = overlay_color[1] as f64 / 255.0;
355 let b = overlay_color[2] as f64 / 255.0;
356
357 render_graticule_cairo_with_color(
358 &overlay_graticule,
359 &cr_pdf,
360 crate::params::GeometryRect {
361 x: layout.map_x,
362 y: layout.map_y,
363 w: layout.map_w,
364 h: layout.map_h,
365 },
366 (r, g, b),
367 );
368 }
369 }
370
371 if draw_border {
373 draw_projection_border_pdf(
374 &cr_pdf,
375 layout.map_x,
376 layout.map_y,
377 layout.map_w,
378 layout.map_h,
379 layout.border_width_px,
380 );
381 }
382
383 if show_colorbar {
384 draw_colorbar_pdf(
385 &cr_pdf,
386 cb_layout,
387 crate::params::ColorbarParams {
388 cmap,
389 minv: scale_params.minv,
390 maxv: scale_params.maxv,
391 scale_type: scale,
392 gamma,
393 hist_scale: hist_scale_opt.as_ref(),
394 latex_rendering,
395 units,
396 extend: ¶ms.display.extend,
397 units_font_size: params.display.units_font_size,
398 map_width: None,
399 },
400 );
401 }
402
403 crate::render::pdf::draw_figure_labels_pdf(
405 &cr_pdf,
406 layout.width,
407 layout.height,
408 ¶ms.display.rlabel,
409 ¶ms.display.llabel,
410 latex_rendering,
411 params.display.label_font_size,
412 );
413
414 surface_pdf.finish();
415}
416
417pub fn plot_mollweide_png(params: MollweideParams) {
418 _plot_mollweide_png_impl_projected(params, render_mollweide_pixels, ProjectionType::Mollweide);
419}
420
421#[derive(Clone, Copy, Debug)]
422pub enum ProjectionType {
423 Mollweide,
424 Hammer,
425}
426
427pub fn _plot_mollweide_png_impl_projected<F>(
428 params: MollweideParams,
429 pixel_renderer: F,
430 projection: ProjectionType,
431) where
432 F: Fn(
433 crate::params::RenderMollweideParams,
434 MollweideLayout,
435 &mut dyn PixelSink,
436 Option<DebugOverlay>,
437 ),
438{
439 let map = params.plot.map;
440 let width = params.plot.width;
441 let filename = params.plot.filename;
442 let minv = params.scale.minv;
443 let maxv = params.scale.maxv;
444 let cmap = params.color.cmap;
445 let show_colorbar = params.display.show_colorbar;
446 let transparent = params.display.transparent;
447 let draw_border = params.display.draw_border;
448 let gamma = params.scale.gamma;
449 let scale = params.scale.scale;
450 let neg_mode = params.scale.neg_mode;
451 let bad_color = params.color.bad_color;
452 let bg_color = params.color.bg_color;
453 let meta = params.meta;
454 let latex_rendering = params.display.latex_rendering;
455 let units = params.display.units.as_deref();
456 let view = params.view;
457 let show_graticule = params.graticule.show_graticule;
458 let grat_coord = params.graticule.grat_coord;
459 let grat_overlay = params.graticule.grat_overlay;
460 let overlay_color = params.graticule.overlay_color;
461 let dpar_deg = params.graticule.dpar_deg;
462 let dmer_deg = params.graticule.dmer_deg;
463 let mask = params.display.mask.as_ref();
464
465 let (layout, cb_layout) = compute_mollweide_layout(
466 width as f64,
467 show_colorbar,
468 params.display.tick_direction.clone(),
469 );
470
471 let font_data = include_bytes!("../../assets/fonts/DejaVuSans.ttf");
472 let font = FontRef::try_from_slice(font_data).expect("Failed to load font");
473
474 let mut values: Vec<f64> = map.iter().filter(|&v| is_seen(*v)).copied().collect();
475
476 if values.is_empty() {
477 let n_maps = map.len();
479 let n_finite = map.iter().filter(|v| v.is_finite()).count();
480 let n_gt_neg1e30 = map.iter().filter(|v| **v > -1e30).count();
481 let n_inf = map.iter().filter(|v| v.is_infinite()).count();
482 let n_nan = map.iter().filter(|v| v.is_nan()).count();
483
484 eprintln!("\n=== DEBUG: No valid HEALPix values found ===");
485 eprintln!("Total pixels in map: {}", n_maps);
486 eprintln!("Finite values: {}", n_finite);
487 eprintln!("Values > -1e30: {}", n_gt_neg1e30);
488 eprintln!("Infinite values: {}", n_inf);
489 eprintln!("NaN values: {}", n_nan);
490 if !map.is_empty() {
491 eprintln!(
492 "Min: {:.6e}, Max: {:.6e}",
493 map.iter().copied().fold(f64::INFINITY, f64::min),
494 map.iter().copied().fold(f64::NEG_INFINITY, f64::max)
495 );
496 eprintln!("First 5 values: {:?}", &map[..5.min(map.len())]);
497 }
498 panic!("Map contains no valid HEALPix values");
499 }
500
501 values.sort_unstable_by(unsafe_float_cmp);
502
503 let bg = Rgba([
504 bg_color[0],
505 bg_color[1],
506 bg_color[2],
507 if transparent { 0 } else { 255 },
508 ]);
509
510 let mut img = RgbaImage::from_pixel(layout.width as u32, layout.height as u32, bg);
511
512 let scale_params = compute_mollweide_scale(map, minv, maxv, gamma, scale);
513
514 let hist_scale = if scale == Scale::Histogram {
515 let range = match (minv, maxv) {
516 (Some(minv), Some(maxv)) => HistogramRange::Explicit {
517 min: minv,
518 max: maxv,
519 },
520 _ => HistogramRange::Full,
521 };
522
523 Some(build_histogram_scale(
524 map, range, 1024, ))
526 } else {
527 None
528 };
529
530 let scale_cache = crate::scale::ScaleCache::new(scale_params.minv, scale_params.maxv, scale);
532
533 let mut sink = PngSink {
534 img: &mut img,
535 x0: layout.map_x as u32,
536 y0: layout.map_y as u32,
537 };
538
539 let debug_overlay = if cfg!(feature = "debug_overlay") {
540 Some(DebugOverlay::grid_only())
541 } else {
542 None
543 };
544
545 pixel_renderer(
546 crate::params::RenderMollweideParams {
547 map,
548 scale: &scale_params,
549 cmap,
550 gamma,
551 scale_type: scale,
552 neg_mode,
553 bad_color,
554 meta,
555 hist_scale: hist_scale.as_ref(),
556 view,
557 mask,
558 scale_cache: Some(&scale_cache),
559 },
560 layout,
561 &mut sink,
562 debug_overlay,
563 );
564
565 if draw_border || show_graticule {
566 use cairo::{Context, Format, ImageSurface};
567
568 let pad = layout.border_width_px.ceil() as i32;
570 let surf_w = layout.map_w as i32 + 2 * pad;
571 let surf_h = layout.map_h as i32 + 2 * pad;
572
573 let mut border_surf = ImageSurface::create(Format::ARgb32, surf_w, surf_h).unwrap();
574
575 {
576 let border_cr = Context::new(&border_surf).unwrap();
577
578 border_cr.set_source_rgba(0.0, 0.0, 0.0, 0.0);
579 border_cr.paint().unwrap();
580
581 if show_graticule {
583 use crate::graticule::{
584 render_graticule_cairo, render_graticule_cairo_with_color,
585 render_graticule_hammer_vectorized, render_graticule_mollweide_vectorized,
586 };
587
588 let grat_coord_sys = grat_coord.unwrap_or(CoordSystem::E);
589
590 let graticule = match projection {
591 ProjectionType::Mollweide => render_graticule_mollweide_vectorized(
592 view,
593 dpar_deg,
594 dmer_deg,
595 grat_coord_sys,
596 view.input_coord,
597 ),
598 ProjectionType::Hammer => render_graticule_hammer_vectorized(
599 view,
600 dpar_deg,
601 dmer_deg,
602 grat_coord_sys,
603 view.input_coord,
604 ),
605 };
606
607 render_graticule_cairo(
609 &graticule,
610 &border_cr,
611 pad as f64,
612 pad as f64,
613 layout.map_w,
614 layout.map_h,
615 );
616
617 if let Some(overlay_sys) = grat_overlay {
619 let overlay_graticule = match projection {
620 ProjectionType::Mollweide => render_graticule_mollweide_vectorized(
621 view,
622 dpar_deg,
623 dmer_deg,
624 overlay_sys,
625 view.input_coord,
626 ),
627 ProjectionType::Hammer => render_graticule_hammer_vectorized(
628 view,
629 dpar_deg,
630 dmer_deg,
631 overlay_sys,
632 view.input_coord,
633 ),
634 };
635
636 let r = overlay_color[0] as f64 / 255.0;
638 let g = overlay_color[1] as f64 / 255.0;
639 let b = overlay_color[2] as f64 / 255.0;
640
641 render_graticule_cairo_with_color(
642 &overlay_graticule,
643 &border_cr,
644 crate::params::GeometryRect {
645 x: pad as f64,
646 y: pad as f64,
647 w: layout.map_w,
648 h: layout.map_h,
649 },
650 (r, g, b),
651 );
652 }
653 }
654
655 if draw_border {
656 draw_projection_border_pdf(
657 &border_cr,
658 pad as f64,
659 pad as f64,
660 layout.map_w,
661 layout.map_h,
662 layout.border_width_px,
663 );
664 }
665 }
667
668 border_surf.flush();
669
670 let stride = border_surf.stride() as usize;
671 let data = border_surf.data().unwrap();
672
673 for y in 0..surf_h {
674 for x in 0..surf_w {
675 let idx = (y as usize) * stride + (x as usize) * 4;
676 let a = data[idx + 3];
677 if a == 0 {
678 continue;
679 }
680
681 let r = data[idx + 2];
682 let g = data[idx + 1];
683 let b = data[idx];
684
685 let dst_x = layout.map_x as i32 + x - pad;
686 let dst_y = layout.map_y as i32 + y - pad;
687
688 if dst_x < 0 || dst_y < 0 {
689 continue;
690 }
691
692 let dst_x = dst_x as u32;
693 let dst_y = dst_y as u32;
694
695 if dst_x >= img.width() || dst_y >= img.height() {
696 continue;
697 }
698
699 let dst = img.get_pixel(dst_x, dst_y);
700 let src = Rgba([r, g, b, a]);
701
702 let alpha = a as f32 / 255.0;
703
704 let out = Rgba([
705 (src[0] as f32 + dst[0] as f32 * (1.0 - alpha)) as u8,
706 (src[1] as f32 + dst[1] as f32 * (1.0 - alpha)) as u8,
707 (src[2] as f32 + dst[2] as f32 * (1.0 - alpha)) as u8,
708 (a as f32 + dst[3] as f32 * (1.0 - alpha)) as u8,
709 ]);
710
711 img.put_pixel(dst_x, dst_y, out);
712 }
713 }
714 }
715
716 if show_colorbar {
717 let mut sink = PngSink {
718 img: &mut img,
719 x0: layout.cbar_pad as u32,
720 y0: layout.cbar_y as u32,
721 };
722
723 render_colorbar_gradient(
724 0,
725 0,
726 layout.cbar_w as u32,
727 layout.cbar_h as u32,
728 cmap,
729 gamma,
730 &mut sink,
731 );
732
733 let ticks = generate_colorbar_ticks(
734 scale_params.minv,
735 scale_params.maxv,
736 &scale,
737 hist_scale.as_ref(),
738 );
739
740 crate::colorbar::draw_colorbar_extends(
742 ¶ms.display.extend,
743 layout.cbar_pad,
744 layout.cbar_y,
745 layout.cbar_w,
746 layout.cbar_h,
747 cmap,
748 &mut img,
749 );
750
751 let major_tick_height = cb_layout.major_tick_height as u32;
753 let minor_tick_height = cb_layout.minor_tick_height as u32;
754
755 let major_tick_width = cb_layout.major_tick_width as u32;
757 let minor_tick_width = cb_layout.minor_tick_width as u32;
758
759 let tick_bottom = cb_layout.tick_bottom as u32;
760
761 let tick_top = tick_bottom.saturating_sub(major_tick_height);
763 for (&t, &val) in ticks.major_positions.iter().zip(ticks.major_values.iter()) {
764 let px = (layout.cbar_pad + (t * layout.cbar_w).round()) as u32;
765 for dx in 0..major_tick_width as i32 {
766 let x = (px as i32 + dx) as u32;
767 if x < layout.width as u32 {
768 for py in tick_top - 1..=tick_bottom {
769 img.put_pixel(x, py, Rgba([0, 0, 0, 255]));
770 }
771 }
772 }
773
774 let label =
776 format_tick_label_with_units(val, scale, Some(t), latex_rendering, units, false);
777 let font_scale = PxScale::from(cb_layout.tick_font_size as f32);
778
779 let mut text_width = 0.0;
781 for ch in label.chars() {
782 let glyph_id = font.glyph_id(ch);
783 text_width += font.h_advance_unscaled(glyph_id) * font_scale.x;
784 }
785 let text_x = px as i32 - (text_width / 2.0) as i32;
786
787 draw_text_mut(
788 &mut img,
789 Rgba([0, 0, 0, 255]),
790 text_x,
791 cb_layout.tick_label_pad as i32,
792 font_scale,
793 &font,
794 &label,
795 );
796 }
797
798 let tick_top = tick_bottom.saturating_sub(minor_tick_height);
800 for (&t, &_val) in ticks.minor_positions.iter().zip(ticks.minor_values.iter()) {
801 let px = (layout.cbar_pad + (t * layout.cbar_w).round()) as u32;
802
803 for dx in 0..minor_tick_width as i32 {
804 let x = (px as i32 + dx) as u32;
805 if x < width {
806 for py in tick_top - 1..=tick_bottom {
807 img.put_pixel(x, py, Rgba([0, 0, 0, 255]));
808 }
809 }
810 }
811 }
812 }
813
814 if show_graticule {
816 let grat_coord_sys = grat_coord.unwrap_or(CoordSystem::E);
818
819 let mut grid = RasterGrid {
821 width: layout.map_w as u32,
822 height: layout.map_h as u32,
823 buffer: vec![Rgba([0, 0, 0, 0]); (layout.map_w * layout.map_h) as usize],
824 valid: vec![true; (layout.map_w * layout.map_h) as usize],
825 };
826
827 for y in 0..layout.map_h as u32 {
829 for x in 0..layout.map_w as u32 {
830 let src_x = layout.map_x as u32 + x;
831 let src_y = layout.map_y as u32 + y;
832 if src_x < img.width() && src_y < img.height() {
833 let pixel = img.get_pixel(src_x, src_y);
834 let idx = (y * grid.width + x) as usize;
835 grid.buffer[idx] = *pixel;
836 }
837 }
838 }
839
840 crate::graticule::render_graticule_mollweide(
842 &mut grid,
843 view,
844 dpar_deg,
845 dmer_deg,
846 grat_coord_sys,
847 view.input_coord,
848 );
849
850 for y in 0..layout.map_h as u32 {
852 for x in 0..layout.map_w as u32 {
853 let dst_x = layout.map_x as u32 + x;
854 let dst_y = layout.map_y as u32 + y;
855 if dst_x < img.width() && dst_y < img.height() {
856 let idx = (y * grid.width + x) as usize;
857 img.put_pixel(dst_x, dst_y, grid.buffer[idx]);
858 }
859 }
860 }
861 }
862
863 if show_colorbar && let Some(units_str) = units {
865 let scale = layout.width / 1200.0;
866 let units_y = (cb_layout.tick_label_pad + 30.0 * scale) as i32;
867
868 if latex_rendering {
869 let latex_font_size =
871 (cb_layout.tick_font_size * 0.467).round().clamp(3.0, 24.0) as u32;
872 if let Some(rendered) =
874 crate::latex_render::render_latex_to_png(units_str, latex_font_size)
875 {
876 let latex_img = image::load_from_memory(&rendered.image_data)
878 .expect("Failed to load rendered LaTeX");
879 let latex_rgba = latex_img.to_rgba8();
880
881 let x_offset = (layout.cbar_pad + layout.cbar_w / 2.0
883 - latex_rgba.width() as f64 / 2.0) as i32;
884
885 for (lx, ly, pixel) in latex_rgba.enumerate_pixels() {
887 let img_x = x_offset + lx as i32;
888 let img_y = units_y + ly as i32;
889
890 if img_x >= 0
891 && img_x < layout.width as i32
892 && img_y >= 0
893 && img_y < layout.height as i32
894 {
895 let alpha = pixel[3] as f32 / 255.0;
896 if alpha > 0.01 {
897 let existing = img.get_pixel(img_x as u32, img_y as u32);
898 let blended = Rgba([
899 ((pixel[0] as f32 * alpha + existing[0] as f32 * (1.0 - alpha))
900 as u8),
901 ((pixel[1] as f32 * alpha + existing[1] as f32 * (1.0 - alpha))
902 as u8),
903 ((pixel[2] as f32 * alpha + existing[2] as f32 * (1.0 - alpha))
904 as u8),
905 255,
906 ]);
907 img.put_pixel(img_x as u32, img_y as u32, blended);
908 }
909 }
910 }
911 } else {
912 let units_label = units_str
914 .strip_prefix('$')
915 .unwrap_or(units_str)
916 .strip_suffix('$')
917 .unwrap_or(units_str);
918
919 let text_width_est =
920 (units_label.len() as f32 * cb_layout.tick_font_size as f32 * 0.6) as i32;
921 let center_x =
922 (layout.cbar_pad + layout.cbar_w / 2.0 - text_width_est as f64 / 2.0) as i32;
923
924 draw_text_mut(
925 &mut img,
926 Rgba([0, 0, 0, 255]),
927 center_x,
928 units_y,
929 PxScale::from(cb_layout.tick_font_size as f32),
930 &font,
931 units_label,
932 );
933 }
934 } else {
935 if let Some(units_label) = crate::colorbar::format_units_label(false, Some(units_str)) {
937 let text_width_est =
938 (units_label.len() as f32 * cb_layout.tick_font_size as f32 * 0.6) as i32;
939 let center_x =
940 (layout.cbar_pad + layout.cbar_w / 2.0 - text_width_est as f64 / 2.0) as i32;
941
942 draw_text_mut(
943 &mut img,
944 Rgba([0, 0, 0, 255]),
945 center_x,
946 units_y,
947 PxScale::from(cb_layout.tick_font_size as f32),
948 &font,
949 &units_label,
950 );
951 }
952 }
953 }
954
955 draw_figure_labels_png(
957 &mut img,
958 layout.width as u32,
959 layout.height as u32,
960 ¶ms.display.rlabel,
961 ¶ms.display.llabel,
962 latex_rendering,
963 params.display.label_font_size,
964 );
965
966 img.save(filename).expect("Failed to save PNG");
967}
968
969pub fn plot_mollweide_auto(params: MollweideParams) {
970 let ext = Path::new(params.plot.filename.as_str())
971 .extension()
972 .and_then(|s| s.to_str())
973 .unwrap_or("")
974 .to_ascii_lowercase();
975
976 match ext.as_str() {
977 "png" => plot_mollweide_png(params),
978 "pdf" => plot_mollweide_pdf(params),
979 _ => {
980 panic!(
981 "Unsupported output format: .{} (expected .png or .pdf)",
982 ext
983 );
984 }
985 }
986}