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 }