readme.md (13861B)
1 # icns 2 3 [](https://github.com/JackMordaunt/icns/actions/workflows/ci.yml) 4 [](https://pkg.go.dev/github.com/jackmordaunt/icns/v4) 5 6 Easily convert `.jpg` and `.png` to `.icns` with the command line tool `icnsify`, or use the library to convert from any `image.Image` to `.icns`. 7 8 `go get github.com/jackmordaunt/icns/v4` 9 10 `icns` files allow for high resolution icons to make your apps look sexy. The most common ways to generate icns files are: 11 12 1. `iconutil`, which is a Mac native cli utility. 13 2. `ImageMagick` which adds a large dependency to your project for such a simple use case. 14 15 With this library you can use pure Go to create `icns` files from any source image, given that you can decode it into an `image.Image`, without any heavyweight dependencies or subprocessing required. You can also use it to create icns files on windows and linux (thanks Go). 16 17 A small CLI app `icnsify` is provided allowing you to create icns files using this library from the command line. It supports piping, which is something `iconutil` does not do, making it substantially easier to wrap or chuck into a shell pipeline. 18 19 Note: `icns` files are written with an icon at every size macOS draws, the retina OSTypes for the larger ones and the colour and mask pair Apple still uses at 16 and 32 pixels. Decoding reaches further back than writing does. Alongside PNG it understands the `is32`, `il32`, `ih32` and `it32` colour and mask elements, the `ARGB` sidebar and toolbar icons, and the 1-, 4- and 8-bit indexed icons of System 7 through Mac OS 8. Where a file holds the same icon at several depths, the richest is returned first. 20 21 Elements that hold a whole image file are passed to `image.Decode`, so they read in whatever formats the program has registered. That is how a JPEG 2000 icon is handled: import a decoder for it and those icons start decoding, while a program that does not carries no codec and skips past them to a size it can read. 22 23 ## GUI 24 25 `preview` is a gui for displaying `icns` files cross-platform. 26 27 ### Go Tool 28 29 ``` 30 go install github.com/jackmordaunt/icns/cmd/preview@latest 31 ``` 32 33 ### Clone 34 35 ``` 36 git clone https://github.com/jackmordaunt/icns 37 cd icns/cmd/preview && go install . 38 ``` 39 40 Note: Gio cannot be cross-compiled right now, so there are no `preview` builds in releases. 41 Note: `preview` has its own `go.mod` and therefore is versioned independently (unversioned). 42 43  44 45 ## Windows Explorer thumbnails 46 47 `cmd/shell-extension` is a Windows shell extension that renders `.icns` thumbnails in Explorer. It is a COM server written in Go and built as a DLL; see [its readme](cmd/shell-extension/readme.md) for build and registration steps. 48 49 ## Command Line 50 51 ### Go Tool 52 53 ``` 54 go install github.com/jackmordaunt/icns/v4/cmd/icnsify@latest 55 ``` 56 57 ### [Scoop](https://scoop.sh/) 58 59 ```powershell 60 scoop bucket add extras # Ensure bucket is added first 61 scoop install icnsify 62 ``` 63 64 Or from my personal bucket: 65 66 ```powershell 67 scoop bucket add jackmordaunt https://github.com/jackmordaunt/scoop-bucket 68 scoop install jackmordaunt/icns # Name is defaulted to repo name. 69 ``` 70 71 ### [Winget](https://learn.microsoft.com/en-us/windows/package-manager/) 72 73 ```powershell 74 winget install icnsify 75 ``` 76 77 ### [Brew](https://brew.sh) 78 79 ```sh 80 brew tap jackmordaunt/homebrew-tap # Ensure tap is added first. 81 brew install icnsify 82 ``` 83 84 ### Clone 85 86 ``` 87 git clone https://github.com/jackmordaunt/icns 88 cd icns && go install ./cmd/icnsify 89 ``` 90 91 Pipe it 92 93 `cat icon.png | icnsify > icon.icns` 94 95 `cat icon.icns | icnsify > icon.png` 96 97 Standard 98 99 `icnsify -i icon.png -o icon.icns` 100 101 `icnsify -i icon.icns -o icon.png` 102 103 Windows icons, which the output path names, or `--format` where there is no path to name them 104 105 `icnsify -i icon.png -o icon.ico` 106 107 `cat icon.png | icnsify -f ico > icon.ico` 108 109 Between the two icon formats, in either direction 110 111 `icnsify -i icon.icns -o icon.ico` 112 113 From an iconset directory, which uses each drawing where it is given instead of resizing one image for every size 114 115 `icnsify -i MyIcon.iconset -o MyIcon.icns` 116 117 `icnsify -i MyIcon.iconset -o MyIcon.ico` 118 119 ## Library 120 121 `go get github.com/jackmordaunt/icns/v4` 122 123 ```go 124 func main() { 125 pngf, err := os.Open("path/to/icon.png") 126 if err != nil { 127 log.Fatalf("opening source image: %v", err) 128 } 129 defer pngf.Close() 130 srcImg, _, err := image.Decode(pngf) 131 if err != nil { 132 log.Fatalf("decoding source image: %v", err) 133 } 134 dest, err := os.Create("path/to/icon.icns") 135 if err != nil { 136 log.Fatalf("opening destination file: %v", err) 137 } 138 defer dest.Close() 139 if err := icns.Encode(dest, srcImg); err != nil { 140 log.Fatalf("encoding icns: %v", err) 141 } 142 } 143 ``` 144 145 ### Per-size artwork 146 147 Icons are usually hand tuned at the small sizes rather than reduced from the large one. `EncodeSlots` takes a drawing per slot and resizes only the slots left empty, filling them from the largest image given. 148 149 ```go 150 images := map[icns.Slot]image.Image{ 151 {Points: 16, Scale: 1}: small, // icon_16x16.png 152 {Points: 512, Scale: 2}: large, // icon_512x512@2x.png 153 } 154 if err := icns.NewEncoder(dest).EncodeSlots(images); err != nil { 155 log.Fatalf("encoding icns: %v", err) 156 } 157 ``` 158 159 A slot is a size and a display scale, because 16x16@2x and 32x32 are both 32 pixels of artwork but fill different elements. `icns.Slots()` lists the ten a file holds and `icns.ParseSlot` reads iconset file names. 160 161 ### Reading one size 162 163 `NewDecoder` identifies the icons without decoding any of them, so reading a single size does not pay for the rest. 164 165 ```go 166 d, err := icns.NewDecoder(src) 167 if err != nil { 168 log.Fatalf("reading icns: %v", err) 169 } 170 for _, icon := range d.Icons() { // Largest first. 171 if icon.Size > 128 { 172 continue 173 } 174 img, err := icon.Decode() 175 ... 176 } 177 ``` 178 179 `Entry.Payload` returns the bytes the file stores, for handling an element yourself. 180 181 ### Checking a file 182 183 `Validate` reports what the platform that owns the format will make of a file, most serious first. It finds the failures that look like success: colour planes ending on their last run lose their tail to Apple silicon, an icon whose mask is absent draws fully opaque, and `icp4` renders everywhere except an app bundle. 184 185 ```go 186 problems, err := icns.Validate(src) 187 if err != nil { 188 log.Fatalf("reading icns: %v", err) 189 } 190 for _, p := range problems { 191 fmt.Println(p) 192 } 193 ``` 194 195 ``` 196 degraded: il32 32: the file holds no l8mk element, so the icon draws fully opaque 197 degraded: icp4 16: does not render from an app bundle, and the file holds no is32 and s8mk at that size 198 advice: the file holds no ic13 256, ic08 256, so macOS scales another icon where they are asked for 199 ``` 200 201 Each finding carries a `Severity`: `Invisible` when the icon is not drawn at all, `Degraded` when it is drawn but not as it was meant to be, and `Advice` when nothing is wrong and something usual is simply absent. 202 203 `ico.Validate` does the same for Windows, where the finding that bites is a PNG frame stored without a 32-bit alpha channel. Every Windows decoder skips such a frame and falls back to a smaller icon, so the file looks correct until it is viewed large. 204 205 `icnsify -c icon.icns` runs the same check from the command line, printing the findings and exiting non-zero when one of them changes what is drawn. 206 207 ## Windows icons 208 209 `ico` is a sibling package for the Windows `.ico` format, with the same shape as the icns API, so one mental model covers both. 210 211 ```go 212 import "github.com/jackmordaunt/icns/v4/ico" 213 214 if err := ico.Encode(dest, srcImg); err != nil { 215 log.Fatalf("encoding ico: %v", err) 216 } 217 ``` 218 219 A file is written with an icon at 256, 128, 64, 48, 32, 24 and 16 pixels, skipping any larger than the source. The 256 is a PNG, since a bitmap at that size is a quarter of a megabyte on its own; the rest are 32-bit bitmaps with the one-bit mask Windows still reads. `NewEncoder(dest).EncodeSizes(images)` takes a drawing per size, as `EncodeSlots` does for icns. 220 221 Decoding reads PNG icons and bitmaps at 1, 4, 8, 24 and 32 bits per pixel, taking the colour table from the file and the alpha from the mask where the pixels carry none. `NewDecoder`, `Icons`, `Entry.Decode` and `Entry.Payload` work as their icns counterparts do, and the package registers itself with `image.Decode`. 222 223 `icnsify` writes `.ico` too, so the format is reachable from the command line without writing a program. 224 225 ## Icons inside binaries 226 227 `exe` is a sibling package that reads the icons a Windows executable or DLL carries. They live in the resource section as a directory in one resource and its images in others, which is an ico file taken apart, so the package puts it back together. 228 229 ```go 230 import "github.com/jackmordaunt/icns/v4/exe" 231 232 groups, err := exe.Icons(binary) // An io.ReaderAt. 233 if err != nil { 234 log.Fatalf("reading icons: %v", err) 235 } 236 for _, group := range groups { // Lowest ordinal first, as Explorer draws them. 237 os.WriteFile("icon.ico", group.ICO(), 0o644) 238 } 239 ``` 240 241 The reassembly is exact: the ico handed to the linker comes back out of the binary byte for byte, which is what the package is tested against. `Group.Decode` returns the largest size as an image, and `exe.Decode` does the same for the first group. 242 243 `icnsify` takes a binary wherever it takes an image, so `icnsify -i app.exe -f icns` converts the icon a program ships with, and `icnsify -c app.exe` checks what Explorer will draw for it. 244 245 ## macOS 26 icons 246 247 macOS 26 draws app icons from a `.icon` bundle compiled into an asset catalog, and reads the older `.icns` where that is absent, so an app targeting both ships both. `appicon` writes the bundle: a manifest naming layers, and the images those layers hold. 248 249 ```go 250 import "github.com/jackmordaunt/icns/v4/appicon" 251 252 if err := appicon.New(art, "App").Write("App.icon"); err != nil { 253 log.Fatalf("writing bundle: %v", err) 254 } 255 ``` 256 257 `Bundle` takes groups of layers with their own shadow, translucency and glass, for an icon built from more than one drawing. `Files` renders the same thing as bytes keyed by path, for writing somewhere other than a directory. 258 259 `icnsify -i art.png -f icon -o App.icon` does it from the command line. 260 261 ### Compiling one 262 263 Turning a bundle into the `Assets.car` macOS reads is `actool`'s work, and `actool` runs only on macOS. It is the single step that needs Apple's tooling; writing the bundle does not, so the machine drawing the icon need not be a Mac. 264 265 ```yaml 266 - runs-on: macos-latest 267 run: | 268 xcrun actool App.icon --compile build \ 269 --app-icon App --include-all-app-icons \ 270 --output-partial-info-plist build/partial.plist \ 271 --minimum-deployment-target 26.0 \ 272 --target-device mac --platform macosx 273 ``` 274 275 `actool` writes three things: the `Assets.car`, an `.icns` for older systems, and a plist naming both through `CFBundleIconFile` and `CFBundleIconName`. The result is a build artifact that changes only when the icon does, so it is generated once and kept. 276 277 ## Development 278 279 The repository is a Go workspace of three modules: 280 281 | Module | Contents | Why separate | 282 |---|---|---| 283 | `github.com/jackmordaunt/icns/v4` (root) | The library and `cmd/icnsify` | Depends only on `golang.org/x/image`; one tag versions both | 284 | `github.com/jackmordaunt/icns/cmd/preview` | The Gio GUI | Keeps Gio's dependency tree out of library consumers' module graphs | 285 | `github.com/jackmordaunt/icns/cmd/shell-extension` | The Windows DLL | Windows-only and needs cgo (mingw) | 286 287 `go.work` ties them together, so every build inside the checkout uses the working-tree library and gopls sees the whole repository. Note that `./...` only matches the module you are in; to cover all three from the root, name them: 288 289 ``` 290 go test ./... ./cmd/preview/... ./cmd/shell-extension/... 291 ``` 292 293 The two command modules require the library at a published tag. Inside the workspace that requirement only shapes the module graph; the code always comes from the working tree, so the tag can lag behind without affecting development. 294 295 CI builds and tests the library on Linux, macOS and Windows, runs the encoder under the race detector, fuzzes the decoder, and reports `gofmt`, `go vet` and `govulncheck`. Two oracles check the formats against the systems that own them: `iconutil` on the macOS runner, and the Windows Imaging Component and `System.Drawing` on the Windows one. The shell extension is tested on Windows, where cgo can reach a C compiler, and `preview` is built on macOS and Windows, since Gio needs a long list of X11 and Wayland headers on Linux. Both command modules are also built with `GOWORK=off`, which is what `go install` sees. 296 297 Releasing: tag the root module (`vX.Y.Z`), which releases the library and `icnsify` together. When `go install .../cmd/preview@latest`, or a shell extension built outside the checkout, should pick up a newer library, bump that module's requirement and tidy it outside the workspace: 298 299 ```powershell 300 $env:GOWORK = 'off'; go get github.com/jackmordaunt/icns/v4@latest; go mod tidy; Remove-Item Env:GOWORK 301 ``` 302 303 ## Roadmap 304 305 - [x] Encoder: `image.Image -> .icns` 306 - [x] Command Line Interface 307 - [x] Encoding 308 - [x] Pipe support 309 - [x] Decoding 310 - [x] Implement Decoder: `.icns -> image.Image` 311 - [x] Symmetric test: `decode(encode(img)) == img` 312 - [x] Windows Explorer thumbnails 313 - [x] Windows `.ico` encoder and decoder 314 - [x] Validation against what the platforms actually accept 315 - [x] Reading icons out of Windows binaries 316 - [x] Writing the macOS 26 icon bundle 317 318 ## Coffee 319 320 If this software is useful to you, consider buying me a coffee! 321 322 [https://liberapay.com/JackMordaunt](https://liberapay.com/JackMordaunt)