Files
pdf-odin-llm/device_raster.odin
T
2026-09-25 23:32:29 +02:00

2142 lines
80 KiB
Odin

package pdf
import "core:math"
import "core:mem"
// The rasterising Device: drawing calls in, pixels out.
//
// Nothing here knows anything about PDF. Everything it needs -- the resolved
// paint, the CTM, the decoded image, the loaded shading -- arrives through the
// Device vtable, and everything it does is assembled out of the primitives
// that were written and frozen before it: `path_flatten` and `stroke_flatten`
// produce geometry, `raster_fill` turns geometry into coverage, `blend_span`
// and `blend_bitmap` turn coverage into pixels, `image_decode` and
// `shading_paint` supply the two sources of colour that are not a single
// value.
//
// ---------------------------------------------------------------------------
// COORDINATE SPACES
//
// Two, and the distinction matters at every boundary below.
//
// PAGE device space what the interpreter's `ctm` maps into, what `area`
// rectangles are given in, and what `base_clip` bounds.
// Every clip box and every mask lives here.
// TARGET space pixels of the bitmap currently being painted, which is
// the page bitmap until a transparency group or a soft
// mask redirects it. A layer's bitmap covers only its own
// box, so target pixel (0,0) is page pixel `org`.
//
// Draw calls translate their CTM by -org (`rdev_ctm`) and rasterise in target
// space; clip and mask lookups translate back by +org. Keeping the clip stack
// in page space is what lets a group be pushed and popped without rebasing
// every mask underneath it.
//
// ---------------------------------------------------------------------------
// THE CLIP STACK -- the one part of this file that is a design rather than an
// assembly of existing parts.
//
// A clip is two things at once: a rectangle outside which nothing is painted,
// and (sometimes) a coverage plane inside it. They are stored separately
// because they have wildly different costs:
//
// * `rd.scissor` is the running intersection of every rectangular clip. It
// is a Raster_Clip, it is what `raster_fill` is handed, and it is free:
// narrowing it turns a path 10^9 units across into a few pixels of work
// because `raster_fill` clamps its raster box to the clip and replaces the
// parts of every edge outside the box with vertical edges standing on the
// box walls.
//
// * `rd.masks` holds the coverage planes, one per non-rectangular clip, each
// an 8-bit Bitmap the size of the box it was rasterised into. They are NOT
// flattened together on push. Flattening would make the paint-time lookup
// O(1) instead of O(depth), but it would also rebuild and re-allocate the
// whole intersected plane on every push -- and the depth is 0 in the
// overwhelming majority of content and 1 in nearly all of the rest, so the
// lookup is already O(1) in practice and the pushes are where the cost
// would actually land.
//
// An axis-aligned rectangle under a rectilinear CTM takes the scissor path.
// `matrix_is_rectilinear` is deliberately tolerance-based, so a page with
// `/Rotate 90` -- whose cos is 6.1e-17 and not 0 -- takes it too, which is the
// case the predicate exists for. The rectangle is recognised after flattening,
// in device space, so `re W n` and a hand-written `m l l l h W n` are the same
// to it. The scissor is the rectangle rounded to the nearest pixel boundary,
// so a clip edge at a fractional coordinate is up to half a pixel off; the
// alternative is an 8-bit plane the size of the page for every `re W n` in
// every content stream, which is not a trade worth making. Anything else --
// a rotated rectangle, a circle, a glyph, a stencil -- becomes a mask.
//
// Clips nest. Each stack entry records the scissor and the mask count that
// were in force BEFORE it, so pushing intersects and popping restores exactly,
// and a pop frees the masks its push added. The interpreter guarantees the
// pushes and pops balance; `pop_clip` on an empty stack is still a no-op
// rather than an underflow.
//
// ---------------------------------------------------------------------------
// TRANSPARENCY
//
// `begin_group` renders into a fresh bitmap covering the group's area and
// `end_group` composites it back through `blend_bitmap` with the group's
// alpha, blend mode AND soft mask. All three belong to the group's RESULT
// rather than to what is drawn inside it (11.6.6), which is why `begin_group`
// carries all three and the paints inside carry none of them. `begin_mask`
// does the same into a mask layer, and `end_mask` turns it into an alpha plane
// with `mask_from_bitmap`, honouring /TR, and installs it as the current soft
// mask.
//
// A soft mask is applied to a painting operation whose `Paint.soft_mask` is
// non-nil, by multiplying its coverage -- which is exactly what the clip masks
// do, through the same code. It is deliberately NOT pushed onto the clip
// stack: `device.odin` says every `clip_*` pushes one entry and `pop_clip`
// removes one, and `end_mask` is not a `clip_*`, so an interpreter written to
// that contract will not pop for it. Keying the application off
// `Paint.soft_mask` instead means "/SMask /None" stops the masking without the
// device having to model the graphics state at all.
//
// The rendered masks live in a small cache keyed by the CONTENTS of the
// Soft_Mask each one belongs to, because an ExtGState renders its mask once --
// when it installs it -- and a `Q` can bring an older one back into force long
// afterwards. A device that kept only the newest would modulate everything
// after that `Q` with whichever mask happened to be installed last. `end_mask`
// is told which Soft_Mask it has just drawn, so a plane is matched by what it
// was rendered from and never claimed by whatever paints next -- a claim the
// inside of a transparency group destroys, every one of its paints carrying no
// mask by design. A paint whose mask is not in the cache is painted unmasked:
// showing content is a much better failure than erasing it with somebody
// else's mask.
//
// Two simplifications, both deliberate and both visible only in cases the
// corpus notes rank as rare:
//
// * A NON-ISOLATED group is given a copy of its backdrop as its initial
// contents, so its finished bitmap already IS "contents over backdrop",
// and it is put BACK onto the page rather than composited over it.
// Compositing it would blend the backdrop with itself: `C over C` under
// Normal gives ao = 2a - a^2, so a backdrop pixel at alpha 0.5 comes back
// at 0.75 -- wrong for every group nested inside another one and for every
// page rendered with a transparent background, and wrong whether or not
// the group draws anything. Replacing is the spec's group-removal step
// (11.4.6) composed with the composite it cancels against, and is exact
// while the group's own alpha is 1 and its blend mode Normal. There is no
// such cancellation with anything else, so those groups are rendered
// ISOLATED instead. Isolated and non-isolated differ only through blend
// modes used INSIDE the group, so the case that is approximated is a
// partially transparent, non-Normal, non-isolated group -- and the
// approximation is the one every renderer without a full backdrop-removal
// pass makes.
//
// * A KNOCKOUT group is rendered as a non-knockout one. Knockout requires
// each element to composite against the group's initial backdrop rather than
// against its siblings, which means either a per-element scratch layer or
// a per-pixel restore; both cost more than the feature is worth here.
//
// ---------------------------------------------------------------------------
// ROBUSTNESS
//
// Content streams are the most malformed data in a PDF. Nothing in this file
// panics, allocates on a number taken from a file without clamping it, or
// loops on anything but a bounded count. A layer whose bitmap cannot be
// allocated suppresses its contents and still accepts its `end_group`, so the
// stack stays balanced and the rest of the page draws. Every non-finite matrix
// and every singular CTM paints nothing rather than reaching the geometry.
// ---------------------------------------------------------------------------
// Limits
// ---------------------------------------------------------------------------
// RDEV_MAX_LAYERS bounds the group and mask stack. Form XObject recursion is
// already capped at MAX_DEPTH by the interpreter and every layer costs a
// bitmap, so this is a backstop against a content stream that opens groups
// without closing them, not a limit any real file approaches.
@(private)
RDEV_MAX_LAYERS :: 32
// RDEV_MAX_TAPS bounds one filtered image sample in each axis, and
// RDEV_MAX_TAP_AREA their product. Sixteen samples across a footprint is well
// past the point where more of them change an 8-bit result; the area cap is
// what stops a 4000x4000 photograph squeezed into one pixel from reading every
// source pixel for that one output.
//
// The cap is only sound while the taps still COVER the footprint. Past 16x
// minification they no longer would -- the sampler walks them at a stride of
// `footprint / taps`, so at 32x it reads every second source pixel and never
// averages the ones between, which is point sampling with extra steps and
// aliases exactly as badly. A one-pixel checkerboard minified 32x came out
// solid black or solid white depending on phase. So past this factor the
// source is BOX-REDUCED by an integer amount first (`rdev_mip_for`), which
// brings the footprint back under the cap with every source pixel counted
// exactly once.
@(private)
RDEV_MAX_TAPS :: 16
@(private)
RDEV_MAX_TAP_AREA :: 256
// RDEV_MAX_MIPS is how many reduced images are kept. A tiling pattern replays
// its cell thousands of times with the same picture at the same scale, so
// without a cache the reduction would be redone for every cell.
@(private)
RDEV_MAX_MIPS :: 2
// RDEV_MIP_BUDGET is the total source pixels one page may spend on reductions.
// Each costs one pass over the image, which is repaid many times over by the
// sampling it makes honest -- but a content stream that forces a fresh
// reduction thousands of times must still stop somewhere. Past it the sampler
// falls back to the strided taps, which alias but cost nothing extra.
@(private)
RDEV_MIP_BUDGET :: 1 << 26
// RDEV_MAX_CLIPS bounds the clip stack. Every other stack in this device is
// capped and this one was not: `gstate.odin` caps q/Q at 64, but a clip needs
// no `q` -- `re W n` repeated pushes one every time and nothing pops until the
// enclosing scope ends, so the only bound was the shared token fuel.
// `device_list.odin` caps the same stack with MAX_DISPLAY_CLIPS.
@(private)
RDEV_MAX_CLIPS :: 4096
// RDEV_MAX_CLIP_MASKS and RDEV_MAX_CLIP_MASK_BYTES bound the coverage planes,
// which are what a clip stack actually costs here: one 8-bit plane the size of
// the intersection of the scissor and the shape, so a shape whose bounds are
// the whole page costs width*height BYTES and narrows nothing for the next one.
// `rdev_mask_row` also walks every live plane for every run of pixels painted,
// so both the memory and the per-span cost are bounded here. Past either cap a
// non-rectangular clip degrades to its bounding box: content shows inside the
// box that the shape excluded, which is the direction every other failure in
// this file takes.
@(private)
RDEV_MAX_CLIP_MASKS :: 64
@(private)
RDEV_MAX_CLIP_MASK_BYTES :: 1 << 25
// RDEV_MAX_SOFT_MASKS is how many rendered soft masks are kept at once. Real
// nesting is one or two; four leaves room and bounds the memory, each plane
// being one byte per pixel of the mask's own area.
@(private)
RDEV_MAX_SOFT_MASKS :: 4
// RDEV_INTERPOLATE_SCALE is the magnification past which an image is smoothed
// even without /Interpolate. A producer that stretches a 2x2 image across a
// page is describing a gradient, not four squares, and every viewer smooths
// it; below this the blocks are what the file asked for.
@(private)
RDEV_INTERPOLATE_SCALE :: 4.0
// RDEV_RECT_TOL is how far a flattened quadrilateral may be from axis-aligned
// and still be taken as a rectangle for the scissor fast path. Device pixels.
@(private)
RDEV_RECT_TOL :: 1.0 / 512
// ---------------------------------------------------------------------------
// Types
// ---------------------------------------------------------------------------
// Rdev_Mask is one coverage plane in force over a region of PAGE device space.
// `outside` is the value the mask takes beyond `box`: 0 for a clip, and for a
// luminosity soft mask the transfer of the backdrop luminosity, which is what
// makes a mask whose form covers only part of the page erase the rest.
@(private)
Rdev_Mask :: struct {
box: Raster_Clip,
bmp: Bitmap, // n = 1, exactly as wide and tall as `box`
outside: u8,
}
// Rdev_Clip is one entry of the clip stack: what to restore when it is popped.
@(private)
Rdev_Clip :: struct {
scissor: Raster_Clip, // the scissor in force BEFORE this entry
n_masks: int, // len(masks) BEFORE this entry
}
// Rdev_Smask is one rendered soft mask and the ExtGState mask it belongs to.
// `end_mask` names the owner, so a plane is matched by what it was drawn from
// and never by guesswork.
//
// The key is the Soft_Mask's CONTENTS, not its address. Everything a render
// depends on is in there -- the form, the CTM it was installed under, the
// backdrop and the transfer -- so two equal masks have one plane between them;
// and a Soft_Mask lives in interpreter scratch, where the same address can
// carry a different mask later in the page, so the address is not an identity
// at all.
@(private)
Rdev_Smask :: struct {
plane: Rdev_Mask,
owner_val: Soft_Mask,
owned: bool,
live: bool,
// Whether any paint has actually gone through this plane. An ExtGState
// that installs a mask and then draws nothing is common, and those planes
// are the ones to evict first.
used: bool,
clock: u64,
}
@(private)
Rdev_Layer_Kind :: enum u8 { Group, Mask }
// Rdev_Layer is a transparency group or a soft-mask group: a bitmap that
// drawing is redirected into, plus everything needed to put the device back
// the way it was.
@(private)
Rdev_Layer :: struct {
kind: Rdev_Layer_Kind,
bmp: Bitmap,
box: Raster_Clip, // page device space
ok: bool, // false: allocation failed, contents suppressed
prev_scissor: Raster_Clip,
prev_mask_base: int,
// Group.
alpha: f64,
blend: Blend_Mode,
knockout: bool,
// The soft mask the group's RESULT is composited through (11.6.6), and
// whether the layer was seeded with a copy of its backdrop.
soft_mask: ^Soft_Mask,
seeded: bool,
// Mask. `owner` is the Soft_Mask this plane belongs to, named by
// `end_mask` rather than guessed from whatever paints next.
luminosity: bool,
backdrop: [3]f64,
owner: ^Soft_Mask,
}
// Rdev_Mip is one box-reduced copy of a decoded image, kept so that a tile
// replayed thousands of times reduces it once. `src` is the identity of the
// image it came from: `image_decode` hands back `&img.rgba`, whose address is
// stable for the life of the Document.
@(private)
Rdev_Mip :: struct {
src: ^Bitmap,
kx, ky: int,
bmp: Bitmap,
}
@(private)
Rdev_Sampler :: enum u8 { Nearest, Bilinear, Box }
@(private)
Raster_Device :: struct {
// The vtable handed to the interpreter. `dev.user` points back here.
dev: Device,
allocator: mem.Allocator,
// Needed by `image_decode` and `shading_paint`, which are the only two
// things below the Device line that still have to reach the Document.
// The vtable carries no Document, so it is set here; see
// `raster_device_make`.
doc: ^Document,
page: ^Bitmap,
base_clip: Raster_Clip,
// Where painting currently lands, and its origin in page device space.
target: ^Bitmap,
org_x, org_y: int,
scissor: Raster_Clip,
clips: [dynamic]Rdev_Clip,
masks: [dynamic]Rdev_Mask,
// Clip pushes refused because the stack was full. The matching pop is
// refused too, so the stack stays balanced.
clip_over: int,
// Bytes of live coverage plane, against RDEV_MAX_CLIP_MASK_BYTES.
mask_bytes: int,
// Masks below this index are hidden. A soft mask's own form is rendered
// outside the clip that was in force when the ExtGState installed it.
mask_base: int,
smasks: [RDEV_MAX_SOFT_MASKS]Rdev_Smask,
smask_clock: u64,
smask_suspended: bool,
mips: [RDEV_MAX_MIPS]Rdev_Mip,
mip_next: int,
mip_budget: int,
layers: [dynamic]Rdev_Layer,
// Depth of failed layers. Non-zero means everything painted is dropped,
// while the hooks keep accepting calls so the stack still balances.
suppress: int,
// Scratch. `fp` is the flattened geometry of whatever is being drawn,
// `scratch` one row of combined mask values, `pix` one row of image
// samples. All three are sized once per target and reused.
fp: Flat_Path,
scratch: [dynamic]u8,
pix: [dynamic]u8,
// First error that stopped a paint. Diagnostic only: a device that cannot
// allocate still has to let the page finish.
err: Error,
}
// ---------------------------------------------------------------------------
// Construction
// ---------------------------------------------------------------------------
// raster_device_make builds a device that paints into `bmp`, which it does NOT
// own, clipped to `base_clip`.
//
// `bmp` must be PREMULTIPLIED, like every bitmap below the Device line. A page
// bitmap cleared to an opaque background satisfies that trivially, since
// premultiplied and straight agree wherever alpha is 1 or 0;
// `bitmap_unpremultiply` at the end of `render_page` is what converts the
// result back for a caller.
//
// `doc` is optional only in the sense that the device survives without one:
// images and shadings need the Document to decode and to record repairs
// against, and are skipped when it is nil.
//
// The result is owned by the caller; free it with `raster_device_destroy`.
@(private)
raster_device_make :: proc(bmp: ^Bitmap, base_clip: Raster_Clip,
allocator := context.allocator,
doc: ^Document = nil) -> (^Raster_Device, Error) {
if bmp == nil || bmp.width <= 0 || bmp.height <= 0 || len(bmp.pixels) == 0 {
return nil, .Invalid_Argument
}
rd, alloc_err := new(Raster_Device, allocator)
if alloc_err != nil || rd == nil {
return nil, .Out_Of_Memory
}
rd.allocator = allocator
rd.doc = doc
rd.mip_budget = RDEV_MIP_BUDGET
rd.page = bmp
rd.target = bmp
rd.base_clip = rdev_clip_intersect(base_clip, Raster_Clip{0, 0, bmp.width, bmp.height})
rd.scissor = rd.base_clip
rd.clips = make([dynamic]Rdev_Clip, 0, 8, allocator)
rd.masks = make([dynamic]Rdev_Mask, 0, 4, allocator)
rd.layers = make([dynamic]Rdev_Layer, 0, 4, allocator)
rd.scratch = make([dynamic]u8, 0, 0, allocator)
rd.pix = make([dynamic]u8, 0, 0, allocator)
rd.fp = flat_path_make(allocator)
rd.dev = Device{
user = rd,
fill_path = rd_fill_path,
stroke_path = rd_stroke_path,
clip_path = rd_clip_path,
clip_stroke_path = rd_clip_stroke_path,
clip_text = rd_clip_text,
clip_image_mask = rd_clip_image_mask,
pop_clip = rd_pop_clip,
fill_text = rd_fill_text,
stroke_text = rd_stroke_text,
fill_image = rd_fill_image,
fill_image_mask = rd_fill_image_mask,
fill_shade = rd_fill_shade,
begin_mask = rd_begin_mask,
end_mask = rd_end_mask,
has_soft_mask = rd_has_soft_mask,
begin_group = rd_begin_group,
end_group = rd_end_group,
begin_tile = rd_begin_tile,
end_tile = rd_end_tile,
close = rd_close,
}
if !rdev_fit_scratch(rd) {
raster_device_destroy(rd)
return nil, .Out_Of_Memory
}
return rd, .None
}
@(private)
raster_device_destroy :: proc(rd: ^Raster_Device) {
if rd == nil { return }
for i in 0 ..< len(rd.layers) {
bitmap_destroy(&rd.layers[i].bmp)
}
for i in 0 ..< len(rd.masks) {
bitmap_destroy(&rd.masks[i].bmp)
}
for i in 0 ..< RDEV_MAX_SOFT_MASKS {
bitmap_destroy(&rd.smasks[i].plane.bmp)
}
for i in 0 ..< RDEV_MAX_MIPS {
bitmap_destroy(&rd.mips[i].bmp)
}
delete(rd.layers)
delete(rd.masks)
delete(rd.clips)
delete(rd.scratch)
delete(rd.pix)
flat_path_destroy(&rd.fp)
allocator := rd.allocator
rd^ = {}
free(rd, allocator)
}
@(private)
raster_device_as_device :: proc(rd: ^Raster_Device) -> ^Device {
if rd == nil { return nil }
return &rd.dev
}
@(private)
rdev_from :: proc(dev: ^Device) -> ^Raster_Device {
if dev == nil || dev.user == nil { return nil }
return (^Raster_Device)(dev.user)
}
// rdev_ready reports whether a paint can proceed at all.
@(private)
rdev_ready :: proc(rd: ^Raster_Device) -> bool {
return rd != nil && rd.suppress == 0 && rd.target != nil && rd.target.width > 0
}
// ---------------------------------------------------------------------------
// Boxes and spaces
// ---------------------------------------------------------------------------
@(private)
rdev_clip_intersect :: proc "contextless" (a, b: Raster_Clip) -> Raster_Clip {
r := Raster_Clip{max(a.x0, b.x0), max(a.y0, b.y0), min(a.x1, b.x1), min(a.y1, b.y1)}
if r.x1 <= r.x0 || r.y1 <= r.y0 { return Raster_Clip{} }
return r
}
@(private)
rdev_clip_empty :: proc "contextless" (c: Raster_Clip) -> bool {
return c.x1 <= c.x0 || c.y1 <= c.y0
}
@(private)
rdev_clip_width :: proc "contextless" (c: Raster_Clip) -> int {
return max(0, c.x1 - c.x0)
}
// rdev_box_outward is the smallest pixel box containing `r`, bounded by
// `bound`. `raster_floor_clamped` and `raster_ceil_clamped` do the work
// because they already refuse to let a huge or non-finite f64 reach an integer
// conversion, and a NaN there widens to the bound rather than collapsing --
// drawing too much is a better failure than dropping the content.
@(private)
rdev_box_outward :: proc "contextless" (r: Rect, bound: Raster_Clip) -> Raster_Clip {
return Raster_Clip{
raster_floor_clamped(r.x0, bound.x0, bound.x1),
raster_floor_clamped(r.y0, bound.y0, bound.y1),
raster_ceil_clamped (r.x1, bound.x0, bound.x1),
raster_ceil_clamped (r.y1, bound.y0, bound.y1),
}
}
// rdev_box_round rounds each edge to the nearest pixel boundary. This is the
// scissor for a rectangular clip, and the half-pixel it can be wrong by is the
// price of not allocating a coverage plane for every `re W n`.
@(private)
rdev_box_round :: proc "contextless" (r: Rect, bound: Raster_Clip) -> Raster_Clip {
if !rect_is_finite(r) { return rdev_box_outward(r, bound) }
return Raster_Clip{
raster_floor_clamped(r.x0 + 0.5, bound.x0, bound.x1),
raster_floor_clamped(r.y0 + 0.5, bound.y0, bound.y1),
raster_floor_clamped(r.x1 + 0.5, bound.x0, bound.x1),
raster_floor_clamped(r.y1 + 0.5, bound.y0, bound.y1),
}
}
// rdev_ctm folds the current layer's origin into an interpreter CTM, so that
// geometry comes out in target pixels.
@(private)
rdev_ctm :: proc(rd: ^Raster_Device, ctm: Matrix) -> Matrix {
return matrix_mul(ctm, translate_matrix(f64(-rd.org_x), f64(-rd.org_y)))
}
// rdev_scissor is the current clip in TARGET pixels, clamped to the target.
@(private)
rdev_scissor :: proc(rd: ^Raster_Device) -> Raster_Clip {
s := rd.scissor
t := Raster_Clip{
s.x0 - rd.org_x, s.y0 - rd.org_y,
s.x1 - rd.org_x, s.y1 - rd.org_y,
}
return rdev_clip_intersect(t, Raster_Clip{0, 0, rd.target.width, rd.target.height})
}
// rdev_cull is the region a path about to be flattened can put ink in: the
// scissor, in the layer-local coordinates `path_flatten` works in, with a
// pixel of margin so that nothing is decided by a rounding at the edge.
//
// Passing it is what bounds the cost of ONE painting operator. `path_flatten`
// caps a whole flattening at MAX_FLATTEN_POINTS and one curve at
// MAX_FLATTEN_DEPTH, but a content stream may spend that million points on
// every fill, stroke and clip it does, and the token budget buys tens of
// thousands of them: sixteen curves with control points ten trillion units
// away exhaust the point cap between them, and 800 painting operators over
// such a path -- 150 KB of content, and nothing of it visible -- measured 79
// seconds. With the clip in hand the flattener stops subdividing what cannot
// reach it, which is exact rather than an approximation -- see
// `flatten_reaches`.
@(private)
rdev_cull :: proc(rd: ^Raster_Device) -> Maybe(Rect) {
if rd == nil || rd.target == nil { return nil }
sc := rdev_scissor(rd)
return Rect{f64(sc.x0) - 1, f64(sc.y0) - 1, f64(sc.x1) + 1, f64(sc.y1) + 1}
}
// rdev_fit_scratch sizes the per-row scratch to the current target. A span can
// never be wider than the target, so this is sized once per retarget rather
// than per operation.
@(private)
rdev_fit_scratch :: proc(rd: ^Raster_Device) -> bool {
if rd.target == nil { return true }
w := rd.target.width
if len(rd.scratch) < w {
if resize(&rd.scratch, w) != nil { return false }
}
if len(rd.pix) < w * 4 {
if resize(&rd.pix, w * 4) != nil { return false }
}
return len(rd.scratch) >= w && len(rd.pix) >= w * 4
}
// rdev_retarget points painting at the topmost layer that has a bitmap, or at
// the page. The layer array is the owner, so the target is recomputed from it
// rather than remembered -- a `[dynamic]` reallocates, and a saved interior
// pointer would dangle the first time a fifth group was pushed.
@(private)
rdev_retarget :: proc(rd: ^Raster_Device) {
rd.target = rd.page
rd.org_x, rd.org_y = 0, 0
for i := len(rd.layers) - 1; i >= 0; i -= 1 {
if rd.layers[i].ok {
rd.target = &rd.layers[i].bmp
rd.org_x = rd.layers[i].box.x0
rd.org_y = rd.layers[i].box.y0
break
}
}
_ = rdev_fit_scratch(rd)
}
// ---------------------------------------------------------------------------
// Masks
// ---------------------------------------------------------------------------
// rdev_smask_lookup returns the rendered plane for `sm`, or nil.
//
// The owner is recorded by `end_mask`, which is told which Soft_Mask it drew,
// so nothing here depends on what paints next. It used to: the plane was
// claimed by the first paint to arrive afterwards, and ANY paint arriving first
// without a mask destroyed the claim -- which is exactly what the inside of a
// transparency group is, every one of whose paints has `soft_mask == nil` by
// design (11.6.6). One group early on therefore disabled an ExtGState mask for
// the rest of the page.
@(private)
rdev_smask_lookup :: proc(rd: ^Raster_Device, sm: ^Soft_Mask) -> ^Rdev_Mask {
if rd.smask_suspended || sm == nil { return nil }
for i in 0 ..< RDEV_MAX_SOFT_MASKS {
if rd.smasks[i].live && rd.smasks[i].owned && rd.smasks[i].owner_val == sm^ {
rd.smask_clock += 1
rd.smasks[i].clock = rd.smask_clock
rd.smasks[i].used = true
return &rd.smasks[i].plane
}
}
return nil
}
// rd_has_soft_mask lets the interpreter skip re-rendering a mask this device
// already holds. An ExtGState naming an /SMask is applied once per `gs`, and
// every one of those used to rasterise the mask's whole form again.
@(private)
rd_has_soft_mask :: proc(dev: ^Device, mask: ^Soft_Mask) -> bool {
rd := rdev_from(dev)
if rd == nil { return false }
return rdev_smask_lookup(rd, mask) != nil
}
@(private)
rdev_smask_for :: proc(rd: ^Raster_Device, paint: Paint) -> ^Rdev_Mask {
return rdev_smask_lookup(rd, paint.soft_mask)
}
// rdev_smask_pinned reports whether an OPEN transparency group still needs
// this plane. A group's result is composited through its mask at `end_group`
// (11.6.6), which can be a whole content stream after the mask was installed,
// and until then nothing has painted through the plane -- which is exactly
// what makes it the first slot the scan below would otherwise give away. An
// ExtGState mask over a group whose contents install masks of their own is
// ordinary illustration output, and it lost the outer mask every time.
@(private)
rdev_smask_pinned :: proc(rd: ^Raster_Device, i: int) -> bool {
if !rd.smasks[i].live || !rd.smasks[i].owned { return false }
for &l in rd.layers {
if l.kind == .Group && l.soft_mask != nil && rd.smasks[i].owner_val == l.soft_mask^ {
return true
}
}
return false
}
// rdev_smask_install takes ownership of a freshly rendered plane, evicting the
// least recently used slot when every one of them is taken.
@(private)
rdev_smask_install :: proc(rd: ^Raster_Device, owner: ^Soft_Mask, plane: Rdev_Mask) {
slot := -1
for i in 0 ..< RDEV_MAX_SOFT_MASKS {
// A plane an open group is still going to composite through is not
// available at any price -- unless every slot holds one, which takes
// RDEV_MAX_SOFT_MASKS nested masked groups and falls through to the
// same unmasked degradation as any other miss.
if rdev_smask_pinned(rd, i) { continue }
// A slot nothing ever painted through goes first, ahead of any mask
// that has been used at least once.
if !rd.smasks[i].live || !rd.smasks[i].used {
slot = i
break
}
if slot < 0 || rd.smasks[i].clock < rd.smasks[slot].clock {
slot = i
}
}
if slot < 0 { slot = 0 }
bitmap_destroy(&rd.smasks[slot].plane.bmp)
rd.smask_clock += 1
rd.smasks[slot] = Rdev_Smask{
plane = plane,
owned = owner != nil,
owner_val = owner != nil ? owner^ : Soft_Mask{},
live = true,
clock = rd.smask_clock,
}
}
@(private)
rdev_smask_drop_all :: proc(rd: ^Raster_Device) {
for i in 0 ..< RDEV_MAX_SOFT_MASKS {
bitmap_destroy(&rd.smasks[i].plane.bmp)
rd.smasks[i] = {}
}
}
@(private)
rdev_has_masks :: proc(rd: ^Raster_Device, sm: ^Rdev_Mask) -> bool {
return sm != nil || len(rd.masks) > rd.mask_base
}
// rdev_mask_apply multiplies one mask's row into `buf`, which is indexed from
// page pixel `px0` on page row `py`.
@(private)
rdev_mask_apply :: proc(m: ^Rdev_Mask, buf: []u8, px0, py: int) {
row: []u8
if py >= m.box.y0 && py < m.box.y1 {
row = bitmap_row(&m.bmp, py - m.box.y0)
}
for i in 0 ..< len(buf) {
if buf[i] == 0 { continue }
v := m.outside
x := px0 + i
if row != nil && x >= m.box.x0 && x < m.box.x1 {
j := x - m.box.x0
if j < len(row) { v = row[j] }
}
switch v {
case 255:
case 0:
buf[i] = 0
case:
buf[i] = u8((int(buf[i]) * int(v) + 127) / 255)
}
}
}
// rdev_mask_window materialises one plane as a bitmap covering exactly `box`,
// which is what `blend_bitmap` wants: a mask aligned with the source it
// modulates. The plane's `outside` value fills whatever `box` reaches beyond
// it, so a luminosity mask whose form covers only part of the page still erases
// the rest. The result is owned by the caller.
@(private)
rdev_mask_window :: proc(rd: ^Raster_Device, m: ^Rdev_Mask, box: Raster_Clip) -> (Bitmap, bool) {
if m == nil || rdev_clip_empty(box) { return {}, false }
out, err := bitmap_make(box.x1 - box.x0, box.y1 - box.y0, 1, rd.allocator)
if err != .None {
// Without the plane the group is composited unmasked. Showing content
// is the better failure, but it is still a failure worth recording.
rd.err = err
return {}, false
}
for y in 0 ..< out.height {
row := bitmap_row(&out, y)
if row == nil { continue }
for i in 0 ..< len(row) { row[i] = 255 }
rdev_mask_apply(m, row, box.x0, box.y0 + y)
}
return out, true
}
// rdev_mask_row builds the combined mask for one run of target pixels, or
// returns nil when nothing masks it -- which is the common case and the one
// worth not paying for. The slice is scratch and is only valid until the next
// call.
@(private)
rdev_mask_row :: proc(rd: ^Raster_Device, sm: ^Rdev_Mask, y, x0, count: int) -> []u8 {
if count <= 0 || !rdev_has_masks(rd, sm) { return nil }
if len(rd.scratch) < count { return nil }
buf := rd.scratch[:count]
for i in 0 ..< count { buf[i] = 255 }
px0 := x0 + rd.org_x
py := y + rd.org_y
for i in rd.mask_base ..< len(rd.masks) {
rdev_mask_apply(&rd.masks[i], buf, px0, py)
}
if sm != nil {
rdev_mask_apply(sm, buf, px0, py)
}
return buf
}
// ---------------------------------------------------------------------------
// Clip stack
// ---------------------------------------------------------------------------
// rdev_push_entry reserves one stack slot, or reports that the stack is full.
// A refused push is counted so that the pop it is paired with is refused too:
// popping somebody else's entry would restore the wrong scissor for the rest
// of the page.
@(private)
rdev_push_entry :: proc(rd: ^Raster_Device) -> bool {
if len(rd.clips) >= RDEV_MAX_CLIPS {
rd.clip_over += 1
return false
}
append(&rd.clips, Rdev_Clip{scissor = rd.scissor, n_masks = len(rd.masks)})
return true
}
@(private)
rdev_push_clip_box :: proc(rd: ^Raster_Device, box: Raster_Clip) {
if !rdev_push_entry(rd) { return }
rd.scissor = rdev_clip_intersect(rd.scissor, box)
}
// rdev_mask_budget_ok reports whether one more coverage plane of `box` fits.
// The caller degrades to a box clip when it does not.
@(private)
rdev_mask_budget_ok :: proc(rd: ^Raster_Device, box: Raster_Clip) -> bool {
if len(rd.masks) >= RDEV_MAX_CLIP_MASKS { return false }
w := box.x1 - box.x0
h := box.y1 - box.y0
if w <= 0 || h <= 0 { return true }
if w > RDEV_MAX_CLIP_MASK_BYTES / h { return false }
return rd.mask_bytes + w * h <= RDEV_MAX_CLIP_MASK_BYTES
}
@(private)
rdev_push_clip_mask :: proc(rd: ^Raster_Device, m: Rdev_Mask) {
m := m
if !rdev_push_entry(rd) {
bitmap_destroy(&m.bmp)
return
}
if m.outside == 0 {
rd.scissor = rdev_clip_intersect(rd.scissor, m.box)
}
rd.mask_bytes += len(m.bmp.pixels)
append(&rd.masks, m)
}
@(private)
rdev_pop_entry :: proc(rd: ^Raster_Device) {
if rd.clip_over > 0 {
rd.clip_over -= 1
return
}
if len(rd.clips) == 0 { return }
e := pop(&rd.clips)
rd.scissor = e.scissor
for len(rd.masks) > e.n_masks {
m := pop(&rd.masks)
rd.mask_bytes = max(0, rd.mask_bytes - len(m.bmp.pixels))
bitmap_destroy(&m.bmp)
}
rd.mask_base = min(rd.mask_base, len(rd.masks))
}
// ---------------------------------------------------------------------------
// Painting: solid colour
// ---------------------------------------------------------------------------
@(private)
Rdev_Solid_Span :: struct {
rd: ^Raster_Device,
rgba: [4]f64,
mode: Blend_Mode,
sm: ^Rdev_Mask,
}
@(private)
rdev_span_solid :: proc(user: rawptr, y: int, x0: int, coverage: []u8) {
c := (^Rdev_Solid_Span)(user)
m := rdev_mask_row(c.rd, c.sm, y, x0, len(coverage))
blend_span(c.rd.target, y, x0, coverage, c.rgba, c.mode, m)
}
// rdev_paint_rgba is the paint's colour and constant alpha, clamped. A /None
// separation marks nothing at all on the page (ISO 32000-2, 8.6.6.4), which is
// spelled here as zero alpha so every caller gets it for free.
@(private)
rdev_paint_rgba :: proc(paint: Paint) -> [4]f64 {
if paint.cs != nil && paint.cs.is_none {
return {0, 0, 0, 0}
}
return {
blend_clamp01(paint.rgb[0]),
blend_clamp01(paint.rgb[1]),
blend_clamp01(paint.rgb[2]),
blend_clamp01(paint.alpha),
}
}
// rdev_fill_flat is where every solid-colour paint in this file ends up:
// coverage from `raster_fill`, modulated by the clip masks and the soft mask,
// composited by `blend_span`.
@(private)
rdev_fill_flat :: proc(rd: ^Raster_Device, fp: ^Flat_Path, even_odd: bool, paint: Paint) {
rgba := rdev_paint_rgba(paint)
if rgba[3] <= 0 { return }
sc := rdev_scissor(rd)
if rdev_clip_empty(sc) { return }
ctx := Rdev_Solid_Span{
rd = rd,
rgba = rgba,
mode = paint.blend,
sm = rdev_smask_for(rd, paint),
}
// With a mask in force the span needs a scratch row, and painting unmasked
// because there was none would show what should have been hidden. The row
// is sized to the page once and no span can be wider than that, so this is
// a backstop rather than a path the device is expected to take.
if rdev_has_masks(rd, ctx.sm) && len(rd.scratch) < rdev_clip_width(sc) {
rd.err = .Out_Of_Memory
return
}
if err := raster_fill(fp, even_odd, sc, rdev_span_solid, &ctx, rd.allocator); err != .None {
rd.err = err
}
}
// ---------------------------------------------------------------------------
// Building a clip mask
// ---------------------------------------------------------------------------
@(private)
Rdev_Mask_Span :: struct {
rd: ^Raster_Device,
box: Raster_Clip, // page device space
bmp: ^Bitmap,
}
@(private)
rdev_span_mask :: proc(user: rawptr, y: int, x0: int, coverage: []u8) {
c := (^Rdev_Mask_Span)(user)
py := y + c.rd.org_y
if py < c.box.y0 || py >= c.box.y1 { return }
row := bitmap_row(c.bmp, py - c.box.y0)
if row == nil { return }
px0 := x0 + c.rd.org_x
for i in 0 ..< len(coverage) {
j := px0 + i - c.box.x0
if j < 0 || j >= len(row) { continue }
// `max` rather than assignment: `raster_fill` never overlaps its own
// spans, but a stencil clip rasterises the same box twice.
row[j] = max(row[j], coverage[i])
}
}
// rdev_mask_alloc makes an empty coverage plane for `box`, in page space.
@(private)
rdev_mask_alloc :: proc(rd: ^Raster_Device, box: Raster_Clip) -> (Rdev_Mask, bool) {
if rdev_clip_empty(box) { return {}, false }
bmp, err := bitmap_make(box.x1 - box.x0, box.y1 - box.y0, 1, rd.allocator)
if err != .None {
rd.err = err
return {}, false
}
return Rdev_Mask{box = box, bmp = bmp}, true
}
// rdev_flat_is_rect reports whether a flattened path is one axis-aligned
// rectangle, and gives it back. This is the test behind the scissor fast path:
// it runs on device-space points, so `re` under any rectilinear CTM -- a
// scale, a flip, a `/Rotate 90` page -- is recognised, and so is a rectangle
// written out as four `l` operators.
@(private)
rdev_flat_is_rect :: proc(fp: ^Flat_Path) -> (Rect, bool) {
if fp == nil || len(fp.subpaths) != 1 { return {}, false }
pts := flat_path_slice(fp, 0)
// Four corners, or five with the first repeated by an explicit close.
if len(pts) == 5 {
if pts[4] != pts[0] { return {}, false }
pts = pts[:4]
}
if len(pts) != 4 { return {}, false }
for p in pts {
if !is_finite(p.x) || !is_finite(p.y) { return {}, false }
}
// Consecutive corners must alternate between sharing an x and sharing a y.
for i in 0 ..< 4 {
a := pts[i]
b := pts[(i + 1) & 3]
dx := abs(a.x - b.x)
dy := abs(a.y - b.y)
if dx > RDEV_RECT_TOL && dy > RDEV_RECT_TOL { return {}, false }
}
r := rect_normalize(Rect{pts[0].x, pts[0].y, pts[2].x, pts[2].y})
// The diagonal alone does not prove it is a rectangle: a degenerate
// "bowtie" shares both corners. Requiring the other two on the box, and
// requiring some area, does.
for p in pts {
near_x := abs(p.x - r.x0) <= RDEV_RECT_TOL || abs(p.x - r.x1) <= RDEV_RECT_TOL
near_y := abs(p.y - r.y0) <= RDEV_RECT_TOL || abs(p.y - r.y1) <= RDEV_RECT_TOL
if !near_x || !near_y { return {}, false }
}
if pts[0].x == pts[2].x || pts[0].y == pts[2].y { return {}, false }
return r, true
}
// rdev_push_clip_flat pushes one clip entry for an already-flattened shape:
// a scissor when it is an axis-aligned rectangle, a coverage plane otherwise.
@(private)
rdev_push_clip_flat :: proc(rd: ^Raster_Device, fp: ^Flat_Path, even_odd: bool) {
if r, is_rect := rdev_flat_is_rect(fp); is_rect {
page_r := Rect{
r.x0 + f64(rd.org_x), r.y0 + f64(rd.org_y),
r.x1 + f64(rd.org_x), r.y1 + f64(rd.org_y),
}
rdev_push_clip_box(rd, rdev_box_round(page_r, rd.base_clip))
return
}
b := flat_path_bounds(fp)
page_b := Rect{
b.x0 + f64(rd.org_x), b.y0 + f64(rd.org_y),
b.x1 + f64(rd.org_x), b.y1 + f64(rd.org_y),
}
box := rdev_clip_intersect(rd.scissor, rdev_box_outward(page_b, rd.base_clip))
if !rdev_mask_budget_ok(rd, box) {
// Out of plane budget. The bounding box still hides everything outside
// the shape's extent, which is the most that can be kept for free.
rdev_push_clip_box(rd, box)
return
}
m, ok := rdev_mask_alloc(rd, box)
if !ok {
// No plane: clip everything away rather than nothing. A clip that
// fails open would leak the whole page through a shape.
rdev_push_clip_box(rd, Raster_Clip{})
return
}
ctx := Rdev_Mask_Span{rd = rd, box = box, bmp = &m.bmp}
sc := rdev_clip_intersect(rdev_scissor(rd), Raster_Clip{
box.x0 - rd.org_x, box.y0 - rd.org_y,
box.x1 - rd.org_x, box.y1 - rd.org_y,
})
if !rdev_clip_empty(sc) {
if err := raster_fill(fp, even_odd, sc, rdev_span_mask, &ctx, rd.allocator); err != .None {
rd.err = err
}
}
rdev_push_clip_mask(rd, m)
}
// ---------------------------------------------------------------------------
// Paths
// ---------------------------------------------------------------------------
// rdev_flatten flattens into the device's scratch path, through the clip, and
// records the damage when the result had to be truncated. MAX_FLATTEN_POINTS
// draws part of a shape no viewer could have drawn all of, which is a repair
// in the same sense every other clamp in the library is: the caller reading
// `doc.repairs` is entitled to know that what came out is not what the file
// asked for.
@(private)
rdev_flatten :: proc(rd: ^Raster_Device, path: ^Path, m: Matrix) {
flat_path_clear(&rd.fp)
path_flatten(&rd.fp, path, m, FLATNESS, rdev_cull(rd))
if len(rd.fp.points) >= MAX_FLATTEN_POINTS {
add_repair(rd.doc, .Content_Syntax, 0)
}
}
@(private)
rd_fill_path :: proc(dev: ^Device, path: ^Path, even_odd: bool, ctm: Matrix, paint: Paint) {
rd := rdev_from(dev)
if !rdev_ready(rd) || path == nil { return }
m := rdev_ctm(rd, ctm)
if !matrix_is_finite(m) { return }
rdev_flatten(rd, path, m)
if paint.pattern != nil {
rdev_fill_pattern(rd, &rd.fp, even_odd, ctm, paint)
return
}
rdev_fill_flat(rd, &rd.fp, even_odd, paint)
}
@(private)
rd_stroke_path :: proc(dev: ^Device, path: ^Path, stroke: ^Stroke_State, ctm: Matrix, paint: Paint) {
rd := rdev_from(dev)
if !rdev_ready(rd) || path == nil { return }
m := rdev_ctm(rd, ctm)
if !matrix_is_finite(m) { return }
flat_path_clear(&rd.fp)
if err := stroke_flatten(&rd.fp, path, stroke, m, FLATNESS, rdev_cull(rd)); err != .None {
rd.err = err
return
}
// A stroke outline is always filled nonzero: the two sides of the pen
// overlap at every join that doubles back, and even-odd would punch holes
// in exactly those places.
if paint.pattern != nil {
rdev_fill_pattern(rd, &rd.fp, false, ctm, paint)
return
}
rdev_fill_flat(rd, &rd.fp, false, paint)
}
@(private)
rd_clip_path :: proc(dev: ^Device, path: ^Path, even_odd: bool, ctm: Matrix) {
rd := rdev_from(dev)
if rd == nil { return }
if rd.suppress > 0 || rd.target == nil || path == nil {
rdev_push_entry(rd)
return
}
m := rdev_ctm(rd, ctm)
if !matrix_is_finite(m) {
rdev_push_clip_box(rd, Raster_Clip{})
return
}
rdev_flatten(rd, path, m)
rdev_push_clip_flat(rd, &rd.fp, even_odd)
}
@(private)
rd_clip_stroke_path :: proc(dev: ^Device, path: ^Path, stroke: ^Stroke_State, ctm: Matrix) {
rd := rdev_from(dev)
if rd == nil { return }
if rd.suppress > 0 || rd.target == nil || path == nil {
rdev_push_entry(rd)
return
}
m := rdev_ctm(rd, ctm)
if !matrix_is_finite(m) {
rdev_push_clip_box(rd, Raster_Clip{})
return
}
flat_path_clear(&rd.fp)
if err := stroke_flatten(&rd.fp, path, stroke, m, FLATNESS, rdev_cull(rd)); err != .None {
rd.err = err
rdev_push_clip_box(rd, Raster_Clip{})
return
}
rdev_push_clip_flat(rd, &rd.fp, false)
}
@(private)
rd_pop_clip :: proc(dev: ^Device) {
rd := rdev_from(dev)
if rd == nil { return }
rdev_pop_entry(rd)
}
// ---------------------------------------------------------------------------
// Text
//
// Every glyph of one showing operator goes into a single Flat_Path and is
// filled once. `Text_Item.trm` maps the glyph outline that `font_glyph_path`
// produces straight to device space -- it already folds in the font matrix,
// the text matrix, the size, the horizontal scale, the rise and the CTM -- so
// the `ctm` argument is only needed for the layer offset.
// ---------------------------------------------------------------------------
// rdev_text_outlines accumulates the glyph outlines of one showing operator
// into the scratch flat path.
//
// `drew` is whether any outline came out. `unknown` is whether any glyph's
// shape could not be determined at all -- a font with no usable program, or a
// Type3 font, whose glyphs are content streams the interpreter runs rather
// than outlines the device can have. Only the clip hook needs the distinction,
// and it needs it badly: clipping to a shape nobody knows must reveal, not
// hide.
@(private)
rdev_text_outlines :: proc(rd: ^Raster_Device, text: ^Text) -> (drew, unknown: bool) {
flat_path_clear(&rd.fp)
if text == nil || text.font == nil { return false, false }
if font_is_type3(text.font) { return false, len(text.items) > 0 }
glyph := path_make(rd.allocator, 32)
defer path_destroy(&glyph)
for &it in text.items {
m := rdev_ctm(rd, it.trm)
if !matrix_is_finite(m) { continue }
path_clear(&glyph)
if font_glyph_path(text.font, it.gid, &glyph) != .None {
unknown = true
continue
}
if path_is_empty(&glyph) { continue }
path_flatten(&rd.fp, &glyph, m)
drew = true
}
return
}
@(private)
rd_fill_text :: proc(dev: ^Device, text: ^Text, ctm: Matrix, paint: Paint) {
rd := rdev_from(dev)
if !rdev_ready(rd) { return }
if drew, _ := rdev_text_outlines(rd, text); !drew { return }
if paint.pattern != nil {
rdev_fill_pattern(rd, &rd.fp, false, ctm, paint)
return
}
rdev_fill_flat(rd, &rd.fp, false, paint)
}
@(private)
rd_stroke_text :: proc(dev: ^Device, text: ^Text, stroke: ^Stroke_State, ctm: Matrix, paint: Paint) {
rd := rdev_from(dev)
if !rdev_ready(rd) || text == nil || text.font == nil { return }
if font_is_type3(text.font) { return }
glyph := path_make(rd.allocator, 32)
defer path_destroy(&glyph)
outline := flat_path_make(rd.allocator)
defer flat_path_destroy(&outline)
// `stroke_flatten` wants the path in the space its width is measured in
// and the matrix that takes it to device space. Here the path is in glyph
// units and `it.trm` is that matrix, but the width is a user-space
// quantity, so it is converted by the ratio of the two matrices'
// expansions first -- which is also what keeps the pen elliptical under an
// anisotropic text matrix, exactly as it is for a path. Dashes are
// dropped: an array measured in user space cannot be walked along a path
// measured in thousandths of an em.
user_scale := matrix_expansion(ctm)
if !is_finite(user_scale) { return }
for &it in text.items {
m := rdev_ctm(rd, it.trm)
if !matrix_is_finite(m) { continue }
glyph_scale := matrix_expansion(m)
if !(glyph_scale > 0) || !is_finite(glyph_scale) { continue }
path_clear(&glyph)
if font_glyph_path(text.font, it.gid, &glyph) != .None { continue }
if path_is_empty(&glyph) { continue }
st := stroke != nil ? stroke^ : STROKE_STATE_DEFAULT
st.line_width = st.line_width * user_scale / glyph_scale
st.dash_count = 0
if !is_finite(st.line_width) { continue }
if stroke_flatten(&outline, &glyph, &st, m) != .None { continue }
}
if len(outline.points) == 0 { return }
rdev_fill_flat(rd, &outline, false, paint)
}
@(private)
rd_clip_text :: proc(dev: ^Device, text: ^Text, ctm: Matrix) {
rd := rdev_from(dev)
if rd == nil { return }
if rd.suppress > 0 || rd.target == nil {
rdev_push_entry(rd)
return
}
// `ctm` is not used: `Text_Item.trm` already maps glyph space to device
// space, and applying the CTM again would square it.
_ = ctm
drew, unknown := rdev_text_outlines(rd, text)
if drew {
rdev_push_clip_flat(rd, &rd.fp, false)
return
}
if unknown {
// A font that would not load, or a Type3 one: the shape is not
// knowable here, so the clip lets everything through. Blanking the
// rest of the page over a broken font is a far worse failure than
// showing what the text would have covered.
rdev_push_entry(rd)
return
}
// Genuinely empty text in render mode 7 clips everything away, which is
// what the operator means and is used deliberately to hide content.
rdev_push_clip_box(rd, Raster_Clip{})
}
// ---------------------------------------------------------------------------
// Images
//
// An image always covers the unit square of the current transform, so the
// quadrilateral that square maps to is rasterised -- which gives the edges the
// same analytic anti-aliasing every other shape gets, and bounds the whole
// operation by the clip for free -- and each covered pixel is mapped back
// through the inverse to be sampled.
//
// WHICH FILTER
//
// minified a box filter: the footprint of one device pixel in image space
// is sampled on a grid of up to 16x16 taps spread evenly across
// it. Point-sampling a 4000-pixel photograph into 200 pixels
// throws away 99.75% of it and aliases badly enough to look like
// a decoding bug, and the tap cap is what keeps the cost of that
// bounded by the OUTPUT size rather than the input's.
// magnified bilinear when the image asks for it with /Interpolate, or when
// it is blown up past RDEV_INTERPOLATE_SCALE -- a producer
// stretching a 2x2 image across a page means a gradient, and
// every viewer smooths it.
// otherwise nearest, which is what a magnified image without /Interpolate
// is specified to look like.
//
// Filtering is done on PREMULTIPLIED values and converted back to straight
// afterwards. Averaging straight RGBA would pull the colour of transparent
// pixels -- which `bitmap_unpremultiply` sets to zero and which is therefore
// black -- into every soft edge.
// ---------------------------------------------------------------------------
@(private)
Rdev_Image_Span :: struct {
rd: ^Raster_Device,
src: ^Bitmap,
m: Matrix, // target device space -> image pixel space
alpha: f64,
mode: Blend_Mode,
sm: ^Rdev_Mask,
kind: Rdev_Sampler,
fx, fy: f64, // device-pixel footprint, in image pixels
tx, ty: int, // box taps per axis
rgba: [4]f64, // the fill colour, for a stencil mask
}
// rdev_texel reads one image pixel as PREMULTIPLIED components on a 0..255
// scale, clamping the coordinates to the edge.
@(private)
rdev_texel :: proc(src: ^Bitmap, x, y: int) -> [4]f64 {
cx := clamp(x, 0, src.width - 1)
cy := clamp(y, 0, src.height - 1)
off, ok := bitmap_pixel_offset(src, cx, cy)
if !ok { return {} }
a := f64(src.pixels[off + 3])
return {
f64(src.pixels[off]) * a / 255,
f64(src.pixels[off + 1]) * a / 255,
f64(src.pixels[off + 2]) * a / 255,
a,
}
}
@(private)
rdev_texel_gray :: proc(src: ^Bitmap, x, y: int) -> f64 {
cx := clamp(x, 0, src.width - 1)
cy := clamp(y, 0, src.height - 1)
off, ok := bitmap_pixel_offset(src, cx, cy)
if !ok { return 0 }
return f64(src.pixels[off])
}
@(private)
rdev_unpremul :: proc(acc: [4]f64) -> [4]u8 {
a := acc[3]
if !(a > 0.5) { return {} }
inv := 1 / a
return {
u8(clamp(acc[0] * inv * 255 + 0.5, 0, 255)),
u8(clamp(acc[1] * inv * 255 + 0.5, 0, 255)),
u8(clamp(acc[2] * inv * 255 + 0.5, 0, 255)),
u8(clamp(a + 0.5, 0, 255)),
}
}
// rdev_box_reduce shrinks `src` by the integer factors (kx, ky), averaging
// every source pixel of each block exactly once. Colour is averaged
// PREMULTIPLIED and handed back straight, which is the same convention
// `rdev_texel` and `rdev_unpremul` meet on, so a reduced image samples
// identically to the original.
@(private)
rdev_box_reduce :: proc(src: ^Bitmap, kx, ky: int, allocator: mem.Allocator) -> (Bitmap, bool) {
if src == nil || kx < 1 || ky < 1 { return {}, false }
if src.n != 1 && src.n != 4 { return {}, false }
w := (src.width + kx - 1) / kx
h := (src.height + ky - 1) / ky
if w <= 0 || h <= 0 { return {}, false }
out, err := bitmap_make(w, h, src.n, allocator)
if err != .None { return {}, false }
out.premultiplied = src.premultiplied
for j in 0 ..< h {
row := bitmap_row(&out, j)
if row == nil { continue }
y0 := j * ky
y1 := min(y0 + ky, src.height)
for i in 0 ..< w {
x0 := i * kx
x1 := min(x0 + kx, src.width)
n := (x1 - x0) * (y1 - y0)
if n <= 0 { continue }
if src.n == 1 {
acc := 0.0
for y in y0 ..< y1 {
for x in x0 ..< x1 { acc += rdev_texel_gray(src, x, y) }
}
row[i] = u8(clamp(acc / f64(n) + 0.5, 0, 255))
continue
}
acc: [4]f64
for y in y0 ..< y1 {
for x in x0 ..< x1 {
t := rdev_texel(src, x, y)
acc[0] += t[0]; acc[1] += t[1]; acc[2] += t[2]; acc[3] += t[3]
}
}
inv := 1 / f64(n)
px := rdev_unpremul({acc[0] * inv, acc[1] * inv, acc[2] * inv, acc[3] * inv})
off := i * 4
row[off] = px[0]; row[off + 1] = px[1]; row[off + 2] = px[2]; row[off + 3] = px[3]
}
}
return out, true
}
// rdev_mip_for gives back `src` reduced by (kx, ky), building it if the cache
// does not already hold it, or nil when the page's reduction budget is spent.
@(private)
rdev_mip_for :: proc(rd: ^Raster_Device, src: ^Bitmap, kx, ky: int) -> ^Bitmap {
for i in 0 ..< RDEV_MAX_MIPS {
m := &rd.mips[i]
if m.src == src && m.kx == kx && m.ky == ky && m.bmp.width > 0 {
return &m.bmp
}
}
cost := src.width * src.height
if cost > rd.mip_budget { return nil }
red, ok := rdev_box_reduce(src, kx, ky, rd.allocator)
if !ok { return nil }
rd.mip_budget -= cost
slot := rd.mip_next
rd.mip_next = (rd.mip_next + 1) % RDEV_MAX_MIPS
bitmap_destroy(&rd.mips[slot].bmp)
rd.mips[slot] = Rdev_Mip{src = src, kx = kx, ky = ky, bmp = red}
return &rd.mips[slot].bmp
}
@(private)
rdev_sample_rgba :: proc(c: ^Rdev_Image_Span, px, py: f64) -> [4]u8 {
src := c.src
switch c.kind {
case .Nearest:
t := rdev_texel(src, int(math.floor(px)), int(math.floor(py)))
return rdev_unpremul(t)
case .Bilinear:
fx := px - 0.5
fy := py - 0.5
x0 := int(math.floor(fx))
y0 := int(math.floor(fy))
u := fx - f64(x0)
v := fy - f64(y0)
p00 := rdev_texel(src, x0, y0)
p10 := rdev_texel(src, x0 + 1, y0)
p01 := rdev_texel(src, x0, y0 + 1)
p11 := rdev_texel(src, x0 + 1, y0 + 1)
acc: [4]f64
for i in 0 ..< 4 {
top := p00[i] + (p10[i] - p00[i]) * u
bot := p01[i] + (p11[i] - p01[i]) * u
acc[i] = top + (bot - top) * v
}
return rdev_unpremul(acc)
case .Box:
acc: [4]f64
sx := c.fx / f64(c.tx)
sy := c.fy / f64(c.ty)
oy := py - c.fy * 0.5 + sy * 0.5
for j in 0 ..< c.ty {
yy := int(math.floor(oy + f64(j) * sy))
ox := px - c.fx * 0.5 + sx * 0.5
for i in 0 ..< c.tx {
t := rdev_texel(src, int(math.floor(ox + f64(i) * sx)), yy)
acc[0] += t[0]; acc[1] += t[1]; acc[2] += t[2]; acc[3] += t[3]
}
}
n := f64(c.tx * c.ty)
return rdev_unpremul({acc[0] / n, acc[1] / n, acc[2] / n, acc[3] / n})
}
return {}
}
@(private)
rdev_sample_gray :: proc(c: ^Rdev_Image_Span, px, py: f64) -> u8 {
src := c.src
switch c.kind {
case .Nearest:
return u8(clamp(rdev_texel_gray(src, int(math.floor(px)), int(math.floor(py))), 0, 255))
case .Bilinear:
fx := px - 0.5
fy := py - 0.5
x0 := int(math.floor(fx))
y0 := int(math.floor(fy))
u := fx - f64(x0)
v := fy - f64(y0)
top := rdev_texel_gray(src, x0, y0) +
(rdev_texel_gray(src, x0 + 1, y0) - rdev_texel_gray(src, x0, y0)) * u
bot := rdev_texel_gray(src, x0, y0 + 1) +
(rdev_texel_gray(src, x0 + 1, y0 + 1) - rdev_texel_gray(src, x0, y0 + 1)) * u
return u8(clamp(top + (bot - top) * v + 0.5, 0, 255))
case .Box:
acc := 0.0
sx := c.fx / f64(c.tx)
sy := c.fy / f64(c.ty)
oy := py - c.fy * 0.5 + sy * 0.5
for j in 0 ..< c.ty {
yy := int(math.floor(oy + f64(j) * sy))
ox := px - c.fx * 0.5 + sx * 0.5
for i in 0 ..< c.tx {
acc += rdev_texel_gray(src, int(math.floor(ox + f64(i) * sx)), yy)
}
}
return u8(clamp(acc / f64(c.tx * c.ty) + 0.5, 0, 255))
}
return 0
}
@(private)
rdev_span_image :: proc(user: rawptr, y: int, x0: int, coverage: []u8) {
c := (^Rdev_Image_Span)(user)
rd := c.rd
n := len(coverage)
if n <= 0 || len(rd.pix) < n * 4 { return }
buf := rd.pix[:n * 4]
p := transform_point(c.m, Point{f64(x0) + 0.5, f64(y) + 0.5})
for i in 0 ..< n {
s := rdev_sample_rgba(c, p.x + f64(i) * c.m.a, p.y + f64(i) * c.m.b)
buf[i * 4] = s[0]
buf[i * 4 + 1] = s[1]
buf[i * 4 + 2] = s[2]
buf[i * 4 + 3] = s[3]
}
msk := rdev_mask_row(rd, c.sm, y, x0, n)
blend_span_rgba8(rd.target, y, x0, buf, c.alpha, c.mode, coverage, msk)
}
@(private)
rdev_span_stencil :: proc(user: rawptr, y: int, x0: int, coverage: []u8) {
c := (^Rdev_Image_Span)(user)
rd := c.rd
n := len(coverage)
if n <= 0 || len(rd.pix) < n { return }
cov := rd.pix[:n]
p := transform_point(c.m, Point{f64(x0) + 0.5, f64(y) + 0.5})
for i in 0 ..< n {
s := rdev_sample_gray(c, p.x + f64(i) * c.m.a, p.y + f64(i) * c.m.b)
cov[i] = u8((int(coverage[i]) * int(s) + 127) / 255)
}
msk := rdev_mask_row(rd, c.sm, y, x0, n)
blend_span(rd.target, y, x0, cov, c.rgba, c.mode, msk)
}
// rdev_flat_unit_quad replaces the flattened path with the unit square under
// `m`. Every image covers exactly that square (ISO 32000-2, 8.9.5.2).
@(private)
rdev_flat_unit_quad :: proc(fp: ^Flat_Path, m: Matrix) {
flat_path_clear(fp)
append(&fp.points,
transform_point(m, Point{0, 0}),
transform_point(m, Point{1, 0}),
transform_point(m, Point{1, 1}),
transform_point(m, Point{0, 1}))
append(&fp.subpaths, Flat_Subpath{start = 0, count = 4, closed = true})
}
// rdev_image_span sets up the sampler for one image draw, or reports false
// when the transform makes it invisible.
@(private)
rdev_image_span :: proc(rd: ^Raster_Device, img: ^Image, src: ^Bitmap,
m: Matrix, paint: Paint) -> (Rdev_Image_Span, bool) {
if src == nil || src.width <= 0 || src.height <= 0 { return {}, false }
inv, ok := matrix_invert(m)
if !ok { return {}, false }
// Unit square to image pixels: x right, y DOWN from the top row, which is
// the opposite of the unit square's y.
unit_to_img := Matrix{f64(src.width), 0, 0, -f64(src.height), 0, f64(src.height)}
dev_to_img := matrix_mul(inv, unit_to_img)
if !matrix_is_finite(dev_to_img) { return {}, false }
c := Rdev_Image_Span{
rd = rd,
src = src,
m = dev_to_img,
alpha = blend_clamp01(paint.alpha),
mode = paint.blend,
sm = rdev_smask_for(rd, paint),
rgba = rdev_paint_rgba(paint),
}
// The footprint of one device pixel, summed over both device axes: an
// image rotated 30 degrees is minified along neither axis alone and along
// both together.
c.fx = clamp(abs(dev_to_img.a) + abs(dev_to_img.c), 0, 1e9)
c.fy = clamp(abs(dev_to_img.b) + abs(dev_to_img.d), 0, 1e9)
if !is_finite(c.fx) || !is_finite(c.fy) { return {}, false }
// Past RDEV_MAX_TAPS the taps would no longer cover the footprint, so the
// source is reduced by an integer factor until they do. Everything below
// then sees a smaller image and a proportionally smaller footprint.
if c.fx > RDEV_MAX_TAPS || c.fy > RDEV_MAX_TAPS {
kx := max(1, int(math.ceil(c.fx / RDEV_MAX_TAPS)))
ky := max(1, int(math.ceil(c.fy / RDEV_MAX_TAPS)))
if red := rdev_mip_for(rd, src, kx, ky); red != nil {
c.src = red
c.m = matrix_mul(c.m, Matrix{1 / f64(kx), 0, 0, 1 / f64(ky), 0, 0})
c.fx /= f64(kx)
c.fy /= f64(ky)
}
}
mag := 1 / max(c.fx, c.fy, 1e-12)
switch {
case c.fx > 1.0001 || c.fy > 1.0001:
c.kind = .Box
case img != nil && img.interpolate, mag >= RDEV_INTERPOLATE_SCALE:
c.kind = .Bilinear
case:
c.kind = .Nearest
}
c.tx = clamp(int(math.ceil(c.fx)), 1, RDEV_MAX_TAPS)
c.ty = clamp(int(math.ceil(c.fy)), 1, RDEV_MAX_TAPS)
for c.tx * c.ty > RDEV_MAX_TAP_AREA {
if c.tx >= c.ty { c.tx -= 1 } else { c.ty -= 1 }
}
return c, true
}
@(private)
rd_fill_image :: proc(dev: ^Device, img: ^Image, ctm: Matrix, paint: Paint) {
rd := rdev_from(dev)
if !rdev_ready(rd) || img == nil || rd.doc == nil { return }
if img.is_mask {
rd_fill_image_mask(dev, img, ctm, paint)
return
}
if blend_clamp01(paint.alpha) <= 0 { return }
m := rdev_ctm(rd, ctm)
if !matrix_is_finite(m) { return }
sc := rdev_scissor(rd)
if rdev_clip_empty(sc) { return }
src, err := image_decode(rd.doc, img)
if err != .None || src == nil { rd.err = err; return }
c, ok := rdev_image_span(rd, img, src, m, paint)
if !ok || src.n != 4 { return }
if rdev_has_masks(rd, c.sm) && len(rd.scratch) < rdev_clip_width(sc) {
rd.err = .Out_Of_Memory
return
}
rdev_flat_unit_quad(&rd.fp, m)
if e := raster_fill(&rd.fp, false, sc, rdev_span_image, &c, rd.allocator); e != .None {
rd.err = e
}
}
@(private)
rd_fill_image_mask :: proc(dev: ^Device, img: ^Image, ctm: Matrix, paint: Paint) {
rd := rdev_from(dev)
if !rdev_ready(rd) || img == nil || rd.doc == nil { return }
m := rdev_ctm(rd, ctm)
if !matrix_is_finite(m) { return }
sc := rdev_scissor(rd)
if rdev_clip_empty(sc) { return }
src, err := image_decode(rd.doc, img)
if err != .None || src == nil { rd.err = err; return }
c, ok := rdev_image_span(rd, img, src, m, paint)
if !ok || src.n != 1 { return }
if c.rgba[3] <= 0 { return }
if rdev_has_masks(rd, c.sm) && len(rd.scratch) < rdev_clip_width(sc) {
rd.err = .Out_Of_Memory
return
}
rdev_flat_unit_quad(&rd.fp, m)
if e := raster_fill(&rd.fp, false, sc, rdev_span_stencil, &c, rd.allocator); e != .None {
rd.err = e
}
}
@(private)
rd_clip_image_mask :: proc(dev: ^Device, img: ^Image, ctm: Matrix) {
rd := rdev_from(dev)
if rd == nil { return }
if rd.suppress > 0 || rd.target == nil || img == nil || rd.doc == nil {
rdev_push_entry(rd)
return
}
m := rdev_ctm(rd, ctm)
src, err := image_decode(rd.doc, img)
if !matrix_is_finite(m) || err != .None || src == nil || src.n != 1 {
rdev_push_clip_box(rd, Raster_Clip{})
return
}
rdev_flat_unit_quad(&rd.fp, m)
b := flat_path_bounds(&rd.fp)
page_b := Rect{
b.x0 + f64(rd.org_x), b.y0 + f64(rd.org_y),
b.x1 + f64(rd.org_x), b.y1 + f64(rd.org_y),
}
box := rdev_clip_intersect(rd.scissor, rdev_box_outward(page_b, rd.base_clip))
if !rdev_mask_budget_ok(rd, box) {
rdev_push_clip_box(rd, box)
return
}
mask, ok := rdev_mask_alloc(rd, box)
if !ok {
rdev_push_clip_box(rd, Raster_Clip{})
return
}
// The stencil is rasterised into the plane the same way a path clip is,
// then multiplied by its own coverage: `rdev_span_stencil` composites, so
// it cannot be reused here.
ctx := Rdev_Mask_Span{rd = rd, box = box, bmp = &mask.bmp}
sc := rdev_clip_intersect(rdev_scissor(rd), Raster_Clip{
box.x0 - rd.org_x, box.y0 - rd.org_y,
box.x1 - rd.org_x, box.y1 - rd.org_y,
})
if !rdev_clip_empty(sc) {
if e := raster_fill(&rd.fp, false, sc, rdev_span_mask, &ctx, rd.allocator); e != .None {
rd.err = e
}
c, span_ok := rdev_image_span(rd, img, src, m, Paint{alpha = 1})
if span_ok {
rdev_mask_multiply_stencil(&mask, &c, rd)
}
}
rdev_push_clip_mask(rd, mask)
}
// rdev_mask_multiply_stencil multiplies an already-rasterised coverage plane
// by the stencil's own samples, so the clip is the intersection of the unit
// square's shape and the bits that are set inside it.
@(private)
rdev_mask_multiply_stencil :: proc(mask: ^Rdev_Mask, c: ^Rdev_Image_Span, rd: ^Raster_Device) {
for j in 0 ..< mask.bmp.height {
row := bitmap_row(&mask.bmp, j)
if row == nil { continue }
// The plane is in page space; the sampler wants target space.
dy := f64(mask.box.y0 + j - rd.org_y) + 0.5
dx := f64(mask.box.x0 - rd.org_x) + 0.5
p := transform_point(c.m, Point{dx, dy})
for i in 0 ..< len(row) {
if row[i] == 0 { continue }
s := rdev_sample_gray(c, p.x + f64(i) * c.m.a, p.y + f64(i) * c.m.b)
row[i] = u8((int(row[i]) * int(s) + 127) / 255)
}
}
}
// ---------------------------------------------------------------------------
// Shadings and patterns
// ---------------------------------------------------------------------------
// rdev_full_mask materialises the combined clip and soft mask as a plane the
// size of the whole target, which is the shape `shading_paint` takes.
//
// It is only built when something actually masks the paint. A shading under a
// non-rectangular clip is the one place in this file where a page-sized
// temporary is allocated, and it is bounded by the target rather than by
// anything a file controls.
@(private)
rdev_full_mask :: proc(rd: ^Raster_Device, sm: ^Rdev_Mask, sc: Raster_Clip) -> (Bitmap, bool) {
if !rdev_has_masks(rd, sm) || rdev_clip_empty(sc) { return {}, false }
out, err := bitmap_make(rd.target.width, rd.target.height, 1, rd.allocator)
if err != .None {
rd.err = err
return {}, false
}
w := sc.x1 - sc.x0
for y in sc.y0 ..< sc.y1 {
row := bitmap_row(&out, y)
if row == nil { continue }
buf := rdev_mask_row(rd, sm, y, sc.x0, w)
if buf == nil {
for x in sc.x0 ..< sc.x1 { row[x] = 255 }
continue
}
copy(row[sc.x0:sc.x1], buf)
}
return out, true
}
@(private)
rd_fill_shade :: proc(dev: ^Device, shade: ^Shading, ctm: Matrix, paint: Paint) {
rd := rdev_from(dev)
if !rdev_ready(rd) || shade == nil || rd.doc == nil { return }
m := rdev_ctm(rd, ctm)
if !matrix_is_finite(m) { return }
sc := rdev_scissor(rd)
if rdev_clip_empty(sc) { return }
sm := rdev_smask_for(rd, paint)
mb, have := rdev_full_mask(rd, sm, sc)
defer if have { bitmap_destroy(&mb) }
if err := shading_paint(rd.doc, shade, rd.target, m, sc,
paint.alpha, paint.blend, have ? &mb : nil); err != .None {
rd.err = err
}
}
// rdev_fill_pattern paints a shape whose Paint names a pattern.
//
// The interpreter is expected to resolve patterns itself -- it is the only
// thing that knows the default user space a pattern's /Matrix is relative to
// (ISO 32000-2, 8.7.3.1), and tiling needs it to replay the cell's content
// stream. This is the fallback for a Paint that reaches the device with a
// pattern still on it: the shape becomes a clip, the shading is painted
// through it, and the CTM of the fill stands in for the default-space CTM,
// which is exactly right in the common case where the pattern is used without
// an intervening `cm` and wrong by that `cm` otherwise. A tiling pattern
// cannot be approximated this way at all and paints nothing.
@(private)
rdev_fill_pattern :: proc(rd: ^Raster_Device, fp: ^Flat_Path, even_odd: bool,
ctm: Matrix, paint: Paint) {
p := paint.pattern
if p == nil || rd.doc == nil || p.kind != .Shading { return }
if blend_clamp01(paint.alpha) <= 0 { return }
sm := rdev_smask_for(rd, paint)
rdev_push_clip_flat(rd, fp, even_odd)
defer rdev_pop_entry(rd)
sc := rdev_scissor(rd)
if rdev_clip_empty(sc) { return }
mb, have := rdev_full_mask(rd, sm, sc)
defer if have { bitmap_destroy(&mb) }
if err := pattern_paint_shading(rd.doc, p, rd.target, rdev_ctm(rd, ctm), sc,
paint.alpha, paint.blend, have ? &mb : nil); err != .None {
rd.err = err
}
}
// ---------------------------------------------------------------------------
// Layers: transparency groups and soft masks
// ---------------------------------------------------------------------------
// rdev_backdrop_copy seeds a non-isolated group with what is already on the
// page underneath it, converting between component counts as it goes.
//
// The layer is PREMULTIPLIED like every bitmap below the Device line, so the
// straight colour `blend_read_straight` gives back has to be multiplied by its
// alpha again before it is stored. Writing it straight is invisible wherever
// the backdrop is opaque -- premultiplied and straight agree at alpha 1 -- and
// wrong everywhere else, which is the same set of cases the composite below
// used to get wrong.
@(private)
rdev_backdrop_copy :: proc(dst, src: ^Bitmap, sx, sy: int) {
dn := dst.n
sn := src.n
for y in 0 ..< dst.height {
drow := bitmap_row(dst, y)
srow := bitmap_row(src, sy + y)
if drow == nil { continue }
for x in 0 ..< dst.width {
doff := x * dn
if doff + dn > len(drow) { break }
if srow == nil { continue }
soff := (sx + x) * sn
if sx + x < 0 || soff + sn > len(srow) { continue }
c, a := blend_read_straight(srow, soff, sn)
cs := blend_source_for(dn, c)
blend_write_premul(drow, doff, dn, [3]f64{cs[0] * a, cs[1] * a, cs[2] * a}, a)
}
}
}
@(private)
rdev_layer_push :: proc(rd: ^Raster_Device, layer: Rdev_Layer, area: Rect, isolated: bool) {
l := layer
l.prev_scissor = rd.scissor
l.prev_mask_base = rd.mask_base
l.ok = false
// A mask's form is positioned by the CTM the ExtGState installed it under
// and is not subject to the clip in force where it is used, so it is
// bounded by the page rather than by the current scissor.
bound := l.kind == .Mask ? rd.base_clip : rd.scissor
box := rdev_clip_intersect(bound, rdev_box_outward(area, rd.base_clip))
if len(rd.layers) >= RDEV_MAX_LAYERS || rdev_clip_empty(box) || rd.target == nil {
append(&rd.layers, l)
rd.suppress += 1
return
}
// A group always carries alpha even when the page does not, or its
// contents would composite back as if the uncovered part were opaque.
n := l.kind == .Mask ? 4 : (rd.target.n <= 2 ? 2 : 4)
bmp, err := bitmap_make(box.x1 - box.x0, box.y1 - box.y0, n, rd.allocator)
if err != .None {
rd.err = err
append(&rd.layers, l)
rd.suppress += 1
return
}
bmp.premultiplied = true
l.bmp = bmp
l.box = box
l.ok = true
// A non-isolated group starts with a copy of its backdrop. That is only
// sound while the group's own alpha is 1 and its blend mode Normal --
// otherwise the backdrop would be counted twice -- and `rdev_layer_finish`
// must then put the layer BACK rather than composite it over, or the
// backdrop's own alpha is counted twice instead: `C over C` with Normal
// takes a pixel at alpha 0.5 to 0.75. See the note there.
if l.kind == .Group && !isolated && !l.knockout &&
l.blend == .Normal && l.alpha >= 1 {
rdev_backdrop_copy(&l.bmp, rd.target, box.x0 - rd.org_x, box.y0 - rd.org_y)
l.seeded = true
}
append(&rd.layers, l)
if l.kind == .Mask {
rd.scissor = box
rd.mask_base = len(rd.masks)
rd.smask_suspended = true
}
rdev_retarget(rd)
}
// rdev_layer_finish pops the topmost layer and does whatever that layer is
// for. It switches on the layer's own recorded kind rather than on which hook
// called it, so a content stream that closes a group with the wrong operator
// unwinds instead of corrupting the stack.
@(private)
rdev_layer_finish :: proc(rd: ^Raster_Device, transfer: ^Function) {
if len(rd.layers) == 0 { return }
l := pop(&rd.layers)
rd.scissor = l.prev_scissor
rd.mask_base = min(l.prev_mask_base, len(rd.masks))
if !l.ok {
rd.suppress = max(0, rd.suppress - 1)
if l.kind == .Mask {
// A mask that could not be rendered masks nothing: the paint that
// was going to use it finds no plane for its Soft_Mask and is
// drawn unmasked. Showing content is the better failure.
rd.smask_suspended = false
}
rdev_retarget(rd)
return
}
rdev_retarget(rd)
switch l.kind {
case .Group:
// 11.6.6: the outer soft mask applies to the group's RESULT. The
// interpreter cleared it for the contents, so if it were not applied
// here it would not be applied anywhere -- which is how a gradient
// fade over a transparency group used to render as a solid shape.
win, has_win := rdev_mask_window(rd, rdev_smask_lookup(rd, l.soft_mask), l.box)
defer if has_win { bitmap_destroy(&win) }
if l.seeded {
// The layer already CONTAINS the backdrop, so compositing it over
// the backdrop again would blend the backdrop with itself. Putting
// it back is what "contents over backdrop" means, and interpolating
// towards the backdrop is exactly what the mask asks for -- Normal
// at alpha 1 is linear in the source alpha, so the two agree.
blend_bitmap_restore(rd.target, &l.bmp,
l.box.x0 - rd.org_x, l.box.y0 - rd.org_y,
has_win ? &win : nil)
} else {
blend_bitmap(rd.target, &l.bmp,
l.box.x0 - rd.org_x, l.box.y0 - rd.org_y,
l.alpha, l.blend, has_win ? &win : nil)
}
bitmap_destroy(&l.bmp)
case .Mask:
plane, err := mask_from_bitmap(&l.bmp, l.luminosity, l.backdrop, transfer, rd.allocator)
bitmap_destroy(&l.bmp)
rd.smask_suspended = false
if err != .None {
rd.err = err
return
}
// Outside the mask's own area the value is what an unpainted pixel
// inside it would have been: the backdrop's luminosity for a
// luminosity mask, zero alpha for an alpha one, both through /TR.
lut := blend_transfer_table(transfer)
rdev_smask_install(rd, l.owner, Rdev_Mask{
box = l.box,
bmp = plane,
outside = l.luminosity ? lut[blend_quantize(blend_lum(l.backdrop))] : lut[0],
})
}
}
@(private)
rd_begin_group :: proc(dev: ^Device, area: Rect, isolated, knockout: bool,
blend: Blend_Mode, alpha: f64, soft_mask: ^Soft_Mask) {
rd := rdev_from(dev)
if rd == nil { return }
rdev_layer_push(rd, Rdev_Layer{
kind = .Group,
alpha = blend_clamp01(alpha),
blend = blend,
knockout = knockout,
soft_mask = soft_mask,
}, area, isolated)
}
@(private)
rd_end_group :: proc(dev: ^Device) {
rd := rdev_from(dev)
if rd == nil { return }
rdev_layer_finish(rd, nil)
}
@(private)
rd_begin_mask :: proc(dev: ^Device, area: Rect, luminosity: bool, backdrop: Paint) {
rd := rdev_from(dev)
if rd == nil { return }
bd := [3]f64{
blend_clamp01(backdrop.rgb[0]),
blend_clamp01(backdrop.rgb[1]),
blend_clamp01(backdrop.rgb[2]),
}
rdev_layer_push(rd, Rdev_Layer{
kind = .Mask,
alpha = 1,
luminosity = luminosity,
backdrop = bd,
}, area, true)
// Even when the layer failed, the mask's form must not be masked by
// whatever was in force where the ExtGState appeared.
rd.smask_suspended = true
}
@(private)
rd_end_mask :: proc(dev: ^Device, mask: ^Soft_Mask, transfer: ^Function) {
rd := rdev_from(dev)
if rd == nil { return }
if len(rd.layers) > 0 {
rd.layers[len(rd.layers) - 1].owner = mask
}
rdev_layer_finish(rd, transfer)
}
// ---------------------------------------------------------------------------
// Tiling patterns
//
// The device clips to the area and answers with the number of cells that meet
// it, which is `device.odin`'s "a device that cannot tile returns a count and
// lets the interpreter place each copy". Replaying the cell is strictly better
// than stamping a pre-rendered one: a rotated or skewed pattern matrix would
// otherwise resample an already-rasterised tile and show seams between the
// copies, and the interpreter is the only thing that can run a content stream
// at all.
// ---------------------------------------------------------------------------
@(private)
rd_begin_tile :: proc(dev: ^Device, area, view: Rect, xstep, ystep: f64, ctm: Matrix) -> int {
rd := rdev_from(dev)
if rd == nil { return 0 }
rdev_push_clip_box(rd, rdev_box_outward(area, rd.base_clip))
if rd.suppress > 0 { return 0 }
if !rect_is_finite(area) || !rect_is_finite(view) || !matrix_is_finite(ctm) { return 0 }
if !(xstep > 0) || !(ystep > 0) || !is_finite(xstep) || !is_finite(ystep) { return 0 }
inv, ok := matrix_invert(ctm)
if !ok { return 0 }
pa := transform_rect(inv, area)
if !rect_is_finite(pa) { return 0 }
i0, i1, okx := tile_range(pa.x0, pa.x1, view.x0, view.x1, xstep)
j0, j1, oky := tile_range(pa.y0, pa.y1, view.y0, view.y1, ystep)
if !okx || !oky { return 0 }
nx := i1 - i0 + 1
ny := j1 - j0 + 1
if nx <= 0 || ny <= 0 { return 0 }
if f64(nx) * f64(ny) > f64(MAX_PATTERN_TILES) { return MAX_PATTERN_TILES }
return nx * ny
}
@(private)
rd_end_tile :: proc(dev: ^Device) {
rd := rdev_from(dev)
if rd == nil { return }
rdev_pop_entry(rd)
}
// ---------------------------------------------------------------------------
// Teardown
// ---------------------------------------------------------------------------
// rd_close unwinds anything the content stream left open. A page that ends
// inside a group is malformed and common; compositing what it drew is much
// better than dropping it.
@(private)
rd_close :: proc(dev: ^Device) {
rd := rdev_from(dev)
if rd == nil { return }
for len(rd.layers) > 0 {
rdev_layer_finish(rd, nil)
}
for len(rd.clips) > 0 {
rdev_pop_entry(rd)
}
rdev_smask_drop_all(rd)
rd.smask_suspended = false
rd.scissor = rd.base_clip
rdev_retarget(rd)
}