giflib-rs 6.1.3

An AI-assisted port of giflib to Rust.
Documentation
#![forbid(unsafe_code)]
// Copyright (c) 1997-2025 Eric S. Raymond
// Copyright (c) 2026 Google LLC
//
// Permission is hereby granted, free of charge, to any person obtaining a copy
// of this software and associated documentation files (the "Software"), to deal
// in the Software without restriction, including without limitation the rights
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
// copies of the Software, and to permit persons to whom the Software is
// furnished to do so, subject to the following conditions:
//
// The above copyright notice and this permission notice shall be included in
// all copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
// SOFTWARE.

//! GIF font and drawing functions — reimplementation of gif_font.c.

use crate::c_types::{GifByteType, SavedImage};
use core::ffi::c_int;

pub const GIF_FONT_WIDTH: usize = 8;
pub const GIF_FONT_HEIGHT: usize = 8;

/// 8×8 ASCII font table, 128 entries. Exact match of GifAsciiTable8x8 from C.
pub(crate) static GIF_ASCII_TABLE_8X8: [[u8; GIF_FONT_WIDTH]; 128] = [
    [0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00], // 0
    [0x3c, 0x42, 0xa5, 0x81, 0xbd, 0x42, 0x3c, 0x00], // 1
    [0x3c, 0x7e, 0xdb, 0xff, 0xc3, 0x7e, 0x3c, 0x00], // 2
    [0x00, 0xee, 0xfe, 0xfe, 0x7c, 0x38, 0x10, 0x00], // 3
    [0x10, 0x38, 0x7c, 0xfe, 0x7c, 0x38, 0x10, 0x00], // 4
    [0x00, 0x3c, 0x18, 0xff, 0xff, 0x08, 0x18, 0x00], // 5
    [0x10, 0x38, 0x7c, 0xfe, 0xfe, 0x10, 0x38, 0x00], // 6
    [0x00, 0x00, 0x18, 0x3c, 0x18, 0x00, 0x00, 0x00], // 7
    [0xff, 0xff, 0xe7, 0xc3, 0xe7, 0xff, 0xff, 0xff], // 8
    [0x00, 0x3c, 0x42, 0x81, 0x81, 0x42, 0x3c, 0x00], // 9
    [0xff, 0xc3, 0xbd, 0x7e, 0x7e, 0xbd, 0xc3, 0xff], // 10
    [0x1f, 0x07, 0x0d, 0x7c, 0xc6, 0xc6, 0x7c, 0x00], // 11
    [0x00, 0x7e, 0xc3, 0xc3, 0x7e, 0x18, 0x7e, 0x18], // 12
    [0x04, 0x06, 0x07, 0x04, 0x04, 0xfc, 0xf8, 0x00], // 13
    [0x0c, 0x0a, 0x0d, 0x0b, 0xf9, 0xf9, 0x1f, 0x1f], // 14
    [0x00, 0x92, 0x7c, 0x44, 0xc6, 0x7c, 0x92, 0x00], // 15
    [0x00, 0x00, 0x60, 0x78, 0x7e, 0x78, 0x60, 0x00], // 16
    [0x00, 0x00, 0x06, 0x1e, 0x7e, 0x1e, 0x06, 0x00], // 17
    [0x18, 0x7e, 0x18, 0x18, 0x18, 0x18, 0x7e, 0x18], // 18
    [0x66, 0x66, 0x66, 0x66, 0x66, 0x00, 0x66, 0x00], // 19
    [0xff, 0xb6, 0x76, 0x36, 0x36, 0x36, 0x36, 0x00], // 20
    [0x7e, 0xc1, 0xdc, 0x22, 0x22, 0x1f, 0x83, 0x7e], // 21
    [0x00, 0x00, 0x00, 0x7e, 0x7e, 0x00, 0x00, 0x00], // 22
    [0x18, 0x7e, 0x18, 0x18, 0x7e, 0x18, 0x00, 0xff], // 23
    [0x18, 0x7e, 0x18, 0x18, 0x18, 0x18, 0x18, 0x00], // 24
    [0x18, 0x18, 0x18, 0x18, 0x18, 0x7e, 0x18, 0x00], // 25
    [0x00, 0x04, 0x06, 0xff, 0x06, 0x04, 0x00, 0x00], // 26
    [0x00, 0x20, 0x60, 0xff, 0x60, 0x20, 0x00, 0x00], // 27
    [0x00, 0x00, 0x00, 0xc0, 0xc0, 0xc0, 0xff, 0x00], // 28
    [0x00, 0x24, 0x66, 0xff, 0x66, 0x24, 0x00, 0x00], // 29
    [0x00, 0x00, 0x10, 0x38, 0x7c, 0xfe, 0x00, 0x00], // 30
    [0x00, 0x00, 0x00, 0xfe, 0x7c, 0x38, 0x10, 0x00], // 31
    [0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00], // ' '
    [0x30, 0x30, 0x30, 0x30, 0x30, 0x00, 0x30, 0x00], // '!'
    [0x66, 0x66, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00], // '"'
    [0x6c, 0x6c, 0xfe, 0x6c, 0xfe, 0x6c, 0x6c, 0x00], // '#'
    [0x10, 0x7c, 0xd2, 0x7c, 0x86, 0x7c, 0x10, 0x00], // '$'
    [0xf0, 0x96, 0xfc, 0x18, 0x3e, 0x72, 0xde, 0x00], // '%'
    [0x30, 0x48, 0x30, 0x78, 0xce, 0xcc, 0x78, 0x00], // '&'
    [0x0c, 0x0c, 0x18, 0x00, 0x00, 0x00, 0x00, 0x00], // '\''
    [0x10, 0x60, 0xc0, 0xc0, 0xc0, 0x60, 0x10, 0x00], // '('
    [0x10, 0x0c, 0x06, 0x06, 0x06, 0x0c, 0x10, 0x00], // ')'
    [0x00, 0x54, 0x38, 0xfe, 0x38, 0x54, 0x00, 0x00], // '*'
    [0x00, 0x18, 0x18, 0x7e, 0x18, 0x18, 0x00, 0x00], // '+'
    [0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x18, 0x70], // ','
    [0x00, 0x00, 0x00, 0x7e, 0x00, 0x00, 0x00, 0x00], // '-'
    [0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x18, 0x00], // '.'
    [0x02, 0x06, 0x0c, 0x18, 0x30, 0x60, 0xc0, 0x00], // '/'
    [0x7c, 0xc6, 0xc6, 0xc6, 0xc6, 0xc6, 0x7c, 0x00], // '0'
    [0x18, 0x38, 0x78, 0x18, 0x18, 0x18, 0x3c, 0x00], // '1'
    [0x7c, 0xc6, 0x06, 0x0c, 0x30, 0x60, 0xfe, 0x00], // '2'
    [0x7c, 0xc6, 0x06, 0x3c, 0x06, 0xc6, 0x7c, 0x00], // '3'
    [0x0e, 0x1e, 0x36, 0x66, 0xfe, 0x06, 0x06, 0x00], // '4'
    [0xfe, 0xc0, 0xc0, 0xfc, 0x06, 0x06, 0xfc, 0x00], // '5'
    [0x7c, 0xc6, 0xc0, 0xfc, 0xc6, 0xc6, 0x7c, 0x00], // '6'
    [0xfe, 0x06, 0x0c, 0x18, 0x30, 0x60, 0x60, 0x00], // '7'
    [0x7c, 0xc6, 0xc6, 0x7c, 0xc6, 0xc6, 0x7c, 0x00], // '8'
    [0x7c, 0xc6, 0xc6, 0x7e, 0x06, 0xc6, 0x7c, 0x00], // '9'
    [0x00, 0x30, 0x00, 0x00, 0x00, 0x30, 0x00, 0x00], // ':'
    [0x00, 0x30, 0x00, 0x00, 0x00, 0x30, 0x20, 0x00], // ';'
    [0x00, 0x1c, 0x30, 0x60, 0x30, 0x1c, 0x00, 0x00], // '<'
    [0x00, 0x00, 0x7e, 0x00, 0x7e, 0x00, 0x00, 0x00], // '='
    [0x00, 0x70, 0x18, 0x0c, 0x18, 0x70, 0x00, 0x00], // '>'
    [0x7c, 0xc6, 0x0c, 0x18, 0x30, 0x00, 0x30, 0x00], // '?'
    [0x7c, 0x82, 0x9a, 0xaa, 0xaa, 0x9e, 0x7c, 0x00], // '@'
    [0x38, 0x6c, 0xc6, 0xc6, 0xfe, 0xc6, 0xc6, 0x00], // 'A'
    [0xfc, 0xc6, 0xc6, 0xfc, 0xc6, 0xc6, 0xfc, 0x00], // 'B'
    [0x7c, 0xc6, 0xc6, 0xc0, 0xc0, 0xc6, 0x7c, 0x00], // 'C'
    [0xf8, 0xcc, 0xc6, 0xc6, 0xc6, 0xcc, 0xf8, 0x00], // 'D'
    [0xfe, 0xc0, 0xc0, 0xfc, 0xc0, 0xc0, 0xfe, 0x00], // 'E'
    [0xfe, 0xc0, 0xc0, 0xfc, 0xc0, 0xc0, 0xc0, 0x00], // 'F'
    [0x7c, 0xc6, 0xc0, 0xce, 0xc6, 0xc6, 0x7e, 0x00], // 'G'
    [0xc6, 0xc6, 0xc6, 0xfe, 0xc6, 0xc6, 0xc6, 0x00], // 'H'
    [0x78, 0x30, 0x30, 0x30, 0x30, 0x30, 0x78, 0x00], // 'I'
    [0x1e, 0x06, 0x06, 0x06, 0xc6, 0xc6, 0x7c, 0x00], // 'J'
    [0xc6, 0xcc, 0xd8, 0xf0, 0xd8, 0xcc, 0xc6, 0x00], // 'K'
    [0xc0, 0xc0, 0xc0, 0xc0, 0xc0, 0xc0, 0xfe, 0x00], // 'L'
    [0xc6, 0xee, 0xfe, 0xd6, 0xc6, 0xc6, 0xc6, 0x00], // 'M'
    [0xc6, 0xe6, 0xf6, 0xde, 0xce, 0xc6, 0xc6, 0x00], // 'N'
    [0x7c, 0xc6, 0xc6, 0xc6, 0xc6, 0xc6, 0x7c, 0x00], // 'O'
    [0xfc, 0xc6, 0xc6, 0xfc, 0xc0, 0xc0, 0xc0, 0x00], // 'P'
    [0x7c, 0xc6, 0xc6, 0xc6, 0xc6, 0xc6, 0x7c, 0x06], // 'Q'
    [0xfc, 0xc6, 0xc6, 0xfc, 0xc6, 0xc6, 0xc6, 0x00], // 'R'
    [0x78, 0xcc, 0x60, 0x30, 0x18, 0xcc, 0x78, 0x00], // 'S'
    [0xfc, 0x30, 0x30, 0x30, 0x30, 0x30, 0x30, 0x00], // 'T'
    [0xc6, 0xc6, 0xc6, 0xc6, 0xc6, 0xc6, 0x7c, 0x00], // 'U'
    [0xc6, 0xc6, 0xc6, 0xc6, 0xc6, 0x6c, 0x38, 0x00], // 'V'
    [0xc6, 0xc6, 0xc6, 0xd6, 0xfe, 0xee, 0xc6, 0x00], // 'W'
    [0xc6, 0xc6, 0x6c, 0x38, 0x6c, 0xc6, 0xc6, 0x00], // 'X'
    [0xc3, 0xc3, 0x66, 0x3c, 0x18, 0x18, 0x18, 0x00], // 'Y'
    [0xfe, 0x0c, 0x18, 0x30, 0x60, 0xc0, 0xfe, 0x00], // 'Z'
    [0x3c, 0x30, 0x30, 0x30, 0x30, 0x30, 0x3c, 0x00], // '['
    [0xc0, 0x60, 0x30, 0x18, 0x0c, 0x06, 0x03, 0x00], // '\\'
    [0x3c, 0x0c, 0x0c, 0x0c, 0x0c, 0x0c, 0x3c, 0x00], // ']'
    [0x00, 0x38, 0x6c, 0xc6, 0x00, 0x00, 0x00, 0x00], // '^'
    [0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xff], // '_'
    [0x30, 0x30, 0x18, 0x00, 0x00, 0x00, 0x00, 0x00], // '`'
    [0x00, 0x00, 0x7c, 0x06, 0x7e, 0xc6, 0x7e, 0x00], // 'a'
    [0xc0, 0xc0, 0xfc, 0xc6, 0xc6, 0xe6, 0xdc, 0x00], // 'b'
    [0x00, 0x00, 0x7c, 0xc6, 0xc0, 0xc0, 0x7e, 0x00], // 'c'
    [0x06, 0x06, 0x7e, 0xc6, 0xc6, 0xce, 0x76, 0x00], // 'd'
    [0x00, 0x00, 0x7c, 0xc6, 0xfe, 0xc0, 0x7e, 0x00], // 'e'
    [0x1e, 0x30, 0x7c, 0x30, 0x30, 0x30, 0x30, 0x00], // 'f'
    [0x00, 0x00, 0x7e, 0xc6, 0xce, 0x76, 0x06, 0x7c], // 'g'
    [0xc0, 0xc0, 0xfc, 0xc6, 0xc6, 0xc6, 0xc6, 0x00], // 'h'
    [0x18, 0x00, 0x38, 0x18, 0x18, 0x18, 0x3c, 0x00], // 'i'
    [0x18, 0x00, 0x38, 0x18, 0x18, 0x18, 0x18, 0xf0], // 'j'
    [0xc0, 0xc0, 0xcc, 0xd8, 0xf0, 0xd8, 0xcc, 0x00], // 'k'
    [0x38, 0x18, 0x18, 0x18, 0x18, 0x18, 0x3c, 0x00], // 'l'
    [0x00, 0x00, 0xcc, 0xfe, 0xd6, 0xc6, 0xc6, 0x00], // 'm'
    [0x00, 0x00, 0xfc, 0xc6, 0xc6, 0xc6, 0xc6, 0x00], // 'n'
    [0x00, 0x00, 0x7c, 0xc6, 0xc6, 0xc6, 0x7c, 0x00], // 'o'
    [0x00, 0x00, 0xfc, 0xc6, 0xc6, 0xe6, 0xdc, 0xc0], // 'p'
    [0x00, 0x00, 0x7e, 0xc6, 0xc6, 0xce, 0x76, 0x06], // 'q'
    [0x00, 0x00, 0x6e, 0x70, 0x60, 0x60, 0x60, 0x00], // 'r'
    [0x00, 0x00, 0x7c, 0xc0, 0x7c, 0x06, 0xfc, 0x00], // 's'
    [0x30, 0x30, 0x7c, 0x30, 0x30, 0x30, 0x1c, 0x00], // 't'
    [0x00, 0x00, 0xc6, 0xc6, 0xc6, 0xc6, 0x7e, 0x00], // 'u'
    [0x00, 0x00, 0xc6, 0xc6, 0xc6, 0x6c, 0x38, 0x00], // 'v'
    [0x00, 0x00, 0xc6, 0xc6, 0xd6, 0xfe, 0x6c, 0x00], // 'w'
    [0x00, 0x00, 0xc6, 0x6c, 0x38, 0x6c, 0xc6, 0x00], // 'x'
    [0x00, 0x00, 0xc6, 0xc6, 0xce, 0x76, 0x06, 0x7c], // 'y'
    [0x00, 0x00, 0xfc, 0x18, 0x30, 0x60, 0xfc, 0x00], // 'z'
    [0x0e, 0x18, 0x18, 0x70, 0x18, 0x18, 0x0e, 0x00], // '{'
    [0x18, 0x18, 0x18, 0x00, 0x18, 0x18, 0x18, 0x00], // '|'
    [0xe0, 0x30, 0x30, 0x1c, 0x30, 0x30, 0xe0, 0x00], // '}'
    [0x00, 0x00, 0x70, 0x9a, 0x0e, 0x00, 0x00, 0x00], // '~'
    [0x00, 0x00, 0x18, 0x3c, 0x66, 0xff, 0x00, 0x00], // 127
];

// ---------------------------------------------------------------------------
//  Safe inner helpers
// ---------------------------------------------------------------------------

/// Draw an 8×8 font string onto the image raster.
pub(crate) fn gif_draw_text8x8(
    image: &mut SavedImage,
    x: c_int,
    y: c_int,
    legend: &[u8],
    color: c_int,
) {
    if x < 0 || y < 0 {
        return;
    }

    let width = image.ImageDesc.Width as usize;
    let raster = image.raster_bits_mut();

    for row in 0..GIF_FONT_HEIGHT {
        let Some(base_offset) =
            width.checked_mul(y as usize + row).and_then(|v| v.checked_add(x as usize))
        else {
            return;
        };
        let mut col = 0usize;
        for &ch_byte in legend {
            let ch = (ch_byte & 0x7f) as usize;
            for j in 0..GIF_FONT_WIDTH {
                if ((GIF_ASCII_TABLE_8X8[ch][row] as u16) & (1u16 << (GIF_FONT_WIDTH - j))) != 0 {
                    if let Some(idx) = base_offset.checked_add(col) {
                        if idx < raster.len() {
                            raster[idx] = color as GifByteType;
                        }
                    }
                }
                col += 1;
            }
        }
    }
}

/// Draw the outline of a rectangle.
pub(crate) fn gif_draw_box(
    image: &mut SavedImage,
    x: c_int,
    y: c_int,
    w: c_int,
    d: c_int,
    color: c_int,
) {
    if x < 0 || y < 0 || w <= 0 || d <= 0 {
        return;
    }
    let width = image.ImageDesc.Width as usize;
    let raster = image.raster_bits_mut();
    let Some(base) = width.checked_mul(y as usize).and_then(|v| v.checked_add(x as usize)) else {
        return;
    };

    // Top and bottom edges
    for j in 0..w as usize {
        if let Some(top) = base.checked_add(j) {
            if top < raster.len() {
                raster[top] = color as GifByteType;
            }
        }
        if let Some(bottom) = (d as usize)
            .checked_mul(width)
            .and_then(|v| base.checked_add(v))
            .and_then(|v| v.checked_add(j))
        {
            if bottom < raster.len() {
                raster[bottom] = color as GifByteType;
            }
        }
    }
    // Left and right edges
    for j in 0..d as usize {
        if let Some(left) = j.checked_mul(width).and_then(|v| base.checked_add(v)) {
            if left < raster.len() {
                raster[left] = color as GifByteType;
            }
        }
        if let Some(right) = j
            .checked_mul(width)
            .and_then(|v| base.checked_add(v))
            .and_then(|v| v.checked_add(w as usize))
        {
            if right < raster.len() {
                raster[right] = color as GifByteType;
            }
        }
    }
}

/// Fill a rectangle.
pub(crate) fn gif_draw_rectangle(
    image: &mut SavedImage,
    x: c_int,
    y: c_int,
    w: c_int,
    d: c_int,
    color: c_int,
) {
    if x < 0 || y < 0 || w <= 0 || d <= 0 {
        return;
    }
    let width = image.ImageDesc.Width as usize;
    let raster = image.raster_bits_mut();
    let Some(bp) = width.checked_mul(y as usize).and_then(|v| v.checked_add(x as usize)) else {
        return;
    };

    for i in 0..d as usize {
        let Some(start) = i.checked_mul(width).and_then(|v| bp.checked_add(v)) else {
            return;
        };
        let Some(end) = start.checked_add(w as usize) else {
            return;
        };
        if end <= raster.len() {
            raster[start..end].fill(color as u8);
        }
    }
}

/// Draw text in a box.
pub(crate) fn gif_draw_boxed_text8x8(
    image: &mut SavedImage,
    x: c_int,
    y: c_int,
    legend: &[u8],
    border: c_int,
    bg: c_int,
    fg: c_int,
) {
    // Compute text dimensions.
    let mut j = 0i32;
    let mut line_count = 0i32;
    let mut text_width = 0i32;
    for &ch in legend {
        if ch == b'\r' {
            if j > text_width {
                text_width = j;
            }
            j = 0;
            line_count += 1;
        } else if ch != b'\t' {
            j += 1;
        }
    }
    line_count += 1;
    if j > text_width {
        text_width = j;
    }

    // Fill the box
    gif_draw_rectangle(
        image,
        x + 1,
        y + 1,
        border + text_width * GIF_FONT_WIDTH as c_int + border - 1,
        border + line_count * GIF_FONT_HEIGHT as c_int + border - 1,
        bg,
    );

    // Split on \r and draw each line
    let mut line_idx = 0i32;
    for line in legend.split(|&b| b == b'\r') {
        let (text, leadspace) = if line.first() == Some(&b'\t') {
            let trimmed = &line[1..];
            let lead = (text_width - trimmed.len() as i32) / 2;
            (trimmed, lead)
        } else {
            (line, 0)
        };

        gif_draw_text8x8(
            image,
            x + border + leadspace * GIF_FONT_WIDTH as c_int,
            y + border + GIF_FONT_HEIGHT as c_int * line_idx,
            text,
            fg,
        );
        line_idx += 1;
    }

    // Draw the outline box
    gif_draw_box(
        image,
        x,
        y,
        border + text_width * GIF_FONT_WIDTH as c_int + border,
        border + line_count * GIF_FONT_HEIGHT as c_int + border,
        fg,
    );
}