Skip to content

Commit e967f7e

Browse files
authored
🍱 Build unified DWIN_SET from source (#28741)
1 parent e84bc72 commit e967f7e

59 files changed

Lines changed: 742 additions & 247 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.
123 KB
Binary file not shown.
13.8 KB
Loading
31.8 KB
Loading
31.7 KB
Loading
5.09 KB
Loading
16 Bytes
Binary file not shown.
17.5 KB
Binary file not shown.

‎buildroot/share/dwin/bin/DWIN_ICO.py‎

Lines changed: 84 additions & 56 deletions
Original file line numberDiff line numberDiff line change
@@ -49,47 +49,38 @@
4949
# 11 5 ??? all zeroes
5050
#
5151
# Other notes:
52-
# * The index of each icon corresponds to the Icon number in dwin.h
53-
# * One exception is number 39: that header entry is blank, and dwin.h
54-
# does not define a name for 39. This is specially handled to
55-
# prevent reordering stock icons.
52+
# * The index of each icon corresponds to the Icon number in dwin_set.h
53+
# (7.ICO) or marlinui/dwin_lcd.h (3.ICO, 4.ICO).
54+
# * Entries with zero length are empty slots (e.g., 39 in 7.ICO).
55+
# All 256 entries are kept so that indexes never shift.
5656

57-
import os, struct
57+
import os, re, struct
5858
from PIL import Image
5959

60-
def getJpegResolution(jpegFile):
61-
"""Returns a 2-tuple containing the jpegFile's (width, height).
62-
"""
63-
img = Image.open(jpegFile)
64-
return img.size
65-
6660
class DWIN_ICO_File():
6761
def __init__(self):
6862
self.entries = [] # list of header entries
6963

70-
def splitFile(self, filename, outDir):
64+
def splitFile(self, filename, outDir, names=None):
7165
if not filename[-4:].lower() == '.ico':
7266
raise RuntimeError('Input file must end in .ico')
7367

68+
if names is None: names = iconNamesFor(filename)
69+
7470
with open(filename, 'rb') as infile:
7571
self._parseHeader(infile)
76-
self._splitEntryData(infile, outDir)
72+
self._splitEntryData(infile, outDir, names)
7773

7874
def _parseHeader(self, infile):
79-
maxEntries = 256
80-
count = 0
81-
icon_nums = _iconNames.keys()
82-
while count < maxEntries:
75+
# Keep all 256 entries so each list index is the icon index
76+
self.entries = []
77+
for count in range(256):
8378
rawBytes = infile.read(16)
8479
entry = Entry()
8580
entry.parseRawData(rawBytes)
86-
# check that it is valid: is offset nonzero?
87-
# Special case: treat missing numbers as valid
88-
if (entry.offset > 0) or count not in icon_nums:
89-
self.entries.append(entry)
90-
count += 1
81+
self.entries.append(entry)
9182

92-
def _splitEntryData(self, infile, outDir):
83+
def _splitEntryData(self, infile, outDir, names):
9384
print('Splitting Entry Data...')
9485
if 0 == len(self.entries):
9586
raise RuntimeError('.ico file is not loaded yet')
@@ -98,24 +89,20 @@ def _splitEntryData(self, infile, outDir):
9889
if not os.path.exists(outDir):
9990
os.mkdir(outDir)
10091

101-
# keep a count
102-
count = 0
103-
for entry in self.entries:
104-
# Skip any empty entries. (Special handling of 39.)
105-
if entry.length == 0:
106-
count += 1
107-
continue
108-
outfilename = os.path.join(outDir, '%03d-ICON_%s.jpg' % (count, _iconNames.get(count, "UNKNOWN")))
92+
for count, entry in enumerate(self.entries):
93+
# Skip empty entries
94+
if entry.length == 0: continue
95+
outfilename = os.path.join(outDir, '%03d-ICON_%s.jpg' % (count, names.get(count, "UNKNOWN")))
10996
with open(outfilename, 'wb') as outfile:
97+
# Seek file position, read length bytes, and write to new output file.
11098
infile.seek(entry.offset)
11199
blob = infile.read(entry.length)
100+
if len(blob) != entry.length:
101+
raise RuntimeError('Icon %d data runs past the end of the file' % count)
112102
outfile.write(blob)
113-
# Seek file position, read length bytes, and write to new output file.
114103
print('(%3d: width=%3d height=%3d offset=%6d len=%4d) ... %s' %
115104
(count, entry.width, entry.height, entry.offset, entry.length, os.path.basename(outfilename)))
116105

117-
count += 1
118-
119106
def createFile(self, iconDir, filename):
120107
"""Create a new .ico file from the contents of iconDir.
121108
@@ -129,29 +116,30 @@ def createFile(self, iconDir, filename):
129116
# 1. Scan icon directory and record all valid files
130117
print('Scanning icon directory', iconDir)
131118
count = 0
132-
for dirEntry in os.scandir(iconDir):
133-
if not dirEntry.is_file():
134-
print('...Ignoring', dirEntry.path)
119+
for dirEntry in sorted(os.scandir(iconDir), key=lambda d: d.name):
120+
# Only consider files named like "071-ICON_StepX.jpg"
121+
m = re.match(r'^(\d{3})-.*\.jpe?g$', dirEntry.name, re.IGNORECASE)
122+
if not dirEntry.is_file() or not m:
135123
continue
136-
# process each file:
137-
try:
138-
index = int(dirEntry.name[0:3])
139-
if not (0 <= index <= 255):
140-
print('...Ignoring invalid index on', dirEntry.path)
141-
continue
142-
# dirEntry.path is iconDir/name
143-
w,h = getJpegResolution(dirEntry.path)
144-
length = dirEntry.stat().st_size
145-
e = self.entries[index]
146-
e.width = w
147-
e.height = h
148-
e.length = length
149-
e.filename = dirEntry.path
150-
count += 1
151-
except Exception as e:
152-
print('Whoops: ', e)
153-
pass
124+
index = int(m.group(1))
125+
if not (0 <= index <= 255):
126+
raise RuntimeError('Invalid icon index in %s' % dirEntry.path)
127+
e = self.entries[index]
128+
if e.filename:
129+
raise RuntimeError('Duplicate icon index %03d: %s and %s' % (index, os.path.basename(e.filename), dirEntry.name))
130+
# dirEntry.path is iconDir/name
131+
img = Image.open(dirEntry.path)
132+
if img.format != 'JPEG':
133+
raise RuntimeError('%s is not a JPEG file' % dirEntry.path)
134+
if img.info.get('progressive') or img.info.get('progression'):
135+
raise RuntimeError('%s is a progressive JPEG (use baseline)' % dirEntry.path)
136+
e.width, e.height = img.size
137+
e.length = dirEntry.stat().st_size
138+
e.filename = dirEntry.path
139+
count += 1
154140
print('...Scanned %d icon files' % (count))
141+
if count == 0:
142+
raise RuntimeError('No icon files found in %s' % iconDir)
155143

156144
# 2. Scan over valid header entries and update offsets
157145
self._updateHeaderOffsets()
@@ -196,7 +184,7 @@ def _getFileContents(self, filename, length):
196184
with open(filename, 'rb') as infile:
197185
contents = infile.read(length)
198186
if len(contents) != length:
199-
raise RuntimeError('Failed to read contents of', filename)
187+
raise RuntimeError('Failed to read contents of %s' % filename)
200188
return contents
201189

202190
class Entry():
@@ -344,3 +332,43 @@ def serialize(self):
344332
253 : "AxisTL",
345333
254 : "AxisC"
346334
}
335+
336+
# Names for MarlinUI's boot icons (3.ICO) and status icons (4.ICO), from marlinui/dwin_lcd.h
337+
_iconNames3 = {
338+
0 : "MarlinBoot",
339+
1 : "OpenSource",
340+
2 : "GitHubURL",
341+
3 : "MarlinURL",
342+
4 : "Copyright"
343+
}
344+
345+
_iconNames4 = {
346+
0 : "LOGO_Marlin",
347+
1 : "HotendOff",
348+
2 : "HotendOn",
349+
3 : "BedOff",
350+
4 : "BedOn",
351+
5 : "Fan0",
352+
6 : "Fan1",
353+
7 : "Fan2",
354+
8 : "Fan3",
355+
9 : "Halted",
356+
10 : "Question",
357+
11 : "Alert",
358+
12 : "RotateCW",
359+
13 : "RotateCCW",
360+
14 : "UpArrow",
361+
15 : "DownArrow",
362+
16 : "Bedline",
363+
17 : "BedLeveledOff",
364+
18 : "BedLeveledOn"
365+
}
366+
367+
def iconNamesFor(filename):
368+
"""Return the icon name map for an ICO file, by its leading set number.
369+
Unknown sets get the 7.ICO (dwin_set.h) names.
370+
"""
371+
m = re.match(r'^(\d+)', os.path.basename(filename))
372+
num = int(m.group(1)) if m else 7
373+
return { 3: _iconNames3, 4: _iconNames4 }.get(num, _iconNames)
374+

‎buildroot/share/dwin/bin/README.md‎

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -131,3 +131,27 @@ $ ./bin/makeIco.py icons-7 7.ICO
131131
...Scanned 16 icon files
132132
Scanning done. 16 icons included.
133133
```
134+
135+
Files in the icon folder must be named `NNN-ICON_Name.jpg` with a unique 3-digit index. Other files are ignored. Duplicate indexes and progressive JPEGs are errors, and the tools exit with a non-zero status on any error.
136+
137+
## Building the unified DWIN_SET
138+
139+
`make_dwin_set.py` builds Marlin's unified `DWIN_SET` for Ender-3 V2 family DWIN displays. One flash of this set serves every DWIN UI: Creality UI, JyersUI, ProUI, and MarlinUI (portrait and landscape). The contents are described in `dwin_set.toml`:
140+
141+
- `DWIN_SET/` holds files copied as-is: Creality's boot and text images, font, and display configuration.
142+
- `icons-3`, `icons-4`, and `icons-7` are combined into `3.ICO`, `4.ICO`, and `7.ICO`.
143+
- Icons 0-92 of `7.ICO` are Creality's stock icons. Marlin's own icons are drawn as SVG in `icons-svg/` and listed under `[render]`.
144+
145+
#### Usage:
146+
147+
```sh
148+
bin/make_dwin_set.py [--render] [--out DIR] [--compare OLD.tar.gz]
149+
```
150+
151+
- `--render` re-renders the `[render]` icons from `icons-svg/` before building. This needs `rsvg-convert` (librsvg). Icons are saved as baseline JPEGs with no chroma subsampling and no metadata.
152+
- `--out` sets the output folder (default `build/`, which Git ignores).
153+
- `--compare` lists the files and icon indexes that differ from an existing `DWIN_SET.tar.gz`.
154+
155+
The script checks that every numbered file fits in the display's 16 banks of 32KB and that no two files of the same kind overlap. It writes `build/DWIN_SET/` (copy this folder to the root of the display's SD card) and a reproducible `build/DWIN_SET.tar.gz` for the Configurations repository: the same sources always give the same bytes.
156+
157+
To add an icon, draw it as an SVG in `icons-svg/`, add an entry under `[render]` with its index, size, and background, give it a name in `common/dwin_set.h` and `bin/DWIN_ICO.py`, then run `bin/make_dwin_set.py --render`.

‎buildroot/share/dwin/bin/makeIco.py‎

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@
1818
# along with this program. If not, see <https://www.gnu.org/licenses/>.
1919
#----------------------------------------------------------------
2020

21-
import os.path, argparse, DWIN_ICO
21+
import os.path, sys, argparse, DWIN_ICO
2222

2323
version = '2.0.7'
2424

@@ -47,3 +47,4 @@
4747

4848
except Exception as e:
4949
print('Error: ', e)
50+
sys.exit(1)

0 commit comments

Comments
 (0)