From 3169e7706e86b963c7e490ec00b428ba36bbb41c Mon Sep 17 00:00:00 2001 From: oppiliappan Date: Thu, 8 Jan 2026 16:14:30 +0000 Subject: [PATCH] ico: add module to deal with ico files Change-Id: mtuvlutzprrtkmsmyrzwsxvyzzvltlmz the ico file format is quite straightforward, it contains a few headers followed by any number of full PNG/BMP images. for our usecase, we only need to be able to embed a single favicon an ico file, this module handles just the conversion of an image.Image to the ico format. Signed-off-by: oppiliappan --- appview/ogcard/card.go | 8 ++-- ico/ico.go | 88 ++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 92 insertions(+), 4 deletions(-) create mode 100644 ico/ico.go diff --git a/appview/ogcard/card.go b/appview/ogcard/card.go index b8fe1e8f..4f53280c 100644 --- a/appview/ogcard/card.go +++ b/appview/ogcard/card.go @@ -453,7 +453,7 @@ func (c *Card) fetchExternalImage(url string) (image.Image, bool) { // Handle SVG separately if contentType == "image/svg+xml" || strings.HasSuffix(url, ".svg") { - return c.convertSVGToPNG(bodyBytes) + return convertSVGToPNG(bodyBytes) } // Support content types are in-sync with the allowed custom avatar file types @@ -493,7 +493,7 @@ func (c *Card) fetchExternalImage(url string) (image.Image, bool) { } // convertSVGToPNG converts SVG data to a PNG image -func (c *Card) convertSVGToPNG(svgData []byte) (image.Image, bool) { +func convertSVGToPNG(svgData []byte) (image.Image, bool) { // Parse the SVG icon, err := oksvg.ReadIconStream(bytes.NewReader(svgData)) if err != nil { @@ -547,8 +547,8 @@ func (c *Card) DrawCircularExternalImage(url string, x, y, size int) error { draw.CatmullRom.Scale(scaledImg, scaledImg.Bounds(), img, srcBounds, draw.Src, nil) // Draw the image with circular clipping - for cy := 0; cy < size; cy++ { - for cx := 0; cx < size; cx++ { + for cy := range size { + for cx := range size { // Calculate distance from center dx := float64(cx - center) dy := float64(cy - center) diff --git a/ico/ico.go b/ico/ico.go new file mode 100644 index 00000000..43138762 --- /dev/null +++ b/ico/ico.go @@ -0,0 +1,88 @@ +package ico + +import ( + "bytes" + "encoding/binary" + "fmt" + "image" + "image/png" +) + +type IconDir struct { + Reserved uint16 // must be 0 + Type uint16 // 1 for ICO, 2 for CUR + Count uint16 // number of images +} + +type IconDirEntry struct { + Width uint8 // 0 means 256 + Height uint8 // 0 means 256 + ColorCount uint8 + Reserved uint8 // must be 0 + ColorPlanes uint16 // 0 or 1 + BitsPerPixel uint16 + SizeInBytes uint32 + Offset uint32 +} + +func ImageToIco(img image.Image) ([]byte, error) { + // encode image as png + var pngBuf bytes.Buffer + if err := png.Encode(&pngBuf, img); err != nil { + return nil, fmt.Errorf("failed to encode PNG: %w", err) + } + pngData := pngBuf.Bytes() + + // get image dimensions + bounds := img.Bounds() + width := bounds.Dx() + height := bounds.Dy() + + // prepare output buffer + var icoBuf bytes.Buffer + + iconDir := IconDir{ + Reserved: 0, + Type: 1, // ICO format + Count: 1, // One image + } + + w := uint8(width) + h := uint8(height) + + // width/height of 256 should be stored as 0 + if width == 256 { + w = 0 + } + if height == 256 { + h = 0 + } + + iconDirEntry := IconDirEntry{ + Width: w, + Height: h, + ColorCount: 0, // 0 for PNG (32-bit) + Reserved: 0, + ColorPlanes: 1, + BitsPerPixel: 32, // PNG with alpha + SizeInBytes: uint32(len(pngData)), + Offset: 6 + 16, // Size of ICONDIR + ICONDIRENTRY + } + + // write IconDir + if err := binary.Write(&icoBuf, binary.LittleEndian, iconDir); err != nil { + return nil, fmt.Errorf("failed to write ICONDIR: %w", err) + } + + // write IconDirEntry + if err := binary.Write(&icoBuf, binary.LittleEndian, iconDirEntry); err != nil { + return nil, fmt.Errorf("failed to write ICONDIRENTRY: %w", err) + } + + // write PNG data directly + if _, err := icoBuf.Write(pngData); err != nil { + return nil, fmt.Errorf("failed to write PNG data: %w", err) + } + + return icoBuf.Bytes(), nil +} -- 2.43.0