2142 lines
80 KiB
Odin
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)
|
||
|
|
}
|