Last active
October 19, 2023 13:23
-
-
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.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// 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