From d48b733e661164e3c403d0010336c8e631c483ec Mon Sep 17 00:00:00 2001 From: Hisham Aburaqibah Date: Tue, 29 Apr 2025 23:44:25 +0200 Subject: [PATCH] initial --- .gitignore | 4 + example/main.odin | 20 ++ reader.odin | 15 ++ tinyvg.odin | 628 ++++++++++++++++++++++++++++++++++++++++++++++ 4 files changed, 667 insertions(+) create mode 100644 .gitignore create mode 100644 example/main.odin create mode 100644 reader.odin create mode 100644 tinyvg.odin diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..58753ee --- /dev/null +++ b/.gitignore @@ -0,0 +1,4 @@ +example.bin +example/example +example/*.tvg +example/*.tvgt diff --git a/example/main.odin b/example/main.odin new file mode 100644 index 0000000..6cc4edb --- /dev/null +++ b/example/main.odin @@ -0,0 +1,20 @@ +package main + +import "core:fmt" +import "core:os/os2" + +import tinyvg "../" + +main :: proc() { + if len(os2.args) < 2 { + fmt.println("Provide a path to a tvg file") + return + } + + tvg, err := tinyvg.load_from_file(os2.args[1]) + if err != nil { + fmt.println(err) + } + + fmt.println(tvg) +} diff --git a/reader.odin b/reader.odin new file mode 100644 index 0000000..77c10f0 --- /dev/null +++ b/reader.odin @@ -0,0 +1,15 @@ +package tinyvg + +import "core:bytes" +import "core:io" + +Reader :: struct { + using r: bytes.Reader +} + +read_data :: #force_inline proc(r: ^Reader, $T: typeid) -> (res: T, err: io.Error) { + b: [size_of(T)]byte + + bytes.reader_read(r, b[:]) or_return + return (^T)(&b[0])^, nil +} diff --git a/tinyvg.odin b/tinyvg.odin new file mode 100644 index 0000000..e77380f --- /dev/null +++ b/tinyvg.odin @@ -0,0 +1,628 @@ +package tinyvg + +import "core:os/os2" +import "core:fmt" +import "core:io" +import "core:slice" +import "core:bytes" +import "core:debug/trace" + +import "base:runtime" + +TINYVG_VERSION :: 1 +TINYVG_MAGIC :: [2]byte {0x72, 0x56} +MIN_POLYGON_POINT_COUNT :: 2 + +ColorEncoding :: enum u8 { + RGBA8888, + RGB565, + RGBAF32, + Custom, +} + +CoordinateRange :: enum u8 { + Default, // 16 bits + Reduced, // 8 bits + Enhanced, // 32 bits +} + +CommandIndex :: enum u8 { + EndOfDocument, + FillPolygon, + FillRectangles, + FillPath, + DrawLines, + DrawLineLoop, + DrawLineStrip, + DrawLinePath, + OutlineFillPolygon, + OutlineFillRectangles, + OutlineFillPath, +} + +StyleKind :: enum u8 { + FlatColored, + LinearGradient, + RadialGradient, +} + +PathInstructionKind :: enum u8 { + Line, + HorizontalLine, + VerticalLine, + CubicBezier, + ArcCircle, + ArcEllipse, + ClosePath, + QuadraticBezier, +} + +Command :: bit_field u8 { + command_index: CommandIndex | 6, + prim_style_kind: StyleKind | 2, +} + +#assert(size_of(Header) == 4) +Header :: struct { + magic: [2]byte, + version: byte, + using _: bit_field u8 { + scale: u8 | 4, + color_encoding: ColorEncoding | 2, + coordinate_range: CoordinateRange | 2, + } +} + +Gradient :: struct { + point_0: Point, + point_1: Point, + color_index_0: u32, + color_index_1: u32, +} + +Style :: struct { + kind: StyleKind, + using _: struct #raw_union { + flat: u32, + linear_gradient: Gradient, + radial_gradient: Gradient, + } +} + +DrawCommand :: union { + FillPolygon, + FillRectangles, + FillPath, + DrawLines, + DrawLineLoop, + DrawLineStrip, + DrawLinePath, + OutlineFillPolygon, + OutlineFillRectangles, + OutlineFillPath, +} + +PathInstruction :: union { + LineInstruction, + HorizontalLineInstruction, + VerticalLineInstruction, + CubicBezierInstruction, + ArcCircleInstruction, + ArcEllipseInstruction, + ClosePathInstruction, + QuadraticBezierInstruction, +} + +LineInstruction :: struct { + position: Point, +} +HorizontalLineInstruction :: struct { + x: Unit, +} +VerticalLineInstruction :: struct { + y: Unit, +} +CubicBezierInstruction :: struct { + control_0, control_1: Point, + point_1: Point, +} +ArcCircleInstruction :: struct { + large_arc: bool, + sweep: bool, + radius: Point, + target: Point, +} +ArcEllipseInstruction :: struct { + large_arc: bool, + sweep: bool, + radius_x: Unit, + radius_y: Unit, + rotation: Unit, + target: Point, +} +ClosePathInstruction :: struct{} +QuadraticBezierInstruction :: struct { + control: Point, + point_1: Point, +} + +Segment :: struct { + start: Point, + instructions: []PathInstruction, +} + +Path :: struct { + segments: []Segment +} + +FillPolygon :: struct { + style: Style, + points: []Point, +} + +FillRectangles :: struct { + style: Style, + rectangles: []Rectangle, +} + +FillPath :: struct { + style: Style, + path: Path, +} + +Line :: struct { + start, end: Point, +} + +DrawLines :: struct { + style: Style, + width: Unit, + lines: []Line, +} + +DrawLineLoop :: struct { + style: Style, + width: Unit, + points: []Point, +} + +DrawLineStrip :: struct { + style: Style, + width: Unit, + points: []Point, +} + +DrawLinePath :: struct { + style: Style, + width: Unit, + path: Path, +} + +OutlineFillPolygon :: struct { + fill_style: Style, + line_style: Style, + width: Unit, + points: []Point, +} + +OutlineFillRectangles :: struct { + fill_style: Style, + line_style: Style, + width: Unit, + rectangles: []Rectangle, +} + +OutlineFillPath :: struct { + fill_style: Style, + line_style: Style, + line_width: Unit, + path: Path, +} + +Rectangle :: struct { + x, y, width, height: Unit +} + +Tag :: bit_field u8 { + instruction: PathInstructionKind | 3, + padding_0: byte | 1, + has_line_width: bool | 1, + padding_1: byte | 3, +} + +Color :: [4]f32 +Unit :: f32 +Point :: [2]Unit + +Tvg :: struct { + width: u32, + height: u32, + colors: []Color, + draw_commands: [dynamic]DrawCommand, +} + +TvgError :: enum { + Unsupported_Color_Encoding, + Unsupported_Version, + Invalid_Signature, + Invalid_Style, + Invalid_File, +} + +Error :: union { + TvgError, + io.Error, + os2.Error, +} + +load_from_file :: proc(filename: string, allocator := context.allocator) -> (tvg: ^Tvg, err: Error) { + data := os2.read_entire_file(filename, allocator) or_return + + return load_from_bytes(data, allocator) +} + +load_from_bytes :: proc(data: []byte, allocator := context.allocator) -> (tvg: ^Tvg, err: Error) { + context.allocator = allocator + + r: Reader + bytes.reader_init(&r, data) + header := read_data(&r, Header) or_return + + if header.magic != TINYVG_MAGIC { + return nil, .Invalid_Signature + } + + if header.version != TINYVG_VERSION { + return nil, .Unsupported_Version + } + + if header.color_encoding == .Custom { + return nil, .Unsupported_Color_Encoding + } + + tvg = new(Tvg) + + switch header.coordinate_range { + case .Default: + tvg.width = cast(u32)(read_data(&r, u16) or_return) + tvg.height = cast(u32)(read_data(&r, u16) or_return) + case .Reduced: + tvg.width = cast(u32)(read_data(&r, u8) or_return) + tvg.height = cast(u32)(read_data(&r, u8) or_return) + case .Enhanced: + tvg.width = read_data(&r, u32) or_return + tvg.height = read_data(&r, u32) or_return + } + + color_count := read_variable_uint(&r) or_return + + tvg.colors = make([]Color, color_count) + + for i in 0..> 5) / 63.0, + f32((rgb565 & 0xF800) >> 11) / 31.0, + 1.0, + } + case .RGBAF32: + // TODO: endianness ditto + tvg.colors[i] = read_data(&r, [4]f32) or_return + } + } + + loop: for { + cmd := read_data(&r, Command) or_return + switch cmd.command_index { + case .EndOfDocument: + assert(cast(int)cmd == 0x00) + break loop + case .FillPolygon: + point_count := (read_variable_uint(&r) or_return) + 1 + if point_count < MIN_POLYGON_POINT_COUNT { + return nil, TvgError.Invalid_File + } + + style := read_style(&r, header, cmd.prim_style_kind) or_return + + points := make([]Point, point_count) + + for i in 0..> 6) + + fill_style := read_style(&r, header, cmd.prim_style_kind) or_return + line_style := read_style(&r, header, secondary_style_kind) or_return + width := read_unit(&r, header) or_return + + points := make([]Point, segment_count) + + for i in 0..> 6) + + fill_style := read_style(&r, header, cmd.prim_style_kind) or_return + line_style := read_style(&r, header, secondary_style_kind) or_return + width := read_unit(&r, header) or_return + + rectangles := make([]Rectangle, rect_count) + + for i in 0..> 6) + + fill_style := read_style(&r, header, cmd.prim_style_kind) or_return + line_style := read_style(&r, header, secondary_style_kind) or_return + line_width := read_unit(&r, header) or_return + + path := read_path(&r, header, cast(u32)segment_count) or_return + append(&tvg.draw_commands, OutlineFillPath{fill_style, line_style, line_width, path}) + case: + panic(fmt.tprintf("Invalid command: %v", cmd)) + } + } + + return +} + +load :: proc{load_from_file, load_from_bytes} + +destroy :: proc(tvg: ^Tvg) { + free(tvg) + + // TODO: free other stuff (mostly slices) +} + +@(private, require_results) +read_variable_uint :: proc(r: ^Reader) -> (var_uint: u32, err: Error) { + count: u32 = 0 + + for { + b := bytes.reader_read_byte(r) or_return + + val := u32(b & 0x7F) << (7 * count) + var_uint |= val + + if b & 0x80 == 0 { + return var_uint, nil + } + + count += 1 + } + + return var_uint, nil +} + +@(private, require_results) +read_style :: proc(r: ^Reader, header: Header, style_kind: StyleKind) -> (style: Style, err: Error) { + switch style_kind { + case .FlatColored: + color_index := read_variable_uint(r) or_return + return Style{kind = .FlatColored, flat = color_index}, nil + case .LinearGradient: + point_0 := Point{read_unit(r, header) or_return, read_unit(r, header) or_return} + point_1 := Point{read_unit(r, header) or_return, read_unit(r, header) or_return} + color_index_0 := read_variable_uint(r) or_return + color_index_1 := read_variable_uint(r) or_return + return Style{ + kind = .LinearGradient, + linear_gradient = { + point_0, point_1, color_index_0, color_index_1, + } + }, nil + case .RadialGradient: + point_0 := Point{read_unit(r, header) or_return, read_unit(r, header) or_return} + point_1 := Point{read_unit(r, header) or_return, read_unit(r, header) or_return} + color_index_0 := read_variable_uint(r) or_return + color_index_1 := read_variable_uint(r) or_return + return Style{ + kind = .RadialGradient, + radial_gradient = { + point_0, point_1, color_index_0, color_index_1, + } + }, nil + } + + return Style{}, .Invalid_Style +} + +@(private, require_results) +read_unit :: proc(r: ^Reader, header: Header) -> (unit: f32, err: Error) { + switch header.coordinate_range { + case .Default: + unit = to_fixed_point(read_data(r, u16) or_return, header.scale) + case .Reduced: + unit = to_fixed_point(read_data(r, u8) or_return, header.scale) + case .Enhanced: + unit = to_fixed_point(read_data(r, u32) or_return, header.scale) + } + + return +} + +@(private, require_results) +read_path :: proc(r: ^Reader, header: Header, segment_count: u32) -> (path: Path, err: Error) { + segments_instructions: [1024]u32 + segments := make([]Segment, segment_count) + + for s in 0.. f32 { + return cast(f32)data / f32(u32(1) << fractional_bits) +}