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:
Kristian 2025-04-22 19:29:14 +02:00
parent 31a4fc3467
commit 9395e3b1b2
6 changed files with 407 additions and 30 deletions

View File

@ -0,0 +1 @@
MIT Licence

View File

@ -23,12 +23,33 @@ go build -o dttt
Resize images to a specified width while maintaining aspect ratio.
```bash
# Single file resize
dttt resize [input_file] [output_file] [width]
# Batch resize all images in a directory
dttt resize [input_directory] [output_directory] [width] --batch
```
Example:
The command supports multiple image formats (JPG, PNG, WebP) and preserves the original file format.
Options:
- `--batch, -b`: Enable batch resizing mode for directories
- `--quality, -q`: Quality for lossy formats (1-100, default: 90)
- `--extensions, -e`: Comma-separated list of file extensions to process in batch mode (default: jpg,jpeg,png,webp)
Examples:
```bash
dttt resize input.jpg output.jpg 300
# Resize a single image
dttt resize input.jpg output.jpg 800
# Resize a single image with custom quality
dttt resize input.webp output.webp 1200 --quality 85
# Batch resize all images in a directory to 1000px width
dttt resize ./photos ./resized 1000 --batch
# Batch resize only JPG and PNG files
dttt resize ./originals ./thumbnails 400 --batch --extensions jpg,png
```
### convert
@ -69,6 +90,36 @@ dttt convert ./photos ./webp_photos --batch --format webp --extensions jpg,png
dttt convert ./input ./output --batch --format jpg --quality 85
```
### favicon
Generate a complete favicon package from a single image source, similar to favicon.io.
```bash
dttt favicon [input_image] [output_dir] [flags]
```
This creates all common favicon formats and sizes required for modern websites, including:
- PNG favicons in various sizes (16x16, 32x32, 48x48 and more)
- Apple Touch Icons for iOS devices
- Android/PWA icons for Android and Progressive Web Apps
- Web app manifest and browserconfig files
- HTML snippet for easy inclusion in your website
Options:
- `--name, -n`: Application name for the web manifest (default: "App")
- `--short-name, -s`: Short application name for the web manifest (default: "App")
Examples:
```bash
# Basic usage
dttt favicon logo.png ./favicons
# With custom application name
dttt favicon logo.png ./favicons --name "My Awesome Website" --short-name "MyApp"
```
The command generates all the necessary favicon files in the output directory, ready to be used in your website. Modern browsers support PNG favicons, so an .ico file is not needed anymore.
### rename
Rename files in bulk based on specified patterns.

190
cmd/favicon.go Normal file
View File

@ -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)
}
}

View File

@ -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!")
}

2
go.mod
View File

@ -12,6 +12,7 @@ require (
github.com/inconshreveable/mousetrap v1.1.0 // indirect
github.com/lucasb-eyer/go-colorful v1.2.0 // indirect
github.com/magiconair/properties v1.8.7 // indirect
github.com/mat/besticon v3.12.0+incompatible // indirect
github.com/mattn/go-isatty v0.0.18 // indirect
github.com/mattn/go-localereader v0.0.1 // indirect
github.com/mattn/go-runewidth v0.0.14 // indirect
@ -35,6 +36,7 @@ require (
go.uber.org/atomic v1.9.0 // indirect
go.uber.org/multierr v1.9.0 // indirect
golang.org/x/exp v0.0.0-20230905200255-921286631fa9 // indirect
golang.org/x/image v0.0.0-20211028202545-6944b10bf410 // indirect
golang.org/x/sync v0.5.0 // indirect
golang.org/x/sys v0.15.0 // indirect
golang.org/x/term v0.6.0 // indirect

6
go.sum
View File

@ -19,6 +19,8 @@ github.com/lucasb-eyer/go-colorful v1.2.0 h1:1nnpGOrhyZZuNyfu1QjKiUICQ74+3FNCN69
github.com/lucasb-eyer/go-colorful v1.2.0/go.mod h1:R4dSotOR9KMtayYi1e77YzuveK+i7ruzyGqttikkLy0=
github.com/magiconair/properties v1.8.7 h1:IeQXZAiQcpL9mgcAe1Nu6cX9LLw6ExEHKjN0VQdvPDY=
github.com/magiconair/properties v1.8.7/go.mod h1:Dhd985XPs7jluiymwWYZ0G4Z61jb3vdS329zhj2hYo0=
github.com/mat/besticon v3.12.0+incompatible h1:1KTD6wisfjfnX+fk9Kx/6VEZL+MAW1LhCkL9Q47H9Bg=
github.com/mat/besticon v3.12.0+incompatible/go.mod h1:mA1auQYHt6CW5e7L9HJLmqVQC8SzNk2gVwouO0AbiEU=
github.com/mattn/go-isatty v0.0.18 h1:DOKFKCQ7FNG2L1rbrmstDN4QVRdS89Nkh85u68Uwp98=
github.com/mattn/go-isatty v0.0.18/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
github.com/mattn/go-localereader v0.0.1 h1:ygSAOl7ZXTx4RdPYinUpg6W99U8jWvWi9Ye2JC/oIi4=
@ -76,6 +78,8 @@ go.uber.org/multierr v1.9.0 h1:7fIwc/ZtS0q++VgcfqFDxSBZVv/Xo49/SYnDFupUwlI=
go.uber.org/multierr v1.9.0/go.mod h1:X2jQV1h+kxSjClGpnseKVIxpmcjrj7MNnI0bnlfKTVQ=
golang.org/x/exp v0.0.0-20230905200255-921286631fa9 h1:GoHiUyI/Tp2nVkLI2mCxVkOjsbSXD66ic0XW0js0R9g=
golang.org/x/exp v0.0.0-20230905200255-921286631fa9/go.mod h1:S2oDrQGGwySpoQPVqRShND87VCbxmc6bL1Yd2oYrm6k=
golang.org/x/image v0.0.0-20211028202545-6944b10bf410 h1:hTftEOvwiOq2+O8k2D5/Q7COC7k5Qcrgc2TFURJYnvQ=
golang.org/x/image v0.0.0-20211028202545-6944b10bf410/go.mod h1:023OzeP/+EPmXeapQh35lcL3II3LrY8Ic+EFFKVhULM=
golang.org/x/sync v0.5.0 h1:60k92dhOjHxJkrqnwsfl8KuaHbn/5dl0lUPUklKo3qE=
golang.org/x/sync v0.5.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
@ -84,8 +88,10 @@ golang.org/x/sys v0.15.0 h1:h48lPFYpsTvQJZF4EKyI4aLHaev3CxivZmv7yZig9pc=
golang.org/x/sys v0.15.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/term v0.6.0 h1:clScbb1cHjoCkyRbWwBEUZ5H/tIFu5TAXIqaZD0Gcjw=
golang.org/x/term v0.6.0/go.mod h1:m6U89DPEgQRMq3DNkDClhWw02AUbt2daBVO4cn4Hv9U=
golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.14.0 h1:ScX5w1eTa3QqT8oi6+ziP7dTV1S2+ALU0bI+0zXKWiQ=
golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/ini.v1 v1.67.0 h1:Dgnx+6+nfE+IfzjUEISNeydPJh9AXNNsWbGP9KzCsOA=
gopkg.in/ini.v1 v1.67.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k=