diff --git a/.github/workflows/goreleaser.yml b/.github/workflows/goreleaser.yml new file mode 100644 index 0000000..e190ee8 --- /dev/null +++ b/.github/workflows/goreleaser.yml @@ -0,0 +1,29 @@ +name: goreleaser + +on: + pull_request: + push: + tags: + - '*' + +jobs: + goreleaser: + runs-on: ubuntu-latest + steps: + - + name: Checkout + uses: actions/checkout@v3 + with: + fetch-depth: 0 + - + name: Set up Go + uses: actions/setup-go@v3 + - + name: Run GoReleaser + uses: goreleaser/goreleaser-action@v3 + with: + distribution: goreleaser + version: latest + args: release --rm-dist + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..b6bb035 --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +watermark-to-image* \ No newline at end of file diff --git a/.goreleaser.yaml b/.goreleaser.yaml new file mode 100644 index 0000000..119bc32 --- /dev/null +++ b/.goreleaser.yaml @@ -0,0 +1,9 @@ +builds: + - + goos: + - darwin + - linux + - windows + goarch: + - amd64 + - arm64 diff --git a/README.md b/README.md index 137c69f..a07ac28 100644 --- a/README.md +++ b/README.md @@ -1,2 +1,38 @@ # watermark-to-image -This application inserts a watermark in the lower right corner into the specified images folder. +This application inserts a watermark in the lower right corner into all images of an specified images folder. + +# Examples +Check out examples/original_images and examples/watermarked_images to get an idea what the application is doing. + +## Usage +1. Build or download prebuild executeable +2. Execute following command: +``` +./watermark-to-image -sourceDirectory ./examples/original_images -targetDirectory ./examples/watermarked_images -watermarkImageFile ./examples/watermark.png +``` + +### Available command line parameter +``` +./watermark-to-image --help + -sourceDirectory string + Source directory of original images + -targetDirectory string + Target directory for watermarked images + -watermarkImageFile string + Path and name of the watermark png image file + -watermarkIncreasement float + Scale factor of the watermark image file (default 100) + -watermarkMarginBottom int + Margin to the bottom edge for the watermark (default 20) + -watermarkMarginRight int + Margin to the right edge for the watermark (default 20) + -watermarkOpacity float + Opacity/Transparency of the watermark image file (default 0.5) +``` + +## Clone and build the project +``` +$ git clone https://github.com/danielchristianschroeter/watermark-to-image +$ cd watermark-to-image +$ go build . +``` diff --git a/examples/original_images/landscape.jpg b/examples/original_images/landscape.jpg new file mode 100644 index 0000000..8caafb8 Binary files /dev/null and b/examples/original_images/landscape.jpg differ diff --git a/examples/original_images/portrait.jpg b/examples/original_images/portrait.jpg new file mode 100644 index 0000000..557888a Binary files /dev/null and b/examples/original_images/portrait.jpg differ diff --git a/examples/watermark.png b/examples/watermark.png new file mode 100644 index 0000000..e5b0d8e Binary files /dev/null and b/examples/watermark.png differ diff --git a/examples/watermarked_images/landscape.jpg b/examples/watermarked_images/landscape.jpg new file mode 100644 index 0000000..0f68856 Binary files /dev/null and b/examples/watermarked_images/landscape.jpg differ diff --git a/examples/watermarked_images/portrait.jpg b/examples/watermarked_images/portrait.jpg new file mode 100644 index 0000000..ab81c84 Binary files /dev/null and b/examples/watermarked_images/portrait.jpg differ diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..4d27765 --- /dev/null +++ b/go.mod @@ -0,0 +1,12 @@ +module watermark-to-image + +go 1.19 + +require ( + github.com/disintegration/imaging v1.6.2 + github.com/gabriel-vasile/mimetype v1.4.1 + github.com/rwcarlsen/goexif v0.0.0-20190401172101-9e8deecbddbd + golang.org/x/image v0.2.0 +) + +require golang.org/x/net v0.0.0-20220722155237-a158d28d115b // indirect diff --git a/go.sum b/go.sum new file mode 100644 index 0000000..9d7b281 --- /dev/null +++ b/go.sum @@ -0,0 +1,35 @@ +github.com/disintegration/imaging v1.6.2 h1:w1LecBlG2Lnp8B3jk5zSuNqd7b4DXhcjwek1ei82L+c= +github.com/disintegration/imaging v1.6.2/go.mod h1:44/5580QXChDfwIclfc/PCwrr44amcmDAg8hxG0Ewe4= +github.com/gabriel-vasile/mimetype v1.4.1 h1:TRWk7se+TOjCYgRth7+1/OYLNiRNIotknkFtf/dnN7Q= +github.com/gabriel-vasile/mimetype v1.4.1/go.mod h1:05Vi0w3Y9c/lNvJOdmIwvrrAhX3rYhfQQCaf9VJcv7M= +github.com/rwcarlsen/goexif v0.0.0-20190401172101-9e8deecbddbd h1:CmH9+J6ZSsIjUK3dcGsnCnO41eRBOnY12zwkn5qVwgc= +github.com/rwcarlsen/goexif v0.0.0-20190401172101-9e8deecbddbd/go.mod h1:hPqNNc0+uJM6H+SuU8sEs5K5IQeKccPqeSjfgcKGgPk= +github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= +golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= +golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= +golang.org/x/image v0.0.0-20191009234506-e7c1f5e7dbb8/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0= +golang.org/x/image v0.2.0 h1:/DcQ0w3VHKCC5p0/P2B0JpAZ9Z++V2KOo2fyU89CXBQ= +golang.org/x/image v0.2.0/go.mod h1:la7oBXb9w3YFjBqaAwtynVioc1ZvOnNteUNrifGNmAI= +golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= +golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= +golang.org/x/net v0.0.0-20220624214902-1bab6f366d9e/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= +golang.org/x/net v0.0.0-20220722155237-a158d28d115b h1:PxfKdU9lEEDYjdIzOtC4qFWgkU2rGHdKlKowJSMN9h0= +golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= +golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= +golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= +golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= +golang.org/x/text v0.5.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= +golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= +golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= diff --git a/main.go b/main.go new file mode 100644 index 0000000..6e409c2 --- /dev/null +++ b/main.go @@ -0,0 +1,212 @@ +package main + +import ( + "flag" + "image" + "image/color" + "image/jpeg" + _ "image/png" + "log" + "os" + "path/filepath" + "strings" + + "github.com/disintegration/imaging" + "github.com/gabriel-vasile/mimetype" + "github.com/rwcarlsen/goexif/exif" + "golang.org/x/image/draw" +) + +// Used for build version information +var version = "development" + +// Global variables used for command line flags +var ( + sourceDirectory string + targetDirectory string + watermarkImageFile string + watermarkIncreasement float64 + watermarkOpacity float64 + watermarkMarginRight int + watermarkMarginBottom int +) + +func init() { + // Initialize command line flags + flag.StringVar(&sourceDirectory, "sourceDirectory", "", "Source directory of original images") + flag.StringVar(&targetDirectory, "targetDirectory", "", "Target directory for watermarked images") + flag.StringVar(&watermarkImageFile, "watermarkImageFile", "", "Path and name of the watermark png image file") + flag.Float64Var(&watermarkIncreasement, "watermarkIncreasement", 100, "Scale factor of the watermark image file") + flag.Float64Var(&watermarkOpacity, "watermarkOpacity", 0.5, "Opacity/Transparency of the watermark image file") + flag.IntVar(&watermarkMarginRight, "watermarkMarginRight", 20, "Margin to the right edge for the watermark") + flag.IntVar(&watermarkMarginBottom, "watermarkMarginBottom", 20, "Margin to the bottom edge for the watermark") +} + +func processImage(file *os.File, targetDirectory string, watermarkImageFile string, watermarkIncreasement float64, watermarkMarginRight int, watermarkMarginBottom int, watermarkOpacity float64) error { + + // Load the watermark png image file using image.Decode + watermarkFile, err := os.Open(watermarkImageFile) + if err != nil { + log.Fatalf("Failed to os.Open of file %+v Error: %+v", watermarkImageFile, err) + } + defer watermarkFile.Close() + watermark, _, err := image.Decode(watermarkFile) + if err != nil { + log.Fatalf("Failed to image.Decode of file %+v Error: %+v", watermarkImageFile, err) + } + + // Read EXIF-Metadata of image + exifData, err := exif.Decode(file) + if err != nil { + log.Fatalf("Failed to exif.Decode of file %+v Error: %+v", file.Name(), err) + } + + // Read Orientation-Tag from EXIF-Metadata + orientation, err := exifData.Get(exif.Orientation) + if err != nil { + log.Fatalf("Failed to exifData.Get of file %+v Error: %+v", file.Name(), err) + } + + // Convert Orientation-Tag to integer value + orientationInt, err := orientation.Int(0) + if err != nil { + log.Fatalf("Failed to convert orientation-tag of file %+v Error: %+v", file.Name(), err) + } + + // Seek to the beginning of the file before calling image.Decode + _, err = file.Seek(0, 0) + if err != nil { + log.Fatalf("Failed to file.Seek of file %+v Error: %+v", file.Name(), err) + } + + // Decode the image from the buffer + img, _, err := image.Decode(file) + if err != nil { + log.Fatalf("Failed to image.Decode of file %+v Error: %+v", file.Name(), err) + } + // Rotate and/or flip image based on Orientation-Tag from EXIF-Metadata + switch orientationInt { + case 1: + // no adjustment needed + case 2: + img = imaging.FlipH(img) + case 3: + img = imaging.Rotate(img, 180, color.Transparent) + case 4: + img = imaging.FlipH(imaging.Rotate(img, 180, color.Transparent)) + case 5: + img = imaging.FlipV(imaging.Rotate(img, 90, color.Transparent)) + case 6: + img = imaging.Rotate(img, 270, color.Transparent) + case 7: + img = imaging.FlipV(imaging.Rotate(img, 270, color.Transparent)) + case 8: + img = imaging.Rotate(img, 90, color.Transparent) + } + + // Set the the margins (pixels). + //marginSide := 15 + //marginBottom := 15 + + // Get the width and the height of our image. + photoWidth := img.Bounds().Dx() + photoHeight := img.Bounds().Dy() + + // Get the width and the height of our watermark. + watermarkWidth := watermark.Bounds().Dx() + watermarkHeight := watermark.Bounds().Dy() + + // Increase the dimensions of the watermark by a given percentage. + //percentage := 250 // increase dimensions + resizedWidth := int(float64(watermarkWidth) * (float64(watermarkIncreasement) / 100.0)) + resizedHeight := int(float64(watermarkHeight) * (float64(watermarkIncreasement) / 100.0)) + resizedWatermark := image.NewRGBA(image.Rect(0, 0, resizedWidth, resizedHeight)) + draw.NearestNeighbor.Scale(resizedWatermark, resizedWatermark.Bounds(), watermark, watermark.Bounds(), draw.Over, nil) + + // Figure out the dstX value. + dstX := photoWidth - resizedWidth - watermarkMarginRight + + // Figure out the dstY value. + dstY := photoHeight - resizedHeight - watermarkMarginBottom + + // Overlay the watermark onto the photo image with a specified opacity/transparency + dst := imaging.Overlay(img, resizedWatermark, image.Pt(dstX, dstY), watermarkOpacity) + + // Save the new image + outFile, err := os.Create(targetDirectory + filepath.Base(file.Name())) + if err != nil { + // handle error + log.Fatalf("Failed to os.Create of file %+v Error: %+v", targetDirectory+filepath.Base(file.Name()), err) + } + defer outFile.Close() + jpeg.Encode(outFile, dst, nil) + + log.Println("Saved watermarked image to " + targetDirectory + filepath.Base(file.Name())) + + return nil +} + +func main() { + log.SetFlags(0) + flag.Usage = func() { + log.Println("watermark-to-image. Version: " + version) + flag.PrintDefaults() + } + flag.Parse() + + if len(sourceDirectory) == 0 || len(targetDirectory) == 0 || len(watermarkImageFile) == 0 { + log.Fatal("Usage: -sourceDirectory -targetDirectory -watermarkImageFile ") + } + + // Add slash to source directory, if not exists + if !strings.HasSuffix(sourceDirectory, "/") { + sourceDirectory += "/" + } + + // Add slash to target directory, if not exists + if !strings.HasSuffix(targetDirectory, "/") { + targetDirectory += "/" + } + + log.Println("Processing imges from directory " + sourceDirectory + "...") + // Open the source directory + dir, err := os.Open(sourceDirectory) + if err != nil { + log.Fatal(err) + } + defer dir.Close() + + // Get a list of all the files in the source directory + files, err := dir.Readdir(-1) + if err != nil { + log.Fatal(err) + } + + // Loop through the images in the source directory + for _, file := range files { + // Open the image + f, err := os.Open(sourceDirectory + file.Name()) + if err != nil { + log.Printf("Failed to os.Open of file %+v Error: %v", sourceDirectory+file.Name(), err) + continue + } + defer f.Close() + + // Skip image/heic + mimeType, err := mimetype.DetectFile(sourceDirectory + file.Name()) + if err != nil { + log.Printf("Failed to mimetype.DetectFile of file %+v Error: %v", sourceDirectory+file.Name(), err) + } + switch mimeType.String() { + case "image/heic": + log.Printf("Skipping %v Reason: heic not supported", sourceDirectory+file.Name()) + continue + } + + // Process the image + if err := processImage(f, targetDirectory, watermarkImageFile, watermarkIncreasement, watermarkMarginRight, watermarkMarginBottom, watermarkOpacity); err != nil { + log.Printf("Failed to processImage of file %+v Error: %v", sourceDirectory+file.Name(), err) + continue + } + } +}