icns

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

commit 8204a2af5ae9eb0d8aabf9640f844d66f133b017
parent eeb191d483b2832b63f1a1e1da839d09cdf871b6
Author: Jack Mordaunt <jackmordaunt.dev@gmail.com>
Date:   Fri, 18 Sep 2026 16:09:38 -0400

icns: move resampling into an internal package

A second icon format needs the same scaling and the same choice of filter,
and neither belongs to icns in particular. InterpolationFunction is now an
alias, so callers see no change while both formats share one type.

Diffstat:
Micns.go | 29++++-------------------------
Micns_test.go | 6++++--
Ainternal/resample/resample.go | 107+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Minterpolation.go | 78++++++++----------------------------------------------------------------------
4 files changed, 123 insertions(+), 97 deletions(-)

diff --git a/icns.go b/icns.go @@ -9,7 +9,7 @@ import ( "slices" "sync" - "golang.org/x/image/draw" + "github.com/jackmordaunt/icns/v4/internal/resample" ) // Encoder encodes ICNS files from a source image. @@ -95,7 +95,7 @@ func NewIconSetFrom(images map[Slot]image.Image, interp InterpolationFunction) ( // The largest artwork stands in for the slots left empty. Ties are broken // by slot so the choice does not depend on map ordering. slices.SortFunc(slots, func(a, b Slot) int { - if order := cmp.Compare(biggestSide(images[b]), biggestSide(images[a])); order != 0 { + if order := cmp.Compare(resample.BiggestSide(images[b]), resample.BiggestSide(images[a])); order != 0 { return order } if order := cmp.Compare(b.Points, a.Points); order != 0 { @@ -131,7 +131,7 @@ func newIconSet(images map[Slot]image.Image, source image.Image, interp Interpol } icons[i] = &Icon{ Type: osType, - Image: resizeSquare(art, osType.Size, interp), + Image: resample.Square(art, osType.Size, interp), } }() } @@ -142,21 +142,6 @@ func newIconSet(images map[Slot]image.Image, source image.Image, interp Interpol return iconSet, nil } -// resizeSquare scales img into a size by size square, ignoring the source -// aspect ratio. An image already that size is returned as it is. -// -// Scaling happens in alpha-premultiplied space, so colour does not bleed out -// of fully transparent pixels into the icon's edges. -func resizeSquare(img image.Image, size uint, interp InterpolationFunction) image.Image { - bounds := img.Bounds() - if bounds.Dx() == int(size) && bounds.Dy() == int(size) { - return img - } - dst := image.NewRGBA(image.Rect(0, 0, int(size), int(size))) - interp.scaler().Scale(dst, dst.Bounds(), img, bounds, draw.Src, nil) - return dst -} - var sizes = []uint{ 1024, 512, @@ -169,7 +154,7 @@ var sizes = []uint{ // findNearestSize finds the biggest icon size we can use for this image. func findNearestSize(img image.Image) uint { - size := biggestSide(img) + size := resample.BiggestSide(img) for _, s := range sizes { if size >= s { return s @@ -178,12 +163,6 @@ func findNearestSize(img image.Image) uint { return 0 } -// biggestSide returns the larger of img's two dimensions. -func biggestSide(img image.Image) uint { - b := img.Bounds() - return uint(max(b.Dx(), b.Dy(), 0)) -} - // sizesFrom returns a slice containing the sizes less than and including max. func sizesFrom(max uint) []uint { for ii, s := range sizes { diff --git a/icns_test.go b/icns_test.go @@ -11,6 +11,8 @@ import ( "io" "reflect" "testing" + + "github.com/jackmordaunt/icns/v4/internal/resample" ) // TestDecode relies on Encode being correct. @@ -191,7 +193,7 @@ func TestResizeKeepsColorOutOfTransparentPixels(t *testing.T) { } // Bilinear has no negative lobes, so every output pixel is a plain // average of its neighbours and the expected values are exact. - got := resizeSquare(src, 32, Bilinear) + got := resample.Square(src, 32, Bilinear) var blended int for y := got.Bounds().Min.Y; y < got.Bounds().Max.Y; y++ { for x := got.Bounds().Min.X; x < got.Bounds().Max.X; x++ { @@ -389,7 +391,7 @@ func TestBiggestSide(t *testing.T) { } for _, tt := range tests { t.Run(tt.desc, func(st *testing.T) { - got := biggestSide(tt.img) + got := resample.BiggestSide(tt.img) if got != tt.want { st.Errorf("want=%d, got=%d", tt.want, got) } diff --git a/internal/resample/resample.go b/internal/resample/resample.go @@ -0,0 +1,107 @@ +// Package resample scales images for the icon formats in this module. +package resample + +import ( + "image" + "math" + + "golang.org/x/image/draw" +) + +// Function is the algorithm used to resize an image. +type Function int + +// Function constants, ordered from fastest to highest quality. +const ( + // Nearest-neighbor interpolation + NearestNeighbor Function = iota + // Bilinear interpolation + Bilinear + // Bicubic interpolation (with cubic hermite spline) + Bicubic + // Mitchell-Netravali interpolation + MitchellNetravali + // Lanczos interpolation (a=2) + Lanczos2 + // Lanczos interpolation (a=3) + Lanczos3 +) + +// scaler returns the resampler implementing f. Values outside the +// enumeration fall back to MitchellNetravali, the default. +func (f Function) scaler() draw.Interpolator { + switch f { + case NearestNeighbor: + return draw.NearestNeighbor + case Bilinear: + return draw.BiLinear + case Bicubic: + // Catmull-Rom is the cubic hermite spline interpolant. + return draw.CatmullRom + case Lanczos2: + return lanczos2 + case Lanczos3: + return lanczos3 + default: + return mitchellNetravali + } +} + +// BiggestSide returns the larger of img's two dimensions. +func BiggestSide(img image.Image) uint { + b := img.Bounds() + return uint(max(b.Dx(), b.Dy(), 0)) +} + +// Square scales img into a size by size square, ignoring the source aspect +// ratio. An image already that size is returned as it is. +// +// Scaling happens in alpha-premultiplied space, so colour does not bleed out +// of fully transparent pixels into the icon's edges. +func Square(img image.Image, size uint, f Function) image.Image { + bounds := img.Bounds() + if bounds.Dx() == int(size) && bounds.Dy() == int(size) { + return img + } + dst := image.NewRGBA(image.Rect(0, 0, int(size), int(size))) + f.scaler().Scale(dst, dst.Bounds(), img, bounds, draw.Src, nil) + return dst +} + +// Kernels receive the distance from the sample as a non-negative value, and +// return the weight to give the pixel at that distance. + +// mitchellNetravali is the Mitchell-Netravali filter with B = C = 1/3, the +// parameters its authors found to be the best compromise between blurring +// and ringing. +var mitchellNetravali = &draw.Kernel{Support: 2, At: func(t float64) float64 { + const b, c = 1.0 / 3.0, 1.0 / 3.0 + switch { + case t < 1: + return ((12-9*b-6*c)*t*t*t + (-18+12*b+6*c)*t*t + (6 - 2*b)) / 6 + case t < 2: + return ((-b-6*c)*t*t*t + (6*b+30*c)*t*t + (-12*b-48*c)*t + (8*b + 24*c)) / 6 + default: + return 0 + } +}} + +var ( + lanczos2 = lanczos(2) + lanczos3 = lanczos(3) +) + +// lanczos returns the Lanczos filter of order a: a sinc windowed by another +// sinc stretched across the kernel's whole support. +func lanczos(a float64) *draw.Kernel { + return &draw.Kernel{Support: a, At: func(t float64) float64 { + switch { + case t == 0: + return 1 + case t < a: + return a * math.Sin(math.Pi*t) * math.Sin(math.Pi*t/a) / (math.Pi * math.Pi * t * t) + default: + return 0 + } + }} +} diff --git a/interpolation.go b/interpolation.go @@ -1,84 +1,22 @@ package icns -import ( - "math" - - "golang.org/x/image/draw" -) +import "github.com/jackmordaunt/icns/v4/internal/resample" // InterpolationFunction is the algorithm used to resize the image. -type InterpolationFunction int +type InterpolationFunction = resample.Function // InterpolationFunction constants, ordered from fastest to highest quality. const ( // Nearest-neighbor interpolation - NearestNeighbor InterpolationFunction = iota + NearestNeighbor = resample.NearestNeighbor // Bilinear interpolation - Bilinear + Bilinear = resample.Bilinear // Bicubic interpolation (with cubic hermite spline) - Bicubic + Bicubic = resample.Bicubic // Mitchell-Netravali interpolation - MitchellNetravali + MitchellNetravali = resample.MitchellNetravali // Lanczos interpolation (a=2) - Lanczos2 + Lanczos2 = resample.Lanczos2 // Lanczos interpolation (a=3) - Lanczos3 -) - -// scaler returns the resampler implementing f. Values outside the -// enumeration fall back to MitchellNetravali, the package default. -func (f InterpolationFunction) scaler() draw.Interpolator { - switch f { - case NearestNeighbor: - return draw.NearestNeighbor - case Bilinear: - return draw.BiLinear - case Bicubic: - // Catmull-Rom is the cubic hermite spline interpolant. - return draw.CatmullRom - case Lanczos2: - return lanczos2 - case Lanczos3: - return lanczos3 - default: - return mitchellNetravali - } -} - -// Kernels receive the distance from the sample as a non-negative value, and -// return the weight to give the pixel at that distance. - -// mitchellNetravali is the Mitchell-Netravali filter with B = C = 1/3, the -// parameters its authors found to be the best compromise between blurring -// and ringing. -var mitchellNetravali = &draw.Kernel{Support: 2, At: func(t float64) float64 { - const b, c = 1.0 / 3.0, 1.0 / 3.0 - switch { - case t < 1: - return ((12-9*b-6*c)*t*t*t + (-18+12*b+6*c)*t*t + (6 - 2*b)) / 6 - case t < 2: - return ((-b-6*c)*t*t*t + (6*b+30*c)*t*t + (-12*b-48*c)*t + (8*b + 24*c)) / 6 - default: - return 0 - } -}} - -var ( - lanczos2 = lanczos(2) - lanczos3 = lanczos(3) + Lanczos3 = resample.Lanczos3 ) - -// lanczos returns the Lanczos filter of order a: a sinc windowed by another -// sinc stretched across the kernel's whole support. -func lanczos(a float64) *draw.Kernel { - return &draw.Kernel{Support: a, At: func(t float64) float64 { - switch { - case t == 0: - return 1 - case t < a: - return a * math.Sin(math.Pi*t) * math.Sin(math.Pi*t/a) / (math.Pi * math.Pi * t * t) - default: - return 0 - } - }} -}