Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
49 changes: 39 additions & 10 deletions crates/pdf-graphics-web/src/surface_pool.rs
Original file line number Diff line number Diff line change
Expand Up @@ -101,31 +101,60 @@ impl SurfacePool {
Ok(BudgetedImage::new(image, reservation))
}

/// Returns the configured maximum number of accounted pixel bytes.
pub(crate) fn limit(&self) -> usize {
self.budget.limit()
}

/// Uploads validated straight RGBA pixels into a budgeted browser surface.
///
/// Reserves temporary storage before conversion and propagates image, budget,
/// and browser failures. The returned surface retains its own storage reservation.
pub(crate) fn upload(&self, image: &Image) -> WebResult<Surface> {
let [w, h] = image.dimensions().ok_or(Error::ResourceLimit)?;
let size = Size::new(w, h);
byte_size(size)?;
pdf_image::raster::validate_image(image)?;

// Account for the Rust conversion buffer and browser ImageData copy before
// allocating either, and retain that reservation until upload has completed.
let _scratch = self.scratch(
byte_size(size)?
.checked_mul(2)
.ok_or(Error::ResourceLimit)?,
)?;
// Account for the browser ImageData copy, and the Rust conversion buffer of
// non-RGBA pixels, before allocating either; retain it until upload completes.
let _scratch = self.scratch(staging_bytes(size, image.pixel_format)?)?;
let surface = self.acquire(size)?;
let data = pdf_image::raster::rgba(image)?;
let data = web_sys::ImageData::new_with_u8_clamped_array_and_sh(Clamped(&data), w, h)?;
let data = match image.pixel_format {
PixelFormat::RGBA8888 => web_sys::ImageData::new_with_u8_clamped_array_and_sh(
Clamped(image.data.as_ref()),
w,
h,
)?,
PixelFormat::Gray8 | PixelFormat::Alpha8 => {
let rgba = pdf_image::raster::rgba(image)?;
web_sys::ImageData::new_with_u8_clamped_array_and_sh(Clamped(&rgba), w, h)?
}
};
surface.context().put_image_data(&data, 0.0, 0.0)?;
Ok(surface)
}
}

/// Returns the accounted bytes `upload` holds at its peak for pixels of this size and
/// format: the destination surface and the staging copies made to fill it.
pub(crate) fn upload_bytes(size: Size<u32>, format: PixelFormat) -> WebResult<usize> {
byte_size(size)?
.checked_add(staging_bytes(size, format)?)
.ok_or(Error::ResourceLimit)
}

/// Returns the bytes of the browser ImageData copy, plus the RGBA conversion buffer
/// that non-RGBA pixels need before that copy.
fn staging_bytes(size: Size<u32>, format: PixelFormat) -> WebResult<usize> {
let bytes = byte_size(size)?;
match format {
PixelFormat::RGBA8888 => Ok(bytes),
PixelFormat::Gray8 | PixelFormat::Alpha8 => {
bytes.checked_mul(2).ok_or(Error::ResourceLimit)
}
}
}

/// Returns RGBA surface storage size, rejecting empty dimensions and byte-count overflow.
pub(crate) fn byte_size(size: Size<u32>) -> WebResult<usize> {
let Size {
Expand Down
96 changes: 93 additions & 3 deletions crates/pdf-graphics-web/src/web_image.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,11 +3,13 @@
use crate::{
error::{WebCanvasBackendError as Error, WebResult},
surface::Surface,
surface_pool::SurfacePool,
surface_pool::{SurfacePool, upload_bytes},
web_canvas_backend::{SavedContext, WebCanvasBackend},
web_paint,
};
use pdf_graphics::{BlendMode, Image, transform::Transform};
use num_traits::ToPrimitive;
use pdf_graphics::{BlendMode, Image, PixelFormat, size::Size, transform::Transform};
use pdf_image::ReduceImage;
use web_sys::CanvasRenderingContext2d;

/// Validated image placement for one target.
Expand All @@ -18,6 +20,7 @@ pub(crate) struct ImageDraw<'a> {
context: &'a CanvasRenderingContext2d,
pool: &'a SurfacePool,
image: &'a Image,
reduction: Reduction,
placement: ImagePlacement,
smoothing: bool,
composite: &'static str,
Expand All @@ -40,6 +43,7 @@ impl<'a> ImageDraw<'a> {
context: &backend.context,
pool: &backend.surfaces,
image,
reduction: Reduction::new(image, &unit_to_backing, &backend.surfaces)?,
placement: ImagePlacement::new(transform)?,
smoothing: !image.replicates_pixels(&unit_to_backing),
composite: web_paint::blend(mode.as_ref()),
Expand All @@ -48,7 +52,16 @@ impl<'a> ImageDraw<'a> {
}

pub(crate) fn draw(self) -> WebResult<()> {
let surface = self.pool.upload(self.image)?;
let surface = match self.reduction.factor {
1 => self.pool.upload(self.image)?,
factor => {
// The reduced copy stays charged until its pixels have been uploaded.
let _reduced = self
.pool
.scratch(self.reduction.reduced_bytes(self.image)?)?;
self.pool.upload(&self.image.reduced(factor)?)?
}
};
let _saved = SavedContext::new(self.context);
self.context.set_global_alpha(f64::from(self.alpha));
self.context
Expand All @@ -58,6 +71,83 @@ impl<'a> ImageDraw<'a> {
}
}

/// The whole factor an image is box-reduced by before upload.
///
/// Uploading more pixels than the backing pixels an image covers only costs
/// storage, and Canvas downsamples without averaging, so a reduced upload is
/// both smaller and smoother. The factor never reduces a side below the backing
/// pixels it spans, unless that is needed for the upload to fit the budget.
#[derive(Clone, Copy)]
struct Reduction {
factor: usize,
}

impl Reduction {
fn new(image: &Image, unit_to_backing: &Transform, pool: &SurfacePool) -> WebResult<Self> {
let width = f64::from(unit_to_backing.sx).hypot(f64::from(unit_to_backing.ky));
let height = f64::from(unit_to_backing.kx).hypot(f64::from(unit_to_backing.sy));
let mut reduction = Self {
factor: covering_factor(image.width, width).min(covering_factor(image.height, height)),
};
let longest = image.width.max(image.height);
while reduction.peak_bytes(image)? > pool.limit() {
if reduction.factor >= longest {
return Err(Error::ResourceLimit);
}
reduction.factor = reduction
.factor
.checked_add(1)
.ok_or(Error::ResourceLimit)?;
}
Ok(reduction)
}

/// Returns the dimensions of the image once reduced.
fn size(&self, image: &Image) -> WebResult<Size<u32>> {
let side = |pixels: usize| {
u32::try_from(pixels.div_ceil(self.factor)).map_err(|_| Error::ResourceLimit)
};
Ok(Size::new(side(image.width)?, side(image.height)?))
}

/// Returns the bytes of the reduced copy, in the image's own pixel format.
fn reduced_bytes(&self, image: &Image) -> WebResult<usize> {
let Size { width, height } = self.size(image)?;
let bytes_per_pixel = match image.pixel_format {
PixelFormat::RGBA8888 => 4,
PixelFormat::Gray8 | PixelFormat::Alpha8 => 1,
};
usize::try_from(width)
.ok()
.zip(usize::try_from(height).ok())
.and_then(|(width, height)| width.checked_mul(height))
.and_then(|pixels| pixels.checked_mul(bytes_per_pixel))
.ok_or(Error::ResourceLimit)
}

/// Returns the accounted bytes drawing holds at its peak: the upload, and the
/// reduced copy it is made from.
fn peak_bytes(&self, image: &Image) -> WebResult<usize> {
let upload = upload_bytes(self.size(image)?, image.pixel_format)?;
match self.factor {
1 => Ok(upload),
_ => upload
.checked_add(self.reduced_bytes(image)?)
.ok_or(Error::ResourceLimit),
}
}
}

/// Returns the largest whole factor that leaves `pixels` source pixels at least as
/// many as the `extent` backing pixels they span, and at least one.
fn covering_factor(pixels: usize, extent: f64) -> usize {
pixels
.to_f64()
.and_then(|pixels| (pixels / extent.max(1.0)).floor().to_usize())
.unwrap_or(1)
.max(1)
}

/// A finite mapping from the image's unit square to logical device space.
struct ImagePlacement {
transform: Transform,
Expand Down
2 changes: 1 addition & 1 deletion crates/pdf-image/src/image_decoder.rs
Original file line number Diff line number Diff line change
Expand Up @@ -74,7 +74,7 @@ fn decode_reduced_image(
return Ok(render_ready_image(reduced, None, &bands.reduced_metadata()));
}
let decoded_samples = DecodedSamples::decode(payload, metadata)?;
render_ready_image(decoded_samples, None, metadata).reduced(factor)
Ok(render_ready_image(decoded_samples, None, metadata).reduced(factor)?)
}

/// Builds a render image from decoded samples whose soft mask, if any, has the image's size.
Expand Down
1 change: 1 addition & 0 deletions crates/pdf-image/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -10,3 +10,4 @@ mod smask_in_data;

pub use error::{ImageRasterError, PdfImageError};
pub use pdf_image::{InlineDataLength, InlineImageHeader, PdfImage};
pub use reduced_image::ReduceImage;
16 changes: 8 additions & 8 deletions crates/pdf-image/src/reduced_image.rs
Original file line number Diff line number Diff line change
Expand Up @@ -118,31 +118,31 @@ impl BoxReduction {
}

/// Allocates the buffer for `height` reduced rows.
fn buffer(&self, height: usize) -> Result<Vec<u8>, PdfImageError> {
fn buffer(&self, height: usize) -> Result<Vec<u8>, ImageRasterError> {
let byte_len = self
.output_width()
.checked_mul(height)
.and_then(|pixels| pixels.checked_mul(self.layout.bytes_per_pixel()))
.ok_or(PdfImageError::Raster(ImageRasterError::ResourceLimit))?;
.ok_or(ImageRasterError::ResourceLimit)?;
let mut data = Vec::new();
data.try_reserve_exact(byte_len)
.map_err(|_| PdfImageError::Raster(ImageRasterError::ResourceLimit))?;
data.try_reserve_exact(byte_len)?;
Ok(data)
}
}

/// Reduces a whole render-ready image.
pub(crate) trait ReduceImage {
/// Returns the image reduced by `factor`.
pub trait ReduceImage {
/// Returns the image reduced by `factor`, each output pixel averaging the
/// `factor` × `factor` block of pixels it covers.
///
/// # Errors
///
/// Returns a resource-limit error if the reduced pixels cannot be allocated.
fn reduced(&self, factor: usize) -> Result<Image, PdfImageError>;
fn reduced(&self, factor: usize) -> Result<Image, ImageRasterError>;
}

impl ReduceImage for Image {
fn reduced(&self, factor: usize) -> Result<Image, PdfImageError> {
fn reduced(&self, factor: usize) -> Result<Image, ImageRasterError> {
let reduction = BoxReduction {
factor,
source_width: self.width,
Expand Down
Loading