pub struct Pixmap { /* private fields */ }Expand description
A premultiplied RGBA8 image: four bytes per pixel, row-major, no padding.
Premultiplied means each colour byte has already been scaled by the alpha
byte, which is what both rasterizers produce and what makes a source-over
blit a plain weighted sum. Pixmap::to_straight_bgra undoes it at the
output boundary, because the oracle’s PNGs and MD5s are taken over a
straight-alpha BGRA buffer.
use pdfrum_render::Pixmap;
let red = Pixmap::filled(2, 2, peniko::Color::from_rgba8(255, 0, 0, 255));
assert_eq!((red.width(), red.height()), (2, 2));
// Premultiplied on the inside; straight at the output boundary.
assert_eq!(red.pixel(0, 0), Some([255, 0, 0, 255]));
assert_eq!(&red.to_straight_rgb()[..3], &[255, 0, 0]);Implementations§
Source§impl Pixmap
impl Pixmap
Sourcepub fn new(width: u32, height: u32) -> Self
pub fn new(width: u32, height: u32) -> Self
A fully transparent pixmap of the given size.
A zero in either axis is legal and yields an empty buffer, because the engine reaches this with clipped-away geometry all the time.
A size whose buffer cannot be allocated yields an empty pixmap
rather than ending the process — see Pixmap::try_new, which is this
without the fallback. Check Pixmap::width rather than assuming the
size you asked for.
use pdfrum_render::Pixmap;
let empty = Pixmap::new(2, 2);
assert_eq!(empty.pixel(0, 0), Some([0, 0, 0, 0]));
// A zero in either axis is legal: clipped-away geometry reaches this.
assert!(Pixmap::new(0, 5).data().is_empty());
// So is a size no allocator can meet; it comes back empty.
assert_eq!(Pixmap::new(131_071, 131_071).width(), 0);Sourcepub fn try_new(width: u32, height: u32) -> Option<Self>
pub fn try_new(width: u32, height: u32) -> Option<Self>
A fully transparent pixmap of the given size, or None when the buffer
it needs cannot be allocated.
width * height * 4 is the whole of the request, and both axes can
come from a file: an image declares its own /Width and /Height, and
the largest pair the sample unpacker accepts asks for 68 GB. Two
refusals, in order: the area cap first
(pdfrum_page::image_area_is_workable), because Linux overcommit
lets try_reserve_exact succeed for tens of gigabytes and the
resize below then zeros them; then the fallible reserve, for a size
inside the cap that this allocator still cannot meet. A Vec that
cannot be grown calls handle_alloc_error, which aborts — no
unwind, so no catch_unwind above it helps and no Result can carry
it.
Pixmap::new is this with the answer taken as an empty pixmap, which
is what a caller that has no way to report the failure wants.
use pdfrum_render::Pixmap;
assert!(Pixmap::try_new(2, 2).is_some());
// Nothing has this much memory, and asking for it must not end the
// process.
assert!(Pixmap::try_new(131_071, 131_071).is_none());Sourcepub fn filled(width: u32, height: u32, color: Color) -> Self
pub fn filled(width: u32, height: u32, color: Color) -> Self
A pixmap of the given size, every pixel set to color.
use pdfrum_render::Pixmap;
let red = Pixmap::filled(2, 1, peniko::Color::from_rgba8(255, 0, 0, 255));
assert_eq!(red.data(), &[255, 0, 0, 255, 255, 0, 0, 255]);Sourcepub fn from_vec(width: u32, height: u32, data: Vec<u8>) -> Option<Self>
pub fn from_vec(width: u32, height: u32, data: Vec<u8>) -> Option<Self>
Wrap an existing premultiplied RGBA8 buffer.
Returns None when data is not exactly width * height * 4 bytes.
use pdfrum_render::Pixmap;
assert!(Pixmap::from_vec(1, 1, vec![255, 0, 0, 255]).is_some());
// Not exactly `width * height * 4` bytes.
assert!(Pixmap::from_vec(1, 1, vec![255, 0, 0]).is_none());Sourcepub fn width(&self) -> u32
pub fn width(&self) -> u32
The pixmap’s width in pixels.
use pdfrum_render::Pixmap;
assert_eq!(Pixmap::new(3, 2).width(), 3);Sourcepub fn height(&self) -> u32
pub fn height(&self) -> u32
The pixmap’s height in pixels.
use pdfrum_render::Pixmap;
assert_eq!(Pixmap::new(3, 2).height(), 2);Sourcepub fn data(&self) -> &[u8] ⓘ
pub fn data(&self) -> &[u8] ⓘ
The premultiplied RGBA8 bytes, row-major.
use pdfrum_render::Pixmap;
// Four bytes per pixel, row-major, no padding.
assert_eq!(Pixmap::new(2, 2).data().len(), 16);Sourcepub fn data_mut(&mut self) -> &mut [u8] ⓘ
pub fn data_mut(&mut self) -> &mut [u8] ⓘ
The premultiplied RGBA8 bytes, mutably.
use pdfrum_render::Pixmap;
let mut p = Pixmap::new(1, 1);
p.data_mut().copy_from_slice(&[255, 0, 0, 255]);
assert_eq!(p.pixel(0, 0), Some([255, 0, 0, 255]));Sourcepub fn into_data(self) -> Vec<u8> ⓘ
pub fn into_data(self) -> Vec<u8> ⓘ
Consume the pixmap, yielding its premultiplied RGBA8 bytes.
use pdfrum_render::Pixmap;
assert_eq!(Pixmap::filled(1, 1, peniko::Color::from_rgba8(255, 0, 0, 255)).into_data(), vec![255, 0, 0, 255]);Sourcepub fn cropped(&self, x: u32, y: u32, width: u32, height: u32) -> Self
pub fn cropped(&self, x: u32, y: u32, width: u32, height: u32) -> Self
A sub-rectangle of this pixmap.
Bytes outside this pixmap are left transparent. Used to lift a non-isolated group’s backdrop out of a parent snapshot without a second GPU round trip’s worth of host pixels.
use pdfrum_render::Pixmap;
let mut src = Pixmap::new(4, 2);
src.set_pixel(1, 0, [1, 2, 3, 255]);
let out = src.cropped(1, 0, 2, 1);
assert_eq!(out.pixel(0, 0), Some([1, 2, 3, 255]));
assert_eq!(out.pixel(1, 0), Some([0, 0, 0, 0]));Sourcepub fn pixel(&self, x: u32, y: u32) -> Option<[u8; 4]>
pub fn pixel(&self, x: u32, y: u32) -> Option<[u8; 4]>
The premultiplied RGBA bytes of one pixel, or None when out of range.
use pdfrum_render::Pixmap;
let red = Pixmap::filled(2, 2, peniko::Color::from_rgba8(255, 0, 0, 255));
assert_eq!(red.pixel(1, 1), Some([255, 0, 0, 255]));
// Out of range, not a panic.
assert_eq!(red.pixel(2, 0), None);Sourcepub fn set_pixel(&mut self, x: u32, y: u32, px: [u8; 4])
pub fn set_pixel(&mut self, x: u32, y: u32, px: [u8; 4])
Overwrite one pixel with premultiplied RGBA bytes. Out of range is a no-op — the shading rasterizers clip by construction and a stray write must not panic on a crafted file.
use pdfrum_render::Pixmap;
let mut p = Pixmap::new(2, 2);
p.set_pixel(1, 0, [255, 0, 0, 255]);
assert_eq!(p.pixel(1, 0), Some([255, 0, 0, 255]));
// Out of range is a no-op: a crafted file must not panic here.
p.set_pixel(9, 9, [255, 255, 255, 255]);Sourcepub fn fill(&mut self, color: Color)
pub fn fill(&mut self, color: Color)
Set every pixel to color.
use pdfrum_render::Pixmap;
let mut p = Pixmap::new(2, 2);
p.fill(peniko::Color::from_rgba8(255, 0, 0, 255));
assert_eq!(p.pixel(0, 0), Some([255, 0, 0, 255]));Sourcepub fn multiply_alpha(&mut self, alpha: f32)
pub fn multiply_alpha(&mut self, alpha: f32)
Scale every channel by alpha.
The scalar is truncated to a byte (0.5 becomes 127, not 128)
and the per-pixel product truncates too. On a premultiplied buffer all
four channels scale, where the oracle scales only the alpha byte of a
straight one — the same image either way.
use pdfrum_render::Pixmap;
let mut p = Pixmap::filled(1, 1, peniko::Color::from_rgba8(255, 0, 0, 255));
// The scalar truncates to a byte: `0.5` is 127, not 128.
p.multiply_alpha(0.5);
assert_eq!(p.pixel(0, 0), Some([127, 0, 0, 127]));Sourcepub fn remove_backdrop(&mut self, backdrop: &Self)
pub fn remove_backdrop(&mut self, backdrop: &Self)
[oracle-bug] Remove a non-isolated group’s initial backdrop from its
finished pixels (ISO 32000 §11.4.6, §11.6.6).
The group’s buffer starts as a copy of the page beneath it, so that copy would be counted a second time when the group is composited back. The spec’s formula, per channel and premultiplied:
C = Cn + (Cn - C0) * (a0 / agn - a0)C0/a0 are the backdrop’s and Cn/agn the group’s. A pixel that
still equals the backdrop is empty. Over an opaque backdrop alpha
cannot rise, so a pixel the group replaced keeps the group’s colour
— otherwise a later ca < 1 multiply has nothing to fade. backdrop
must match this pixmap’s dimensions; a mismatch is a no-op, as for
Self::multiply_alpha_mask.
use pdfrum_render::Pixmap;
let backdrop = Pixmap::filled(1, 1, peniko::Color::from_rgba8(255, 0, 0, 255));
let mut group = backdrop.clone();
// The group painted nothing over its backdrop, so removing it
// leaves a transparent pixel -- the same image once composited back.
group.remove_backdrop(&backdrop);
assert_eq!(group.pixel(0, 0), Some([0, 0, 0, 0]));Sourcepub fn knockout_over(&mut self, next: &Self)
pub fn knockout_over(&mut self, next: &Self)
[oracle-bug] Lay next over this pixmap under knockout
composition (ISO 32000 §11.6.6): where next has any coverage it
replaces what is here, rather than blending over it.
That is the whole of the knockout rule stated pixel-wise. Each object in a knockout group composites against the group’s initial backdrop, so an earlier object’s contribution at a pixel a later one also covers never reaches the result; only the coverage-weighted mix at the edges of the later object’s own antialiasing keeps any of it.
A mismatch in dimensions is a no-op, on the same invariant as
Self::multiply_alpha_mask.
use pdfrum_render::Pixmap;
let mut base = Pixmap::filled(1, 1, peniko::Color::from_rgba8(0, 0, 255, 255));
let next = Pixmap::filled(1, 1, peniko::Color::from_rgba8(255, 0, 0, 255));
// Full coverage replaces rather than blending.
base.knockout_over(&next);
assert_eq!(base.pixel(0, 0), Some([255, 0, 0, 255]));Sourcepub fn multiply_alpha_mask(&mut self, mask: &AlphaMask)
pub fn multiply_alpha_mask(&mut self, mask: &AlphaMask)
Multiply every channel by a coverage mask, PDFium’s
MultiplyAlphaMask. The mask must match the pixmap’s dimensions
exactly — an invariant, not a preference: a mismatched mask silently
disables masking on both backends, so the engine never emits one.
use pdfrum_render::Pixmap;
use pdfrum_render::AlphaMask;
let mut p = Pixmap::filled(1, 1, peniko::Color::from_rgba8(255, 0, 0, 255));
p.multiply_alpha_mask(&AlphaMask::filled(1, 1, 128));
assert_eq!(p.pixel(0, 0), Some([128, 0, 0, 128]));
// A mismatched mask is a no-op, which is why the engine never emits one.
p.multiply_alpha_mask(&AlphaMask::filled(4, 4, 0));
assert_eq!(p.pixel(0, 0), Some([128, 0, 0, 128]));Sourcepub fn luminosity_mask(&self) -> AlphaMask
pub fn luminosity_mask(&self) -> AlphaMask
The 8-bit luminosity of every pixel under the oracle’s gray weights, for a soft mask’s luminosity readback (ISO 32000 §11.6.5.2).
Deliberately not either backend’s luminance helper: both use BT.709 coefficients and the oracle uses NTSC ones on a 0..100 integer scale.
use pdfrum_render::Pixmap;
// The oracle's NTSC weights on a 0..100 integer scale, not BT.709.
let blue = Pixmap::filled(1, 1, peniko::Color::from_rgba8(0, 0, 255, 255));
assert_eq!(blue.luminosity_mask().data(), &[28]);Sourcepub fn alpha_mask(&self) -> AlphaMask
pub fn alpha_mask(&self) -> AlphaMask
The alpha channel on its own, for an alpha-type soft mask.
use pdfrum_render::Pixmap;
let red = Pixmap::filled(1, 1, peniko::Color::from_rgba8(255, 0, 0, 255));
assert_eq!(red.alpha_mask().data(), &[255]);Sourcepub fn to_straight_bgra(&self) -> Vec<u8> ⓘ
pub fn to_straight_bgra(&self) -> Vec<u8> ⓘ
The straight-alpha BGRA bytes the oracle hashes and encodes.
Straight-alpha BGRA bytes, four per pixel.
use pdfrum_render::Pixmap;
let red = Pixmap::filled(1, 1, peniko::Color::from_rgba8(255, 0, 0, 255));
assert_eq!(red.to_straight_bgra(), vec![0, 0, 255, 255]);Sourcepub fn to_opaque_bgra(&self) -> Vec<u8> ⓘ
pub fn to_opaque_bgra(&self) -> Vec<u8> ⓘ
Straight BGRA with the alpha byte forced to 0xFF.
A page with no transparency is rendered into a 32bpp buffer whose
fourth byte is padding, and a hash taken over these bytes sees that
padding as 0xFF rather than as whatever the render left there.
use pdfrum_render::Pixmap;
let clear = Pixmap::new(1, 1);
assert_eq!(clear.to_opaque_bgra(), vec![0, 0, 0, 255]);Sourcepub fn to_straight_rgb(&self) -> Vec<u8> ⓘ
pub fn to_straight_rgb(&self) -> Vec<u8> ⓘ
The straight-alpha RGB bytes, three per pixel — the shape the oracle’s PNG encoder writes for a page with no transparency.
use pdfrum_render::Pixmap;
let red = Pixmap::filled(1, 1, peniko::Color::from_rgba8(255, 0, 0, 255));
assert_eq!(red.to_straight_rgb(), vec![255, 0, 0]);Sourcepub fn to_straight_rgba(&self) -> Vec<u8> ⓘ
pub fn to_straight_rgba(&self) -> Vec<u8> ⓘ
The straight-alpha RGBA bytes, four per pixel — the shape the oracle’s PNG encoder writes for a page that has transparency.
use pdfrum_render::Pixmap;
let red = Pixmap::filled(1, 1, peniko::Color::from_rgba8(255, 0, 0, 255));
assert_eq!(red.to_straight_rgba(), vec![255, 0, 0, 255]);Source§impl Pixmap
Encoding to PNG, behind the png feature.
impl Pixmap
Encoding to PNG, behind the png feature.
Sourcepub fn encode_png(&self) -> Result<Vec<u8>, Error>
Available on crate feature png only.
pub fn encode_png(&self) -> Result<Vec<u8>, Error>
png only.The pixmap as a PNG file’s bytes: eight-bit RGBA, unpremultiplied.
The alpha is undone on the way out because PNG’s is straight
(ISO 15948 §6.2) and this buffer’s is premultiplied. A viewer
multiplies by alpha when it composites, so writing the premultiplied
bytes would darken every partly transparent pixel a second time –
a 50% red would leave here as 128,0,0,128 and land as 64,0,0.
Fully opaque pixels are identical either way, which is why this is
invisible on most pages.
§Errors
Error::Png when the encoder refuses the dimensions.