#!/usr/bin/env python # Copyright 2024 Google LLC # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. import StringIO import argparse import os import struct import sys import png import itertools import generate_c_byte_array from pebble_image_routines import (rgba32_triplet_to_argb8, num_colors_to_bitdepth, get_reduction_func) SCALE_TO_GCOLOR8 = 64 WHITE_COLOR_MAP = { 'white': 1, 'black': 0, 'transparent': 0, } BLACK_COLOR_MAP = { 'white': 0, 'black': 1, 'transparent': 0, } # translates bitdepth to supported PBI format #0 is special case of legacy 1Bit format bitdepth_dict = {0: 0, #GBitmapFormat1Bit 8: 1, #GBitmapFormat8Bit 1: 2, #GBitmapFormat1BitPalette 2: 3, #GBitmapFormat2BitPalette 4: 4} #GBitmapFormat4BitPalette FORMAT_BW = "bw" FORMAT_COLOR = "color" FORMAT_COLOR_RAW = "color_raw" # forces output to be ARGB8 (no palette) FORMAT_CHOICES = [FORMAT_BW, FORMAT_COLOR, FORMAT_COLOR_RAW] DEFAULT_FORMAT = FORMAT_BW TRUNCATE = "truncate" NEAREST = "nearest" COLOR_REDUCTION_CHOICES = [TRUNCATE, NEAREST] DEFAULT_COLOR_REDUCTION = NEAREST # Bitmap struct only contains a color palette for GBitmapFormat(1/2/4)BitPalette # Bitmap struct (NB: All fields are little-endian) # (uint16_t) row_size_bytes # (uint16_t) info_flags # bit 0 : is heap allocated (must be zero for bitmap files) # bits 1-5 : bitmap_format # bits 6-11 : reserved, must be 0 # bits 12-15 : file version # (int16_t) bounds.origin.x # (int16_t) bounds.origin.y # (int16_t) bounds.size.w # (int16_t) bounds.size.h # (uint8_t)[] image data (row_size_bytes-aligned, 0-padded rows of bits) # [optional] (uint8_t)[] argb8 palette data (0-padded to 2 ** bitdepth) class PebbleBitmap(object): def __init__(self, path, color_map=WHITE_COLOR_MAP, bitmap_format=DEFAULT_FORMAT, color_reduction_method=DEFAULT_COLOR_REDUCTION, crop=True, bitdepth=None, palette_name='pebble64'): self.palette_name = palette_name self.version = 1 self.path = path self.name, _ = os.path.splitext(os.path.basename(path)) self.color_map = color_map self.palette = None # only used in color mode for <=16 colors self.bitdepth = bitdepth # number of bits per pixel, 0 for legacy b&w if bitmap_format == FORMAT_BW: self.bitdepth = 0 self.bitmap_format = bitmap_format self.color_reduction_method = color_reduction_method width, height, pixels, metadata = png.Reader(filename=path).asRGBA8() # convert planar boxed row flat pixel to 2d array of (R, G, B, A) self._im_pixels = [] for row in pixels: row_list = [] for (r, g, b, a) in grouper(row, 4): row_list.append((r, g, b, a)) self._im_pixels.append(row_list) self._im_size = (width, height) self._set_bbox(crop) def _set_bbox(self, crop=True): left, top = (0, 0) right, bottom = self._im_size if crop: alphas = [[p[3] for p in row] for row in self._im_pixels] alphas_transposed = zip(*alphas) for row in alphas: if any(row): break top += 1 for row in reversed(alphas): if any(row): break bottom -= 1 for row in alphas_transposed: if any(row): break left += 1 for row in reversed(alphas_transposed): if any(row): break right -= 1 self.x = left self.y = top self.w = right - left self.h = bottom - top def row_size_bytes(self): """ Return the length of the bitmap's row in bytes. On b/w, row lengths are rounded up to the nearest word, padding up to 3 empty bytes per row. On color, row lengths are rounded up to the nearest byte """ if self.bitmap_format == FORMAT_COLOR_RAW: return self.w elif self.bitmap_format == FORMAT_COLOR: # adds (8 / bitdepth) - 1 to round up (ceil) to the next nearest byte return (self.w + ((8 / self.bitdepth) - 1)) / (8 / self.bitdepth) else: row_size_padded_words = (self.w + 31) / 32 return row_size_padded_words * 4 def info_flags(self): """Returns the type and version of bitmap.""" format_value = bitdepth_dict[self.bitdepth] return self.version << 12 | format_value << 1 def pbi_header(self): return struct.pack('> f, '#pragma once' for h in header_paths: print>> f, "#include \"{0}\"".format(h) f.close() def grouper(iterable, n, fillvalue=None): from itertools import izip_longest args = [iter(iterable)] * n return izip_longest(fillvalue=fillvalue, *args) def process_cmd_line_args(): parser = argparse.ArgumentParser(description="Generate pebble-usable files from png images") parser_parent = argparse.ArgumentParser(add_help=False) parser_parent.add_argument('--disable_crop', required=False, action='store_true', help='Disable transparent region cropping for PBI output') parser_parent.add_argument('--color_reduction_method', metavar='method', required=False, nargs=1, default=NEAREST, choices=COLOR_REDUCTION_CHOICES, help="Method used to convert colors to Pebble's color palette, " "options are [{}, {}]".format(NEAREST, TRUNCATE)) subparsers = parser.add_subparsers(help="commands", dest='which') bitmap_format = {"dest": "format", "metavar": "BITMAP_FORMAT", "choices": FORMAT_CHOICES, "nargs": "?", "default": DEFAULT_FORMAT, "help": "resulting GBitmap format"} input_png = {"dest": "input_png", "metavar": "INPUT_PNG", "help": "The png image to process"} output_pbi = {"dest": "output_pbi", "metavar": "OUTPUT_PBI", "help": "The pbi output file"} pbi_parser = subparsers.add_parser('pbi', parents=[parser_parent], help="make a .pbi (pebble binary image) file") for arg in [bitmap_format, input_png, output_pbi]: pbi_parser.add_argument(**arg) pbi_parser.set_defaults(func=cmd_pbi) h_parser = subparsers.add_parser('header', parents=[parser_parent], help="make a .h file") for arg in [bitmap_format, input_png]: h_parser.add_argument(**arg) h_parser.set_defaults(func=cmd_header) white_pbi_parser = subparsers.add_parser('white_trans_pbi', parents=[parser_parent], help="make a .pbi (pebble binary image) file for a white transparency layer") for arg in [input_png, output_pbi]: white_pbi_parser.add_argument(**arg) white_pbi_parser.set_defaults(func=cmd_white_trans_pbi) black_pbi_parser = subparsers.add_parser('black_trans_pbi', parents=[parser_parent], help="make a .pbi (pebble binary image) file for a black transparency layer") for arg in [input_png, output_pbi]: black_pbi_parser.add_argument(**arg) black_pbi_parser.set_defaults(func=cmd_black_trans_pbi) args = parser.parse_args() args.func(args) def main(): if (len(sys.argv) < 2): # process everything in the bitmaps folder process_all_bitmaps() else: # process an individual file process_cmd_line_args() if __name__ == "__main__": main()