123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342 |
- # DWIN_ICO
- # - Dissect and create DWIN .ico files for their LCD displays.
- #
- # Copyright (c) 2020 Brent Burton
- #
- # This program is free software: you can redistribute it and/or modify
- # it under the terms of the GNU General Public License as published by
- # the Free Software Foundation, either version 3 of the License, or
- # (at your option) any later version.
- #
- # This program is distributed in the hope that it will be useful,
- # but WITHOUT ANY WARRANTY; without even the implied warranty of
- # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- # GNU General Public License for more details.
- #
- # You should have received a copy of the GNU General Public License
- # along with this program. If not, see <https://www.gnu.org/licenses/>.
- #----------------------------------------------------------------
- #
- # This is not a normal Microsoft .ICO file, but it has a similar
- # structure for containing a number of icon images. Each icon is
- # a small JPG file.
- #
- # The file has a directory header containing fixed-length
- # records, and each record points to its data at an offset later
- # in the file.
- #
- # The directory entries are 16 bytes each, and the entire
- # directory is 4KB (0 - 0x1000). This supports 256 entries.
- #
- # Multibyte values are in Big Endian format.
- #
- # Header: (offset 0x0)
- # W H offset ?? len ?? ??
- # Entry 0: xxxx xxxx 00001000 xx 10a2 00 00000000
- # Entry 1: xxxx xxxx 000020a2 xx 0eac 00 00000000
- # Entry 2: xxxx xxxx 00002f4e xx 0eaa 00 00000000
- # ...
- # 0x00001000: ffd8 ffe1 0018 ... jpeg exif and data follow .. ffd9
- # 0x000020a2: ffd8 ffe1 ...
- # ...rest of ICO entries' data...
- #
- # Header structure:
- # Offset Len What
- # 0 2 width
- # 2 2 height
- # 4 4 file byte position from SEEK_BEG
- # 8 3 length of data
- # 11 5 ??? all zeroes
- #
- # Other notes:
- # * The index of each icon corresponds to the Icon number in dwin.h
- # * One exception is number 39: that header entry is blank, and dwin.h
- # does not define a name for 39. This is specially handled to
- # prevent reordering stock icons.
-
- import os
- import struct
- from PIL import Image
-
- def getJpegResolution(jpegFile):
- """Returns a 2-tuple containing the jpegFile's (width, height).
- """
- img = Image.open(jpegFile)
- return img.size
-
- class DWIN_ICO_File():
- def __init__(self):
- self.entries = [] # list of header entries
-
- def splitFile(self, filename, outDir):
- if not filename[-4:].lower() == '.ico':
- raise RuntimeError('Input file must end in .ico')
-
- with open(filename, 'rb') as infile:
- self._parseHeader(infile)
- self._splitEntryData(infile, outDir)
-
- return
-
- def _parseHeader(self, infile):
- maxEntries = 256
- count = 0
- validEntries = 0
- while count < maxEntries:
- rawBytes = infile.read(16)
- entry = Entry()
- entry.parseRawData(rawBytes)
- # check that it is valid: is offset nonzero?
- # Special case: treat 39 as valid
- if (entry.offset > 0) or (count == 39):
- validEntries += 1
- self.entries.append(entry)
- count += 1
- return
-
- def _splitEntryData(self, infile, outDir):
- print('Splitting Entry Data...')
- if 0 == len(self.entries):
- raise RuntimeError('.ico file is not loaded yet')
-
- # check for output dir:
- if not os.path.exists(outDir):
- os.mkdir(outDir)
-
- # keep a count
- count = 0
- for entry in self.entries:
- # Skip any empty entries. (Special handling of 39.)
- if entry.length == 0:
- count += 1
- continue
- # Seek file position, read length bytes, and write to new output file.
- print('%02d: offset: 0x%06x len: 0x%04x width: %d height: %d' %
- (count, entry.offset, entry.length, entry.width, entry.height))
- outfilename = os.path.join(outDir,
- '%03d-%s.jpg' % (count, _iconNames[count]))
- with open(outfilename, 'wb') as outfile:
- infile.seek(entry.offset)
- blob = infile.read(entry.length)
- outfile.write(blob)
- print('Wrote %d bytes to %s' % (entry.length, outfilename))
-
- count += 1
- return
-
- def createFile(self, iconDir, filename):
- '''Create a new .ico file from the contents of iconDir.
-
- The contents of iconDir are processed to get image
- resolution, and a new entry is created for each.
-
- Each filename must have a leading number followed by a
- dash, which is the icon index. E.g., "071-ICON_StepX.jpg".
- '''
- self.entries = [Entry() for i in range(0,256)]
- # 1. Scan icon directory and record all valid files
- print('Scanning icon directory', iconDir)
- count = 0
- for dirEntry in os.scandir(iconDir):
- if not dirEntry.is_file():
- print('...Ignoring', dirEntry.path)
- continue
- # process each file:
- try:
- index = int(dirEntry.name[0:3])
- if (index < 0) or (index > 255):
- print('...Ignoring invalid index on', dirEntry.path)
- continue
- #dirEntry.path is iconDir/name
- w,h = getJpegResolution(dirEntry.path)
- length = dirEntry.stat().st_size
- e = self.entries[index]
- e.width = w
- e.height = h
- e.length = length
- e.filename = dirEntry.path
- count += 1
- except Exception as e:
- print('Whoops: ', e)
- pass
- print('...Scanned %d icon files' % (count))
-
- # 2. Scan over valid header entries and update offsets
- self._updateHeaderOffsets()
-
- # 3. Write out header to .ico file, the append each icon file
- self._combineAndWriteIcoFile(filename)
- print('Scanning done. %d icons included.' % (count))
-
- def _updateHeaderOffsets(self):
- """Iterate over all header entries and update their offsets.
- """
- offset = 256 * 16
- for i in range(0,256):
- e = self.entries[i]
- if e.length == 0:
- continue
- e.offset = offset
- offset += e.length
- #print('%03d: (%d x %d) len=%d off=%d' %
- # (i, e.width, e.height, e.length, e.offset))
- return
-
- def _combineAndWriteIcoFile(self, filename):
- """Write out final .ico file.
- All header entries are updated, so write out
- the final header contents, and concat each icon
- file to the .ico.
- """
- with open(filename, 'wb') as outfile:
- # 1. Write header directory
- for e in self.entries:
- outfile.write( e.serialize() )
- if outfile.tell() != 4096:
- raise RuntimeError('Header directory write failed. Not 4096 bytes')
- # 2. For each entry, concat the icon file data
- for e in self.entries:
- if 0 == e.length: continue
- guts = self._getFileContents(e.filename, e.length)
- outfile.write(guts)
- return
-
- def _getFileContents(self, filename, length):
- """Read contents of filename, and return bytes"""
- with open(filename, 'rb') as infile:
- contents = infile.read(length)
- if len(contents) != length:
- raise RuntimeError('Failed to read contents of', filename)
- return contents
-
- class Entry():
- '''Entry objects record resolution and size information
- about each icon stored in an ICO file.
- '''
- __slots__ = ('width', 'height', 'offset', 'length', 'filename')
-
- def __init__(self, w=0, h=0, length=0, offset=0):
- self.width = w
- self.height = h
- self.offset = offset
- self.length = length
- self.filename = None
-
- def parseRawData(self, rawEntryBytes):
- if len(rawEntryBytes) != 16:
- raise RuntimeError('Entry: data must be 16 bytes long')
-
- # Split data into bigendian fields
- (w, h, off, len3, len21, b1,b2,b3,b4,b5) = \
- struct.unpack('>HHLBHBBBBB', rawEntryBytes)
- self.width = w
- self.height = h
- self.offset = off
- self.length = len3 * 65536 + len21
- return
-
- def serialize(self):
- """Convert this Entry's information into a 16-byte
- .ico directory entry record. Return bytes object.
- """
- len21 = self.length % 65536
- len3 = self.length // 65536
- rawdata = struct.pack('>HHLBHBBBBB', self.width, self.height,
- self.offset, len3, len21,
- 0, 0, 0, 0, 0)
- return rawdata
-
- _iconNames = {
- 0 : 'ICON_LOGO',
- 1 : 'ICON_Print_0',
- 2 : 'ICON_Print_1',
- 3 : 'ICON_Prepare_0',
- 4 : 'ICON_Prepare_1',
- 5 : 'ICON_Control_0',
- 6 : 'ICON_Control_1',
- 7 : 'ICON_Leveling_0',
- 8 : 'ICON_Leveling_1',
- 9 : 'ICON_HotendTemp',
- 10 : 'ICON_BedTemp',
- 11 : 'ICON_Speed',
- 12 : 'ICON_Zoffset',
- 13 : 'ICON_Back',
- 14 : 'ICON_File',
- 15 : 'ICON_PrintTime',
- 16 : 'ICON_RemainTime',
- 17 : 'ICON_Setup_0',
- 18 : 'ICON_Setup_1',
- 19 : 'ICON_Pause_0',
- 20 : 'ICON_Pause_1',
- 21 : 'ICON_Continue_0',
- 22 : 'ICON_Continue_1',
- 23 : 'ICON_Stop_0',
- 24 : 'ICON_Stop_1',
- 25 : 'ICON_Bar',
- 26 : 'ICON_More',
- 27 : 'ICON_Axis',
- 28 : 'ICON_CloseMotor',
- 29 : 'ICON_Homing',
- 30 : 'ICON_SetHome',
- 31 : 'ICON_PLAPreheat',
- 32 : 'ICON_ABSPreheat',
- 33 : 'ICON_Cool',
- 34 : 'ICON_Language',
- 35 : 'ICON_MoveX',
- 36 : 'ICON_MoveY',
- 37 : 'ICON_MoveZ',
- 38 : 'ICON_Extruder',
- # no 39
- 40 : 'ICON_Temperature',
- 41 : 'ICON_Motion',
- 42 : 'ICON_WriteEEPROM',
- 43 : 'ICON_ReadEEPROM',
- 44 : 'ICON_ResumeEEPROM',
- 45 : 'ICON_Info',
- 46 : 'ICON_SetEndTemp',
- 47 : 'ICON_SetBedTemp',
- 48 : 'ICON_FanSpeed',
- 49 : 'ICON_SetPLAPreheat',
- 50 : 'ICON_SetABSPreheat',
- 51 : 'ICON_MaxSpeed',
- 52 : 'ICON_MaxAccelerated',
- 53 : 'ICON_MaxJerk',
- 54 : 'ICON_Step',
- 55 : 'ICON_PrintSize',
- 56 : 'ICON_Version',
- 57 : 'ICON_Contact',
- 58 : 'ICON_StockConfiguraton',
- 59 : 'ICON_MaxSpeedX',
- 60 : 'ICON_MaxSpeedY',
- 61 : 'ICON_MaxSpeedZ',
- 62 : 'ICON_MaxSpeedE',
- 63 : 'ICON_MaxAccX',
- 64 : 'ICON_MaxAccY',
- 65 : 'ICON_MaxAccZ',
- 66 : 'ICON_MaxAccE',
- 67 : 'ICON_MaxSpeedJerkX',
- 68 : 'ICON_MaxSpeedJerkY',
- 69 : 'ICON_MaxSpeedJerkZ',
- 70 : 'ICON_MaxSpeedJerkE',
- 71 : 'ICON_StepX',
- 72 : 'ICON_StepY',
- 73 : 'ICON_StepZ',
- 74 : 'ICON_StepE',
- 75 : 'ICON_Setspeed',
- 76 : 'ICON_SetZOffset',
- 77 : 'ICON_Rectangle',
- 78 : 'ICON_BLTouch',
- 79 : 'ICON_TempTooLow',
- 80 : 'ICON_AutoLeveling',
- 81 : 'ICON_TempTooHigh',
- 82 : 'ICON_NoTips_C',
- 83 : 'ICON_NoTips_E',
- 84 : 'ICON_Continue_C',
- 85 : 'ICON_Continue_E',
- 86 : 'ICON_Cancel_C',
- 87 : 'ICON_Cancel_E',
- 88 : 'ICON_Confirm_C',
- 89 : 'ICON_Confirm_E',
- 90 : 'ICON_Info_0',
- 91 : 'ICON_Info_1'
- }
|