From f3d5a5ce5e0603308e4c82a4e6115ef1434123d9 Mon Sep 17 00:00:00 2001 From: Aleksandra Date: Sun, 10 Dec 2023 21:42:21 +0100 Subject: [PATCH] PNG2C rewrite in Golang (#91) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Krystian Bacławski --- build/common.mk | 2 +- tools/Makefile | 2 +- tools/png2c.py | 477 ------------------------------- tools/png2c/Makefile | 3 + tools/png2c/bitmap/bitmap.go | 143 +++++++++ tools/png2c/bitmap/template.tpl | 28 ++ tools/png2c/go.mod | 3 + tools/png2c/main.go | 137 +++++++++ tools/png2c/palette/palette.go | 63 ++++ tools/png2c/palette/template.tpl | 8 + tools/png2c/params/params.go | 114 ++++++++ tools/png2c/pixmap/pixmap.go | 170 +++++++++++ tools/png2c/pixmap/template.tpl | 17 ++ tools/png2c/sprite/sprite.go | 104 +++++++ tools/png2c/sprite/template.tpl | 35 +++ tools/png2c/util/util.go | 97 +++++++ 16 files changed, 924 insertions(+), 479 deletions(-) delete mode 100755 tools/png2c.py create mode 100644 tools/png2c/Makefile create mode 100644 tools/png2c/bitmap/bitmap.go create mode 100644 tools/png2c/bitmap/template.tpl create mode 100644 tools/png2c/go.mod create mode 100644 tools/png2c/main.go create mode 100644 tools/png2c/palette/palette.go create mode 100644 tools/png2c/palette/template.tpl create mode 100644 tools/png2c/params/params.go create mode 100644 tools/png2c/pixmap/pixmap.go create mode 100644 tools/png2c/pixmap/template.tpl create mode 100644 tools/png2c/sprite/sprite.go create mode 100644 tools/png2c/sprite/template.tpl create mode 100644 tools/png2c/util/util.go diff --git a/build/common.mk b/build/common.mk index fa227074..f402ba64 100644 --- a/build/common.mk +++ b/build/common.mk @@ -60,7 +60,7 @@ CONV2D := $(TOPDIR)/tools/conv2d.py GRADIENT := $(TOPDIR)/tools/gradient.py TMXCONV := $(TOPDIR)/tools/tmxconv/tmxconv PCHG2C := $(TOPDIR)/tools/pchg2c/pchg2c -PNG2C := $(TOPDIR)/tools/png2c.py +PNG2C := $(TOPDIR)/tools/png2c/png2c PSF2C := $(TOPDIR)/tools/psf2c.py SYNC2C := $(TOPDIR)/tools/sync2c/sync2c SVG2C := $(TOPDIR)/tools/svg2c/svg2c diff --git a/tools/Makefile b/tools/Makefile index 11a61c6f..11d19293 100644 --- a/tools/Makefile +++ b/tools/Makefile @@ -1,5 +1,5 @@ TOPDIR := $(realpath ..) -SUBDIRS := dumphunk dumpilbm maketmx pchg2c ptdump sync2c tmxconv svg2c +SUBDIRS := dumphunk dumpilbm maketmx pchg2c ptdump sync2c tmxconv svg2c png2c include $(TOPDIR)/build/common.mk diff --git a/tools/png2c.py b/tools/png2c.py deleted file mode 100755 index 3a3c6fd9..00000000 --- a/tools/png2c.py +++ /dev/null @@ -1,477 +0,0 @@ -#!/usr/bin/env python3 - -from PIL import Image -from array import array -from math import ceil, log -import argparse - - -def coerce(name, typ, value): - try: - return typ(value) - except ValueError: - raise SystemExit(f'{name}: could not convert {value} to {typ}!') - - -def convert(name, cast, value, result): - if isinstance(cast, tuple): - names = name.split(',') - values = value.split('x') - if len(names) > 1: - assert len(names) == len(values) - for n, c, v in zip(names, cast, values): - result[n] = coerce(n, c, v) - else: - result[name] = tuple(coerce(n, c, v) - for n, c, v in zip(name, cast, values)) - else: - result[name] = coerce(name, cast, value) - - -def parse(desc, *params): - mandatory = [] - optional = {} - result = {} - - for param in params: - if len(param) == 2: - mandatory.append(param) - elif len(param) == 3: - name, cast, defval = param - optional[name] = cast - result[name] = defval - else: - raise RuntimeError - - desc = desc.split(',') - - for name, cast in mandatory: - try: - value = desc.pop(0) - except ValueError: - raise SystemExit(f'Missing {name} argument!') - - convert(name, cast, value, result) - - for s in desc: - if not s: - continue - - if s[0] in '+-': - name, value = s[1:], bool(s[0] == '+') - else: - try: - name, value = s.split('=', 1) - except ValueError: - raise SystemExit(f'Malformed optional argument {s}!') - - if name not in optional: - raise SystemExit(f'Unknown optional argument {name}!') - - convert(name, optional[name], value, result) - - return result - - -def planar(pix, width, height, depth): - data = array('H') - padding = array('B', [0 for _ in range(16 - (width & 15))]) - - for offset in range(0, width * height, width): - row = pix[offset:offset + width] - if width & 15: - row.extend(padding) - for p in range(depth): - bits = [(byte >> p) & 1 for byte in row] - for i in range(0, width, 16): - word = 0 - for j in range(16): - word = word * 2 + bits[i + j] - data.append(word) - - return data - - -def chunky4(im): - pix = im.load() - width, height = im.size - data = array('B') - - for y in range(height): - for x in range(0, (width + 1) & ~1, 2): - x0 = pix[x, y] & 15 - if x + 1 < width: - x1 = pix[x + 1, y] & 15 - else: - x1 = 0 - data.append((x0 << 4) | x1) - - return data - - -def rgb12(im): - pix = im.load() - width, height = im.size - data = array('H') - - for y in range(height): - for x in range(width): - r, g, b = pix[x, y] - data.append(((r & 0xf0) << 4) | (g & 0xf0) | (b >> 4)) - - return data - - -def do_bitmap(im, desc): - if im.mode not in ['1', 'L', 'P']: - raise SystemExit('Only 8-bit images supported.') - - param = parse(desc, - ('name', str), - ('width,height,depth', (int, int, int)), - ('extract_at', (int, int), (0, 0)), - ('interleaved', bool, False), - ('cpuonly', bool, False), - ('shared', bool, False), - ('limit_depth', bool, False), - ('onlydata', bool, False)) - - name = param['name'] - has_width = param['width'] - has_height = param['height'] - has_depth = param['depth'] - x, y = param['extract_at'] - interleaved = param['interleaved'] - cpuonly = param['cpuonly'] - shared = param['shared'] - limit_depth = param['limit_depth'] - onlydata = param['onlydata'] - - w, h = im.size - im = im.copy().crop((x, y, min(x + has_width, w), min(y + has_height, h))) - - pix = array('B', im.getdata()) - - width, height = im.size - colors = im.getextrema()[1] + 1 - depth = int(ceil(log(colors, 2))) - if limit_depth: - depth = min(depth, has_depth) - - if width != has_width or height != has_height or depth != has_depth: - raise SystemExit( - 'Image is {}, expected {}!'.format( - 'x'.join(map(str, [width, height, depth])), - 'x'.join(map(str, [has_width, has_height, has_depth])))) - - bytesPerRow = ((width + 15) & ~15) // 8 - wordsPerRow = bytesPerRow // 2 - bplSize = bytesPerRow * height - bpl = planar(pix, width, height, depth) - - data_chip = '' if cpuonly else '__data_chip' - - print(f'static {data_chip} u_short _{name}_bpl[] = {{') - if interleaved: - for i in range(0, depth * wordsPerRow * height, wordsPerRow): - words = ['0x%04x' % bpl[i + x] for x in range(wordsPerRow)] - print(' %s,' % ','.join(words)) - else: - for i in range(0, depth * wordsPerRow, wordsPerRow): - for y in range(height): - words = ['0x%04x' % bpl[i + x] for x in range(wordsPerRow)] - print(' %s,' % ','.join(words)) - i += wordsPerRow * depth - print('};') - print('') - - print(f'#define {name}_width {width}') - print(f'#define {name}_height {height}') - print(f'#define {name}_depth {depth}') - print(f'#define {name}_bytesPerRow {bytesPerRow}') - print(f'#define {name}_bplSize {bplSize}') - print(f'#define {name}_size {bplSize*depth}') - print('') - - if onlydata: - return - - print('%sconst __data BitmapT %s = {' % - ('' if shared else 'static ', name)) - print(f' .width = {width},') - print(f' .height = {height},') - print(f' .depth = {depth},') - print(f' .bytesPerRow = {bytesPerRow},') - print(f' .bplSize = {bplSize},') - flags = ['BM_STATIC'] - if cpuonly: - flags.append('BM_CPUONLY') - if interleaved: - flags.append('BM_INTERLEAVED') - print(' .flags = %s,' % '|'.join(flags)) - print(' .planes = {') - for i in range(depth): - if interleaved: - offset = i * bytesPerRow - else: - offset = i * bplSize - print(f' (void *)_{name}_bpl + {offset},') - print(' }') - print('};') - - -def do_sprite(im, desc): - if im.mode not in ['1', 'L', 'P']: - raise SystemExit('Only 8-bit images supported.') - - param = parse(desc, - ('name', str), - ('height', int), - ('count', int), - ('attached', bool, False)) - - name = param['name'] - has_height = param['height'] - has_count = param['count'] - attached = param['attached'] - - pix = array('B', im.getdata()) - - width, height = im.size - colors = im.getextrema()[1] + 1 - depth = int(ceil(log(colors, 2))) - - if height != has_height: - raise SystemExit( - f'Image height is {height}, expected {has_height}!') - - exp_width = has_count * 16 - if width != exp_width: - raise SystemExit( - f'Image width is {width}, expected {exp_width}!') - - if not attached and depth != 2: - raise SystemExit(f'Image depth is {depth}, expected 2!') - - if attached and depth != 4: - raise SystemExit(f'Image depth is {depth}, expected 4!') - - stride = ((width + 15) & ~15) // 16 - bpl = planar(pix, width, height, depth) - - n = width // 16 - if attached: - n *= 2 - - print(f'#define {name}_height {height}') - print(f'#define {name}_sprites {n}') - print('') - - sprites = [] - - for i in range(n): - sprite = name - if width > 16: - sprite += str(i) - - attached_sprite = attached and i % 2 == 1 - - offset = stride * 2 if attached_sprite else 0 - offset += i // 2 if attached else i - - attached_str = str(attached_sprite).lower() - - print(f'static __data_chip SprDataT {sprite}_sprdat = {{') - print(f' .pos = SPRPOS(0, 0),') - print(f' .ctl = SPRCTL(0, 0, {attached_str}, {height}),') - print(' .data = {') - for j in range(0, stride * depth * height, stride * depth): - words = bpl[offset + j], bpl[offset + j + stride] - print(' { 0x%04x, 0x%04x },' % words) - print(' /* sprite channel terminator */') - print(' { 0x0000, 0x0000 },') - print(' }') - print('};') - print('') - - sprites.append((sprite, attached_str)) - - if n > 1: - print(f'static __data SpriteT {name}[{n}] = {{') - for sprite, attached_str in sprites: - print(' {') - print(f' .sprdat = &{sprite}_sprdat,') - print(f' .height = {height},') - print(f' .attached = {attached_str},') - print(' },') - print('};') - else: - print(f'static __data SpriteT {name} = {{') - print(f' .sprdat = &{name}_sprdat,') - print(f' .height = {height},') - print(f' .attached = false,') - print('};') - - -def do_pixmap(im, desc): - param = parse(desc, - ('name', str), - ('width,height,bpp', (int, int, int)), - ('limit_bpp', bool, False), - ('displayable', bool, False), - ('onlydata', bool, False)) - - name = param['name'] - has_width = param['width'] - has_height = param['height'] - has_bpp = param['bpp'] - onlydata = param['onlydata'] - displayable = param['displayable'] - limit_bpp = param['limit_bpp'] - - width, height = im.size - - if width != has_width or height != has_height: - raise SystemExit('Image size is %dx%d, expected %dx%d!' % ( - width, height, has_width, has_height)) - - if has_bpp not in [4, 8, 12]: - raise SystemExit('Wrong specification: bits per pixel %d!' % has_bpp) - - stride = width - pixeltype = None - data = None - - data_chip = '__data_chip' if displayable else '' - - if im.mode in ['1', 'L', 'P']: - if has_bpp > 8: - raise SystemExit('Expected grayscale / color mapped image!') - - colors = im.getextrema()[1] + 1 - bpp = int(ceil(log(colors, 2))) - if limit_bpp: - bpp = min(bpp, has_bpp) - - if bpp > has_bpp: - raise SystemExit( - 'Image\'s bits per pixel is %d, expected %d!' % (bpp, has_bpp)) - - if has_bpp == 4: - pixeltype = 'PM_CMAP4' - data = chunky4(im) - stride = (width + 1) // 2 - else: - pixeltype = 'PM_CMAP8' - data = array('B', im.getdata()) - - print('static %s u_char %s_pixels[%d] = {' % - (data_chip, name, stride * height)) - for i in range(0, stride * height, stride): - row = ['0x%02x' % p for p in data[i:i + stride]] - print(' %s,' % ', '.join(row)) - print('};') - print('') - elif im.mode in ['RGB']: - if has_bpp <= 8: - raise SystemExit('Expected RGB true color image!') - pixeltype = 'PM_RGB12' - data = rgb12(im) - - print('static u_short %s_pixels[%d] = {' % (name, stride * height)) - for i in range(0, stride * height, stride): - row = ['0x%04x' % p for p in data[i:i + stride]] - print(' %s,' % ', '.join(row)) - print('};') - print('') - else: - raise SystemExit('Image pixel format %s not handled!' % im.mode) - - print('#define %s_width %d' % (name, width)) - print('#define %s_height %d' % (name, height)) - print('') - - if not onlydata: - print('static const __data PixmapT %s = {' % name) - print(' .type = %s,' % pixeltype) - print(' .width = %d,' % width) - print(' .height = %d,' % height) - print(' .pixels = %s_pixels' % name) - print('};') - print('') - - -def do_palette(im, desc): - if im.mode != 'P': - raise SystemExit('Only 8-bit images with palette supported.') - - param = parse(desc, - ('name', str), - ('colors', int), - ('shared', bool, False), - ('store_unused', bool, False)) - - name = param['name'] - has_colors = param['colors'] - shared = param['shared'] - store_unused = param['store_unused'] - - pal = im.getpalette() - colors = im.getextrema()[1] + 1 - - if pal is None: - raise SystemExit('Image has no palette!') - if colors > has_colors: - raise SystemExit('Image has {} colors, expected at most {}!' - .format(colors, has_colors)) - - if store_unused: - colors = max(colors, has_colors) - - cmap = [pal[i * 3:(i + 1) * 3] for i in range(colors)] - count = len(cmap) - - print(f"#define {name}_colors_count {count}\n") - - static = '' if shared else 'static ' - - print(f'{static}__data u_short {name}_colors[{count}] = {{') - for r, g, b in cmap: - print(' 0x%x%x%x,' % (r >> 4, g >> 4, b >> 4)) - print('};\n') - - -if __name__ == '__main__': - parser = argparse.ArgumentParser( - description='Converts an image to bitmap.') - parser.add_argument('--bitmap', type=str, action='append', - help='Output Amiga bitmap [name,dimensions,flags]') - parser.add_argument('--pixmap', type=str, - help='Output pixel map ' - '[name,width,height,type,flags]') - parser.add_argument('--sprite', type=str, - help='Output Amiga sprite [name]') - parser.add_argument('--palette', type=str, - help='Output Amiga palette [name,colors]') - parser.add_argument('path', metavar='PATH', type=str, - help='Input image filename') - args = parser.parse_args() - - im = Image.open(args.path) - - if args.palette: - do_palette(im, args.palette) - print('') - - if args.bitmap: - for bm in args.bitmap: - do_bitmap(im, bm) - print('') - - if args.pixmap: - do_pixmap(im, args.pixmap) - print('') - - if args.sprite: - do_sprite(im, args.sprite) - print('') diff --git a/tools/png2c/Makefile b/tools/png2c/Makefile new file mode 100644 index 00000000..dead18a9 --- /dev/null +++ b/tools/png2c/Makefile @@ -0,0 +1,3 @@ +TOPDIR := $(realpath ../..) + +include $(TOPDIR)/build/go.mk diff --git a/tools/png2c/bitmap/bitmap.go b/tools/png2c/bitmap/bitmap.go new file mode 100644 index 00000000..620896fe --- /dev/null +++ b/tools/png2c/bitmap/bitmap.go @@ -0,0 +1,143 @@ +package bitmap + +import ( + _ "embed" + "fmt" + "image" + "log" + "strings" + + "ghostown.pl/png2c/util" +) + +//go:embed template.tpl +var tpl string + +func Make(in *image.Paletted, cfg image.Config, opts map[string]any) string { + o := bindParams(opts) + + // Set and validate depth + depth := util.GetDepth(in.Pix) + if o.LimitDepth { + depth = min(o.Depth, depth) + } + + // Crop the image if needed (--extract_at option) + pix := in.Pix + if o.SubImage { + pix = util.CutImage(o.StartX, o.StartY, o.Width, o.Height, cfg, in.Pix) + cfg.Width = o.Width + cfg.Height = o.Height + } + + // Validate the image size + if o.Width != cfg.Width || o.Height != cfg.Height { + log.Panicf("Image size is wrong: expected %vx%v, got %vx%v", + o.Width, o.Height, cfg.Width, cfg.Height) + } + + // Validate image depth + if o.Depth < depth { + log.Panicf("Image depth is wrong: expected %v, got %v", o.Depth, depth) + } + + o.BytesPerRow = ((o.Width + 15) & ^15) / 8 + o.WordsPerRow = o.BytesPerRow / 2 + o.BplSize = o.BytesPerRow * o.Height + o.Size = o.BplSize * o.Depth + + // Calculate the binary data + bpl := util.Planar(pix, o.Width, o.Height, depth, o.Interleaved) + + for i := 0; i < depth*o.WordsPerRow*o.Height; i = i + o.WordsPerRow { + words := []string{} + for x := 0; x < o.WordsPerRow; x++ { + f := fmt.Sprintf("0x%04x", bpl[i+x]) + words = append(words, f) + } + o.BplData = append(o.BplData, strings.Join(words, ",")) + } + + flags := []string{"BM_STATIC"} + if o.CpuOnly { + flags = append(flags, "BM_CPUONLY") + } + if o.Interleaved { + flags = append(flags, "BM_INTERLEAVED") + } + o.Flags = strings.Join(flags, "|") + + for i := 0; i < depth; i++ { + offset := 0 + if o.Interleaved { + offset = i * o.BytesPerRow + } else { + offset = i * o.BplSize + } + ptr := fmt.Sprintf("(void *)_%s_bpl + %v", o.Name, offset) + o.BplPtrs = append(o.BplPtrs, ptr) + } + + out := util.CompileTemplate(tpl, o) + + return out +} + +func bindParams(p map[string]any) (out Opts) { + out.Name = p["name"].(string) + out.Width = p["width"].(int) + out.Height = p["height"].(int) + out.Depth = p["depth"].(int) + + if v, ok := p["interleaved"]; ok { + out.Interleaved = v.(bool) + } + if v, ok := p["limit_depth"]; ok { + out.LimitDepth = v.(bool) + } + if v, ok := p["cpuonly"]; ok { + out.CpuOnly = v.(bool) + } + if v, ok := p["shared"]; ok { + out.Shared = v.(bool) + } + if v, ok := p["onlydata"]; ok { + out.OnlyData = v.(bool) + } + if v, ok := p["displayable"]; ok { + out.Displayable = v.(bool) + } + + if coords, _ := p["extract_at"].([]int); coords[0] >= 0 { + out.SubImage = true + out.StartX = coords[0] + out.StartY = coords[1] + } + + return out +} + +type Opts struct { + Name string + Width int + Height int + Depth int + Interleaved bool + LimitDepth bool + CpuOnly bool + Shared bool + OnlyData bool + Displayable bool + // Template-specific data + BytesPerRow int + WordsPerRow int + Flags string + Planes string + BplSize int + Size int + SubImage bool + StartX int + StartY int + BplData []string + BplPtrs []string +} diff --git a/tools/png2c/bitmap/template.tpl b/tools/png2c/bitmap/template.tpl new file mode 100644 index 00000000..f099dd0c --- /dev/null +++ b/tools/png2c/bitmap/template.tpl @@ -0,0 +1,28 @@ +static {{ if not .CpuOnly }}__data_chip{{ end }} u_short _{{ .Name }}_bpl[] = { + {{ range .BplData }} + {{- . -}}, + {{ end -}} +}; + +#define {{ .Name }}_width {{ .Width }} +#define {{ .Name }}_height {{ .Height }} +#define {{ .Name }}_depth {{ .Depth }} +#define {{ .Name }}_bytesPerRow {{ .BytesPerRow }} +#define {{ .Name }}_bplSize {{ .BplSize }} +#define {{ .Name }}_size {{ .Size}} +{{ if not .OnlyData }} +{{ if not .Shared }}static {{ end }}const __data BitmapT {{ .Name }} = { + .width = {{ .Width }}, + .height = {{ .Height }}, + .depth = {{ .Depth }}, + .bytesPerRow = {{ .BytesPerRow }}, + .bplSize = {{ .BplSize }}, + .flags = {{ .Flags }}, + .planes = { + {{ range .BplPtrs }} + {{- . -}}, + {{ end -}} + } +}; +{{ end }} + diff --git a/tools/png2c/go.mod b/tools/png2c/go.mod new file mode 100644 index 00000000..6dd14065 --- /dev/null +++ b/tools/png2c/go.mod @@ -0,0 +1,3 @@ +module ghostown.pl/png2c + +go 1.21.0 diff --git a/tools/png2c/main.go b/tools/png2c/main.go new file mode 100644 index 00000000..1f719ea8 --- /dev/null +++ b/tools/png2c/main.go @@ -0,0 +1,137 @@ +package main + +import ( + "flag" + "fmt" + "image" + "io" + "log" + "os" + "strings" + + b "ghostown.pl/png2c/bitmap" + p "ghostown.pl/png2c/palette" + pms "ghostown.pl/png2c/params" + "ghostown.pl/png2c/pixmap" + "ghostown.pl/png2c/sprite" + "ghostown.pl/png2c/util" +) + +type arrayFlag []string + +func (i *arrayFlag) String() string { + return "my string representation" +} + +func (i *arrayFlag) Set(value string) error { + *i = append(*i, value) + return nil +} + +var ( + bitmapVar arrayFlag + pixmapVar arrayFlag + spriteVar arrayFlag + paletteVar arrayFlag +) + +func init() { + flag.Var(&bitmapVar, "bitmap", "Output Amiga bitmap [name,dimensions,flags]") + flag.Var(&pixmapVar, "pixmap", "Output pixel map [name,width,height,type,flags]") + flag.Var(&spriteVar, "sprite", "Output Amiga sprite [name]") + flag.Var(&paletteVar, "palette", "Output Amiga palette [name,colors]") + + flag.Parse() +} + +func main() { + r, err := os.Open(flag.Arg(0)) + if err != nil { + log.Panicf("Failed to open file %q", flag.Arg(0)) + } + file, _ := io.ReadAll(r) + + img, cfg, err := util.DecodePNG(file) + if err != nil { + log.Panic(err) + } + + var out string + + if len(paletteVar) > 0 { + // Check if image has a palette + pm, _ := img.(*image.Paletted) + if pm == nil { + log.Panic("Only paletted images are supported!") + } + for _, flag := range paletteVar { + opts := pms.ParseOpts(flag, + pms.Param{Name: "name", CastType: pms.TYPE_STRING}, + pms.Param{Name: "count", CastType: pms.TYPE_INT}, + pms.Param{Name: "shared", CastType: pms.TYPE_BOOL, Value: false}, + pms.Param{Name: "store_unused", CastType: pms.TYPE_BOOL, Value: false}, + ) + out += p.Make(pm, cfg, opts) + } + } + + if len(bitmapVar) > 0 { + // Check if image has a palette + pm, _ := img.(*image.Paletted) + if pm == nil { + log.Panic("only paletted images are supported") + } + + for _, flag := range bitmapVar { + opts := pms.ParseOpts(flag, + pms.Param{Name: "name", CastType: pms.TYPE_STRING}, + pms.Param{Name: "width,height,depth", CastType: pms.TYPE_INT}, + pms.Param{Name: "extract_at", CastType: pms.TYPE_INT, Value: "-1x-1"}, + pms.Param{Name: "interleaved", CastType: pms.TYPE_BOOL, Value: false}, + pms.Param{Name: "cpuonly", CastType: pms.TYPE_BOOL, Value: false}, + pms.Param{Name: "shared", CastType: pms.TYPE_BOOL, Value: false}, + pms.Param{Name: "limit_depth", CastType: pms.TYPE_BOOL, Value: false}, + pms.Param{Name: "onlydata", CastType: pms.TYPE_BOOL, Value: false}, + ) + + out += b.Make(pm, cfg, opts) + } + + } + + if len(pixmapVar) > 0 { + for _, flag := range pixmapVar { + opts := pms.ParseOpts(flag, + pms.Param{Name: "name", CastType: pms.TYPE_STRING}, + pms.Param{Name: "width,height,bpp", CastType: pms.TYPE_INT}, + pms.Param{Name: "limit_bpp", CastType: pms.TYPE_BOOL, Value: false}, + pms.Param{Name: "displayable", CastType: pms.TYPE_BOOL, Value: false}, + pms.Param{Name: "onlydata", CastType: pms.TYPE_BOOL, Value: false}, + ) + out += pixmap.Make(img, cfg, opts) + } + } + + if len(spriteVar) > 0 { + pm, _ := img.(*image.Paletted) + if pm == nil { + log.Panic("only paletted images are supported") + } + for _, flag := range spriteVar { + opts := pms.ParseOpts(flag, + pms.Param{Name: "name", CastType: pms.TYPE_STRING}, + pms.Param{Name: "height", CastType: pms.TYPE_INT}, + pms.Param{Name: "count", CastType: pms.TYPE_INT}, + pms.Param{Name: "attached", CastType: pms.TYPE_BOOL, Value: false}, + ) + out += sprite.Make(pm, cfg, opts) + } + } + + inName := strings.Split(r.Name(), ".")[0] + outName := fmt.Sprintf("%s.c", inName) + err = os.WriteFile(outName, []byte(out), 0777) + if err != nil { + log.Panicf("Failed to write file %q", flag.Arg(0)) + } +} diff --git a/tools/png2c/palette/palette.go b/tools/png2c/palette/palette.go new file mode 100644 index 00000000..dc958e75 --- /dev/null +++ b/tools/png2c/palette/palette.go @@ -0,0 +1,63 @@ +package palette + +import ( + _ "embed" + "fmt" + "image" + "log" + "slices" + + "ghostown.pl/png2c/util" +) + +//go:embed template.tpl +var tpl string + +func Make(in *image.Paletted, cfg image.Config, opts map[string]any) string { + o := bindParams(opts) + + if !o.StoreUnused { + // Clean up the palette + o.Count = int(slices.Max(in.Pix)) + 1 + } + + pal := in.Palette[0:o.Count] + + if len(pal) > o.Count { + log.Panicf("Expected max %v colors, got %v", o.Count, len(pal)) + } + + // Calculate the color data + for _, v := range pal { + c := util.RGB12(v) + o.ColorsData = append(o.ColorsData, fmt.Sprintf("0x%03x", c)) + } + + // Compile the template + out := util.CompileTemplate(tpl, o) + + return out +} + +func bindParams(p map[string]any) (out Opts) { + out.Name = p["name"].(string) + out.Count = p["count"].(int) + + if v, ok := p["shared"]; ok { + out.Shared = v.(bool) + } + if v, ok := p["store_unused"]; ok { + out.StoreUnused = v.(bool) + } + + return out +} + +type Opts struct { + Name string + Count int + Shared bool + StoreUnused bool + // Template-specific data. + ColorsData []string +} diff --git a/tools/png2c/palette/template.tpl b/tools/png2c/palette/template.tpl new file mode 100644 index 00000000..c3517757 --- /dev/null +++ b/tools/png2c/palette/template.tpl @@ -0,0 +1,8 @@ +#define {{ .Name }}_colors_count {{ .Count }} + +{{if not .Shared }}static {{ end }}__data u_short {{ .Name }}_colors[{{ .Count }}] = { + {{ range .ColorsData }} + {{- . -}}, + {{ end -}} +}; + diff --git a/tools/png2c/params/params.go b/tools/png2c/params/params.go new file mode 100644 index 00000000..7a3c0fbf --- /dev/null +++ b/tools/png2c/params/params.go @@ -0,0 +1,114 @@ +package params + +import ( + "log" + "strconv" + "strings" +) + +type Param struct { + Name string + CastType int + Value any +} + +const ( + TYPE_STRING int = 1 + TYPE_INT int = 2 + TYPE_BOOL int = 3 +) + +func ParseOpts(s string, params ...Param) map[string]any { + var mandatory []Param + var optional []Param + result := map[string]any{} + + for _, v := range params { + if v.Value == nil { + mandatory = append(mandatory, v) + } else { + optional = append(optional, v) + } + } + + ss := strings.Split(s, ",") + + // parse mandatory arguments + for _, v := range mandatory { + if len(ss) == 0 { + log.Panicf("Missing argument %q!", v.Name) + } + v.Value = ss[0] + result = cast(v, result) + ss = ss[1:] + } + + // parse optional arguments + for _, o := range optional { + var p Param + var found bool + for _, s := range ss { + if strings.Contains(s, o.Name) { + if strings.HasPrefix(s, "+") || strings.HasPrefix(s, "-") { + p.Name = s[1:] + p.Value = string(s[0]) == "+" + p.CastType = o.CastType + } else if strings.Contains(s, "=") { + e := strings.Split(s, "=") + if len(e) != 2 { + log.Panic("Malformed optional argument") + } + p.Name = e[0] + p.Value = e[1] + p.CastType = o.CastType + + } + result = cast(p, result) + found = true + break + } + } + if !found { + result = cast(o, result) + } + } + + return result +} + +func cast(p Param, out map[string]any) map[string]any { + if p.CastType == TYPE_INT { + names := strings.Split(p.Name, ",") + values := strings.Split(p.Value.(string), "x") + if len(values) == len(names) { + for i, name := range names { + var nv any + switch p.CastType { + case TYPE_STRING: + nv = values[i] + case TYPE_INT: + v, err := strconv.Atoi(values[i]) + if err != nil { + log.Panic(err) + } + nv = v + } + out[name] = nv + } + } else if len(names) == 1 && len(values) > 1 { + vs := []int{} + for _, v := range values { + ve, err := strconv.Atoi(v) + if err != nil { + log.Panic(err) + } + vs = append(vs, ve) + } + out[names[0]] = vs + } + } else { + out[p.Name] = p.Value + } + + return out +} diff --git a/tools/png2c/pixmap/pixmap.go b/tools/png2c/pixmap/pixmap.go new file mode 100644 index 00000000..b80bc177 --- /dev/null +++ b/tools/png2c/pixmap/pixmap.go @@ -0,0 +1,170 @@ +package pixmap + +import ( + _ "embed" + "fmt" + "image" + "log" + "strings" + + "ghostown.pl/png2c/util" +) + +//go:embed template.tpl +var tpl string + +func Make(in image.Image, cfg image.Config, opts map[string]any) string { + o := bindParams(opts) + + // Validate bpp + if o.Bpp != 4 && o.Bpp != 8 && o.Bpp != 12 { + log.Panicf("Wrong specification: bits per pixel: %v!", o.Bpp) + } + + // Validate image size + if o.Width != cfg.Width || o.Height != cfg.Height { + log.Panicf("Image size is wrong: expected %vx%v, got %vx%v!", + o.Width, o.Height, cfg.Width, cfg.Height) + } + + var data []uint + var dataFmtStr string + + // Handle RGB images + if rgbm, _ := in.(*image.RGBA); rgbm != nil { + if o.Bpp <= 8 { + log.Panic("Expected RGB true color image!") + } + + // Calculate the data + o.Size = o.Width * o.Height + o.Type = "PM_RGB12" + o.Stride = o.Width + o.PixType = "u_short" + + // Binary data + data = rgb12(*rgbm, o.Height, o.Width) + dataFmtStr = "0x%04x" + } else { + // Handle paletted and grayscale images + var pix []uint8 + if pm, _ := in.(*image.Paletted); pm != nil { + pix = pm.Pix + } else if gray, _ := in.(*image.Gray); gray != nil { + pix = gray.Pix + } else { + log.Panic("Expected color mapped or grayscale image!") + } + + // Set and validate bpp + if o.Bpp > 8 { + log.Panic("Depth too big!") + } + bpp := util.GetDepth(pix) + if o.LimitBpp { + bpp = min(o.Bpp, bpp) + } + + // Validate image depth + if o.Bpp < bpp { + log.Panicf("Image depth is wrong: expected %v, got %v", o.Bpp, bpp) + } + + // Calculate the data + if o.Bpp == 4 { + o.Type = "PM_CMAP4" + o.Stride = (o.Width + 1) / 2 + data = chunky4(in, pix, o.Width, o.Height) + } else { + o.Type = "PM_CMAP8" + o.Stride = o.Width + data = make([]uint, 0, len(pix)) + for _, v := range pix { + data = append(data, uint(v)) + } + } + + o.Size = o.Stride * o.Height + o.PixType = "u_char" + + dataFmtStr = "0x%02x" + } + + for i := 0; i < o.Stride*o.Height; i += o.Stride { + row := []string{} + for _, v := range data[i : i+o.Stride] { + o := fmt.Sprintf(dataFmtStr, v) + row = append(row, o) + } + o.PixData = append(o.PixData, strings.Join(row, ", ")) + } + + out := util.CompileTemplate(tpl, o) + + return out +} + +func chunky4(im image.Image, pix []uint8, width, height int) (out []uint) { + for y := 0; y < height; y++ { + for x := 0; x < ((width + 1) & ^1); x += 2 { + var x0, x1 uint8 + if gray, _ := im.(*image.Gray); gray != nil { + x0 = gray.GrayAt(x, y).Y & 15 + x1 = gray.GrayAt(x+1, y).Y & 15 + } else if pm, _ := im.(*image.Paletted); pm != nil { + x0 = pm.ColorIndexAt(x, y) & 15 + x1 = pm.ColorIndexAt(x+1, y) & 15 + } + if x+1 >= width { + x1 = 0 + } + out = append(out, uint((x0<<4)|x1)) + } + } + return out +} + +func rgb12(img image.RGBA, height, width int) (out []uint) { + for y := 0; y < height; y++ { + for x := 0; x < width; x++ { + c := util.RGB12(img.At(x, y)) + out = append(out, c) + } + } + return out +} + +func bindParams(p map[string]any) (out Opts) { + out.Name = p["name"].(string) + out.Width = p["width"].(int) + out.Height = p["height"].(int) + out.Bpp = p["bpp"].(int) + + if v, ok := p["limit_bpp"]; ok { + out.LimitBpp = v.(bool) + } + if v, ok := p["onlydata"]; ok { + out.OnlyData = v.(bool) + } + if v, ok := p["displayable"]; ok { + out.Displayable = v.(bool) + } + + return out +} + +type Opts struct { + Name string + Width int + Height int + Bpp int + LimitBpp bool + Displayable bool + OnlyData bool + // Template-specific data + Size int + Stride int + Type string + PixData []string + PixType string +} diff --git a/tools/png2c/pixmap/template.tpl b/tools/png2c/pixmap/template.tpl new file mode 100644 index 00000000..637a19a7 --- /dev/null +++ b/tools/png2c/pixmap/template.tpl @@ -0,0 +1,17 @@ +static {{ if .Displayable }}__data_chip {{ end }}{{ .PixType }} {{ .Name }}_pixels[{{ .Size }}] = { + {{ range .PixData }} + {{- . -}}, + {{ end -}} +}; + +#define {{ .Name }}_width {{ .Width }} +#define {{ .Name }}_height {{ .Height }} +{{ if not .OnlyData }} +static const __data PixmapT {{ .Name }} = { + .type = {{ .Type }}, + .width = {{ .Width }}, + .height = {{ .Height }}, + .pixels = {{ .Name }}_pixels +}; +{{ end -}} + diff --git a/tools/png2c/sprite/sprite.go b/tools/png2c/sprite/sprite.go new file mode 100644 index 00000000..d48c0068 --- /dev/null +++ b/tools/png2c/sprite/sprite.go @@ -0,0 +1,104 @@ +package sprite + +import ( + _ "embed" + "fmt" + "image" + "log" + + "ghostown.pl/png2c/util" +) + +//go:embed template.tpl +var tpl string + +func Make(in *image.Paletted, cfg image.Config, opts map[string]any) string { + o := bindParams(opts) + + // Validate image' size + if o.Width != cfg.Width || o.Height != cfg.Height { + got := fmt.Sprintf("%vx%v", cfg.Height, cfg.Width) + exp := fmt.Sprintf("%vx%v", o.Height, o.Width) + log.Panicf("Image size is wrong: expected %q, got %q", exp, got) + } + + // Validate depth + depth := util.GetDepth(in.Pix) + if !o.Attached && depth != 2 { + log.Panicf("Image depth is %v, expected 2!", depth) + } + + var stride int = ((o.Width + 15) & ^15) / 16 + // Binary data + bpl := util.Planar(in.Pix, o.Width, o.Height, depth, true) + n := o.Width / 16 + if o.Attached { + n = n * 2 + } + + // Calculate the sprite data + o.Count = n + o.Sprites = make([]Sprite, n) + for i := 0; i < n; i++ { + o.Sprites[i].Name = o.Name + if o.Width > 16 { + o.Sprites[i].Name += fmt.Sprintf("%v", i) + } + + o.Sprites[i].Attached = o.Attached && i%2 == 1 + + offset := 0 + if o.Sprites[i].Attached { + offset = stride * 2 + } + if o.Attached { + offset += i / 2 + } else { + offset += i + } + + words := []string{} + for j := 0; j < (stride * depth * o.Height); j += (stride * depth) { + a := bpl[offset+j] + b := bpl[offset+j+stride] + words = append(words, fmt.Sprintf("{ 0x%04x, 0x%04x },", a, b)) + } + words = append(words, "/* sprite channel terminator */") + words = append(words, "{ 0x0000, 0x0000 },") + + o.Sprites[i].Data = words + o.Sprites[i].Height = o.Height + } + + out := util.CompileTemplate(tpl, o) + + return out +} + +func bindParams(p map[string]any) (out Opts) { + out.Name = p["name"].(string) + out.Height = p["height"].(int) + out.Count = p["count"].(int) + if v, ok := p["attached"]; ok { + out.Attached = v.(bool) + } + out.Width = out.Count * 16 + + return out +} + +type Opts struct { + Count int + Sprite + // Template-specific data + Sprites []Sprite +} + +type Sprite struct { + Name string + Height int + Attached bool + // Template-specific data + Data []string + Width int +} diff --git a/tools/png2c/sprite/template.tpl b/tools/png2c/sprite/template.tpl new file mode 100644 index 00000000..43cf9600 --- /dev/null +++ b/tools/png2c/sprite/template.tpl @@ -0,0 +1,35 @@ +#define {{.Name}}_height {{.Height}} +#define {{.Name}}_sprites {{.Count}} + +{{ range .Sprites }} +static __data_chip SprDataT {{.Name}}_sprdat = { + .pos = SPRPOS(0, 0), + .ctl = SPRCTL(0, 0, {{.Attached}}, {{.Height}}), + .data = { + {{ range .Data -}} + {{ . }} + {{ end -}} + } +}; +{{ end }} + +{{ if eq .Count 1 }} +static __data SpriteT {{.Name}} = { +{{ range .Sprites }} + .sprdat = &{{.Name}}_sprdat, + .height = {{.Height}}, + .attached = {{.Attached}}, +{{ end }} +}; +{{else}} +static __data SpriteT {{.Name}}[{{.Count}}] = { + {{ range .Sprites -}} + { + .sprdat = &{{.Name}}_sprdat, + .height = {{.Height}}, + .attached = {{.Attached}}, + }, + {{ end }} +}; +{{ end }} + diff --git a/tools/png2c/util/util.go b/tools/png2c/util/util.go new file mode 100644 index 00000000..35b28f8c --- /dev/null +++ b/tools/png2c/util/util.go @@ -0,0 +1,97 @@ +package util + +import ( + "bytes" + "fmt" + "image" + "image/color" + "image/png" + "log" + "math" + "slices" + "strings" + "text/template" +) + +func CompileTemplate(tpl string, data any) string { + tmpl, err := template.New("template").Parse(tpl) + if err != nil { + log.Panic(err) + } + + var buf strings.Builder + err = tmpl.Execute(&buf, data) + if err != nil { + log.Panic(err) + } + + return buf.String() +} + +func CutImage(startX, startY, width, height int, img image.Config, pix []uint8) []uint8 { + offset := img.Width*startY + startX + out := []uint8{} + + for i := 0; i < height; i++ { + out = append(out, pix[offset:offset+width]...) + offset += img.Width + } + + return out +} + +func GetDepth(pix []uint8) int { + return int(math.Ceil(math.Log2(float64(slices.Max(pix) + 1)))) +} + +func DecodePNG(file []byte) (image.Image, image.Config, error) { + cfg, err := png.DecodeConfig(bytes.NewReader(file)) + if err != nil { + return nil, image.Config{}, fmt.Errorf("expected a PNG image, err: %v", err) + } + + img, err := png.Decode(bytes.NewReader(file)) + if err != nil { + return nil, image.Config{}, err + } + + return img, cfg, nil +} + +func Planar(pix []uint8, width, height, depth int, interleaved bool) []uint16 { + wordsPerRow := (width + 15) / 16 + + data := make([]uint16, height*depth*wordsPerRow) + + for y := 0; y < height; y++ { + row := make([]uint8, (width+15)&-15) + copy(row, pix[y*width:(y+1)*width]) + + for p := 0; p < depth; p++ { + bits := make([]uint16, len(row)) + // extract bits at position p and put them into the least significant bit + for i, b := range row { + bits[i] = uint16((b >> p) & 1) + } + // merge them into full words and write to bitmap + for x := 0; x < width; x = x + 16 { + var word uint16 = 0 + for i := 0; i < 16; i++ { + word = word*2 + bits[x+i] + } + if interleaved { + data[(y*depth+p)*wordsPerRow+x/16] = word + } else { + data[(p*height+y)*wordsPerRow+x/16] = word + } + } + } + } + + return data +} + +func RGB12(c color.Color) uint { + r, g, b, _ := c.RGBA() // 16-bit components + return uint(((r & 0xf000) >> 4) | ((g & 0xf000) >> 8) | ((b & 0xf000) >> 12)) +}