This commit is contained in:
2025-04-29 23:46:39 +02:00
parent 4ac8682a15
commit d48b733e66
4 changed files with 667 additions and 0 deletions
+4
View File
@@ -0,0 +1,4 @@
example.bin
example/example
example/*.tvg
example/*.tvgt
+20
View File
@@ -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)
}
+15
View File
@@ -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
}
+628
View File
@@ -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..<color_count {
#partial switch header.color_encoding {
case .RGBA8888:
tvg.colors[i] = {
f32(bytes.reader_read_byte(&r) or_return) / 255.0,
f32(bytes.reader_read_byte(&r) or_return) / 255.0,
f32(bytes.reader_read_byte(&r) or_return) / 255.0,
f32(bytes.reader_read_byte(&r) or_return) / 255.0,
}
case .RGB565:
// TODO: is the endianness correct here
rgb565 := read_data(&r, u16) or_return
tvg.colors[i] = {
f32(rgb565 & 0x001F) / 31.0,
f32((rgb565 & 0x07E0) >> 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..<point_count {
points[i] = Point{read_unit(&r, header) or_return, read_unit(&r, header) or_return}
}
append(&tvg.draw_commands, FillPolygon{style = style, points = points})
case .FillRectangles:
rectangle_count := (read_variable_uint(&r) or_return) + 1
style := read_style(&r, header, cmd.prim_style_kind) or_return
rectangles := make([]Rectangle, rectangle_count)
for i in 0..<rectangle_count {
rectangles[i] = Rectangle{
read_unit(&r, header) or_return,
read_unit(&r, header) or_return,
read_unit(&r, header) or_return,
read_unit(&r, header) or_return,
}
}
append(&tvg.draw_commands, FillRectangles{style = style, rectangles = rectangles})
case .FillPath:
segment_count := (read_variable_uint(&r) or_return) + 1
style := read_style(&r, header, cmd.prim_style_kind) or_return
path := read_path(&r, header, segment_count) or_return
append(&tvg.draw_commands, FillPath{style, path})
case .DrawLines:
line_count := (read_variable_uint(&r) or_return) + 1
style := read_style(&r, header, cmd.prim_style_kind) or_return
width := read_unit(&r, header) or_return
lines := make([]Line, line_count)
for i in 0..<line_count {
lines[i] = Line{
Point{read_unit(&r, header) or_return, read_unit(&r, header) or_return},
Point{read_unit(&r, header) or_return, read_unit(&r, header) or_return},
}
}
append(&tvg.draw_commands, DrawLines{style, width, lines})
case .DrawLineLoop:
point_count := (read_variable_uint(&r) or_return) + 1
style := read_style(&r, header, cmd.prim_style_kind) or_return
width := read_unit(&r, header) or_return
points := make([]Point, point_count)
for i in 0..<point_count {
points[i] = Point{read_unit(&r, header) or_return, read_unit(&r, header) or_return}
}
append(&tvg.draw_commands, DrawLineLoop{style, width, points})
case .DrawLineStrip:
point_count := (read_variable_uint(&r) or_return) + 1
style := read_style(&r, header, cmd.prim_style_kind) or_return
width := read_unit(&r, header) or_return
points := make([]Point, point_count)
for i in 0..<point_count {
points[i] = Point{read_unit(&r, header) or_return, read_unit(&r, header) or_return}
}
append(&tvg.draw_commands, DrawLineStrip{style, width, points})
case .DrawLinePath:
segment_count := (read_variable_uint(&r) or_return) + 1
style := read_style(&r, header, cmd.prim_style_kind) or_return
width := read_unit(&r, header) or_return
path := read_path(&r, header, segment_count) or_return
append(&tvg.draw_commands, DrawLinePath{style, width, path})
case .OutlineFillPolygon:
b := bytes.reader_read_byte(&r) or_return
segment_count := (b & 0x3F) + 1
secondary_style_kind := StyleKind(b >> 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..<segment_count {
points[i] = Point{read_unit(&r, header) or_return, read_unit(&r, header) or_return}
}
append(&tvg.draw_commands, OutlineFillPolygon{fill_style, line_style, width, points})
case .OutlineFillRectangles:
b := bytes.reader_read_byte(&r) or_return
rect_count := (b & 0x3F) + 1
secondary_style_kind := StyleKind(b >> 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..<rect_count {
rectangles[i] = Rectangle{
read_unit(&r, header) or_return,
read_unit(&r, header) or_return,
read_unit(&r, header) or_return,
read_unit(&r, header) or_return,
}
}
append(&tvg.draw_commands, OutlineFillRectangles{fill_style, line_style, width, rectangles})
case .OutlineFillPath:
b := bytes.reader_read_byte(&r) or_return
segment_count := (b & 0x3F) + 1
secondary_style_kind := StyleKind(b >> 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..<segment_count {
segments_instructions[s] = (read_variable_uint(r) or_return) + 1
}
for s in 0..<segment_count {
segments[s].start = Point{read_unit(r, header) or_return, read_unit(r, header) or_return}
instructions := make([]PathInstruction, segments_instructions[s])
for i in 0..<segments_instructions[s] {
tag := read_data(r, Tag) or_return
line_width: f32
if tag.has_line_width {
line_width = read_unit(r, header) or_return
}
switch tag.instruction {
case .Line:
position := Point{read_unit(r, header) or_return, read_unit(r, header) or_return}
instructions[i] = LineInstruction{position}
case .HorizontalLine:
x := read_unit(r, header) or_return
instructions[i] = HorizontalLineInstruction{x}
case .VerticalLine:
y := read_unit(r, header) or_return
instructions[i] = VerticalLineInstruction{y}
case .CubicBezier:
control_0 := Point{read_unit(r, header) or_return, read_unit(r, header) or_return}
control_1 := 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}
instructions[i] = CubicBezierInstruction{control_0, control_1, point_1}
case .ArcCircle:
b := bytes.reader_read_byte(r) or_return
radius := read_unit(r, header) or_return
target := Point{read_unit(r, header) or_return, read_unit(r, header) or_return}
large_arc := bool(b & 0x1)
sweep := bool(b & 0x2)
instructions[i] = ArcCircleInstruction{large_arc, sweep, radius, target}
case .ArcEllipse:
b := bytes.reader_read_byte(r) or_return
radius_x := read_unit(r, header) or_return
radius_y := read_unit(r, header) or_return
rotation := read_unit(r, header) or_return
target := Point{read_unit(r, header) or_return, read_unit(r, header) or_return}
large_arc := bool(b & 0x1)
sweep := bool(b & 0x2)
instructions[i] = ArcEllipseInstruction{large_arc, sweep, radius_x, radius_y, rotation, target}
case .ClosePath:
instructions[i] = ClosePathInstruction{}
case .QuadraticBezier:
control := 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}
instructions[i] = QuadraticBezierInstruction{control, point_1}
}
}
segments[s].instructions = instructions
}
path.segments = segments
return
}
@(private, require_results)
to_fixed_point :: proc(#any_int data: int, fractional_bits: u8) -> f32 {
return cast(f32)data / f32(u32(1) << fractional_bits)
}