mirror of
https://github.com/storytold/photocraft.git
synced 2026-11-02 22:30:20 -06:00
This commit is contained in:
+19
-11
@@ -76,8 +76,13 @@ pub struct Coverage {
|
||||
}
|
||||
|
||||
impl Coverage {
|
||||
pub fn new(width: usize, height: usize) -> Self {
|
||||
Self { width, height, acc: vec![0.0; width * height + 2] }
|
||||
/// A zeroed accumulator, or `None` when its bytes cannot be reserved.
|
||||
pub fn try_new(width: usize, height: usize) -> Option<Self> {
|
||||
let n = width.checked_mul(height)?.checked_add(2)?;
|
||||
let mut acc = Vec::new();
|
||||
acc.try_reserve_exact(n).ok()?;
|
||||
acc.resize(n, 0.0);
|
||||
Some(Self { width, height, acc })
|
||||
}
|
||||
|
||||
/// Adds a line segment (pixel coordinates, y down).
|
||||
@@ -148,15 +153,18 @@ impl Coverage {
|
||||
}
|
||||
|
||||
/// Resolves the accumulation into per-pixel coverage in 0..=1.
|
||||
pub fn finish(self) -> Vec<f32> {
|
||||
/// The accumulated coverage, or `None` when the output cannot be reserved. The input
|
||||
/// accumulator is still live while this builds, so both count toward a caller's budget.
|
||||
pub fn finish(self) -> Option<Vec<f32>> {
|
||||
let n = self.width * self.height;
|
||||
let mut out = Vec::with_capacity(n);
|
||||
let mut out = Vec::new();
|
||||
out.try_reserve_exact(n).ok()?;
|
||||
let mut sum = 0.0f32;
|
||||
for v in &self.acc[..n] {
|
||||
sum += v;
|
||||
out.push(sum.abs().min(1.0));
|
||||
}
|
||||
out
|
||||
Some(out)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -240,9 +248,9 @@ mod tests {
|
||||
|
||||
#[test]
|
||||
fn square_area_exact() {
|
||||
let mut c = Coverage::new(10, 10);
|
||||
let mut c = Coverage::try_new(10, 10).unwrap();
|
||||
rect_to(&mut c, &Xform::IDENTITY, 2.25, 3.5, 6.75, 8.0);
|
||||
let v = c.finish();
|
||||
let v = c.finish().unwrap();
|
||||
assert!((sum(&v) - 4.5 * 4.5).abs() < 1e-3, "{}", sum(&v));
|
||||
assert_eq!(v[5 * 10 + 4], 1.0);
|
||||
assert_eq!(v[0], 0.0);
|
||||
@@ -250,13 +258,13 @@ mod tests {
|
||||
|
||||
#[test]
|
||||
fn triangle_area_and_clipping() {
|
||||
let mut c = Coverage::new(8, 8);
|
||||
let mut c = Coverage::try_new(8, 8).unwrap();
|
||||
// Right triangle partly outside on the left and bottom.
|
||||
let pts = [(-4.0, 2.0), (6.0, 2.0), (-4.0, 12.0)];
|
||||
for i in 0..3 {
|
||||
c.line(pts[i], pts[(i + 1) % 3]);
|
||||
}
|
||||
let v = c.finish();
|
||||
let v = c.finish().unwrap();
|
||||
// Visible part: region x in [0,6], y in [2,8] under the hypotenuse x + y <= 8.
|
||||
// Area = ∫_{y=2}^{8} (8 - y) dy = 18.
|
||||
assert!((sum(&v) - 18.0).abs() < 1e-3, "{}", sum(&v));
|
||||
@@ -266,7 +274,7 @@ mod tests {
|
||||
#[test]
|
||||
fn circle_area_via_pen() {
|
||||
use skrifa::outline::OutlinePen;
|
||||
let mut c = Coverage::new(40, 40);
|
||||
let mut c = Coverage::try_new(40, 40).unwrap();
|
||||
{
|
||||
let mut p = Pen::new(&mut c, Xform([1.0, 0.0, 0.0, -1.0, 20.0, 20.0]));
|
||||
// Cubic circle approximation, r = 15.
|
||||
@@ -278,7 +286,7 @@ mod tests {
|
||||
p.curve_to(k, -r, r, -k, r, 0.0);
|
||||
p.close();
|
||||
}
|
||||
let a = sum(&c.finish());
|
||||
let a = sum(&c.finish().unwrap());
|
||||
let exact = std::f32::consts::PI * 225.0;
|
||||
assert!((a - exact).abs() / exact < 0.005, "{a} vs {exact}");
|
||||
}
|
||||
|
||||
@@ -21,6 +21,40 @@ pub const FAUX_ITALIC_DEG: f32 = 12.0;
|
||||
pub const FAUX_BOLD_RADIUS: f32 = 0.036;
|
||||
/// Largest raster we produce (pixels), as a guard against absurd sizes.
|
||||
const MAX_PIXELS: u64 = 256 * 1024 * 1024;
|
||||
/// Peak working bytes one rasterization may keep live. [`MAX_PIXELS`] does not bound them: the
|
||||
/// accumulator and the output surface are both `stride` deep, and a faux-bold pass keeps one more
|
||||
/// coverage buffer live, so a CMYK bold render reserves several times what a plain grey one does
|
||||
/// for the same pixels.
|
||||
const MAX_RASTER_BYTES: u64 = if cfg!(target_pointer_width = "64") { 4 << 30 } else { 1 << 30 };
|
||||
|
||||
/// Whether a `w` x `h` render's peak working bytes fit `max_bytes`: the f32 accumulator and the
|
||||
/// output surface (both `stride` deep), plus the coverage buffers live at the peak. A plain pass
|
||||
/// peaks inside `Coverage::finish`, which holds its accumulator and the vector it builds beside
|
||||
/// it; a faux-bold pass peaks one buffer higher, since the resolved plain coverage is kept while
|
||||
/// the bold pass resolves into it. `false` when the arithmetic overflows, so an absurd size is
|
||||
/// refused rather than wrapping.
|
||||
fn fits_raster_budget(w: usize, h: usize, stride: usize, sample: SampleType, bold: bool, max_bytes: u64) -> bool {
|
||||
let f32s = size_of::<f32>() as u64;
|
||||
let peak = (w as u64).checked_mul(h as u64).and_then(|px| {
|
||||
let deep = px.checked_mul(stride as u64)?.checked_mul(f32s + sample.bytes() as u64)?;
|
||||
let coverage = px.checked_mul(if bold { 3 } else { 2 })?.checked_mul(f32s)?;
|
||||
deep.checked_add(coverage)
|
||||
});
|
||||
peak.is_some_and(|b| b <= max_bytes)
|
||||
}
|
||||
|
||||
/// A zeroed buffer, or `None` when its bytes cannot be reserved.
|
||||
fn zeroed(len: usize) -> Option<Vec<f32>> {
|
||||
let mut v = Vec::new();
|
||||
v.try_reserve_exact(len).ok()?;
|
||||
v.resize(len, 0.0);
|
||||
Some(v)
|
||||
}
|
||||
|
||||
/// What a refused or unrenderable layout returns.
|
||||
fn nothing(format: PixelFormat) -> Rendered {
|
||||
Rendered { surface: Surface::new(format), rect: Rect::new(0, 0, 0, 0) }
|
||||
}
|
||||
|
||||
/// Maps raw area-coverage to the opacity Photoshop's anti-alias modes imply. Exponents above 1 thin
|
||||
/// the fringe (sharper); below 1 widen it (stronger smoothing).
|
||||
@@ -277,8 +311,14 @@ pub fn rasterize_warped(layout: &TextLayout, transform: &Affine, format: PixelFo
|
||||
}
|
||||
let n = format.mode.color_channels();
|
||||
let stride = n + 1;
|
||||
let bold = layout.glyphs.iter().any(|g| is_bold(layout, g));
|
||||
if !fits_raster_budget(w, h, stride, format.sample, bold, MAX_RASTER_BYTES) {
|
||||
return nothing(format);
|
||||
}
|
||||
// Premultiplied accumulation.
|
||||
let mut acc = vec![0.0f32; w * h * stride];
|
||||
let Some(mut acc) = zeroed(w * h * stride) else {
|
||||
return nothing(format);
|
||||
};
|
||||
let xf = Xform([1.0, 0.0, 0.0, 1.0, -rect.x0 as f64, -rect.y0 as f64]).mul(&Xform(transform.m));
|
||||
let mut colors: Vec<Color> = Vec::new();
|
||||
for st in &layout.styles {
|
||||
@@ -287,14 +327,23 @@ pub fn rasterize_warped(layout: &TextLayout, transform: &Affine, format: PixelFo
|
||||
}
|
||||
}
|
||||
for c in &colors {
|
||||
let mut cov = Coverage::new(w, h);
|
||||
let Some(mut cov) = Coverage::try_new(w, h) else {
|
||||
return nothing(format);
|
||||
};
|
||||
draw_pass(layout, &xf, &mut cov, Some(c), warp, Pass::Plain);
|
||||
let mut cov = cov.finish();
|
||||
if layout.glyphs.iter().any(|g| is_bold(layout, g)) {
|
||||
let Some(mut cov) = cov.finish() else {
|
||||
return nothing(format);
|
||||
};
|
||||
if bold {
|
||||
for k in 0..BOLD_OFFSETS.len() {
|
||||
let mut bc = Coverage::new(w, h);
|
||||
let Some(mut bc) = Coverage::try_new(w, h) else {
|
||||
return nothing(format);
|
||||
};
|
||||
draw_pass(layout, &xf, &mut bc, Some(c), warp, Pass::Bold(k));
|
||||
for (a, b) in cov.iter_mut().zip(bc.finish()) {
|
||||
let Some(bold_cov) = bc.finish() else {
|
||||
return nothing(format);
|
||||
};
|
||||
for (a, b) in cov.iter_mut().zip(bold_cov) {
|
||||
*a = a.max(b);
|
||||
}
|
||||
}
|
||||
@@ -445,7 +494,8 @@ pub fn outlines(layout: &TextLayout, transform: &Affine, warp: Option<&Warp>) ->
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::{MAX_PIXELS, rect_from_bounds};
|
||||
use super::{MAX_PIXELS, fits_raster_budget, rect_from_bounds, zeroed};
|
||||
use photocraft_color::SampleType;
|
||||
use photocraft_geom::Rect;
|
||||
|
||||
#[test]
|
||||
@@ -466,4 +516,25 @@ mod tests {
|
||||
assert_eq!(rect_from_bounds(bounds), first, "bounds: {bounds:?}");
|
||||
}
|
||||
}
|
||||
|
||||
/// #4979: the pixel cap says nothing about bytes, so the peak is admitted from the channel
|
||||
/// depth of the accumulator and surface, the sample size and the faux-bold coverage buffers.
|
||||
/// A 100 x 100 grey+alpha render at 8-bit keeps 2 x (4 + 1) + 2 x 4 = 18 bytes a pixel live.
|
||||
#[test]
|
||||
fn the_byte_budget_follows_channels_depth_and_faux_bold() {
|
||||
let px = 100 * 100;
|
||||
assert!(fits_raster_budget(100, 100, 2, SampleType::U8, false, px * 18));
|
||||
assert!(!fits_raster_budget(100, 100, 2, SampleType::U8, false, px * 18 - 1), "the allowance is exact");
|
||||
|
||||
// The same pixels cost more with more channels, a wider sample, or a bold pass.
|
||||
assert!(!fits_raster_budget(100, 100, 5, SampleType::U8, false, px * 18), "CMYK is deeper");
|
||||
assert!(!fits_raster_budget(100, 100, 2, SampleType::F32, false, px * 18), "f32 output is wider");
|
||||
assert!(!fits_raster_budget(100, 100, 2, SampleType::U8, true, px * 18), "bold holds one more buffer");
|
||||
assert!(fits_raster_budget(100, 100, 2, SampleType::U8, true, px * 22), "2 x 5 + 3 x 4 bytes a pixel");
|
||||
|
||||
// A size whose peak overflows is refused rather than wrapping to a small number.
|
||||
assert!(!fits_raster_budget(usize::MAX, usize::MAX, 5, SampleType::F32, true, u64::MAX));
|
||||
// Nothing is reserved for a buffer that cannot be served.
|
||||
assert!(zeroed(usize::MAX).is_none());
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user