Support multiple sizes in ICO and improve code quality
This commit is contained in:
parent
b5a1649bc0
commit
f8fed4e41e
1 changed files with 209 additions and 131 deletions
|
@ -2,86 +2,123 @@
|
||||||
|
|
||||||
# All values in ICO/CUR files are represented in little-endian byte order.
|
# All values in ICO/CUR files are represented in little-endian byte order.
|
||||||
|
|
||||||
# Header
|
# Broad file structure:
|
||||||
#
|
# _______________________
|
||||||
# ICONDIR structure
|
# | ICO header |
|
||||||
# _________________________________________________________________________________
|
# |-----------------------|
|
||||||
# | Offset | Size (in bytes) | Purpose |
|
# | Icon directories 1..n |
|
||||||
# |--------|-----------------|------------------------------------------------------|
|
# |-----------------------|
|
||||||
# | 0 | 2 | Reserved. Must always be 0. |
|
# | Image data 1..n |
|
||||||
# |--------|-----------------|------------------------------------------------------|
|
# |_______________________|
|
||||||
# | 2 | 2 | Specifies image type: 1 for icon (.ICO) image, |
|
|
||||||
# | | | 2 for cursor (.CUR) image. Other values are invalid. |
|
|
||||||
# |--------|-----------------|------------------------------------------------------|
|
|
||||||
# | 4 | 2 | Specifies number of images in the file. |
|
|
||||||
# |________|_________________|______________________________________________________|
|
|
||||||
|
|
||||||
# Structure of image directory
|
# ICO header:
|
||||||
# _______________________________________
|
# _______________________________________________________________________________
|
||||||
# | Image #1 | Entry for the first image |
|
# | Offset | Size (bytes) | Purpose |
|
||||||
# |----------|----------------------------|
|
# |--------|--------------|-------------------------------------------------------|
|
||||||
# | Image #2 | Entry for the second image |
|
# | 0 | 2 | Reserved. Must always be 0. |
|
||||||
# |----------|----------------------------|
|
# |--------|--------------|-------------------------------------------------------|
|
||||||
# | ... | |
|
# | 2 | 2 | Specifies image type: 1 for icon (.ICO) image, |
|
||||||
# |----------|----------------------------|
|
# | | | 2 for cursor (.CUR) image. Other values are invalid. |
|
||||||
# | Image #n | Entry for the last image |
|
# |--------|--------------|-------------------------------------------------------|
|
||||||
# |__________|____________________________|
|
# | 4 | 2 | Specifies number of images in the file. |
|
||||||
|
# |________|______________|_______________________________________________________|
|
||||||
|
|
||||||
# Image entry
|
# Icon directory structure:
|
||||||
#
|
# _______________________________________________________________________________
|
||||||
# ICONDIRENTRY structure
|
# | Offset | Size (bytes) | Purpose |
|
||||||
# _________________________________________________________________________________
|
# |--------|--------------|-------------------------------------------------------|
|
||||||
# | Offset | Size (in bytes) | Purpose |
|
# | 0 | 1 | Specifies image width in pixels. Can be any number |
|
||||||
# |--------|-----------------|------------------------------------------------------|
|
# | | | between 0 and 255. Value 0 means image width is 256 |
|
||||||
# | 0 (06) | 1 | Specifies image width in pixels. Can be any number |
|
# | | | pixels. |
|
||||||
# | | | between 0 and 255. Value 0 means image width is 256 |
|
# |--------|--------------|-------------------------------------------------------|
|
||||||
# | | | pixels. |
|
# | 1 | 1 | Specifies image height in pixels. Can be any number |
|
||||||
# |--------|-----------------|------------------------------------------------------|
|
# | | | between 0 and 255. Value 0 means image height is 256 |
|
||||||
# | 1 (07) | 1 | Specifies image height in pixels. Can be any number |
|
# | | | pixels. |
|
||||||
# | | | between 0 and 255. Value 0 means image height is 256 |
|
# |--------|--------------|-------------------------------------------------------|
|
||||||
# | | | pixels. |
|
# | 2 | 1 | Specifies number of colors in the color palette. |
|
||||||
# |--------|-----------------|------------------------------------------------------|
|
# | | | Should be 0 if the image does not use a color palette |
|
||||||
# | 2 (08) | 1 | Specifies number of colors in the color palette. |
|
# |--------|--------------|-------------------------------------------------------|
|
||||||
# | | | Should be 0 if the image does not use a color palette|
|
# | 3 | 1 | Reserved. Should be 0. |
|
||||||
# |--------|-----------------|------------------------------------------------------|
|
# |--------|--------------|-------------------------------------------------------|
|
||||||
# | 3 (09) | 1 | Reserved. Should be 0. |
|
# | 4 | 2 | In ICO format: Specifies color planes. |
|
||||||
# |--------|-----------------|------------------------------------------------------|
|
# | | | Should be 0 or 1. |
|
||||||
# | 4 (10) | 2 | In ICO format: Specifies color planes. |
|
# | | | In CUR format: Specifies the horizontal coordinates |
|
||||||
# | | | Should be 0 or 1. |
|
# | | | of the hotspot in number of pixels from the left. |
|
||||||
# | | | In CUR format: Specifies the horizontal coordinates |
|
# |--------|--------------|-------------------------------------------------------|
|
||||||
# | | | of the hotspot in number of pixels from the left. |
|
# | 6 | 2 | In ICO format: Specifies bits per pixel. |
|
||||||
# |--------|-----------------|------------------------------------------------------|
|
# | | | In CUR format: Specifies the vertical coordinates of |
|
||||||
# | 6 (12) | 2 | In ICO format: Specifies bits per pixel. |
|
# | | | the hotspot in number of pixels from the top. |
|
||||||
# | | | In CUR format: Specifies the vertical coordinates of |
|
# |--------|--------------|-------------------------------------------------------|
|
||||||
# | | | the hotspot in number of pixels from the top. |
|
# | 8 | 4 | Specifies the size of the image's data in bytes |
|
||||||
# |--------|-----------------|------------------------------------------------------|
|
# |--------|--------------|-------------------------------------------------------|
|
||||||
# | 8 (14) | 4 | Specifies the size of the image's data in bytes |
|
# | 12 | 4 | Specifies the offset of BMP or PNG data from the |
|
||||||
# |--------|-----------------|------------------------------------------------------|
|
# | | | beginning of the ICO/CUR file |
|
||||||
# |12 (18) | 4 | Specifies the offset of BMP or PNG data from the |
|
# |________|______________|_______________________________________________________|
|
||||||
# | | | beginning of the ICO/CUR file |
|
|
||||||
# |________|_________________|______________________________________________________|
|
|
||||||
|
|
||||||
import binascii
|
# Image data structure
|
||||||
|
# BMP, starting from the BITMAPINFOHEADER, ignoring normal file header:
|
||||||
|
# _______________________________________________________________________________
|
||||||
|
# | Offset | Size (bytes) | Purpose |
|
||||||
|
# |--------|--------------|-------------------------------------------------------|
|
||||||
|
# | 0 | 4 | The size of this header. Always 40. |
|
||||||
|
# |--------|--------------|-------------------------------------------------------|
|
||||||
|
# | 4 | 4 | Image width in pixels, signed. |
|
||||||
|
# |--------|--------------|-------------------------------------------------------|
|
||||||
|
# | 8 | 4 | Image height in pixels, signed. |
|
||||||
|
# |--------|--------------|-------------------------------------------------------|
|
||||||
|
# | 12 | 2 | Number of color planes. Always 1. |
|
||||||
|
# |--------|--------------|-------------------------------------------------------|
|
||||||
|
# | 14 | 2 | Bits per pixel aka color depth. |
|
||||||
|
# |--------|--------------|-------------------------------------------------------|
|
||||||
|
# | 16 | 4 | Compression method. 0 for None aka BI_RGB. |
|
||||||
|
# |--------|--------------|-------------------------------------------------------|
|
||||||
|
# | 20 | 4 | Image bytes length. 0 for BI_RGB because inferred. |
|
||||||
|
# |--------|--------------|-------------------------------------------------------|
|
||||||
|
# | 24 | 4 | Horizontal print resolution. |
|
||||||
|
# |--------|--------------|-------------------------------------------------------|
|
||||||
|
# | 28 | 4 | Vertical print resolution |
|
||||||
|
# |--------|--------------|-------------------------------------------------------|
|
||||||
|
# | 32 | 4 | Number of colors in palette. 0 for 2^n. |
|
||||||
|
# |--------|--------------|-------------------------------------------------------|
|
||||||
|
# | 36 | 4 | Number of important colors. 0 for all. |
|
||||||
|
# |--------|--------------|-------------------------------------------------------|
|
||||||
|
# | 40 | n | Pixel bytes, r, g, b, a. |
|
||||||
|
# |________|______________|_______________________________________________________|
|
||||||
|
|
||||||
|
import os
|
||||||
import sys
|
import sys
|
||||||
from PIL import Image
|
from PIL import Image
|
||||||
import os
|
|
||||||
print(os.getcwd())
|
|
||||||
try:
|
|
||||||
INPUTFILE = sys.argv[1]
|
|
||||||
except:
|
|
||||||
print('Please provide an image file')
|
|
||||||
quit()
|
|
||||||
|
|
||||||
def little_endian(hexstring):
|
|
||||||
#print(hexstring)
|
|
||||||
assert len(hexstring) % 2 == 0
|
|
||||||
doublets = [hexstring[x*2:(x*2)+2] for x in range(len(hexstring)//2)]
|
|
||||||
#print(doublets)
|
|
||||||
doublets.reverse()
|
|
||||||
hexstring = ''.join(doublets)
|
|
||||||
return hexstring
|
|
||||||
|
|
||||||
def fit_into_bounds(self, iw, ih, fw, fh):
|
ICO_HEADER_LENGTH = 6
|
||||||
|
ICON_DIRECTORY_ENTRY_LENGTH = 16
|
||||||
|
BMP_HEADER_LENGTH = 40
|
||||||
|
|
||||||
|
|
||||||
|
def chunk_sequence(sequence, chunk_length, allow_incomplete=True):
|
||||||
|
'''
|
||||||
|
Given a sequence, divide it into sequences of length `chunk_length`.
|
||||||
|
|
||||||
|
allow_incomplete:
|
||||||
|
If True, allow the final chunk to be shorter if the
|
||||||
|
given sequence is not an exact multiple of `chunk_length`.
|
||||||
|
If False, the incomplete chunk will be discarded.
|
||||||
|
'''
|
||||||
|
(complete, leftover) = divmod(len(sequence), chunk_length)
|
||||||
|
if not allow_incomplete:
|
||||||
|
leftover = 0
|
||||||
|
|
||||||
|
chunk_count = complete + min(leftover, 1)
|
||||||
|
|
||||||
|
chunks = []
|
||||||
|
for x in range(chunk_count):
|
||||||
|
left = chunk_length * x
|
||||||
|
right = left + chunk_length
|
||||||
|
chunks.append(sequence[left:right])
|
||||||
|
|
||||||
|
return chunks
|
||||||
|
|
||||||
|
def fit_into_bounds(iw, ih, fw, fh):
|
||||||
'''
|
'''
|
||||||
Given the w+h of the image and the w+h of the frame,
|
Given the w+h of the image and the w+h of the frame,
|
||||||
return new w+h that fits the image into the frame
|
return new w+h that fits the image into the frame
|
||||||
|
@ -94,72 +131,113 @@ def fit_into_bounds(self, iw, ih, fw, fh):
|
||||||
h = int(ih * ratio)
|
h = int(ih * ratio)
|
||||||
return (w, h)
|
return (w, h)
|
||||||
|
|
||||||
def image_to_ico(filename):
|
def little(x, length):
|
||||||
print('Icofying %s' % filename)
|
return x.to_bytes(length, byteorder='little')
|
||||||
|
|
||||||
|
def load_image(filename):
|
||||||
image = Image.open(filename)
|
image = Image.open(filename)
|
||||||
if min(image.size) > 256:
|
if min(image.size) > 256:
|
||||||
w = image.size[0]
|
(w, h) = image.size
|
||||||
h = image.size[1]
|
image = image.resize(fit_into_bounds(w, h, 256, 256), resample=Image.ANTIALIAS)
|
||||||
image = image.resize((256, 256), resample=Image.ANTIALIAS)
|
|
||||||
image = image.convert('RGBA')
|
image = image.convert('RGBA')
|
||||||
|
return image
|
||||||
|
|
||||||
print('Building ico header')
|
def build_ico_header_blob(image_count):
|
||||||
output_bytes = ''
|
datablob = (b''
|
||||||
output_bytes += '00' '00' # reserved 0
|
+ little(0, 2) # reserved
|
||||||
output_bytes += '01' '00' # 1 = ico
|
+ little(1, 2) # 1 = ico type
|
||||||
output_bytes += '01' '00' # number of images
|
+ little(image_count, 2)
|
||||||
#### ICO HEADER ####
|
)
|
||||||
|
return datablob
|
||||||
|
|
||||||
print('Building image entry')
|
def build_icon_directory_blob(image, offset_from_start):
|
||||||
# width and height. 00 acts as 256
|
(width, height) = image.size
|
||||||
output_bytes += '%02x' % image.size[0] if image.size[0] < 256 else '00'
|
datablob = (b''
|
||||||
output_bytes += '%02x' % image.size[1] if image.size[1] < 256 else '00'
|
+ little(width if width < 256 else 0, 1)
|
||||||
output_bytes += '00' # colors in palette
|
+ little(height if height < 256 else 0, 1)
|
||||||
output_bytes += '00' # reserved 0
|
+ little(0, 1) # colors in palette
|
||||||
output_bytes += '01' '00' # color planes
|
+ little(0, 1) # reserved
|
||||||
output_bytes += '20' '00' # bits per pixel (32)
|
+ little(1, 2) # color planes
|
||||||
output_bytes += little_endian('%08x' % ((image.size[0] * image.size[1] * 4)+8192)) # image bytes size
|
+ little(32, 2) # bit depth
|
||||||
output_bytes += '16' '00' '00' '00' # image offset from start of file (begins right after this)
|
+ little((width * height * 4) + BMP_HEADER_LENGTH, 4) # image bytes length
|
||||||
#### IMAGE ENTRY ####
|
+ little(offset_from_start, 4)
|
||||||
|
)
|
||||||
|
return datablob
|
||||||
|
|
||||||
print('Building image header')
|
def build_image_data_blob(image):
|
||||||
output_bytes += '28' '00' '00' '00' # BMP DIB header size (always 40)
|
datablob = (b''
|
||||||
output_bytes += little_endian('%08x' % image.size[0])
|
+ little(40, 4) # header size
|
||||||
output_bytes += little_endian('%08x' % (image.size[1] * 2)) # I'm not sure why * 2
|
+ little(image.size[0], 4)
|
||||||
output_bytes += '01' '00' # color planes
|
# "Even if the AND mask is not supplied, if the image is in Windows BMP
|
||||||
output_bytes += '20' '00' # bits per pixel (32)
|
# format, the BMP header must still specify a doubled height." - wikipedia
|
||||||
output_bytes += '00' '00' '00' '00' # Compression (None)
|
+ little(image.size[1] * 2, 4)
|
||||||
image_bytesize = image.size[0] * image.size[1] * 4
|
+ little(1, 2) # color planes
|
||||||
# BMP pixel array must always end on a 4th byte.
|
+ little(32, 2) # bit depth
|
||||||
image_nullpadding = 4 - (image_bytesize % 4)
|
+ little(0, 4) # no compression
|
||||||
image_nullpadding = 0 if image_nullpadding == 4 else image_nullpadding
|
+ little(0, 4) # bytes length, inferred
|
||||||
output_bytes += little_endian('%08x' % (image_bytesize + image_nullpadding)) # BMP file size
|
+ little(0, 4) # hor print
|
||||||
output_bytes += '00' '00' '00' '00' # Print width (unnecessary)
|
+ little(0, 4) # ver print
|
||||||
output_bytes += '00' '00' '00' '00' # Print height (unecessary)
|
+ little(0, 4) # palette
|
||||||
output_bytes += '00' '00' '00' '00' # colors in palette
|
+ little(0, 4) # important palette
|
||||||
output_bytes += '00' '00' '00' '00' # "important colors"
|
)
|
||||||
#### IMAGE HEADER ####
|
pixeldata = []
|
||||||
|
# Image.getdata() is a list of (r, g, b, a) channels
|
||||||
|
# But the BMP are written (b, g, r, a)
|
||||||
|
# Also they are written from bottom to top.
|
||||||
|
pixels = list(image.getdata())
|
||||||
|
pixels = reversed(chunk_sequence(pixels, image.size[0]))
|
||||||
|
pixels = [line for chunk in pixels for line in chunk]
|
||||||
|
for pixel in pixels:
|
||||||
|
(r, g, b, a) = pixel
|
||||||
|
pixeldata.extend((b, g, r, a))
|
||||||
|
datablob += bytes(pixeldata)
|
||||||
|
return datablob
|
||||||
|
|
||||||
print('Writing pixels')
|
def images_to_ico(images):
|
||||||
image_data = ''
|
# For some reason Windows reads the icons in reverse order.
|
||||||
for y in range(image.size[1]-1, -1, -1):
|
images.reverse()
|
||||||
for x in range(image.size[0]):
|
|
||||||
pixel = image.getpixel( (x, y) )
|
|
||||||
#print(pixel)
|
|
||||||
r = '%02x' % pixel[0]
|
|
||||||
g = '%02x' % pixel[1]
|
|
||||||
b = '%02x' % pixel[2]
|
|
||||||
o = '%02x' % pixel[3]
|
|
||||||
image_data += b + g + r + o
|
|
||||||
|
|
||||||
image_data += '00' * 8192
|
# The directory entries need to know their image's address, so therefore
|
||||||
output_bytes += image_data
|
# we must know the lengths of all the image binaries before we can write
|
||||||
output_bytes = binascii.a2b_hex(output_bytes)
|
# any directory entries.
|
||||||
name = filename.split('.')[0] + '.ico'
|
# We will calculate the image blobs first, store them separately,
|
||||||
|
# and then put them after the directory blobs.
|
||||||
|
datablobs = []
|
||||||
|
imageblobs = []
|
||||||
|
|
||||||
|
ico_header_blob = build_ico_header_blob(image_count=len(images))
|
||||||
|
datablobs.append(ico_header_blob)
|
||||||
|
|
||||||
|
for (index, image) in enumerate(images):
|
||||||
|
imageblob = build_image_data_blob(image)
|
||||||
|
imageblobs.append(imageblob)
|
||||||
|
|
||||||
|
# Since the ICO header and directory entries are of fixed length, we know
|
||||||
|
# the location of the first image.
|
||||||
|
# After that, the offset just gains the size of the previous image.
|
||||||
|
offset_from_start = ICO_HEADER_LENGTH + (len(images) * ICON_DIRECTORY_ENTRY_LENGTH)
|
||||||
|
for (index, (image, imageblob)) in enumerate(zip(images, imageblobs)):
|
||||||
|
directoryblob = build_icon_directory_blob(image, offset_from_start=offset_from_start)
|
||||||
|
datablobs.append(directoryblob)
|
||||||
|
offset_from_start += len(imageblob)
|
||||||
|
|
||||||
|
datablobs.extend(imageblobs)
|
||||||
|
|
||||||
|
final_data = b''.join(datablobs)
|
||||||
|
return final_data
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
try:
|
||||||
|
inputfiles = sys.argv[1:]
|
||||||
|
except:
|
||||||
|
print('Please provide an image file')
|
||||||
|
raise SystemExit
|
||||||
|
print('Iconifying', inputfiles)
|
||||||
|
images = [load_image(filename) for filename in inputfiles]
|
||||||
|
final_data = images_to_ico(images)
|
||||||
|
name = os.path.splitext(inputfiles[0])[0] + '.ico'
|
||||||
output_file = open(name, 'wb')
|
output_file = open(name, 'wb')
|
||||||
output_file.write(output_bytes)
|
output_file.write(final_data)
|
||||||
output_file.close()
|
output_file.close()
|
||||||
print('Finished %s.' % name)
|
print('Finished %s.' % name)
|
||||||
|
|
||||||
|
|
||||||
image_to_ico(INPUTFILE)
|
|
Loading…
Reference in a new issue