Skip to content

Commit bf40db7

Browse files
committed
Add icon (.info) file generation
1 parent 26ae3ff commit bf40db7

File tree

6 files changed

+238
-11
lines changed

6 files changed

+238
-11
lines changed

.github/workflows/amiga_demo.yml

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -17,14 +17,14 @@ jobs:
1717
steps:
1818
- name: Checkout source
1919
uses: actions/checkout@v4
20-
- name: Install amitools
20+
- name: Install Python dependencies
2121
run: |
2222
python3 -m venv venv
2323
./venv/bin/python3 -m pip install --upgrade pip setuptools wheel
24-
./venv/bin/python3 -m pip install amitools
25-
- name: Update package index
24+
./venv/bin/python3 -m pip install amitools Pillow
25+
- name: Update apt package index
2626
run: sudo apt update
27-
- name: Install dependencies
27+
- name: Install apt package dependencies
2828
run: sudo apt install -y gimp netpbm
2929
- name: Install ipng2iff
3030
run: |
@@ -40,6 +40,7 @@ jobs:
4040
sudo cp salvador /usr/local/bin/
4141
- name: Build ADF using make
4242
run: |
43+
source venv/bin/activate
4344
make adf XDF_TOOL=venv/bin/xdftool
4445
- name: Upload ADF artifact(s)
4546
uses: actions/upload-artifact@v4

.gitignore

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,13 @@
11
# build artefacts
22
build/
33
uae/dh0/main
4-
# assets that are auto-generated
4+
# assets/icons that are auto-generated
55
assets/*.png
66
assets/*.iff
77
assets/*.raw
88
assets/*.zx0
9+
icons/*.png
10+
icons/*.info
911
# src that we don't care about
1012
include/*_palette.i
1113
# compiled tools

Makefile

Lines changed: 24 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -66,13 +66,27 @@ PALETTE_DIR := ./include
6666

6767
# The target ADF dir
6868
ADF_DIR := ./uae/dh0
69+
6970
# The Target Binary Program
70-
TARGET := $(ADF_DIR)/main
71+
TARGET_NAME := main
72+
TARGET := $(ADF_DIR)/$(TARGET_NAME)
73+
74+
# Icons
75+
ICONS_DIR := ./icons
76+
TARGET_ICON := $(ICONS_DIR)/$(TARGET_NAME).info
77+
ICON_WIDTH=64
78+
ICON_HEIGHT=32
79+
ICON_PALETTE=1 # 1 = 1.x colours, 2 = 2.x colours
7180

7281
# Generic rule to create a RAW asset from XCF
7382
$(ASSETS_DIR)/%.raw: $(ASSETS_DIR)/%.xcf
7483
@./scripts/convert_assets_to_raw.sh -x -p -r -s -i $(PALETTE_DIR) "$<"
7584

85+
# Generic rule to create a .INFO icon from XCF
86+
$(ICONS_DIR)/%.info: $(ICONS_DIR)/%.xcf
87+
@./scripts/convert_assets_to_raw.sh -x "$<"
88+
@./scripts/amiga-icon-converter.py --width=$(ICON_WIDTH) --height=$(ICON_HEIGHT) --palette=$(ICON_PALETTE) "$(basename $<).png"
89+
7690
# Generic rule to compress a RAW asset using zx0
7791
$(ASSETS_DIR)/%_raw.zx0: $(ASSETS_DIR)/%.raw
7892
$(ZX0) "$<" "$@"
@@ -120,11 +134,12 @@ ADF_FILE := amiga_demo.adf
120134
ADF_VOLUME_NAME := 'Amiga Demo'
121135
XDF_TOOL := xdftool
122136

123-
adf: clean_adf $(TARGET)
137+
adf: clean_adf $(TARGET) $(TARGET_ICON)
124138
$(XDF_TOOL) $(ADF_FILE) create
125139
$(XDF_TOOL) $(ADF_FILE) format $(ADF_VOLUME_NAME)
126140
$(XDF_TOOL) $(ADF_FILE) boot install boot1x
127-
$(XDF_TOOL) $(ADF_FILE) write $(ADF_DIR)/main
141+
$(XDF_TOOL) $(ADF_FILE) write $(TARGET)
142+
$(XDF_TOOL) $(ADF_FILE) write $(TARGET_ICON)
128143
$(XDF_TOOL) $(ADF_FILE) write $(ADF_DIR)/s
129144
$(XDF_TOOL) $(ADF_FILE) list
130145

@@ -149,8 +164,12 @@ clean_assets:
149164
@rm -f $(ASSETS_DIR)/*.raw
150165
@rm -f $(ASSETS_DIR)/*.zx0
151166

167+
clean_icons:
168+
rm -f $(ICONS_DIR)/*.png
169+
rm -f $(ICONS_DIR)/*.info
170+
152171
# clean EVERYTHING
153-
clean_all: clean clean_tools clean_assets clean_adf
172+
clean_all: clean clean_tools clean_assets clean_icons clean_adf
154173

155174
#Non-File Targets
156-
.PHONY: adf all assets clean clean_tools clean_assets clean_all
175+
.PHONY: adf all assets clean clean_tools clean_assets clean_icons clean_all

README.md

Lines changed: 33 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ The following features are provided and demonstrated:
1414
* Conversion of [GIMP](https://www.gimp.org/) authored image assets (*`.xcf`) into `.png`, `.iff` and `.raw` (interleaved) formats
1515
* Compression ('packing') or raw assets using the '`zx0`' format
1616
* Generation of palette (`COLORxx` register) data for image assets in copper list format
17+
* Generation of a Workbench 'tool' icon (*.info) for the main executable from a GIMP image (*.xcf)
1718
* Host compilation of assembler (`vasm`) and linker (`vlink`) tools included.
1819
* Building to a [UAE](https://en.wikipedia.org/wiki/UAE_(emulator)) emulated hard drive (`dh0`) folder
1920
* Building of a ***bootable*** AmigaDOS floppy disk format image (`ADF`) file.
@@ -29,6 +30,7 @@ for CI/CD automated building in the the cloud.
2930
* No support for attached sprites palette generation
3031
* No special treatment for AGA
3132
* Bare bones 'no frills' bootable AmigaDOS ADFs. i.e. no loading messages etc.
33+
* Only a target executable ('tool' type) icon is generated. No custom disk icon.
3234

3335
## Demo App ##
3436

@@ -203,8 +205,9 @@ The various useful targets are described in the table below
203205
| Target | Description |
204206
|-----------|-------------|
205207
| `all` | Convert assets, build tools, assemble source `.s` files into `.o` object files and link program into `uae/dh0`. |
206-
| `adf` | Everything in the `all` target PLUS generation of floppy disk image (ADF) file. |
208+
| `adf` | Everything in the `all` target PLUS generation of icons and floppy disk image (ADF) file. |
207209
| `assets` | Only converts image assets (and generates palette include files) |
210+
| `icons` | Only converts icons |
208211
| `tools` | Only builds the tools (`vasm` & `vlink`) |
209212

210213
By default the target will not include `linedebug` data and will be stripped of symbols. To preserver these pass `DEBUG=1`:
@@ -220,6 +223,7 @@ The Makefile also supports a set of 'clean-up' targets to remove files:
220223
| `clean` | Removes built target program and `.o` object files.|
221224
| `clean_adf` | Removes the floppy disk image (ADF) file. |
222225
| `clean_assets` | Removes converted image asset files (and generated palette include files) |
226+
| `clean_icons` | Removes converted icon files |
223227
| `clean_tools` | Removes object and executable files for the tools (`vasm` & `vlink`)|
224228
| `clean_all` | Removes all of the above|
225229

@@ -258,6 +262,34 @@ Options:
258262

259263
If no `input_file` is specified, the script will works as a wildcard selecting all applicable files in the specified (or default) 'assets' directory.
260264

265+
## Icon Conversion Script ##
266+
267+
The icon asset conversion script (`amiga-icon-converter.py`) is located in the [scripts](./scripts/) directory.
268+
It's used by the `Makefile`, but it can also be used standalone to do more selective conversions if required:
269+
270+
Passing `-h` (or `--help`) to the script shows usage information:
271+
272+
```none
273+
$ ./scripts/amiga-icon-converter.py -h
274+
usage: amiga-icon-converter.py [-h] [--type {1,2,3,4,5,6,7,8}] [--width WIDTH] [--height HEIGHT] [--output OUTPUT] [--palette {1,2}] input_image
275+
276+
Convert image to Amiga Workbench icon
277+
278+
positional arguments:
279+
input_image Path to input image file
280+
281+
options:
282+
-h, --help show this help message and exit
283+
--type {1,2,3,4,5,6,7,8}
284+
Icon type (1: Disk, 2: Drawer, 3: Tool, 4: Project, 5: Trashcan, 6: Device, 7: Kickstart ROM, 8: Appicon, default: 3)
285+
--width WIDTH Icon width in pixels (default: 48)
286+
--height HEIGHT Icon height in pixels (default: 48)
287+
--output OUTPUT Optional output path for the .info file
288+
--palette {1,2} Palette version (1 for 1.x, 2 for 2.x, default: 2)
289+
```
290+
291+
If no output file is specified with `--output` the output will have the same name and path as the `input_image` but the file extension changed to `.info`
292+
261293
## GitHub Actions Workflow ##
262294

263295
This repository includes a [GitHub Actions](https://docs.github.com/en/actions) (GHA)

icons/main.xcf

991 Bytes
Binary file not shown.

scripts/amiga-icon-converter.py

Lines changed: 173 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,173 @@
1+
#!/usr/bin/env python
2+
3+
import os
4+
import struct
5+
import argparse
6+
from PIL import Image
7+
8+
# Standard Amiga icon palettes
9+
PALETTES = {
10+
'1.x': [
11+
(0x55, 0xAA, 0xFF), # color 0 - light blue
12+
(0xFF, 0xFF, 0xFF), # color 1 - white
13+
(0x00, 0x00, 0x00), # color 2 - black
14+
(0xFF, 0x88, 0x00) # color 3 - orange
15+
],
16+
'2.x': [
17+
(0x95, 0x95, 0x95), # color 0 - gray
18+
(0x00, 0x00, 0x00), # color 1 - black
19+
(0xFF, 0xFF, 0xFF), # color 2 - white
20+
(0x3B, 0x67, 0xA2) # color 3 - blue
21+
]
22+
}
23+
24+
def closest_color(pixel, palette):
25+
"""Find the closest color in the palette."""
26+
return min(range(len(palette)),
27+
key=lambda i: sum((a-b)**2 for a, b in zip(pixel[:3], palette[i])))
28+
29+
def convert_to_bitplanes(image, palette):
30+
"""Convert PIL Image to Amiga bitplane format."""
31+
width, height = image.width, image.height
32+
padded_width = ((width + 15) >> 4) << 4 # Pad to 16-bit word boundary
33+
bitplanes = []
34+
35+
for plane in range(2): # 2 bitplanes = 4 colors
36+
plane_data = bytearray(padded_width * height // 8)
37+
for y in range(height):
38+
for x in range(width):
39+
pixel = image.getpixel((x, y))
40+
color_index = closest_color(pixel, palette)
41+
42+
# Check if this color's bit is set in this bitplane
43+
if color_index & (1 << plane):
44+
byte_index = (y * padded_width + x) // 8
45+
bit_offset = 7 - (x % 8)
46+
plane_data[byte_index] |= (1 << bit_offset)
47+
48+
bitplanes.append(plane_data)
49+
50+
return padded_width, bitplanes
51+
52+
def create_amiga_icon(input_image_path, icon_type=3, icon_width=48, icon_height=48, output_path=None, palette_version=2):
53+
"""Create an Amiga Workbench icon from an input image."""
54+
# Open input image and resize/convert
55+
img = Image.open(input_image_path).convert('RGB')
56+
57+
# Resize image to specified icon size
58+
img = img.resize((icon_width, icon_height), Image.LANCZOS)
59+
60+
# Select palette based on version
61+
palette_key = f'{palette_version}.x'
62+
if palette_key not in PALETTES:
63+
raise ValueError(f"Invalid palette version. Choose 1 or 2.")
64+
palette = PALETTES[palette_key]
65+
66+
# Convert image to bitplanes
67+
padded_width, bitplanes = convert_to_bitplanes(img, palette)
68+
69+
# Prepare icon file path
70+
if output_path is None:
71+
# Default behavior: base name of input image with .info extension
72+
icon_path = os.path.splitext(input_image_path)[0] + '.info'
73+
else:
74+
# Use specified output path
75+
icon_path = output_path
76+
77+
# Create icon file
78+
with open(icon_path, 'wb') as f:
79+
# DiskObject structure
80+
# Magic number and version
81+
f.write(struct.pack('>H', 0xE310)) # do_Magic
82+
f.write(struct.pack('>H', 1)) # do_Version
83+
84+
# Gadget details
85+
f.write(struct.pack('>I', 0)) # do_Gadget.NextGadget (unused)
86+
f.write(struct.pack('>h', 0)) # do_Gadget.LeftEdge
87+
f.write(struct.pack('>h', 0)) # do_Gadget.TopEdge
88+
f.write(struct.pack('>H', icon_width)) # do_Gadget.Width
89+
f.write(struct.pack('>H', icon_height)) # do_Gadget.Height
90+
f.write(struct.pack('>H', 5)) # do_Gadget.Flags
91+
f.write(struct.pack('>H', 3)) # do_Gadget.Activation
92+
f.write(struct.pack('>H', 1)) # do_Gadget.GadgetType
93+
f.write(struct.pack('>I', 1)) # do_Gadget.GadgetRender (non-zero)
94+
f.write(struct.pack('>I', 0)) # do_Gadget.SelectRender
95+
f.write(struct.pack('>I', 0)) # do_Gadget.GadgetText
96+
f.write(struct.pack('>I', 0)) # do_Gadget.MutualExclude
97+
f.write(struct.pack('>I', 0)) # do_Gadget.SpecialInfo
98+
f.write(struct.pack('>H', 0)) # do_Gadget.GadgetID
99+
f.write(struct.pack('>I', 1)) # do_Gadget.UserData (OS 2.x revision)
100+
101+
# Icon type
102+
f.write(struct.pack('>B', icon_type))
103+
f.write(b'\x00') # padding
104+
105+
# No default tool or tooltypes
106+
f.write(struct.pack('>I', 0)) # do_DefaultTool
107+
f.write(struct.pack('>I', 0)) # do_ToolTypes
108+
109+
# Icon position
110+
f.write(struct.pack('>i', 0)) # do_CurrentX
111+
f.write(struct.pack('>i', 0)) # do_CurrentY
112+
113+
# No DrawerData
114+
f.write(struct.pack('>I', 0)) # do_DrawerData
115+
116+
# No ToolWindow
117+
f.write(struct.pack('>I', 0)) # do_ToolWindow
118+
119+
# Default stack size
120+
f.write(struct.pack('>I', 4096)) # do_StackSize
121+
122+
# Image structure for normal state
123+
f.write(struct.pack('>h', 0)) # LeftEdge
124+
f.write(struct.pack('>h', 0)) # TopEdge
125+
f.write(struct.pack('>H', icon_width)) # Width
126+
f.write(struct.pack('>H', icon_height)) # Height
127+
f.write(struct.pack('>H', 2)) # Depth (2 bitplanes)
128+
f.write(struct.pack('>I', 1)) # ImageData presence flag
129+
f.write(struct.pack('>B', 0b11)) # PlanePick
130+
f.write(struct.pack('>B', 0)) # PlaneOnOff
131+
f.write(struct.pack('>I', 0)) # NextImage
132+
133+
# Write bitplane data
134+
for bitplane in bitplanes:
135+
f.write(bitplane)
136+
137+
print(f"Amiga Workbench icon created at: {icon_path}")
138+
139+
def main():
140+
parser = argparse.ArgumentParser(description='Convert image to Amiga Workbench icon')
141+
parser.add_argument('input_image', help='Path to input image file')
142+
143+
# Make icon type optional with default of 3 (Tool)
144+
parser.add_argument('--type', type=int, choices=range(1, 9), default=3,
145+
help='Icon type (1: Disk, 2: Drawer, 3: Tool, 4: Project, 5: Trashcan, 6: Device, 7: Kickstart ROM, 8: Appicon, default: 3)')
146+
147+
# Add configurable icon size arguments with defaults
148+
parser.add_argument('--width', type=int, default=48,
149+
help='Icon width in pixels (default: 48)')
150+
parser.add_argument('--height', type=int, default=48,
151+
help='Icon height in pixels (default: 48)')
152+
153+
# Add optional output path argument
154+
parser.add_argument('--output', type=str,
155+
help='Optional output path for the .info file')
156+
157+
# Add optional palette version argument
158+
parser.add_argument('--palette', type=int, choices=[1, 2], default=2,
159+
help='Palette version (1 for 1.x, 2 for 2.x, default: 2)')
160+
161+
args = parser.parse_args()
162+
163+
create_amiga_icon(
164+
args.input_image,
165+
args.type,
166+
args.width,
167+
args.height,
168+
args.output,
169+
args.palette
170+
)
171+
172+
if __name__ == '__main__':
173+
main()

0 commit comments

Comments
 (0)