mirror of
https://github.com/panda3d/panda3d.git
synced 2025-09-29 08:15:18 -04:00
deploy-ng: support adding icons to Windows binaries (part of #468)
This commit is contained in:
parent
22833686e3
commit
2fe0599255
47
direct/src/dist/commands.py
vendored
47
direct/src/dist/commands.py
vendored
@ -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:
|
||||
|
@ -332,36 +332,34 @@ class Icon:
|
||||
|
||||
return True
|
||||
|
||||
def makeICO(self, fn):
|
||||
""" Writes the images to a Windows ICO file. Returns True on success. """
|
||||
def generateMissingImages(self):
|
||||
""" Generates image sizes that should be present but aren't by scaling
|
||||
from the next higher size. """
|
||||
|
||||
if not isinstance(fn, Filename):
|
||||
fn = Filename.fromOsSpecific(fn)
|
||||
fn.setBinary()
|
||||
for required_size in (48, 32, 24, 16):
|
||||
if required_size in self.images:
|
||||
continue
|
||||
|
||||
count = 0
|
||||
for size in self.images.keys():
|
||||
if size <= 256:
|
||||
count += 1
|
||||
sizes = sorted(self.images.keys())
|
||||
for from_size in sizes:
|
||||
if from_size > required_size:
|
||||
break
|
||||
|
||||
ico = open(fn, 'wb')
|
||||
ico.write(struct.pack('<HHH', 0, 1, count))
|
||||
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))
|
||||
|
||||
# Write the directory
|
||||
for size, image in self.images.items():
|
||||
if size == 256:
|
||||
ico.write('\0\0')
|
||||
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:
|
||||
ico.write(struct.pack('<BB', size, size))
|
||||
bpp = 32 if image.hasAlpha() else 24
|
||||
ico.write(struct.pack('<BBHHII', 0, 0, 1, bpp, 0, 0))
|
||||
Icon.notify.warning("Cannot generate %dx%d icon; no higher resolution image available" % (required_size, required_size))
|
||||
|
||||
# Now write the actual icons
|
||||
ptr = 14
|
||||
for size, image in self.images.items():
|
||||
loc = ico.tell()
|
||||
bpp = 32 if image.hasAlpha() else 24
|
||||
ico.write(struct.pack('<IiiHHIIiiII', 40, size, size * 2, 1, bpp, 0, 0, 0, 0, 0, 0))
|
||||
def _write_bitmap(self, fp, image, size, bpp):
|
||||
""" Writes the bitmap header and data of an .ico file. """
|
||||
|
||||
fp.write(struct.pack('<IiiHHIIiiII', 40, size, size * 2, 1, bpp, 0, 0, 0, 0, 0, 0))
|
||||
|
||||
# XOR mask
|
||||
if bpp == 24:
|
||||
@ -370,26 +368,168 @@ class Icon:
|
||||
for y in xrange(size):
|
||||
for x in xrange(size):
|
||||
r, g, b = image.getXel(x, size - y - 1)
|
||||
ico.write(struct.pack('<BBB', int(b * 255), int(g * 255), int(r * 255)))
|
||||
ico.write(rowalign)
|
||||
else:
|
||||
fp.write(struct.pack('<BBB', int(b * 255), int(g * 255), int(r * 255)))
|
||||
fp.write(rowalign)
|
||||
|
||||
elif bpp == 32:
|
||||
for y in xrange(size):
|
||||
for x in xrange(size):
|
||||
r, g, b, a = image.getXelA(x, size - y - 1)
|
||||
ico.write(struct.pack('<BBBB', int(b * 255), int(g * 255), int(r * 255), int(a * 255)))
|
||||
fp.write(struct.pack('<BBBB', int(b * 255), int(g * 255), int(r * 255), int(a * 255)))
|
||||
|
||||
# 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))))
|
||||
elif bpp == 8:
|
||||
# We'll have to generate a palette of 256 colors.
|
||||
hist = PNMImage.Histogram()
|
||||
if image.hasAlpha():
|
||||
# Make a copy without alpha channel.
|
||||
image2 = PNMImage(image)
|
||||
image2.premultiplyAlpha()
|
||||
image2.removeAlpha()
|
||||
else:
|
||||
image2 = image
|
||||
image2.make_histogram(hist)
|
||||
colors = list(hist.get_pixels())
|
||||
if len(colors) > 256:
|
||||
# Palette too large; remove infrequent colors.
|
||||
colors.sort(key=hist.get_count, reverse=True)
|
||||
|
||||
# Go back to write the location
|
||||
dataend = ico.tell()
|
||||
ico.seek(ptr)
|
||||
ico.write(struct.pack('<II', dataend - loc, loc))
|
||||
ico.seek(dataend)
|
||||
ptr += 16
|
||||
# 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('<BBBB', b, g, r, 0))
|
||||
i += 1
|
||||
if i < 256:
|
||||
# Fill the rest with zeroes.
|
||||
fp.write(b'\x00' * (4 * (256 - i)))
|
||||
|
||||
# Write indices. Align rows to 4-byte boundary.
|
||||
rowalign = b'\0' * (-size & 3)
|
||||
for y in xrange(size):
|
||||
for x in xrange(size):
|
||||
pixel = image2.get_pixel(x, size - y - 1)
|
||||
index = colors.index(pixel)
|
||||
if index >= 256:
|
||||
# Find closest pixel instead.
|
||||
index = closest_indices[index - 256]
|
||||
fp.write(struct.pack('<B', index))
|
||||
fp.write(rowalign)
|
||||
else:
|
||||
raise ValueError("Invalid bpp %d" % (bpp))
|
||||
|
||||
# Create an AND mask, aligned to 4-byte boundary
|
||||
if image.hasAlpha() and bpp <= 8:
|
||||
rowalign = b'\0' * (-((size + 7) >> 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('<B', mask))
|
||||
mask = 0
|
||||
num_bits = 7
|
||||
if num_bits < 7:
|
||||
fp.write(struct.pack('<B', mask))
|
||||
fp.write(rowalign)
|
||||
else:
|
||||
andsize = (size + 7) >> 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. """
|
||||
|
||||
if not isinstance(fn, Filename):
|
||||
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('<HHH', 0, 1, count))
|
||||
|
||||
# Write 8-bpp image headers for sizes under 256x256.
|
||||
for size, image in self.images.items():
|
||||
if size >= 256:
|
||||
continue
|
||||
ico.write(struct.pack('<BB', size, size))
|
||||
|
||||
# Calculate row sizes
|
||||
xorsize = size
|
||||
if xorsize % 4 != 0:
|
||||
xorsize += 4 - (xorsize % 4)
|
||||
andsize = (size + 7) >> 3
|
||||
if andsize % 4 != 0:
|
||||
andsize += 4 - (andsize % 4)
|
||||
datasize = 40 + 256 * 4 + (xorsize + andsize) * size
|
||||
|
||||
ico.write(struct.pack('<BBHHII', 0, 0, 1, 8, datasize, dataoffs))
|
||||
dataoffs += datasize
|
||||
|
||||
# Write 24/32-bpp image headers.
|
||||
for size, image in self.images.items():
|
||||
if size > 256:
|
||||
continue
|
||||
elif size == 256:
|
||||
ico.write('\0\0')
|
||||
else:
|
||||
ico.write(struct.pack('<BB', size, size))
|
||||
|
||||
# Calculate the size so we can write the offset within the file.
|
||||
if image.hasAlpha():
|
||||
bpp = 32
|
||||
xorsize = size * 4
|
||||
else:
|
||||
bpp = 24
|
||||
xorsize = size * 3 + (-(size * 3) & 3)
|
||||
andsize = (size + 7) >> 3
|
||||
if andsize % 4 != 0:
|
||||
andsize += 4 - (andsize % 4)
|
||||
datasize = 40 + (xorsize + andsize) * size
|
||||
|
||||
ico.write(struct.pack('<BBHHII', 0, 0, 1, bpp, datasize, dataoffs))
|
||||
dataoffs += datasize
|
||||
|
||||
# Now write the actual icon bitmap data.
|
||||
for size, image in self.images.items():
|
||||
if size < 256:
|
||||
self._write_bitmap(ico, image, size, 8)
|
||||
|
||||
for size, image in self.images.items():
|
||||
if size <= 256:
|
||||
bpp = 32 if image.hasAlpha() else 24
|
||||
self._write_bitmap(ico, image, size, bpp)
|
||||
|
||||
assert ico.tell() == dataoffs
|
||||
ico.close()
|
||||
|
||||
return True
|
||||
@ -406,9 +546,9 @@ class Icon:
|
||||
icns = open(stream, 'wb')
|
||||
icns.write(b'icns\0\0\0\0')
|
||||
|
||||
icon_types = {16: 'is32', 32: 'il32', 48: 'ih32', 128: 'it32'}
|
||||
mask_types = {16: 's8mk', 32: 'l8mk', 48: 'h8mk', 128: 't8mk'}
|
||||
png_types = {256: 'ic08', 512: 'ic09'}
|
||||
icon_types = {16: b'is32', 32: b'il32', 48: b'ih32', 128: b'it32'}
|
||||
mask_types = {16: b's8mk', 32: b'l8mk', 48: b'h8mk', 128: b't8mk'}
|
||||
png_types = {256: b'ic08', 512: b'ic09'}
|
||||
|
||||
pngtype = PNMFileTypeRegistry.getGlobalPtr().getTypeFromExtension("png")
|
||||
|
||||
|
Loading…
x
Reference in New Issue
Block a user