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) }