|
@@ -10,7 +10,7 @@ from direct.directnotify.DirectNotifyGlobal import *
|
|
|
from direct.showbase.AppRunnerGlobal import appRunner
|
|
from direct.showbase.AppRunnerGlobal import appRunner
|
|
|
from panda3d.core import PandaSystem, HTTPClient, Filename, VirtualFileSystem, Multifile
|
|
from panda3d.core import PandaSystem, HTTPClient, Filename, VirtualFileSystem, Multifile
|
|
|
from panda3d.core import TiXmlDocument, TiXmlDeclaration, TiXmlElement, readXmlStream
|
|
from panda3d.core import TiXmlDocument, TiXmlDeclaration, TiXmlElement, readXmlStream
|
|
|
-from panda3d.core import PNMImage, PNMFileTypeRegistry
|
|
|
|
|
|
|
+from panda3d.core import PNMImage, PNMFileTypeRegistry, StringStream
|
|
|
from direct.stdpy.file import *
|
|
from direct.stdpy.file import *
|
|
|
from direct.p3d.HostInfo import HostInfo
|
|
from direct.p3d.HostInfo import HostInfo
|
|
|
# This is important for some reason
|
|
# This is important for some reason
|
|
@@ -332,6 +332,135 @@ class Icon:
|
|
|
|
|
|
|
|
return True
|
|
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 (256, 128, 48, 32, 16):
|
|
|
|
|
+ if required_size in self.images:
|
|
|
|
|
+ continue
|
|
|
|
|
+
|
|
|
|
|
+ sizes = sorted(self.images.keys())
|
|
|
|
|
+ if required_size * 2 in sizes:
|
|
|
|
|
+ from_size = required_size * 2
|
|
|
|
|
+ else:
|
|
|
|
|
+ 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('<IiiHHIIiiII', 40, size, size * 2, 1, bpp, 0, 0, 0, 0, 0, 0))
|
|
|
|
|
+
|
|
|
|
|
+ # XOR mask
|
|
|
|
|
+ if bpp == 24:
|
|
|
|
|
+ # Align rows to 4-byte boundary
|
|
|
|
|
+ rowalign = '\0' * (-(size * 3) & 3)
|
|
|
|
|
+ for y in xrange(size):
|
|
|
|
|
+ for x in xrange(size):
|
|
|
|
|
+ r, g, b = image.getXel(x, size - y - 1)
|
|
|
|
|
+ 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)
|
|
|
|
|
+ fp.write(struct.pack('<BBBB', int(b * 255), int(g * 255), int(r * 255), int(a * 255)))
|
|
|
|
|
+
|
|
|
|
|
+ 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)
|
|
|
|
|
+
|
|
|
|
|
+ # 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):
|
|
def makeICO(self, fn):
|
|
|
""" Writes the images to a Windows ICO file. Returns True on success. """
|
|
""" Writes the images to a Windows ICO file. Returns True on success. """
|
|
|
|
|
|
|
@@ -339,57 +468,71 @@ class Icon:
|
|
|
fn = Filename.fromOsSpecific(fn)
|
|
fn = Filename.fromOsSpecific(fn)
|
|
|
fn.setBinary()
|
|
fn.setBinary()
|
|
|
|
|
|
|
|
|
|
+ # ICO files only support resolutions up to 256x256.
|
|
|
count = 0
|
|
count = 0
|
|
|
for size in self.images.keys():
|
|
for size in self.images.keys():
|
|
|
|
|
+ if size < 256:
|
|
|
|
|
+ count += 1
|
|
|
if size <= 256:
|
|
if size <= 256:
|
|
|
count += 1
|
|
count += 1
|
|
|
|
|
+ dataoffs = 6 + count * 16
|
|
|
|
|
|
|
|
ico = open(fn, 'wb')
|
|
ico = open(fn, 'wb')
|
|
|
ico.write(struct.pack('<HHH', 0, 1, count))
|
|
ico.write(struct.pack('<HHH', 0, 1, count))
|
|
|
|
|
|
|
|
- # Write the directory
|
|
|
|
|
|
|
+ # Write 8-bpp image headers for sizes under 256x256.
|
|
|
for size, image in self.images.items():
|
|
for size, image in self.images.items():
|
|
|
- if size == 256:
|
|
|
|
|
|
|
+ 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')
|
|
ico.write('\0\0')
|
|
|
else:
|
|
else:
|
|
|
ico.write(struct.pack('<BB', size, size))
|
|
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))
|
|
|
|
|
|
|
|
|
|
- # 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))
|
|
|
|
|
-
|
|
|
|
|
- # XOR mask
|
|
|
|
|
- if bpp == 24:
|
|
|
|
|
- # Align rows to 4-byte boundary
|
|
|
|
|
- rowalign = '\0' * (-(size * 3) & 3)
|
|
|
|
|
- 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)
|
|
|
|
|
|
|
+ # Calculate the size so we can write the offset within the file.
|
|
|
|
|
+ if image.hasAlpha():
|
|
|
|
|
+ bpp = 32
|
|
|
|
|
+ xorsize = size * 4
|
|
|
else:
|
|
else:
|
|
|
- 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)))
|
|
|
|
|
|
|
+ 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
|
|
|
|
|
|
|
|
- # 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))))
|
|
|
|
|
|
|
+ # Now write the actual icon bitmap data.
|
|
|
|
|
+ for size, image in self.images.items():
|
|
|
|
|
+ if size < 256:
|
|
|
|
|
+ self._write_bitmap(ico, image, size, 8)
|
|
|
|
|
|
|
|
- # 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
|
|
|
|
|
|
|
+ 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()
|
|
ico.close()
|
|
|
|
|
|
|
|
return True
|
|
return True
|
|
@@ -401,32 +544,35 @@ class Icon:
|
|
|
fn = Filename.fromOsSpecific(fn)
|
|
fn = Filename.fromOsSpecific(fn)
|
|
|
fn.setBinary()
|
|
fn.setBinary()
|
|
|
|
|
|
|
|
- vfs = VirtualFileSystem.getGlobalPtr()
|
|
|
|
|
- stream = vfs.openWriteFile(fn, False, True)
|
|
|
|
|
- icns = open(stream, 'wb')
|
|
|
|
|
|
|
+ icns = open(fn, 'wb')
|
|
|
icns.write(b'icns\0\0\0\0')
|
|
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', 1024: b'ic10'}
|
|
|
|
|
|
|
|
pngtype = PNMFileTypeRegistry.getGlobalPtr().getTypeFromExtension("png")
|
|
pngtype = PNMFileTypeRegistry.getGlobalPtr().getTypeFromExtension("png")
|
|
|
|
|
|
|
|
- for size, image in self.images.items():
|
|
|
|
|
- if size in png_types:
|
|
|
|
|
- if pngtype is None:
|
|
|
|
|
- continue
|
|
|
|
|
- icns.write(png_types[size])
|
|
|
|
|
- icns.write(b'\0\0\0\0')
|
|
|
|
|
- start = icns.tell()
|
|
|
|
|
-
|
|
|
|
|
|
|
+ for size, image in sorted(self.images.items(), key=lambda item:item[0]):
|
|
|
|
|
+ if size in png_types and pngtype is not None:
|
|
|
|
|
+ stream = StringStream()
|
|
|
image.write(stream, "", pngtype)
|
|
image.write(stream, "", pngtype)
|
|
|
- pngsize = icns.tell() - start
|
|
|
|
|
- icns.seek(start - 4)
|
|
|
|
|
- icns.write(struct.pack('>I', pngsize + 8))
|
|
|
|
|
- icns.seek(start + pngsize)
|
|
|
|
|
|
|
+ pngdata = stream.data
|
|
|
|
|
+
|
|
|
|
|
+ icns.write(png_types[size])
|
|
|
|
|
+ icns.write(struct.pack('>I', len(pngdata)))
|
|
|
|
|
+ icns.write(pngdata)
|
|
|
|
|
|
|
|
elif size in icon_types:
|
|
elif size in icon_types:
|
|
|
|
|
+ # If it has an alpha channel, we write out a mask too.
|
|
|
|
|
+ if image.hasAlpha():
|
|
|
|
|
+ icns.write(mask_types[size])
|
|
|
|
|
+ icns.write(struct.pack('>I', size * size + 8))
|
|
|
|
|
+
|
|
|
|
|
+ for y in xrange(size):
|
|
|
|
|
+ for x in xrange(size):
|
|
|
|
|
+ icns.write(struct.pack('<B', int(image.getAlpha(x, y) * 255)))
|
|
|
|
|
+
|
|
|
icns.write(icon_types[size])
|
|
icns.write(icon_types[size])
|
|
|
icns.write(struct.pack('>I', size * size * 4 + 8))
|
|
icns.write(struct.pack('>I', size * size * 4 + 8))
|
|
|
|
|
|
|
@@ -435,15 +581,6 @@ class Icon:
|
|
|
r, g, b = image.getXel(x, y)
|
|
r, g, b = image.getXel(x, y)
|
|
|
icns.write(struct.pack('>BBBB', 0, int(r * 255), int(g * 255), int(b * 255)))
|
|
icns.write(struct.pack('>BBBB', 0, int(r * 255), int(g * 255), int(b * 255)))
|
|
|
|
|
|
|
|
- if not image.hasAlpha():
|
|
|
|
|
- continue
|
|
|
|
|
- icns.write(mask_types[size])
|
|
|
|
|
- icns.write(struct.pack('>I', size * size + 8))
|
|
|
|
|
-
|
|
|
|
|
- for y in xrange(size):
|
|
|
|
|
- for x in xrange(size):
|
|
|
|
|
- icns.write(struct.pack('<B', int(image.getAlpha(x, y) * 255)))
|
|
|
|
|
-
|
|
|
|
|
length = icns.tell()
|
|
length = icns.tell()
|
|
|
icns.seek(4)
|
|
icns.seek(4)
|
|
|
icns.write(struct.pack('>I', length))
|
|
icns.write(struct.pack('>I', length))
|