Enhance image resizing functionality with batch processing and quality options. Update README with new usage examples and commands for favicon generation. Add indirect dependencies for besticon and x/image.
This commit is contained in:
+190
@@ -0,0 +1,190 @@
|
||||
package cmd
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"image"
|
||||
"image/png"
|
||||
"log"
|
||||
"os"
|
||||
"path/filepath"
|
||||
|
||||
"github.com/nfnt/resize"
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
var faviconSizes = []int{16, 32, 48, 64, 72, 96, 128, 144, 152, 192, 196, 512}
|
||||
var appleTouchSizes = []int{57, 60, 72, 76, 114, 120, 144, 152, 180}
|
||||
var androidSizes = []int{36, 48, 72, 96, 144, 192, 196, 512}
|
||||
|
||||
// faviconCmd represents the favicon command
|
||||
var faviconCmd = &cobra.Command{
|
||||
Use: "favicon [input_image] [output_dir]",
|
||||
Short: "Generate a complete favicon package from an image",
|
||||
Long: `Generate a complete favicon package from a single image.
|
||||
This creates all common favicon formats and sizes required for modern websites, including:
|
||||
- PNG favicons in various sizes (16x16, 32x32, 48x48, etc.)
|
||||
- Apple Touch Icons
|
||||
- Android/PWA icons
|
||||
- Web app manifest and browserconfig files
|
||||
|
||||
Example: dttt favicon logo.png ./favicons --name "My Awesome App" --short-name "MyApp"`,
|
||||
Args: cobra.ExactArgs(2),
|
||||
Run: func(cmd *cobra.Command, args []string) {
|
||||
inputFile := args[0]
|
||||
outputDir := args[1]
|
||||
|
||||
appName, _ := cmd.Flags().GetString("name")
|
||||
shortName, _ := cmd.Flags().GetString("short-name")
|
||||
|
||||
generateFavicons(inputFile, outputDir, appName, shortName)
|
||||
},
|
||||
}
|
||||
|
||||
func init() {
|
||||
rootCmd.AddCommand(faviconCmd)
|
||||
|
||||
// Add flags for web manifest customization
|
||||
faviconCmd.Flags().StringP("name", "n", "App", "Application name for the web manifest")
|
||||
faviconCmd.Flags().StringP("short-name", "s", "App", "Short application name for the web manifest")
|
||||
}
|
||||
|
||||
func generateFavicons(inputFile, outputDir, appName, shortName string) {
|
||||
// Open and decode the input image
|
||||
input, err := os.Open(inputFile)
|
||||
if err != nil {
|
||||
log.Fatal("Error opening input file:", err)
|
||||
}
|
||||
defer input.Close()
|
||||
|
||||
// Decode the input image
|
||||
srcImg, _, err := image.Decode(input)
|
||||
if err != nil {
|
||||
log.Fatal("Error decoding input image:", err)
|
||||
}
|
||||
|
||||
// Create output directory if it doesn't exist
|
||||
if err := os.MkdirAll(outputDir, 0755); err != nil {
|
||||
log.Fatal("Error creating output directory:", err)
|
||||
}
|
||||
|
||||
// Generate standard favicons (PNG)
|
||||
fmt.Println("Generating favicons...")
|
||||
for _, size := range faviconSizes {
|
||||
generateResizedPNG(srcImg, outputDir, fmt.Sprintf("favicon-%dx%d.png", size, size), size)
|
||||
}
|
||||
|
||||
// Generate specific named favicons
|
||||
generateResizedPNG(srcImg, outputDir, "favicon-16x16.png", 16)
|
||||
generateResizedPNG(srcImg, outputDir, "favicon-32x32.png", 32)
|
||||
generateResizedPNG(srcImg, outputDir, "favicon-48x48.png", 48)
|
||||
generateResizedPNG(srcImg, outputDir, "favicon.png", 32) // Default favicon
|
||||
|
||||
// Generate Apple Touch Icons
|
||||
fmt.Println("Generating Apple Touch Icons...")
|
||||
for _, size := range appleTouchSizes {
|
||||
generateResizedPNG(srcImg, outputDir, fmt.Sprintf("apple-touch-icon-%dx%d.png", size, size), size)
|
||||
}
|
||||
// Generate default Apple Touch Icon
|
||||
generateResizedPNG(srcImg, outputDir, "apple-touch-icon.png", 180)
|
||||
generateResizedPNG(srcImg, outputDir, "apple-touch-icon-precomposed.png", 180)
|
||||
|
||||
// Generate Android/PWA icons
|
||||
fmt.Println("Generating Android/PWA icons...")
|
||||
for _, size := range androidSizes {
|
||||
generateResizedPNG(srcImg, outputDir, fmt.Sprintf("android-chrome-%dx%d.png", size, size), size)
|
||||
}
|
||||
|
||||
// Generate web app manifest
|
||||
fmt.Println("Generating web app manifest...")
|
||||
generateWebManifest(outputDir, appName, shortName)
|
||||
|
||||
// Generate browserconfig
|
||||
fmt.Println("Generating browserconfig...")
|
||||
generateBrowserconfig(outputDir)
|
||||
|
||||
// Generate HTML include snippet
|
||||
fmt.Println("Generating HTML include snippet...")
|
||||
generateHTMLSnippet(outputDir)
|
||||
|
||||
fmt.Println("Note: No .ico file was generated; modern browsers support PNG favicons.")
|
||||
fmt.Println("Favicon package generated successfully in", outputDir)
|
||||
}
|
||||
|
||||
func generateResizedPNG(srcImg image.Image, outputDir, filename string, size int) {
|
||||
resized := resize.Resize(uint(size), uint(size), srcImg, resize.Lanczos3)
|
||||
|
||||
outPath := filepath.Join(outputDir, filename)
|
||||
outFile, err := os.Create(outPath)
|
||||
if err != nil {
|
||||
log.Printf("Error creating %s: %v", filename, err)
|
||||
return
|
||||
}
|
||||
defer outFile.Close()
|
||||
|
||||
if err := png.Encode(outFile, resized); err != nil {
|
||||
log.Printf("Error encoding %s: %v", filename, err)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
func generateWebManifest(outputDir, appName, shortName string) {
|
||||
manifest := fmt.Sprintf(`{
|
||||
"name": "%s",
|
||||
"short_name": "%s",
|
||||
"icons": [
|
||||
{
|
||||
"src": "/android-chrome-192x192.png",
|
||||
"sizes": "192x192",
|
||||
"type": "image/png"
|
||||
},
|
||||
{
|
||||
"src": "/android-chrome-512x512.png",
|
||||
"sizes": "512x512",
|
||||
"type": "image/png"
|
||||
}
|
||||
],
|
||||
"theme_color": "#ffffff",
|
||||
"background_color": "#ffffff",
|
||||
"display": "standalone"
|
||||
}`, appName, shortName)
|
||||
|
||||
err := os.WriteFile(filepath.Join(outputDir, "site.webmanifest"), []byte(manifest), 0644)
|
||||
if err != nil {
|
||||
log.Printf("Error writing site.webmanifest: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func generateBrowserconfig(outputDir string) {
|
||||
config := `<?xml version="1.0" encoding="utf-8"?>
|
||||
<browserconfig>
|
||||
<msapplication>
|
||||
<tile>
|
||||
<square70x70logo src="/favicon-72x72.png"/>
|
||||
<square150x150logo src="/favicon-144x144.png"/>
|
||||
<square310x310logo src="/favicon-192x192.png"/>
|
||||
<TileColor>#ffffff</TileColor>
|
||||
</tile>
|
||||
</msapplication>
|
||||
</browserconfig>`
|
||||
|
||||
err := os.WriteFile(filepath.Join(outputDir, "browserconfig.xml"), []byte(config), 0644)
|
||||
if err != nil {
|
||||
log.Printf("Error writing browserconfig.xml: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func generateHTMLSnippet(outputDir string) {
|
||||
snippet := `<!-- Favicon -->
|
||||
<link rel="icon" type="image/png" sizes="32x32" href="/favicon-32x32.png">
|
||||
<link rel="icon" type="image/png" sizes="16x16" href="/favicon-16x16.png">
|
||||
<link rel="apple-touch-icon" sizes="180x180" href="/apple-touch-icon.png">
|
||||
<link rel="manifest" href="/site.webmanifest">
|
||||
<meta name="msapplication-config" content="/browserconfig.xml">
|
||||
<meta name="msapplication-TileColor" content="#ffffff">
|
||||
<meta name="theme-color" content="#ffffff">`
|
||||
|
||||
err := os.WriteFile(filepath.Join(outputDir, "favicon-snippet.html"), []byte(snippet), 0644)
|
||||
if err != nil {
|
||||
log.Printf("Error writing favicon-snippet.html: %v", err)
|
||||
}
|
||||
}
|
||||
+155
-28
@@ -4,56 +4,150 @@ import (
|
||||
"fmt"
|
||||
"image"
|
||||
"image/jpeg"
|
||||
"image/png"
|
||||
"log"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"github.com/chai2010/webp"
|
||||
"github.com/nfnt/resize"
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
// resizeCmd represents the resize command
|
||||
var resizeCmd = &cobra.Command{
|
||||
Use: "resize [input_file] [output_file] [width]",
|
||||
Short: "Resize an image to the specified width",
|
||||
Long: `Resize an image to the specified width using Lanczos3 interpolation.
|
||||
Example: dttt resize input.jpg output.jpg 300`,
|
||||
Args: cobra.ExactArgs(3),
|
||||
Use: "resize [input] [output] [width]",
|
||||
Short: "Resize images to the specified width",
|
||||
Long: `Resize images to the specified width using Lanczos3 interpolation.
|
||||
Single file mode:
|
||||
dttt resize input.jpg output.jpg 300
|
||||
|
||||
Batch mode (resize all images in a directory):
|
||||
dttt resize ./photos ./resized 800 --batch`,
|
||||
Args: cobra.RangeArgs(2, 3),
|
||||
Run: func(cmd *cobra.Command, args []string) {
|
||||
if len(args) == 0 {
|
||||
cmd.Help()
|
||||
return
|
||||
isBatch, _ := cmd.Flags().GetBool("batch")
|
||||
quality, _ := cmd.Flags().GetInt("quality")
|
||||
extensions, _ := cmd.Flags().GetString("extensions")
|
||||
|
||||
if isBatch {
|
||||
if len(args) != 3 {
|
||||
log.Fatal("Batch mode requires input directory, output directory, and width")
|
||||
}
|
||||
|
||||
inputDir := args[0]
|
||||
outputDir := args[1]
|
||||
widthStr := args[2]
|
||||
|
||||
width, err := strconv.Atoi(widthStr)
|
||||
if err != nil {
|
||||
log.Fatal("Invalid width:", err)
|
||||
}
|
||||
|
||||
batchResize(inputDir, outputDir, width, extensions, quality)
|
||||
} else {
|
||||
if len(args) != 3 {
|
||||
log.Fatal("Single file mode requires input file, output file, and width")
|
||||
}
|
||||
|
||||
inputFile := args[0]
|
||||
outputFile := args[1]
|
||||
widthStr := args[2]
|
||||
|
||||
width, err := strconv.Atoi(widthStr)
|
||||
if err != nil {
|
||||
log.Fatal("Invalid width:", err)
|
||||
}
|
||||
|
||||
resizeImage(inputFile, outputFile, width, quality)
|
||||
}
|
||||
|
||||
inputFile := args[0]
|
||||
outputFile := args[1]
|
||||
widthStr := args[2]
|
||||
|
||||
width, err := strconv.Atoi(widthStr)
|
||||
if err != nil {
|
||||
log.Fatal("Invalid width:", err)
|
||||
}
|
||||
|
||||
resizeImage(inputFile, outputFile, width)
|
||||
},
|
||||
}
|
||||
|
||||
func init() {
|
||||
rootCmd.AddCommand(resizeCmd)
|
||||
|
||||
// Add flags
|
||||
resizeCmd.Flags().BoolP("batch", "b", false, "Enable batch resizing mode for directories")
|
||||
resizeCmd.Flags().IntP("quality", "q", 90, "Quality for lossy formats (1-100)")
|
||||
resizeCmd.Flags().StringP("extensions", "e", "jpg,jpeg,png,webp", "Comma-separated list of file extensions to process in batch mode")
|
||||
}
|
||||
|
||||
func resizeImage(inputFile, outputFile string, width int) {
|
||||
func batchResize(inputDir, outputDir string, width int, extensions string, quality int) {
|
||||
// Ensure output directory exists
|
||||
if err := os.MkdirAll(outputDir, 0755); err != nil {
|
||||
log.Fatal("Failed to create output directory:", err)
|
||||
}
|
||||
|
||||
// Parse extensions
|
||||
validExts := strings.Split(extensions, ",")
|
||||
for i, ext := range validExts {
|
||||
validExts[i] = strings.ToLower(strings.TrimSpace(ext))
|
||||
if !strings.HasPrefix(validExts[i], ".") {
|
||||
validExts[i] = "." + validExts[i]
|
||||
}
|
||||
}
|
||||
|
||||
// Get all files from input directory
|
||||
files, err := os.ReadDir(inputDir)
|
||||
if err != nil {
|
||||
log.Fatal("Error reading input directory:", err)
|
||||
}
|
||||
|
||||
// Track statistics
|
||||
successCount := 0
|
||||
errorCount := 0
|
||||
|
||||
for _, file := range files {
|
||||
if file.IsDir() {
|
||||
continue
|
||||
}
|
||||
|
||||
// Check if file has a valid extension
|
||||
fileExt := strings.ToLower(filepath.Ext(file.Name()))
|
||||
isValidExt := false
|
||||
for _, ext := range validExts {
|
||||
if ext == fileExt {
|
||||
isValidExt = true
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if !isValidExt {
|
||||
continue
|
||||
}
|
||||
|
||||
// Generate input and output file paths
|
||||
inputPath := filepath.Join(inputDir, file.Name())
|
||||
outputPath := filepath.Join(outputDir, file.Name())
|
||||
|
||||
// Resize the file
|
||||
err := resizeImageWithError(inputPath, outputPath, width, quality)
|
||||
if err != nil {
|
||||
fmt.Printf("Error resizing %s: %v\n", file.Name(), err)
|
||||
errorCount++
|
||||
} else {
|
||||
successCount++
|
||||
}
|
||||
}
|
||||
|
||||
fmt.Printf("Batch resize complete: %d files resized successfully, %d failed\n", successCount, errorCount)
|
||||
}
|
||||
|
||||
func resizeImageWithError(inputFile, outputFile string, width, quality int) error {
|
||||
// Open the input image file
|
||||
input, err := os.Open(inputFile)
|
||||
if err != nil {
|
||||
log.Fatal("Error opening input file:", err)
|
||||
return fmt.Errorf("error opening input file: %w", err)
|
||||
}
|
||||
defer input.Close()
|
||||
|
||||
// Decode the input image file
|
||||
img, _, err := image.Decode(input)
|
||||
img, format, err := image.Decode(input)
|
||||
if err != nil {
|
||||
log.Fatal("Error decoding input image:", err)
|
||||
return fmt.Errorf("error decoding input image: %w", err)
|
||||
}
|
||||
|
||||
// Resize the image to the target width
|
||||
@@ -62,15 +156,48 @@ func resizeImage(inputFile, outputFile string, width int) {
|
||||
// Create the output image file
|
||||
output, err := os.Create(outputFile)
|
||||
if err != nil {
|
||||
log.Fatal("Error creating output file:", err)
|
||||
return fmt.Errorf("error creating output file: %w", err)
|
||||
}
|
||||
defer output.Close()
|
||||
|
||||
// Write the resized image to the output file
|
||||
if err := jpeg.Encode(output, resizedImg, nil); err != nil {
|
||||
log.Fatal("Error encoding resized image:", err)
|
||||
// Determine the output format based on the file extension
|
||||
fileExt := strings.ToLower(filepath.Ext(outputFile))
|
||||
|
||||
// Encode based on output format
|
||||
switch fileExt {
|
||||
case ".jpg", ".jpeg":
|
||||
err = jpeg.Encode(output, resizedImg, &jpeg.Options{Quality: quality})
|
||||
case ".png":
|
||||
err = png.Encode(output, resizedImg)
|
||||
case ".webp":
|
||||
err = webp.Encode(output, resizedImg, &webp.Options{Lossless: quality == 100, Quality: float32(quality)})
|
||||
default:
|
||||
// Use input format if output format is not recognized
|
||||
switch format {
|
||||
case "jpeg":
|
||||
err = jpeg.Encode(output, resizedImg, &jpeg.Options{Quality: quality})
|
||||
case "png":
|
||||
err = png.Encode(output, resizedImg)
|
||||
case "webp":
|
||||
err = webp.Encode(output, resizedImg, &webp.Options{Lossless: quality == 100, Quality: float32(quality)})
|
||||
default:
|
||||
// Default to JPEG if format is unknown
|
||||
err = jpeg.Encode(output, resizedImg, &jpeg.Options{Quality: quality})
|
||||
}
|
||||
}
|
||||
|
||||
fmt.Println("Image resized successfully!")
|
||||
if err != nil {
|
||||
return fmt.Errorf("error encoding resized image: %w", err)
|
||||
}
|
||||
|
||||
fmt.Printf("Resized %s from %s to %dpx width\n", filepath.Base(inputFile), format, width)
|
||||
return nil
|
||||
}
|
||||
|
||||
func resizeImage(inputFile, outputFile string, width, quality int) {
|
||||
err := resizeImageWithError(inputFile, outputFile, width, quality)
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
fmt.Println("Image resized successfully!")
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user