use excalidraw_render::{
DiagnosticDetails, ErrorCode, ErrorDescriptor, Measurement, OutputFormat, ResourceValue, Sizing,
};
#[derive(Clone, Copy, Debug, PartialEq)]
pub(crate) struct PixelLimit {
needed: Option<u64>,
limit: u64,
}
#[derive(Clone, Copy, Debug, PartialEq)]
pub(crate) struct SceneSize {
pub(crate) width: f64,
pub(crate) height: f64,
pub(crate) sizing: Sizing,
}
impl PixelLimit {
pub(crate) fn of(error: &ErrorDescriptor) -> Option<Self> {
if error.code() != ErrorCode::RESOURCE_RASTER_SURFACE_EXCEEDED
|| error.format() != Some(OutputFormat::Png)
|| error.element_id().is_some()
{
return None;
}
let Some(DiagnosticDetails::ResourceLimit {
field: "max_raster_surface_pixels",
configured: ResourceValue::Integer(limit),
measurement,
..
}) = error.details()
else {
return None;
};
let needed = match measurement {
Measurement::Attempted {
value: ResourceValue::Integer(needed),
} => Some(*needed),
Measurement::AtLeast { .. } => None,
_ => return None,
};
Some(Self {
needed,
limit: *limit,
})
}
fn confirmed(self, scene: Option<SceneSize>) -> Option<SceneSize> {
let scene = scene?;
let pixels = canvas(scene, scene.sizing).map(|(width, height)| width * height);
match self.needed {
Some(needed) => (pixels == Some(needed)).then_some(scene),
None => pixels
.is_none_or(|pixels| pixels > self.limit)
.then_some(scene),
}
}
pub(crate) fn message(self, scene: Option<SceneSize>) -> String {
let Some(needed) = self.needed else {
let (_, limit) = megapixels(self.limit, self.limit);
return format!("the PNG would be over the {limit}-megapixel limit");
};
let (needed, limit) = megapixels(needed, self.limit);
let size = match self
.confirmed(scene)
.and_then(|scene| canvas(scene, scene.sizing))
{
Some((width, height)) => format!("{width} × {height} pixels ({needed} megapixels)"),
None => format!("{needed} megapixels"),
};
format!("the PNG would be {size}, over the {limit}-megapixel limit")
}
pub(crate) fn hint(self, scene: Option<SceneSize>) -> String {
const ALTERNATIVE: &str = "or render to SVG or PDF instead";
let Some(scene) = self.confirmed(scene) else {
return format!(
"use --max-dimension {} or lower, {ALTERNATIVE}",
square_side(self.limit)
);
};
if let Some(maximum) = largest_max_dimension(scene, self.limit) {
format!("use --max-dimension {maximum}, the largest size that fits, {ALTERNATIVE}")
} else if let Some(hundredths) = largest_scale(scene, self.limit) {
format!(
"use --scale {}, the largest scale that fits, {ALTERNATIVE}",
decimal_hundredths(hundredths)
)
} else {
format!(
"use --max-dimension {} or lower, {ALTERNATIVE}",
square_side(self.limit)
)
}
}
}
fn canvas(scene: SceneSize, sizing: Sizing) -> Option<(u64, u64)> {
let largest = scene.width.max(scene.height);
let scale = match sizing {
Sizing::Scale(scale) => scale,
Sizing::MaxDimension(maximum) if maximum < largest => maximum / largest,
Sizing::MaxDimension(_) => 1.0,
_ => return None,
};
let side = |logical: f64| {
let value = logical * scale;
(value.is_finite() && value < f64::from(u32::MAX) + 1.0).then(|| value.trunc() as u64)
};
Some((side(scene.width)?, side(scene.height)?))
}
fn within(scene: SceneSize, sizing: Sizing, limit: u64) -> bool {
canvas(scene, sizing).is_some_and(|(width, height)| width * height <= limit)
}
fn fits(scene: SceneSize, sizing: Sizing, limit: u64) -> bool {
within(scene, sizing, limit)
&& canvas(scene, sizing).is_some_and(|(width, height)| width >= 1 && height >= 1)
}
fn largest_max_dimension(scene: SceneSize, limit: u64) -> Option<u64> {
if within(scene, Sizing::Scale(1.0), limit) {
return None;
}
let within_at = |maximum: u64| within(scene, Sizing::MaxDimension(maximum as f64), limit);
let (mut low, mut high) = (0_u64, scene.width.max(scene.height).ceil() as u64);
while high - low > 1 {
let middle = low + (high - low) / 2;
if within_at(middle) {
low = middle;
} else {
high = middle;
}
}
fits(scene, Sizing::MaxDimension(low as f64), limit).then_some(low)
}
fn largest_scale(scene: SceneSize, limit: u64) -> Option<u64> {
let estimate = (limit as f64 / (scene.width * scene.height)).sqrt() * 100.0;
let mut hundredths = estimate.floor() as u64 + 1;
while hundredths > 0 && !fits(scene, Sizing::Scale(hundredths as f64 / 100.0), limit) {
hundredths -= 1;
}
(hundredths > 0).then_some(hundredths)
}
fn square_side(limit: u64) -> u64 {
let mut side = (limit as f64).sqrt() as u64;
while side * side > limit {
side -= 1;
}
while (side + 1) * (side + 1) <= limit {
side += 1;
}
side
}
fn decimal_hundredths(hundredths: u64) -> String {
let (whole, fraction) = (hundredths / 100, hundredths % 100);
match fraction {
0 => whole.to_string(),
fraction if fraction % 10 == 0 => format!("{whole}.{}", fraction / 10),
fraction => format!("{whole}.{fraction:02}"),
}
}
fn megapixels(value: u64, other: u64) -> (String, String) {
let format = |pixels: u64, decimals: usize| format!("{:.decimals$}", pixels as f64 / 1e6);
(1..=6)
.map(|decimals| (format(value, decimals), format(other, decimals)))
.find(|(value, other)| value != other)
.unwrap_or_else(|| (format(value, 1), format(other, 1)))
}
pub(crate) fn svg_logical_size(svg: &[u8]) -> Option<(f64, f64)> {
let head = std::str::from_utf8(&svg[..svg.len().min(512)]).ok()?;
let view_box = head.split_once("viewBox=\"0 0 ")?.1.split_once('"')?.0;
let (width, height) = view_box.split_once(' ')?;
Some((width.parse().ok()?, height.parse().ok()?))
}
#[cfg(test)]
mod tests {
use super::{
canvas, decimal_hundredths, fits, largest_max_dimension, largest_scale, megapixels,
square_side, svg_logical_size, PixelLimit, SceneSize,
};
use excalidraw_render::Sizing;
const LIMIT: u64 = 33_554_432;
fn scene(width: f64, height: f64, sizing: Sizing) -> SceneSize {
SceneSize {
width,
height,
sizing,
}
}
#[test]
fn the_canvas_follows_the_renderer_sizing_rule() {
let box_scene = scene(40.0, 30.0, Sizing::Scale(1.0));
for (sizing, expected) in [
(Sizing::Scale(1.0), (40, 30)),
(Sizing::Scale(1.5), (60, 45)),
(Sizing::MaxDimension(20.0), (20, 15)),
(Sizing::MaxDimension(100.0), (40, 30)),
] {
assert_eq!(canvas(box_scene, sizing), Some(expected), "{sizing:?}");
}
assert_eq!(canvas(box_scene, Sizing::Scale(1e300)), None);
}
#[test]
fn the_largest_max_dimension_fits_and_one_more_does_not() {
for (width, height) in [
(6524.45062991657, 5993.189064640477),
(7020.0, 5020.0),
(5020.0, 7020.0),
(100_000.0, 1000.0),
(8000.0, 8000.0),
(5793.0, 5793.0),
] {
let scene = scene(width, height, Sizing::Scale(1.0));
let maximum = largest_max_dimension(scene, LIMIT).expect("a maximum fits");
assert!(fits(scene, Sizing::MaxDimension(maximum as f64), LIMIT));
assert!(!fits(
scene,
Sizing::MaxDimension(maximum as f64 + 1.0),
LIMIT
));
}
assert_eq!(
largest_max_dimension(
scene(6524.45062991657, 5993.189064640477, Sizing::Scale(1.0)),
LIMIT
),
Some(6044)
);
assert_eq!(
largest_max_dimension(scene(4020.0, 3020.0, Sizing::Scale(2.0)), LIMIT),
None
);
}
#[test]
fn the_largest_scale_is_a_whole_hundredth_that_fits() {
let scene = scene(4020.0, 3020.0, Sizing::Scale(2.0));
assert_eq!(largest_scale(scene, LIMIT), Some(166));
assert!(fits(scene, Sizing::Scale(1.66), LIMIT));
assert!(!fits(scene, Sizing::Scale(1.67), LIMIT));
}
#[test]
fn a_square_side_bounds_every_aspect_ratio() {
assert_eq!(square_side(LIMIT), 5792);
assert_eq!(square_side(16), 4);
assert_eq!(square_side(15), 3);
}
#[test]
fn megapixels_use_the_decimals_that_tell_them_apart() {
assert_eq!(
megapixels(39_098_332, LIMIT),
("39.1".to_owned(), "33.6".to_owned())
);
assert_eq!(
megapixels(33_563_049, LIMIT),
("33.56".to_owned(), "33.55".to_owned())
);
assert_eq!(
megapixels(33_555_000, LIMIT),
("33.555".to_owned(), "33.554".to_owned())
);
}
#[test]
fn scales_print_as_short_decimals() {
assert_eq!(decimal_hundredths(166), "1.66");
assert_eq!(decimal_hundredths(150), "1.5");
assert_eq!(decimal_hundredths(200), "2");
assert_eq!(decimal_hundredths(5), "0.05");
}
#[test]
fn the_logical_size_is_read_from_the_svg_view_box() {
assert_eq!(
svg_logical_size(
br#"<svg version="1.1" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 6524.45062991657 5993.189064640477" width="13048.9" height="11986.3">"#
),
Some((6524.45062991657, 5993.189064640477))
);
assert_eq!(svg_logical_size(b"<svg>"), None);
}
#[test]
fn a_size_that_does_not_account_for_the_error_is_not_used() {
let limit = PixelLimit {
needed: Some(39_098_332),
limit: LIMIT,
};
let varmq = scene(6524.45062991657, 5993.189064640477, Sizing::Scale(1.0));
assert_eq!(
limit.message(Some(varmq)),
"the PNG would be 6524 × 5993 pixels (39.1 megapixels), over the 33.6-megapixel limit"
);
assert_eq!(
limit.hint(Some(varmq)),
"use --max-dimension 6044, the largest size that fits, or render to SVG or PDF instead"
);
let other = scene(7020.0, 5020.0, Sizing::Scale(1.0));
assert_eq!(
limit.message(Some(other)),
"the PNG would be 39.1 megapixels, over the 33.6-megapixel limit"
);
assert_eq!(
limit.hint(Some(other)),
"use --max-dimension 5792 or lower, or render to SVG or PDF instead"
);
let unknown = PixelLimit {
needed: None,
limit: LIMIT,
};
assert_eq!(
unknown.message(None),
"the PNG would be over the 33.6-megapixel limit"
);
}
}