initial commit

This commit is contained in:
2026-09-25 23:32:29 +02:00
commit fff8b677f8
114 changed files with 84233 additions and 0 deletions
+187
View File
@@ -0,0 +1,187 @@
package pdf
import "core:mem"
// Bitmap is an 8-bit-per-component raster image, the thing `render_page`
// hands back and the thing an image XObject decodes into.
//
// Alpha is NOT premultiplied in a Bitmap that leaves the library: that is what
// a caller uploading to a texture expects, and premultiplying would silently
// change colours for anyone who blends with the usual
// (SRC_ALPHA, ONE_MINUS_SRC_ALPHA) setup. Compositing internally does use
// premultiplied values; `bitmap_unpremultiply` is where the conversion back
// happens, and `premultiplied` records which state a bitmap is in.
Bitmap :: struct {
width: int,
height: int,
// Components per pixel: 1 = grey, 2 = grey+alpha, 3 = RGB, 4 = RGBA.
n: int,
// Bytes per row. Rows are contiguous by default but a bitmap can be a
// window onto a larger buffer, so never assume width*n.
stride: int,
pixels: []u8,
premultiplied: bool,
// The allocator `pixels` came from, so `bitmap_destroy` needs no argument.
allocator: mem.Allocator,
// False when `pixels` is borrowed and must not be freed.
owns_pixels: bool,
}
// bitmap_make allocates a zeroed bitmap. `n` must be 1, 2, 3 or 4.
//
// The returned Bitmap owns its pixels; free it with `bitmap_destroy`.
bitmap_make :: proc(width, height, n: int, allocator := context.allocator) -> (bmp: Bitmap, err: Error) {
if width <= 0 || height <= 0 || n < 1 || n > 4 {
return {}, .Invalid_Argument
}
// Guard the multiplication itself, not just the product: on a 32-bit
// target width*height*n can wrap before the comparison ever runs.
if width > MAX_BITMAP_PIXELS || height > MAX_BITMAP_PIXELS {
return {}, .Bitmap_Too_Large
}
if width * height > MAX_BITMAP_PIXELS {
return {}, .Bitmap_Too_Large
}
stride := width * n
pixels, alloc_err := make([]u8, stride * height, allocator)
if alloc_err != nil {
return {}, .Out_Of_Memory
}
return Bitmap{
width = width,
height = height,
n = n,
stride = stride,
pixels = pixels,
allocator = allocator,
owns_pixels = true,
}, .None
}
bitmap_destroy :: proc(bmp: ^Bitmap) {
if bmp == nil { return }
if bmp.owns_pixels && bmp.pixels != nil {
delete(bmp.pixels, bmp.allocator)
}
bmp^ = {}
}
// bitmap_row returns row `y`, or nil if it is out of range.
bitmap_row :: proc(bmp: ^Bitmap, y: int) -> []u8 {
if bmp == nil || y < 0 || y >= bmp.height { return nil }
off := y * bmp.stride
if off + bmp.stride > len(bmp.pixels) { return nil }
return bmp.pixels[off:off + bmp.stride]
}
bitmap_pixel_offset :: proc(bmp: ^Bitmap, x, y: int) -> (int, bool) {
if bmp == nil || x < 0 || y < 0 || x >= bmp.width || y >= bmp.height {
return 0, false
}
off := y * bmp.stride + x * bmp.n
if off + bmp.n > len(bmp.pixels) { return 0, false }
return off, true
}
// bitmap_clear fills every pixel with `value` in all components.
bitmap_clear :: proc(bmp: ^Bitmap, value: u8 = 0) {
if bmp == nil { return }
for &b in bmp.pixels { b = value }
}
// bitmap_fill paints a solid colour. Components beyond `bmp.n` are ignored,
// and a missing alpha component means the colour is written opaque.
bitmap_fill :: proc(bmp: ^Bitmap, color: [4]u8) {
if bmp == nil { return }
n := bmp.n
for y in 0 ..< bmp.height {
row := bitmap_row(bmp, y)
for x := 0; x + n <= len(row); x += n {
for c in 0 ..< n {
row[x + c] = color[c]
}
}
}
}
// bitmap_unpremultiply converts a premultiplied bitmap to straight alpha in
// place. A no-op on a bitmap that is already straight, or has no alpha.
bitmap_unpremultiply :: proc(bmp: ^Bitmap) {
if bmp == nil || !bmp.premultiplied { return }
if bmp.n != 2 && bmp.n != 4 {
bmp.premultiplied = false
return
}
ai := bmp.n - 1
for y in 0 ..< bmp.height {
row := bitmap_row(bmp, y)
for x := 0; x + bmp.n <= len(row); x += bmp.n {
a := row[x + ai]
if a == 0 {
// Fully transparent: colour is unrecoverable, and leaving the
// multiplied zeros makes bilinear filtering pull black halos
// into the edges. Zero is the least-bad choice and matches
// what every other renderer does.
for c in 0 ..< ai { row[x + c] = 0 }
continue
}
if a == 255 { continue }
for c in 0 ..< ai {
v := (int(row[x + c]) * 255 + int(a) / 2) / int(a)
row[x + c] = u8(min(v, 255))
}
}
}
bmp.premultiplied = false
}
// bitmap_premultiply converts straight alpha to premultiplied in place.
bitmap_premultiply :: proc(bmp: ^Bitmap) {
if bmp == nil || bmp.premultiplied { return }
if bmp.n != 2 && bmp.n != 4 {
bmp.premultiplied = true
return
}
ai := bmp.n - 1
for y in 0 ..< bmp.height {
row := bitmap_row(bmp, y)
for x := 0; x + bmp.n <= len(row); x += bmp.n {
a := int(row[x + ai])
if a == 255 { continue }
for c in 0 ..< ai {
row[x + c] = u8((int(row[x + c]) * a + 127) / 255)
}
}
}
bmp.premultiplied = true
}
// bitmap_to_rgba returns an RGBA8 copy, expanding grey and adding an opaque
// alpha channel as needed. Callers uploading to a GPU texture want this.
// The result is allocated with `allocator` and owned by the caller.
bitmap_to_rgba :: proc(bmp: ^Bitmap, allocator := context.allocator) -> (out: Bitmap, err: Error) {
if bmp == nil { return {}, .Invalid_Argument }
out = bitmap_make(bmp.width, bmp.height, 4, allocator) or_return
for y in 0 ..< bmp.height {
src := bitmap_row(bmp, y)
dst := bitmap_row(&out, y)
for x in 0 ..< bmp.width {
si := x * bmp.n
di := x * 4
if si + bmp.n > len(src) || di + 4 > len(dst) { break }
switch bmp.n {
case 1:
dst[di] = src[si]; dst[di + 1] = src[si]; dst[di + 2] = src[si]; dst[di + 3] = 255
case 2:
dst[di] = src[si]; dst[di + 1] = src[si]; dst[di + 2] = src[si]; dst[di + 3] = src[si + 1]
case 3:
dst[di] = src[si]; dst[di + 1] = src[si + 1]; dst[di + 2] = src[si + 2]; dst[di + 3] = 255
case 4:
dst[di] = src[si]; dst[di + 1] = src[si + 1]; dst[di + 2] = src[si + 2]; dst[di + 3] = src[si + 3]
}
}
}
out.premultiplied = bmp.premultiplied
return out, .None
}