04a1f33cb1
- Full-screen Bubble Tea TUI with cream background fill using PadLine/FillBlankLines - Self-update command (--update) pulling from GitHub releases - install.sh for curl one-liner installation - Terminal Lovers section on web landing page with install command and CLI features - All 7 format categories, glob/directory batch support, auto-download ffmpeg
107 lines
2.8 KiB
Go
107 lines
2.8 KiB
Go
package converter
|
|
|
|
import (
|
|
"fmt"
|
|
"image"
|
|
"image/gif"
|
|
"image/jpeg"
|
|
"image/png"
|
|
"os"
|
|
"strings"
|
|
|
|
"golang.org/x/image/bmp"
|
|
"golang.org/x/image/tiff"
|
|
"golang.org/x/image/webp"
|
|
)
|
|
|
|
func convertImage(inputPath, outputPath, targetFormat string) error {
|
|
f, err := os.Open(inputPath)
|
|
if err != nil {
|
|
return fmt.Errorf("opening image: %w", err)
|
|
}
|
|
defer f.Close()
|
|
|
|
// Decode input — Go's image package auto-registers png, jpeg, gif via import
|
|
// We also need x/image decoders for bmp, tiff, webp
|
|
img, format, err := image.Decode(f)
|
|
if err != nil {
|
|
// Try specific decoders as fallback
|
|
f.Seek(0, 0)
|
|
img, err = tryDecodeImage(f, inputPath)
|
|
if err != nil {
|
|
return fmt.Errorf("decoding image (%s): %w", format, err)
|
|
}
|
|
}
|
|
|
|
out, err := os.Create(outputPath)
|
|
if err != nil {
|
|
return fmt.Errorf("creating output: %w", err)
|
|
}
|
|
defer out.Close()
|
|
|
|
target := strings.ToLower(targetFormat)
|
|
switch target {
|
|
case "png":
|
|
return png.Encode(out, img)
|
|
case "jpg", "jpeg":
|
|
return jpeg.Encode(out, img, &jpeg.Options{Quality: 92})
|
|
case "gif":
|
|
return gif.Encode(out, img, &gif.Options{NumColors: 256})
|
|
case "bmp":
|
|
return bmp.Encode(out, img)
|
|
case "tiff", "tif":
|
|
return tiff.Encode(out, img, &tiff.Options{Compression: tiff.Deflate})
|
|
case "webp":
|
|
// Go doesn't have a webp encoder in stdlib. Use ffmpeg as fallback.
|
|
out.Close()
|
|
os.Remove(outputPath)
|
|
return convertImageViaFFmpeg(inputPath, outputPath, target)
|
|
case "avif":
|
|
out.Close()
|
|
os.Remove(outputPath)
|
|
return convertImageViaFFmpeg(inputPath, outputPath, target)
|
|
case "ico":
|
|
// ICO is just a small PNG wrapped in ICO container for simple cases.
|
|
// We'll convert to PNG via ffmpeg or write a 256x256 PNG for now.
|
|
out.Close()
|
|
os.Remove(outputPath)
|
|
return convertImageViaFFmpeg(inputPath, outputPath, target)
|
|
default:
|
|
out.Close()
|
|
os.Remove(outputPath)
|
|
return fmt.Errorf("unsupported image target format: %s", target)
|
|
}
|
|
}
|
|
|
|
func tryDecodeImage(f *os.File, path string) (image.Image, error) {
|
|
ext := strings.ToLower(path)
|
|
switch {
|
|
case strings.HasSuffix(ext, ".webp"):
|
|
return webp.Decode(f)
|
|
case strings.HasSuffix(ext, ".bmp"):
|
|
return bmp.Decode(f)
|
|
case strings.HasSuffix(ext, ".tiff"), strings.HasSuffix(ext, ".tif"):
|
|
return tiff.Decode(f)
|
|
default:
|
|
return nil, fmt.Errorf("unable to decode image: %s", path)
|
|
}
|
|
}
|
|
|
|
func convertImageViaFFmpeg(inputPath, outputPath, format string) error {
|
|
args := []string{"-y", "-i", inputPath}
|
|
|
|
switch format {
|
|
case "webp":
|
|
args = append(args, "-quality", "90", outputPath)
|
|
case "avif":
|
|
args = append(args, "-c:v", "libaom-av1", "-still-picture", "1", outputPath)
|
|
case "ico":
|
|
// Scale to 256x256 for ICO
|
|
args = append(args, "-vf", "scale=256:256:force_original_aspect_ratio=decrease,pad=256:256:(ow-iw)/2:(oh-ih)/2", outputPath)
|
|
default:
|
|
args = append(args, outputPath)
|
|
}
|
|
|
|
return mediaConvert(inputPath, outputPath, format, args)
|
|
}
|