From 2fe059925569a2b481e8d43b49c7b175b4682c99 Mon Sep 17 00:00:00 2001 From: rdb Date: Sat, 17 Aug 2019 22:42:52 +0200 Subject: [PATCH] deploy-ng: support adding icons to Windows binaries (part of #468) --- direct/src/dist/commands.py | 47 +++++++ direct/src/p3d/DeploymentTools.py | 214 ++++++++++++++++++++++++------ 2 files changed, 224 insertions(+), 37 deletions(-) diff --git a/direct/src/dist/commands.py b/direct/src/dist/commands.py index e5d9ac4f72..204fed4852 100644 --- a/direct/src/dist/commands.py +++ b/direct/src/dist/commands.py @@ -14,12 +14,14 @@ import struct import imp import string import time +import tempfile import setuptools import distutils.log from . import FreezeTool from . import pefile +from direct.p3d.DeploymentTools import Icon import panda3d.core as p3d @@ -224,6 +226,7 @@ class build_apps(setuptools.Command): self.exclude_patterns = [] self.include_modules = {} self.exclude_modules = {} + self.icons = {} self.platforms = [ 'manylinux1_x86_64', 'macosx_10_6_x86_64', @@ -298,6 +301,7 @@ class build_apps(setuptools.Command): key: _parse_list(value) for key, value in _parse_dict(self.exclude_modules).items() } + self.icons = _parse_dict(self.icons) self.platforms = _parse_list(self.platforms) self.plugins = _parse_list(self.plugins) self.extra_prc_files = _parse_list(self.extra_prc_files) @@ -357,6 +361,18 @@ class build_apps(setuptools.Command): tmp.update(self.package_data_dirs) self.package_data_dirs = tmp + self.icon_objects = {} + for app, iconpaths in self.icons.items(): + if not isinstance(iconpaths, list) and not isinstance(iconpaths, tuple): + iconpaths = (iconpaths,) + + iconobj = Icon() + for iconpath in iconpaths: + iconobj.addImage(iconpath) + + iconobj.generateMissingImages() + self.icon_objects[app] = iconobj + def run(self): self.announce('Building platforms: {0}'.format(','.join(self.platforms)), distutils.log.INFO) @@ -433,6 +449,22 @@ class build_apps(setuptools.Command): return wheelpaths + def update_pe_resources(self, appname, runtime): + """Update resources (e.g., icons) in windows PE file""" + + icon = self.icon_objects.get( + appname, + self.icon_objects.get('*', None), + ) + + if icon is not None: + pef = pefile.PEFile() + pef.open(runtime, 'r+') + pef.add_icon(icon) + pef.add_resource_section() + pef.write_changes() + pef.close() + def bundle_macos_app(self, builddir): """Bundle built runtime into a .app for macOS""" @@ -618,6 +650,18 @@ class build_apps(setuptools.Command): stub_path = os.path.join(os.path.dirname(dtool_path), '..', 'bin', stub_name) stub_file = open(stub_path, 'rb') + # Do we need an icon? On Windows, we need to add this to the stub + # before we add the blob. + if 'win' in platform: + temp_file = tempfile.NamedTemporaryFile(suffix='-icon.exe', delete=False) + temp_file.write(stub_file.read()) + stub_file.close() + temp_file.close() + self.update_pe_resources(appname, temp_file.name) + stub_file = open(temp_file.name, 'rb') + else: + temp_file = None + freezer.generateRuntimeFromStub(target_path, stub_file, use_console, { 'prc_data': prcexport if self.embed_prc_data else None, 'default_prc_dir': self.default_prc_dir, @@ -633,6 +677,9 @@ class build_apps(setuptools.Command): }, self.log_append) stub_file.close() + if temp_file: + os.unlink(temp_file.name) + # Copy the dependencies. search_path = [builddir] if use_wheels: diff --git a/direct/src/p3d/DeploymentTools.py b/direct/src/p3d/DeploymentTools.py index 9c390fbf07..70bf1da1b7 100644 --- a/direct/src/p3d/DeploymentTools.py +++ b/direct/src/p3d/DeploymentTools.py @@ -332,6 +332,132 @@ class Icon: return True + def generateMissingImages(self): + """ Generates image sizes that should be present but aren't by scaling + from the next higher size. """ + + for required_size in (48, 32, 24, 16): + if required_size in self.images: + continue + + sizes = sorted(self.images.keys()) + for from_size in sizes: + if from_size > required_size: + break + + if from_size > required_size: + Icon.notify.warning("Generating %dx%d icon by scaling down %dx%d image" % (required_size, required_size, from_size, from_size)) + + image = PNMImage(required_size, required_size) + if self.images[from_size].hasAlpha(): + image.addAlpha() + image.quickFilterFrom(self.images[from_size]) + self.images[required_size] = image + else: + Icon.notify.warning("Cannot generate %dx%d icon; no higher resolution image available" % (required_size, required_size)) + + def _write_bitmap(self, fp, image, size, bpp): + """ Writes the bitmap header and data of an .ico file. """ + + fp.write(struct.pack(' 256: + # Palette too large; remove infrequent colors. + colors.sort(key=hist.get_count, reverse=True) + + # Find the closest color on the palette matching each color + # that didn't fit. This is certainly not the best palette + # generation code, but it'll do for now. + closest_indices = [] + for color in colors[256:]: + closest_index = 0 + closest_diff = 1025 + for i, closest_color in enumerate(colors[:256]): + diff = abs(color.get_red() - closest_color.get_red()) \ + + abs(color.get_green() - closest_color.get_green()) \ + + abs(color.get_blue() - closest_color.get_blue()) + if diff < closest_diff: + closest_index = i + closest_diff = diff + assert closest_diff < 100 + closest_indices.append(closest_index) + + # Write the palette. + i = 0 + while i < 256 and i < len(colors): + r, g, b, a = colors[i] + fp.write(struct.pack('= 256: + # Find closest pixel instead. + index = closest_indices[index - 256] + fp.write(struct.pack('> 3) & 3) + for y in xrange(size): + mask = 0 + num_bits = 7 + for x in xrange(size): + a = image.get_alpha_val(x, size - y - 1) + if a <= 1: + mask |= (1 << num_bits) + num_bits -= 1 + if num_bits < 0: + fp.write(struct.pack('> 3 + if andsize % 4 != 0: + andsize += 4 - (andsize % 4) + fp.write(b'\x00' * (andsize * size)) + def makeICO(self, fn): """ Writes the images to a Windows ICO file. Returns True on success. """ @@ -339,57 +465,71 @@ class Icon: fn = Filename.fromOsSpecific(fn) fn.setBinary() + # ICO files only support resolutions up to 256x256. count = 0 for size in self.images.keys(): + if size < 256: + count += 1 if size <= 256: count += 1 + dataoffs = 6 + count * 16 ico = open(fn, 'wb') ico.write(struct.pack('= 256: + continue + ico.write(struct.pack('> 3 + if andsize % 4 != 0: + andsize += 4 - (andsize % 4) + datasize = 40 + 256 * 4 + (xorsize + andsize) * size + + ico.write(struct.pack(' 256: + continue + elif size == 256: ico.write('\0\0') else: ico.write(struct.pack('> 3 + if andsize % 4 != 0: + andsize += 4 - (andsize % 4) + datasize = 40 + (xorsize + andsize) * size - # Empty AND mask, aligned to 4-byte boundary - #TODO: perhaps we should convert alpha into an AND mask - # to support older versions of Windows that don't support alpha. - ico.write('\0' * (size * (size / 8 + (-((size / 8) * 3) & 3)))) + ico.write(struct.pack('