icns

Easily create .icns files (Mac Icons) with this Go library or the included CLI.
Log | Files | Refs | LICENSE

appicon.go (5423B)


      1 // Package appicon writes the icon bundle Icon Composer authors, which macOS
      2 // 26 compiles into an app's icon.
      3 //
      4 // A bundle is a directory named with a .icon extension holding icon.json and
      5 // an Assets directory of layer images. The manifest names the layers and says
      6 // how they are lit, shadowed and filled; the compiler decides what the icon
      7 // looks like at each size, so a bundle carries artwork rather than
      8 // renditions.
      9 //
     10 // Nothing here compiles a bundle. That is actool's work, and it runs only on
     11 // macOS; a bundle written by this package is the input it takes.
     12 //
     13 // # How actool reports a manifest it cannot read
     14 //
     15 // It does not. Given a value it cannot make sense of, actool from Xcode 26.6
     16 // falls over with an exception from inside its own asset selection, naming
     17 // neither the field nor the file. A manifest that is merely well formed JSON
     18 // proves nothing, which is why this package is held against the tool rather
     19 // than against a schema.
     20 package appicon
     21 
     22 import (
     23 	"errors"
     24 	"image"
     25 )
     26 
     27 // Errors returned when a bundle cannot be written. They are wrapped with
     28 // detail, so compare with errors.Is.
     29 var (
     30 	// ErrNoLayers means the bundle names no artwork to draw.
     31 	ErrNoLayers = errors.New("no layers to write")
     32 	// ErrDuplicateLayer means two layers share a name, so one image would
     33 	// overwrite the other.
     34 	ErrDuplicateLayer = errors.New("two layers share a name")
     35 	// ErrEmptyFill means a fill names neither a gradient, a colour nor a
     36 	// fill the system provides.
     37 	ErrEmptyFill = errors.New("fill names no colour")
     38 )
     39 
     40 // Bundle is an icon: a manifest naming layers, and the images they hold.
     41 type Bundle struct {
     42 	// Fill is how the space behind the layers is filled. Empty is written as
     43 	// "automatic", which leaves the choice to the compiler, and is ignored
     44 	// when Fills is given.
     45 	Fill string
     46 	// Fills is the fill specialised by appearance, for an icon whose
     47 	// background differs in dark mode or when tinted.
     48 	Fills []Specialized[Fill]
     49 	// Groups are composited back to front.
     50 	Groups []Group
     51 	// Platforms are the platforms the icon offers square artwork for. Empty
     52 	// is written as macOS alone.
     53 	Platforms []string
     54 }
     55 
     56 // Group is a set of layers that share lighting, shadow and translucency.
     57 type Group struct {
     58 	// Layers are composited back to front within the group.
     59 	Layers []Layer
     60 	// Shadow is the shadow cast beneath the group. Nil leaves it out of the
     61 	// manifest.
     62 	Shadow *Shadow
     63 	// Translucency is how far the group lets the material behind it through.
     64 	// Nil leaves it out of the manifest, and it is ignored when
     65 	// Translucencies is given.
     66 	Translucency *Translucency
     67 	// Translucencies is the translucency specialised by appearance.
     68 	Translucencies []Specialized[Translucency]
     69 	// Lighting is how the group is lit, such as "individual" or "combined".
     70 	Lighting string
     71 	// Specular asks for a specular highlight across the group.
     72 	Specular bool
     73 	// BlurMaterial is the material the group blurs what is behind it with.
     74 	// Nil leaves it out, and it is ignored when BlurMaterials is given.
     75 	BlurMaterial *float64
     76 	// BlurMaterials is the blur material specialised by appearance.
     77 	BlurMaterials []Specialized[float64]
     78 	// BlendModes is how the group composites, specialised by appearance,
     79 	// such as "normal" or "lighten".
     80 	BlendModes []Specialized[string]
     81 }
     82 
     83 // Layer is one image in the stack.
     84 type Layer struct {
     85 	// Name identifies the layer in the manifest and names its file.
     86 	Name string
     87 	// Image is the artwork. It is written as a PNG at the size it is given.
     88 	Image image.Image
     89 	// Glass asks for the layer to be treated as glass, which the compiler
     90 	// lights and refracts rather than drawing flat.
     91 	Glass bool
     92 	// Hidden keeps the layer in the manifest without drawing it.
     93 	Hidden bool
     94 	// Position is where the layer sits. Nil leaves it where it was drawn.
     95 	Position *Position
     96 	// Fills is the layer's own fill, specialised by appearance. A layer
     97 	// given one is filled with it rather than with its image's colour.
     98 	Fills []Specialized[Fill]
     99 	// BlendModes is how the layer composites, specialised by appearance.
    100 	BlendModes []Specialized[string]
    101 }
    102 
    103 // Shadow is the shadow a group casts.
    104 type Shadow struct {
    105 	// Kind is how the shadow takes its colour, such as "neutral" or
    106 	// "layer-color".
    107 	Kind string `json:"kind"`
    108 	// Opacity is how dark it is, from 0 to 1.
    109 	Opacity float64 `json:"opacity"`
    110 }
    111 
    112 // Translucency is how far a group lets what is behind it through.
    113 type Translucency struct {
    114 	Enabled bool `json:"enabled"`
    115 	// Value is the amount, from 0 to 1.
    116 	Value float64 `json:"value"`
    117 }
    118 
    119 // Defaults written where a bundle leaves a choice open.
    120 const (
    121 	// FillAutomatic leaves the fill to the compiler.
    122 	FillAutomatic = "automatic"
    123 	// ShadowNeutral takes the shadow's colour from neither the layer nor the
    124 	// background.
    125 	ShadowNeutral = "neutral"
    126 	// PlatformMac is the platform macOS icons declare.
    127 	PlatformMac = "macOS"
    128 )
    129 
    130 // New returns a bundle holding one layer, which is what a single image makes.
    131 // The shadow and translucency match what Icon Composer writes for an icon
    132 // composed the same way.
    133 func New(img image.Image, name string) Bundle {
    134 	return Bundle{
    135 		Fill: FillAutomatic,
    136 		Groups: []Group{{
    137 			Layers:       []Layer{{Name: name, Image: img}},
    138 			Shadow:       &Shadow{Kind: ShadowNeutral, Opacity: 0.5},
    139 			Translucency: &Translucency{Enabled: true, Value: 0.5},
    140 		}},
    141 		Platforms: []string{PlatformMac},
    142 	}
    143 }