mirror of
https://github.com/google/pebble.git
synced 2025-03-15 16:51:21 +00:00
250 lines
9.8 KiB
Python
250 lines
9.8 KiB
Python
|
#!/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 png
|
||
|
import itertools
|
||
|
import StringIO
|
||
|
|
||
|
import pebble_image_routines
|
||
|
|
||
|
# color reduction methods
|
||
|
TRUNCATE = "truncate"
|
||
|
NEAREST = "nearest"
|
||
|
COLOR_REDUCTION_CHOICES = [TRUNCATE, NEAREST]
|
||
|
SUPPORTED_PALETTES = ('pebble2', 'pebble64')
|
||
|
DEFAULT_COLOR_REDUCTION = NEAREST
|
||
|
|
||
|
# Public APIs
|
||
|
def convert_png_to_pebble_png(input_filename, output_filename,
|
||
|
palette_name, color_reduction_method=DEFAULT_COLOR_REDUCTION,
|
||
|
bitdepth=None):
|
||
|
"""
|
||
|
Convert a png to a pblpng and write it to output_filename
|
||
|
"""
|
||
|
|
||
|
output_png_writer, image_data = _convert_png_to_pebble_png_writer(
|
||
|
input_filename, palette_name, color_reduction_method, force_bitdepth=bitdepth)
|
||
|
|
||
|
with open(output_filename, 'wb') as output_file:
|
||
|
output_png_writer.write_array(output_file, image_data)
|
||
|
|
||
|
|
||
|
def convert_png_to_pebble_png_bytes(input_filename, palette_name,
|
||
|
color_reduction_method=DEFAULT_COLOR_REDUCTION,
|
||
|
bitdepth=None):
|
||
|
"""
|
||
|
Convert a png to a pblpng and return a string with the raw data
|
||
|
"""
|
||
|
|
||
|
output_png, image_data = _convert_png_to_pebble_png_writer(
|
||
|
input_filename, palette_name, color_reduction_method, force_bitdepth=bitdepth)
|
||
|
|
||
|
output_str = StringIO.StringIO()
|
||
|
output_png.write_array(output_str, image_data)
|
||
|
|
||
|
return output_str.getvalue()
|
||
|
|
||
|
|
||
|
# Implementation
|
||
|
def _convert_png_to_pebble_png_writer(input_filename, palette_name, color_reduction_method,
|
||
|
force_bitdepth=None):
|
||
|
input_png = png.Reader(filename=input_filename)
|
||
|
|
||
|
# sbit breaks pypngs convert_rgb_to_rgba routine
|
||
|
# and is unnecessary, as it is only an optional optimization
|
||
|
# so disable it by loading the PNG pre-data and disabling sbit
|
||
|
input_png.preamble()
|
||
|
input_png.sbit = None
|
||
|
|
||
|
# open as RGBA 32-bit (allows for simpler parsing cases)
|
||
|
width, height, pixels, metadata = input_png.asRGBA8()
|
||
|
|
||
|
# convert RGBA 32-bit boxed rows to list for output
|
||
|
rgba32_list = grouper(itertools.chain.from_iterable(pixels), 4)
|
||
|
|
||
|
color_reduction_func = pebble_image_routines.get_reduction_func(palette_name,
|
||
|
color_reduction_method)
|
||
|
is_grey, has_alpha, bitdepth, palette = get_palette_for_png(input_filename,
|
||
|
palette_name,
|
||
|
color_reduction_method)
|
||
|
|
||
|
if force_bitdepth is not None:
|
||
|
if bitdepth > force_bitdepth:
|
||
|
raise Exception("Tried to force {} bits; need at least {}."
|
||
|
.format(force_bitdepth, bitdepth))
|
||
|
|
||
|
# If we're forcing a particular bitdepth, and it's not the one we were going
|
||
|
# to use, skip the greyscale dance.
|
||
|
if bitdepth != force_bitdepth:
|
||
|
is_grey = False
|
||
|
bitdepth = force_bitdepth
|
||
|
|
||
|
transparent_grey = None
|
||
|
# determine the grey value for tRNs transparency
|
||
|
if is_grey and has_alpha:
|
||
|
if bitdepth == 4:
|
||
|
# 4 available shades of grey are occupied
|
||
|
transparent_grey = 0xC # bitdepth 4 supported value
|
||
|
else:
|
||
|
greyscale_list = [0, 255, 85, 170] # in order of bitdepth required
|
||
|
for lum in greyscale_list:
|
||
|
# find the first unused greyscale value in terms of available bitdepth
|
||
|
if (lum, lum, lum, 255) not in palette:
|
||
|
# transparent grey value for requested greyscale bitdepth
|
||
|
transparent_grey = lum >> (8 - bitdepth)
|
||
|
break
|
||
|
|
||
|
# second pass of pixel data, converts rgba32 pixels to greyscale or palettized output
|
||
|
image = []
|
||
|
for (r, g, b, a) in rgba32_list:
|
||
|
# operating on original pixel values, need to do the same color reduction
|
||
|
# as when the palette was generated
|
||
|
(r, g, b, a) = color_reduction_func(r, g, b, a)
|
||
|
|
||
|
if is_grey:
|
||
|
# convert red channel (as luminosity value) to a greyscale at bitdepth
|
||
|
# if transparent, output the transparent_grey value for that bitdepth
|
||
|
if a == 0:
|
||
|
image.append(transparent_grey)
|
||
|
else:
|
||
|
image.append(r >> (8 - bitdepth))
|
||
|
elif has_alpha:
|
||
|
# append the palette index for output
|
||
|
image.append(palette.index((r, g, b, a)))
|
||
|
else:
|
||
|
# append the palette index for output
|
||
|
image.append(palette.index((r, g, b)))
|
||
|
|
||
|
if is_grey:
|
||
|
# remove the palette for greyscale output with writer
|
||
|
palette = None
|
||
|
|
||
|
output_png = png.Writer(width=width, height=height, compression=9, bitdepth=bitdepth,
|
||
|
palette=palette, greyscale=is_grey, transparent=transparent_grey)
|
||
|
|
||
|
return (output_png, image)
|
||
|
|
||
|
|
||
|
def get_palette_for_png(input_filename, palette_name, color_reduction_method):
|
||
|
input_png = png.Reader(filename=input_filename)
|
||
|
|
||
|
# sbit breaks pypngs convert_rgb_to_rgba routine
|
||
|
# and is unnecessary, as it is only an optional optimization
|
||
|
# so disable it by loading the PNG pre-data and disabling sbit
|
||
|
input_png.preamble()
|
||
|
input_png.sbit = None
|
||
|
|
||
|
# open as RGBA 32-bit (allows for simpler parsing cases)
|
||
|
width, height, pixels, metadata = input_png.asRGBA8()
|
||
|
|
||
|
palette = [] # rgba32 image palette
|
||
|
is_grey = True # does the image only contain greyscale pixels (and only full or opaque)
|
||
|
has_alpha = False # does the image contain alpha
|
||
|
|
||
|
# iterators are one shot, so make a copy of just the iterator and not the data
|
||
|
# to be able to parse the data twice (we do not modify the pixel data itself)
|
||
|
# once to generate the palette
|
||
|
# once to output the final pixel data as greyscale or palette indexes
|
||
|
pixels, pixels2 = itertools.tee(pixels)
|
||
|
|
||
|
# Figure out what color reduction algorithm we should be using.
|
||
|
color_reduction_func = pebble_image_routines.get_reduction_func(palette_name,
|
||
|
color_reduction_method)
|
||
|
|
||
|
# convert RGBA 32-bit image colors to pebble color table
|
||
|
for (r, g, b, a) in grouper(itertools.chain.from_iterable(pixels2), 4):
|
||
|
(r, g, b, a) = color_reduction_func(r, g, b, a)
|
||
|
|
||
|
if (r, g, b, a) not in palette:
|
||
|
palette.append((r, g, b, a))
|
||
|
# Check if image contains any transparent pixels
|
||
|
if (a != 0xFF):
|
||
|
has_alpha = True
|
||
|
# greyscale only if rgb is gray and opaque or fully transparent
|
||
|
if is_grey and not (((r == g == b) and a == 255) or (r, g, b, a) == (0, 0, 0, 0)):
|
||
|
is_grey = False
|
||
|
|
||
|
# Calculate required bit depth
|
||
|
|
||
|
# get the bitdepth for the number of colors
|
||
|
bitdepth = pebble_image_routines.num_colors_to_bitdepth(len(palette))
|
||
|
if is_grey:
|
||
|
# for Greyscale, it is the required colors that set the bitdepth
|
||
|
# so if image contains LightGray or DarkGray it requires bitdepth 2
|
||
|
if (85, 85, 85, 255) in palette or (170, 170, 170, 255) in palette:
|
||
|
# if palette contains all 4 greyscale and transparent, bump up bitdepth
|
||
|
if (len(palette)) >= 5:
|
||
|
grey_bitdepth = 4
|
||
|
else:
|
||
|
grey_bitdepth = 2
|
||
|
else:
|
||
|
# if palette contains black, white and transparent, bump up bitdepth
|
||
|
if (len(palette)) >= 3:
|
||
|
grey_bitdepth = 2
|
||
|
else:
|
||
|
grey_bitdepth = 1
|
||
|
if grey_bitdepth > bitdepth:
|
||
|
is_grey = False
|
||
|
else:
|
||
|
bitdepth = grey_bitdepth
|
||
|
|
||
|
|
||
|
# update data for RGB output format
|
||
|
if not has_alpha:
|
||
|
# recreate the palette without an alpha channel to support RGB PNG
|
||
|
palette = [(p_r, p_g, p_b) for p_r, p_g, p_b, p_a in palette]
|
||
|
|
||
|
return is_grey, has_alpha, bitdepth, palette
|
||
|
|
||
|
|
||
|
def grouper(iterable, n, fillvalue=None):
|
||
|
from itertools import izip_longest
|
||
|
|
||
|
args = [iter(iterable)] * n
|
||
|
return izip_longest(fillvalue=fillvalue, *args)
|
||
|
|
||
|
|
||
|
def get_ideal_palette(is_color=False):
|
||
|
if is_color:
|
||
|
return 'pebble64'
|
||
|
else:
|
||
|
return 'pebble2'
|
||
|
|
||
|
|
||
|
def main():
|
||
|
import argparse
|
||
|
|
||
|
parser = argparse.ArgumentParser(
|
||
|
description='Convert PNG to 64-color palettized or grayscale PNG')
|
||
|
parser.add_argument('input_filename', type=str, help='png file to convert')
|
||
|
parser.add_argument('output_filename', type=str, help='converted file output')
|
||
|
parser.add_argument('--palette', type=str, required=False,
|
||
|
choices=SUPPORTED_PALETTES, default='pebble64',
|
||
|
help="Specify the standard palette of the resulting png. Colors will be "
|
||
|
"converted to this lower bit depth using the color_reduction_method "
|
||
|
"arg.")
|
||
|
parser.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))
|
||
|
args = parser.parse_args()
|
||
|
|
||
|
convert_png_to_pebble_png(args.input_filename, args.output_filename,
|
||
|
args.palette, args.color_reduction_method)
|
||
|
|
||
|
if __name__ == '__main__':
|
||
|
main()
|