mirror of
https://github.com/blacktop/ipsw.git
synced 2026-05-08 12:22:26 +00:00
chore: remove libheif dependency; use sips for HEIC conversion
Replace the `github.com/strukturag/libheif-go` CGo dependency with macOS-native `sips` for HEIC/AVIF wallpaper thumbnail conversion. - Drop `libheif` from go.mod, Makefile, Goreleaser config, and CI - Remove `cgo` build tag requirement from wallpaper package - Implement `convertWithSips()` using temp files and the `sips` CLI - Refactor thumbnail decoding into `extractThumbnailPreview()` helper - Update tests to cover sips-based HEIC conversion path
This commit is contained in:
@@ -137,7 +137,6 @@ jobs:
|
||||
cd build
|
||||
cmake ..
|
||||
sudo make install
|
||||
sudo apt-get install -yq --fix-missing libheif1 libheif-plugin-libde265 heif-gdk-pixbuf libheif-dev # for wallpaper downloader
|
||||
- name: Build Dependencies (macOS)
|
||||
if: matrix.platform == 'macos-13'
|
||||
run: |
|
||||
|
||||
@@ -47,7 +47,7 @@ jobs:
|
||||
# mkdir /tmp/homebrew
|
||||
# cd /tmp; curl -L https://github.com/Homebrew/brew/tarball/master | tar xz --strip 1 -C homebrew
|
||||
# sudo mv /tmp/homebrew /usr/local/homebrew
|
||||
# arch -x86_64 /usr/local/homebrew/bin/brew install --quiet unicorn libusb libheif pkg-config
|
||||
# arch -x86_64 /usr/local/homebrew/bin/brew install --quiet unicorn libusb pkg-config
|
||||
# env:
|
||||
# HOMEBREW_NO_AUTO_UPDATE: 1
|
||||
|
||||
|
||||
@@ -405,7 +405,6 @@ brews:
|
||||
type: optional
|
||||
- name: git-delta
|
||||
type: optional
|
||||
- name: libheif
|
||||
conflicts:
|
||||
- ipsw-frida
|
||||
install: |
|
||||
|
||||
@@ -22,7 +22,7 @@ x86-brew: ## Install the x86_64 homebrew on Apple Silicon
|
||||
mkdir /tmp/homebrew
|
||||
cd /tmp; curl -L https://github.com/Homebrew/brew/tarball/master | tar xz --strip 1 -C homebrew
|
||||
sudo mv /tmp/homebrew /usr/local/homebrew
|
||||
arch -x86_64 /usr/local/homebrew/bin/brew install unicorn libusb libheif
|
||||
arch -x86_64 /usr/local/homebrew/bin/brew install unicorn libusb
|
||||
|
||||
.PHONY: setup
|
||||
setup: build-deps dev-deps ## Install all the build and dev dependencies
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
//go:build darwin && cgo && wallpaper
|
||||
//go:build darwin && wallpaper
|
||||
|
||||
/*
|
||||
Copyright © 2026 blacktop
|
||||
|
||||
@@ -50,7 +50,7 @@ require (
|
||||
github.com/go-viper/mapstructure/v2 v2.5.0
|
||||
github.com/gocolly/colly/v2 v2.3.0
|
||||
github.com/golang-jwt/jwt/v5 v5.3.1
|
||||
github.com/gomarkdown/markdown v0.0.0-20260411013819-759bbc3e3207
|
||||
github.com/gomarkdown/markdown v0.0.0-20260412113850-134a5b2cce7f
|
||||
github.com/google/gousb v1.1.3
|
||||
github.com/google/uuid v1.6.0
|
||||
github.com/hashicorp/go-version v1.9.0
|
||||
@@ -70,7 +70,6 @@ require (
|
||||
github.com/spf13/cobra v1.10.2
|
||||
github.com/spf13/pflag v1.0.10
|
||||
github.com/spf13/viper v1.21.0
|
||||
github.com/strukturag/libheif-go v0.0.0-20250130134905-55b3482bea15
|
||||
github.com/twmb/murmur3 v1.1.8
|
||||
github.com/ulikunitz/xz v0.5.15
|
||||
github.com/unicorn-engine/unicorn v0.0.0-20260217064959-7c5db94191de
|
||||
|
||||
@@ -423,8 +423,8 @@ github.com/golang/protobuf v1.5.2/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiu
|
||||
github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek=
|
||||
github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps=
|
||||
github.com/golang/snappy v0.0.3/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q=
|
||||
github.com/gomarkdown/markdown v0.0.0-20260411013819-759bbc3e3207 h1:p7t34F7K4OCRQblcDhNJnP46Uaarz3z2cLcvOZYxWn8=
|
||||
github.com/gomarkdown/markdown v0.0.0-20260411013819-759bbc3e3207/go.mod h1:JDGcbDT52eL4fju3sZ4TeHGsQwhG9nbDV21aMyhwPoA=
|
||||
github.com/gomarkdown/markdown v0.0.0-20260412113850-134a5b2cce7f h1:C5vKBogs/Qf5ID8F8XuRO8SFL+5SH7JMJrAfdLAZ2iA=
|
||||
github.com/gomarkdown/markdown v0.0.0-20260412113850-134a5b2cce7f/go.mod h1:JDGcbDT52eL4fju3sZ4TeHGsQwhG9nbDV21aMyhwPoA=
|
||||
github.com/google/btree v0.0.0-20180813153112-4030bb1f1f0c/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ=
|
||||
github.com/google/btree v1.0.0/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ=
|
||||
github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M=
|
||||
@@ -810,8 +810,6 @@ github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXl
|
||||
github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
|
||||
github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U=
|
||||
github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U=
|
||||
github.com/strukturag/libheif-go v0.0.0-20250130134905-55b3482bea15 h1:aFa2PvtQulG5uVQ8adH84JCwRZ2rjiZnRUU/mWxJRG8=
|
||||
github.com/strukturag/libheif-go v0.0.0-20250130134905-55b3482bea15/go.mod h1:ZW0m/zWIvFqFSpPdiWRje8xdwyWJqt3Cnt6bVlDti8g=
|
||||
github.com/subosito/gotenv v1.2.0/go.mod h1:N0PQaV/YGNqwC0u51sEeR/aUtSLEXKX9iv69rRypqCw=
|
||||
github.com/subosito/gotenv v1.6.0 h1:9NlTDc1FTs4qu0DDq7AEtTPNw6SVm7uBMsUCUjABIf8=
|
||||
github.com/subosito/gotenv v1.6.0/go.mod h1:Dk4QP5c2W3ibzajGcXpNraDfq2IrhjMIvMSWPKKo0FU=
|
||||
|
||||
+88
-50
@@ -1,21 +1,22 @@
|
||||
//go:build darwin && cgo && wallpaper
|
||||
//go:build darwin && wallpaper
|
||||
|
||||
package wallpaper
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"fmt"
|
||||
"image"
|
||||
"image/jpeg"
|
||||
"image/png"
|
||||
"io"
|
||||
"net/http"
|
||||
"os"
|
||||
"os/exec"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
|
||||
"github.com/blacktop/go-plist"
|
||||
"github.com/blacktop/ipsw/internal/download"
|
||||
"github.com/disintegration/imaging"
|
||||
_ "github.com/strukturag/libheif-go"
|
||||
)
|
||||
|
||||
const (
|
||||
@@ -54,6 +55,85 @@ type MESUAssets struct {
|
||||
SigningKey string `plist:"SigningKey,omitempty"`
|
||||
}
|
||||
|
||||
func resizeJPEGThumbnail(imgBytes []byte) ([]byte, error) {
|
||||
img, err := jpeg.Decode(bytes.NewReader(imgBytes))
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("unable to decode thumbnail.jpg: %v", err)
|
||||
}
|
||||
|
||||
resizedImg := imaging.Resize(img, 0, 700, imaging.Lanczos)
|
||||
var buf bytes.Buffer
|
||||
if err := jpeg.Encode(&buf, resizedImg, &jpeg.Options{Quality: 90}); err != nil {
|
||||
return nil, fmt.Errorf("unable to encode resized thumbnail: %v", err)
|
||||
}
|
||||
|
||||
return buf.Bytes(), nil
|
||||
}
|
||||
|
||||
func resizePNGThumbnail(imgBytes []byte) ([]byte, error) {
|
||||
img, err := png.Decode(bytes.NewReader(imgBytes))
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("unable to decode thumbnail.png: %v", err)
|
||||
}
|
||||
|
||||
resizedImg := imaging.Resize(img, 0, 700, imaging.Lanczos)
|
||||
var buf bytes.Buffer
|
||||
if err := png.Encode(&buf, resizedImg); err != nil {
|
||||
return nil, fmt.Errorf("unable to encode resized thumbnail: %v", err)
|
||||
}
|
||||
|
||||
return buf.Bytes(), nil
|
||||
}
|
||||
|
||||
// convertWithSips uses macOS ImageIO via sips for HEIC-family wallpaper previews.
|
||||
func convertWithSips(imgBytes []byte, sourceExt string) ([]byte, error) {
|
||||
tempDir, err := os.MkdirTemp("", "ipsw-wallpaper-*")
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("unable to create temporary directory for %s preview: %w", sourceExt, err)
|
||||
}
|
||||
defer os.RemoveAll(tempDir)
|
||||
|
||||
sourcePath := filepath.Join(tempDir, "thumbnail"+sourceExt)
|
||||
if err := os.WriteFile(sourcePath, imgBytes, 0o600); err != nil {
|
||||
return nil, fmt.Errorf("unable to write temporary %s preview: %w", sourceExt, err)
|
||||
}
|
||||
|
||||
outputPath := filepath.Join(tempDir, "thumbnail.png")
|
||||
cmd := exec.Command("sips", "--resampleHeight", "700", "-s", "format", "png", sourcePath, "--out", outputPath)
|
||||
if output, err := cmd.CombinedOutput(); err != nil {
|
||||
return nil, fmt.Errorf("sips failed to convert %s preview: %w (%s)", sourceExt, err, strings.TrimSpace(string(output)))
|
||||
}
|
||||
|
||||
converted, err := os.ReadFile(outputPath)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("unable to read converted %s preview: %w", sourceExt, err)
|
||||
}
|
||||
|
||||
return converted, nil
|
||||
}
|
||||
|
||||
func extractThumbnailPreview(imgBytes []byte, thumbnailFile string) ([]byte, error) {
|
||||
thumbnailExt := strings.ToLower(filepath.Ext(thumbnailFile))
|
||||
if bytes.HasPrefix(imgBytes, []byte("\x89PNG")) {
|
||||
thumbnailExt = ".png"
|
||||
}
|
||||
|
||||
switch thumbnailExt {
|
||||
case ".avif", ".heic", ".heif":
|
||||
converted, err := convertWithSips(imgBytes, thumbnailExt)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("unable to decode %s preview: %w", thumbnailExt, err)
|
||||
}
|
||||
return converted, nil
|
||||
case ".jpeg", ".jpg":
|
||||
return resizeJPEGThumbnail(imgBytes)
|
||||
case ".png":
|
||||
return resizePNGThumbnail(imgBytes)
|
||||
default:
|
||||
return nil, fmt.Errorf("unsupported thumbnail format: %s", thumbnailExt)
|
||||
}
|
||||
}
|
||||
|
||||
// FetchWallpaperPlist fetches and decodes the Apple wallpaper plist
|
||||
func FetchWallpaperPlist() (*MESUAssets, error) {
|
||||
resp, err := http.Get(updateURL)
|
||||
@@ -72,7 +152,7 @@ func FetchWallpaperPlist() (*MESUAssets, error) {
|
||||
return &assets, nil
|
||||
}
|
||||
|
||||
// ExtractThumbnailBytes downloads a wallpaper zip from the given URL and extracts the thumbnail.jpg to a byte slice,
|
||||
// ExtractThumbnailBytes downloads a wallpaper zip from the given URL and extracts the thumbnail image to a byte slice,
|
||||
// resizing it to a fixed height while preserving aspect ratio.
|
||||
func ExtractThumbnailBytes(url, proxy string, insecure bool) ([]byte, error) {
|
||||
zr, err := download.NewRemoteZipReader(url, &download.RemoteConfig{
|
||||
@@ -118,58 +198,16 @@ func ExtractThumbnailBytes(url, proxy string, insecure bool) ([]byte, error) {
|
||||
if filepath.Base(f.Name) == thumbnailFile {
|
||||
rc, err := f.Open()
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("unable to open thumbnail.jpg: %v", err)
|
||||
return nil, fmt.Errorf("unable to open thumbnail image %s: %v", thumbnailFile, err)
|
||||
}
|
||||
defer rc.Close()
|
||||
imgBytes, err := io.ReadAll(rc)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("unable to read thumbnail.jpg: %v", err)
|
||||
}
|
||||
if bytes.HasPrefix(imgBytes, []byte("\x89PNG")) {
|
||||
thumbnailFile = "thumbnail.png"
|
||||
}
|
||||
switch filepath.Ext(thumbnailFile) {
|
||||
case ".heic":
|
||||
img, format, err := image.Decode(bytes.NewReader(imgBytes))
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("unable to decode thumbnail.heic: %v", err)
|
||||
}
|
||||
if format != "heif" && format != "heic" && format != "avif" {
|
||||
return nil, fmt.Errorf("unsupported thumbnail format: %s", format)
|
||||
}
|
||||
resizedImg := imaging.Resize(img, 0, 700, imaging.Lanczos)
|
||||
var buf bytes.Buffer
|
||||
if err := png.Encode(&buf, resizedImg); err != nil {
|
||||
return nil, fmt.Errorf("could not encode image as PNG: %w", err)
|
||||
}
|
||||
return buf.Bytes(), nil
|
||||
case ".jpeg", ".jpg":
|
||||
img, err := jpeg.Decode(bytes.NewReader(imgBytes))
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("unable to decode thumbnail.jpg: %v", err)
|
||||
}
|
||||
resizedImg := imaging.Resize(img, 0, 700, imaging.Lanczos)
|
||||
var buf bytes.Buffer
|
||||
if err := jpeg.Encode(&buf, resizedImg, &jpeg.Options{Quality: 90}); err != nil {
|
||||
return nil, fmt.Errorf("unable to encode resized thumbnail: %v", err)
|
||||
}
|
||||
return buf.Bytes(), nil
|
||||
case ".png":
|
||||
img, err := png.Decode(bytes.NewReader(imgBytes))
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("unable to decode thumbnail.jpg: %v", err)
|
||||
}
|
||||
resizedImg := imaging.Resize(img, 0, 700, imaging.Lanczos)
|
||||
var buf bytes.Buffer
|
||||
if err := png.Encode(&buf, resizedImg); err != nil {
|
||||
return nil, fmt.Errorf("unable to encode resized thumbnail: %v", err)
|
||||
}
|
||||
return buf.Bytes(), nil
|
||||
default:
|
||||
return nil, fmt.Errorf("unsupported thumbnail format: %s", filepath.Ext(thumbnailFile))
|
||||
return nil, fmt.Errorf("unable to read thumbnail image %s: %v", thumbnailFile, err)
|
||||
}
|
||||
return extractThumbnailPreview(imgBytes, thumbnailFile)
|
||||
}
|
||||
}
|
||||
|
||||
return nil, fmt.Errorf("unable to find thumbnail.jpg in wallpaper zip")
|
||||
return nil, fmt.Errorf("unable to find thumbnail image in wallpaper zip")
|
||||
}
|
||||
|
||||
+76
-34
@@ -1,45 +1,87 @@
|
||||
//go:build darwin && cgo && wallpaper
|
||||
//go:build darwin && wallpaper
|
||||
|
||||
package wallpaper
|
||||
|
||||
import (
|
||||
"reflect"
|
||||
"bytes"
|
||||
"image"
|
||||
"image/color"
|
||||
"image/png"
|
||||
"os"
|
||||
"os/exec"
|
||||
"path/filepath"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestExtractThumbnailBytes(t *testing.T) {
|
||||
type args struct {
|
||||
url string
|
||||
proxy string
|
||||
insecure bool
|
||||
t.Helper()
|
||||
|
||||
got, err := ExtractThumbnailBytes(
|
||||
"https://updates.cdn-apple.com/2022/mobileassets/012-19617/B488E2A1-B291-4E42-AD9A-7111CB03A2AB/com_apple_MobileAsset_Wallpaper/605957001046c16663cb44a4b4ba12c3bcc9281b.zip",
|
||||
"",
|
||||
false,
|
||||
)
|
||||
if err != nil {
|
||||
t.Fatalf("ExtractThumbnailBytes() error = %v", err)
|
||||
}
|
||||
tests := []struct {
|
||||
name string
|
||||
args args
|
||||
want []byte
|
||||
wantErr bool
|
||||
}{
|
||||
{
|
||||
name: "test",
|
||||
args: args{
|
||||
url: "https://updates.cdn-apple.com/2022/mobileassets/012-19617/B488E2A1-B291-4E42-AD9A-7111CB03A2AB/com_apple_MobileAsset_Wallpaper/605957001046c16663cb44a4b4ba12c3bcc9281b.zip",
|
||||
proxy: "",
|
||||
insecure: false,
|
||||
},
|
||||
want: nil,
|
||||
wantErr: false,
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
got, err := ExtractThumbnailBytes(tt.args.url, tt.args.proxy, tt.args.insecure)
|
||||
if (err != nil) != tt.wantErr {
|
||||
t.Errorf("ExtractThumbnailBytes() error = %v, wantErr %v", err, tt.wantErr)
|
||||
return
|
||||
}
|
||||
if !reflect.DeepEqual(got, tt.want) {
|
||||
t.Errorf("ExtractThumbnailBytes() = %v, want %v", got, tt.want)
|
||||
}
|
||||
})
|
||||
if len(got) == 0 {
|
||||
t.Fatal("ExtractThumbnailBytes() returned no thumbnail bytes")
|
||||
}
|
||||
}
|
||||
|
||||
func TestConvertWithSipsHEIC(t *testing.T) {
|
||||
if _, err := exec.LookPath("sips"); err != nil {
|
||||
t.Skipf("sips not available: %v", err)
|
||||
}
|
||||
|
||||
tempDir := t.TempDir()
|
||||
pngPath := filepath.Join(tempDir, "source.png")
|
||||
heicPath := filepath.Join(tempDir, "source.heic")
|
||||
|
||||
img := image.NewNRGBA(image.Rect(0, 0, 2, 3))
|
||||
for y := 0; y < 3; y++ {
|
||||
for x := 0; x < 2; x++ {
|
||||
img.Set(x, y, color.NRGBA{R: uint8(40 * x), G: uint8(60 * y), B: 200, A: 255})
|
||||
}
|
||||
}
|
||||
|
||||
var pngBytes bytes.Buffer
|
||||
if err := png.Encode(&pngBytes, img); err != nil {
|
||||
t.Fatalf("png.Encode() error = %v", err)
|
||||
}
|
||||
if err := os.WriteFile(pngPath, pngBytes.Bytes(), 0o600); err != nil {
|
||||
t.Fatalf("os.WriteFile() error = %v", err)
|
||||
}
|
||||
|
||||
cmd := exec.Command("sips", "-s", "format", "heic", pngPath, "--out", heicPath)
|
||||
if output, err := cmd.CombinedOutput(); err != nil {
|
||||
t.Fatalf("sips HEIC encode failed: %v (%s)", err, bytes.TrimSpace(output))
|
||||
}
|
||||
|
||||
heicBytes, err := os.ReadFile(heicPath)
|
||||
if err != nil {
|
||||
t.Fatalf("os.ReadFile() error = %v", err)
|
||||
}
|
||||
|
||||
converted, err := convertWithSips(heicBytes, ".heic")
|
||||
if err != nil {
|
||||
t.Fatalf("convertWithSips() error = %v", err)
|
||||
}
|
||||
if len(converted) == 0 {
|
||||
t.Fatal("convertWithSips() returned no bytes")
|
||||
}
|
||||
|
||||
cfg, format, err := image.DecodeConfig(bytes.NewReader(converted))
|
||||
if err != nil {
|
||||
t.Fatalf("image.DecodeConfig() error = %v", err)
|
||||
}
|
||||
if format != "png" {
|
||||
t.Fatalf("image.DecodeConfig() format = %s, want png", format)
|
||||
}
|
||||
if cfg.Height != 700 {
|
||||
t.Fatalf("converted height = %d, want 700", cfg.Height)
|
||||
}
|
||||
if cfg.Width == 0 {
|
||||
t.Fatal("converted width = 0")
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user