github.com/richardwilkes/toolbox@v1.121.0/formats/icon/icon.go (about)

     1  // Copyright (c) 2016-2024 by Richard A. Wilkes. All rights reserved.
     2  //
     3  // This Source Code Form is subject to the terms of the Mozilla Public
     4  // License, version 2.0. If a copy of the MPL was not distributed with
     5  // this file, You can obtain one at http://mozilla.org/MPL/2.0/.
     6  //
     7  // This Source Code Form is "Incompatible With Secondary Licenses", as
     8  // defined by the Mozilla Public License, version 2.0.
     9  
    10  package icon
    11  
    12  import (
    13  	"image"
    14  
    15  	"golang.org/x/image/draw"
    16  )
    17  
    18  // Stack a set of images on top of each other, producing a new image. The first image in the series will be on the
    19  // bottom and the last will be on the top. If the images are of different sizes, the resulting image will be the size of
    20  // the largest image and all other images will be centered within that area.
    21  func Stack(images ...image.Image) image.Image {
    22  	var width, height int
    23  	for _, img := range images {
    24  		bounds := img.Bounds()
    25  		if width < bounds.Dx() {
    26  			width = bounds.Dx()
    27  		}
    28  		if height < bounds.Dy() {
    29  			height = bounds.Dy()
    30  		}
    31  	}
    32  	base := image.NewRGBA(image.Rect(0, 0, width, height))
    33  	for _, img := range images {
    34  		bounds := img.Bounds()
    35  		w := bounds.Dx()
    36  		h := bounds.Dy()
    37  		draw.Copy(base, image.Pt((width-w)/2, (height-h)/2), img, bounds, draw.Over, nil)
    38  	}
    39  	return base
    40  }
    41  
    42  // ImageAt provides storage for an image and an origin point.
    43  type ImageAt struct {
    44  	Image  image.Image
    45  	Origin image.Point
    46  }
    47  
    48  // StackAt stacks a set of images on top of each other, producing a new image. The first image in the series will be on
    49  // the bottom and the last will be on the top. The resulting image will be the size of the largest area covered based on
    50  // each image's size plus origin. Note that if an origin has a negative value, it will be normalized such that the
    51  // largest negative will become the new origin for the resulting image.
    52  func StackAt(images ...*ImageAt) image.Image {
    53  	var x, y, width, height int
    54  	for _, img := range images {
    55  		bounds := img.Image.Bounds()
    56  		if x > img.Origin.X {
    57  			x = img.Origin.X
    58  		}
    59  		if y > img.Origin.Y {
    60  			y = img.Origin.Y
    61  		}
    62  		w := bounds.Dx() + img.Origin.X
    63  		if width < w {
    64  			width = w
    65  		}
    66  		h := bounds.Dy() + img.Origin.Y
    67  		if height < h {
    68  			height = h
    69  		}
    70  	}
    71  	base := image.NewRGBA(image.Rect(0, 0, width, height))
    72  	for _, img := range images {
    73  		bounds := img.Image.Bounds()
    74  		draw.Copy(base, image.Pt(img.Origin.X-x, img.Origin.Y-y), img.Image, bounds, draw.Over, nil)
    75  	}
    76  	return base
    77  }
    78  
    79  // Scale an image.
    80  func Scale(img image.Image, width, height int) image.Image {
    81  	bounds := img.Bounds()
    82  	w := bounds.Dx()
    83  	h := bounds.Dy()
    84  	if w == width && h == height {
    85  		return img
    86  	}
    87  	scaled := image.NewRGBA(image.Rect(0, 0, width, height))
    88  	draw.CatmullRom.Scale(scaled, scaled.Bounds(), img, bounds, draw.Over, nil)
    89  	return scaled
    90  }
    91  
    92  // ScaleTo scales the image to the desired sizes. If an image cannot be scaled exactly to the desired size, it will be
    93  // scaled proportionally and then centered within the available space.
    94  func ScaleTo(img image.Image, sizes []image.Point) []image.Image {
    95  	list := make([]image.Image, 0, len(sizes))
    96  	bounds := img.Bounds()
    97  	width := bounds.Dx()
    98  	height := bounds.Dy()
    99  	for _, size := range sizes {
   100  		w, h := ScaleProportionally(width, height, size.X, size.Y)
   101  		scaled := Scale(img, w, h)
   102  		if w != size.X || h != size.Y {
   103  			scaled = StackAt(
   104  				&ImageAt{
   105  					Image:  image.NewRGBA(image.Rect(0, 0, size.X, size.Y)),
   106  					Origin: image.Point{},
   107  				},
   108  				&ImageAt{
   109  					Image:  scaled,
   110  					Origin: image.Pt((size.X-w)/2, (size.Y-h)/2),
   111  				},
   112  			)
   113  		}
   114  		list = append(list, scaled)
   115  	}
   116  	return list
   117  }
   118  
   119  // ScaleProportionally returns the width and height that are closest to the desired values without distorting the size.
   120  func ScaleProportionally(currentWidth, currentHeight, desiredWidth, desiredHeight int) (width, height int) {
   121  	if desiredWidth != currentWidth || desiredHeight != currentHeight {
   122  		scaleX := float64(desiredWidth) / float64(currentWidth)
   123  		scaleY := float64(desiredHeight) / float64(currentHeight)
   124  		scale := min(scaleX, scaleY)
   125  		return int(float64(currentWidth) * scale), int(float64(currentHeight) * scale)
   126  	}
   127  	return currentWidth, currentHeight
   128  }
   129  
   130  // ScaleUpProportionally returns the width and height that are closest to the desired values without distorting the
   131  // size, but won't decrease the current values.
   132  func ScaleUpProportionally(currentWidth, currentHeight, desiredWidth, desiredHeight int) (width, height int) {
   133  	if desiredWidth != currentWidth || desiredHeight != currentHeight {
   134  		scaleX := float64(desiredWidth) / float64(currentWidth)
   135  		scaleY := float64(desiredHeight) / float64(currentHeight)
   136  		if scale := min(scaleX, scaleY); scale > 1 {
   137  			return int(float64(currentWidth) * scale), int(float64(currentHeight) * scale)
   138  		}
   139  	}
   140  	return currentWidth, currentHeight
   141  }
   142  
   143  // ScaleDownProportionally returns the width and height that are closest to the desired values without distorting the
   144  // size, but won't increase the current values.
   145  func ScaleDownProportionally(currentWidth, currentHeight, desiredWidth, desiredHeight int) (width, height int) {
   146  	if desiredWidth != currentWidth || desiredHeight != currentHeight {
   147  		scaleX := float64(desiredWidth) / float64(currentWidth)
   148  		scaleY := float64(desiredHeight) / float64(currentHeight)
   149  		if scale := min(scaleX, scaleY); scale < 1 {
   150  			return int(float64(currentWidth) * scale), int(float64(currentHeight) * scale)
   151  		}
   152  	}
   153  	return currentWidth, currentHeight
   154  }