Skip to content

Instantly share code, notes, and snippets.

@jakubtomsu
Last active October 19, 2023 13:23
Show Gist options
  • Save jakubtomsu/01ba7b4dbc61b8f1201d691e55492b29 to your computer and use it in GitHub Desktop.
Save jakubtomsu/01ba7b4dbc61b8f1201d691e55492b29 to your computer and use it in GitHub Desktop.
A simple command line utility for packing channels from multiple textures into one image.
// pachchan
// A simple command line utility for packing channels from multiple textures into one image.
// The implementation is kind of a hack :P
package packchan
import "core:os"
import "core:fmt"
import "core:strings"
import "core:strconv"
import "core:path/filepath"
import stbi "vendor:stb/image"
Image :: struct {
name: string,
data: [][4]u8 `fmt:"-"`,
size: [2]int,
}
Channel :: struct {
image: int,
channel: int,
}
main :: proc() {
context.allocator = context.temp_allocator
args := os.args[1:]
if len(args) == 0 || args[0] == "help" {
fmt.println("pachchan - Image channel packing utility")
fmt.println()
fmt.println("Parameters:")
fmt.println("\tout:\tOutput image name")
fmt.println("\tqual:\tOutput image compression quality (JPG only for now).\n\t\tHiger number means better quality.")
fmt.println("\trgba:\tInput channels. Value uses image:channels syntax.\n\t\tChannels can use RGBA/XYZW/0123 names.")
fmt.println("")
fmt.println("Supported image types:")
fmt.println("\tpng")
fmt.println("\tjpg")
fmt.println("\ttga")
fmt.println("\tbmp")
fmt.println()
fmt.println("How to merge two textures example:")
fmt.println("\tpachchan rg=image_a.png:ba b=image_b.jpg:r out=out.png")
return
}
verbose := false
output_name := "out.png"
output_quality: int
image_map := make(map[string]int)
images := make([dynamic]Image, 0, 4)
output_channels: [4]Channel
num_output_channels: int
output_size: [2]int
for arg in args {
switch arg {
case "verbose":
verbose = true
continue
}
spl := strings.split(arg, "=")
if len(spl) != 2 {
panic("Wrong argument syntax. Please use 'arg=value'")
}
name := spl[0]
val := spl[1]
switch name {
case "out":
output_name = val
case "qual":
output_quality = strconv.atoi(val)
case:
val_spl := strings.split(val, ":")
image_name := val_spl[0]
image_channel_str := val_spl[1]
image_channels := make([dynamic]int, 0, 4)
image_index, ok := image_map[image_name]
if !ok {
if image_data, ok := load_image_from_file(image_name); ok {
image_index = len(images)
append(&images, image_data)
image_map[image_name] = image_index
output_size = {
max(output_size.x, image_data.size.x),
max(output_size.y, image_data.size.y),
}
} else {
panic("Failed to load image")
}
}
for ch in image_channel_str {
index := channel_name_to_index(ch)
assert(index >= 0)
append(&image_channels, index)
}
for ch, i in name {
index := channel_name_to_index(ch)
output_channels[index] = {
image = image_index,
channel = image_channels[i],
}
num_output_channels = max(num_output_channels, index + 1)
}
}
}
if verbose {
fmt.println(output_name)
fmt.println(image_map)
fmt.println(images)
fmt.println(output_channels)
fmt.println(num_output_channels)
fmt.println(output_size)
}
for img in images {
if img.size.x != output_size.x && img.size.y != output_size.y {
fmt.printf("Warning: image %s does not match the output size.\n", img.name)
}
}
output_data := make([]u8, num_output_channels * output_size.x * output_size.y)
for x in 0 ..< output_size.x {
for y in 0 ..< output_size.y {
for ch in 0 ..< num_output_channels {
channel := output_channels[ch]
img := images[channel.image]
if x < img.size.x && y < img.size.y {
val := img.data[x + y * img.size.x][channel.channel]
output_data[(x + y * output_size.y) * num_output_channels + ch] = val
}
}
}
}
switch ext := filepath.ext(output_name); ext {
case:
fmt.println("Unknown image type:", ext)
case ".png":
stbi.write_png(
fmt.ctprintf("%s", output_name),
i32(output_size.x),
i32(output_size.y),
i32(num_output_channels),
&output_data[0],
i32(output_size.x * num_output_channels),
)
case ".jpg", ".jpeg":
stbi.write_jpg(
fmt.ctprintf("%s", output_name),
i32(output_size.x),
i32(output_size.y),
i32(num_output_channels),
&output_data[0],
clamp(i32(output_quality), 1, 100),
)
case ".tga":
stbi.write_tga(
fmt.ctprintf("%s", output_name),
i32(output_size.x),
i32(output_size.y),
i32(num_output_channels),
&output_data[0],
)
case ".bmp":
stbi.write_bmp(
fmt.ctprintf("%s", output_name),
i32(output_size.x),
i32(output_size.y),
i32(num_output_channels),
&output_data[0],
)
}
}
load_image_from_file :: proc(name: string) -> (Image, bool) {
width, height, channels: i32
buf := stbi.load(fmt.ctprintf("%s", name), &width, &height, &channels, 4)
if buf == nil {
return {}, false
}
return {
name = name,
data = (cast([^][4]u8)buf)[: width * height],
size = {int(width), int(height)},
}, true
}
channel_name_to_index :: proc(name: rune) -> int {
switch name {
case '0', 'r', 'x': return 0
case '1', 'g', 'y': return 1
case '2', 'b', 'z': return 2
case '3', 'a', 'w': return 3
}
return -1
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment