【Go】:图片上添加水印的全面指南——从基础到高级特性

前言

在数字内容日益重要的今天,保护版权和标识来源变得关键。为图片添加水印有助于声明所有权、提升品牌认知度,并防止未经授权的使用。本文将介绍如何用Go语言实现图片水印,包括静态图片和带旋转、倾斜效果的文字水印,帮助您有效保护数字内容。我们将逐步解析关键步骤,确保清晰易懂。

一、准备工作

为了顺利实现图片水印功能,您需要完成以下几个准备步骤:

1.安装Go语言环境:确保您的开发环境中已经安装了Go语言,并具备基本的Go编程知识。

2.安装必要的库

  • golang.org/x/image/draw:支持高质量缩放及其他图像绘制操作。
  • github.com/disintegration/imaging:提供简便的API用于图像变换,如旋转和倾斜。

3.准备图像资源

  • 主图 (Base Image):这是您想要添加水印的原始图像。它可以是任何您有权处理的图像文件。
  • 水印图 (Watermark Image):这是将被放置在主图之上的图像,通常是一个透明背景的PNG文件,这样可以确保它不会遮挡主图的重要细节。

确保您拥有上述所有工具和资源后,就可以开始编写代码来实现图片水印功能了。接下来的章节将逐步指导您如何加载主图、应用水印图并保存最终结果。

二、图片加水印

2.1 图片水印

2.1.1 打开主图

首先,我们需要打开并读取主图文件。这一步确保了程序能够访问到用户想要处理的原始图像。

// 打开主图文件
mainImageFile, err := os.Open("main.png")
if err != nil {
	log.Fatalf("Failed to open main image: %v", err)
}
defer mainImageFile.Close()

2.1.2 解码主图

接下来,从输入流中读取原始图像并解码它。如果解码过程中出现问题,程序将返回错误信息。这里我们使用image.Decode函数自动识别图像格式。

mainImageFile, err := os.Open("main.png")
if err != nil {
	log.Fatalf("Failed to open main image: %v", err)
}
defer mainImageFile.Close()

2.1.3 打开水印图片

然后,我们需要打开水印图片文件。与主图类似,我们也需要确保能够正确读取和解码水印图像。

// 打开水印图片
watermarkImageFile, err := os.Open("logo.png") // 可以替换为其他图片文件名
if err != nil {
	log.Fatalf("Failed to open watermark image: %v", err)
}
defer watermarkImageFile.Close()

2.1.4 解码水印图片

接下来,从输入流中读取水印图像并解码它。如果解码过程中出现问题,程序将返回错误信息。这里我们再次使用image.Decode函数自动识别图像格式。

// 解码水印
watermarkImage, _, err := image.Decode(watermarkImageFile)
if err != nil {
	log.Fatalf("Failed to decode watermark image: %v", err)
}

2.1.5 计算缩放比例

为了保证水印不会过于显眼或遮挡过多内容,根据原始图像的尺寸计算水印的最大宽度和高度。通常,我们会设定最大值为原始图像宽高的25%。然后基于这些最大值计算出适当的缩放比例。

// 获取主图和水印的边界矩形
mainImageBounds := mainImage.Bounds()
watermarkImageBounds := watermarkImage.Bounds()

// 计算水印的最大尺寸
maxWatermarkWidth := int(float64(mainImageBounds.Max.X) * 0.25)  // 最大宽度为主图宽度的25%
maxWatermarkHeight := int(float64(mainImageBounds.Max.Y) * 0.25) // 最大高度为主图高度的25%

// 计算水印的缩放比例
scale := 1.0
if watermarkImageBounds.Max.X > maxWatermarkWidth || watermarkImageBounds.Max.Y > maxWatermarkHeight {
	scale = math.Min(
		float64(maxWatermarkWidth)/float64(watermarkImageBounds.Max.X),
		float64(maxWatermarkHeight)/float64(watermarkImageBounds.Max.Y),
	)
}

// 应用缩放比例
watermarkWidth := int(float64(watermarkImageBounds.Max.X) * scale)
watermarkHeight := int(float64(watermarkImageBounds.Max.Y) * scale)

2.1.6 创建新的图像

创建一个新的RGBA图像,其大小与原始图像相同,并将原始图像复制到这个新图像中。

// 创建一个新的图像,大小与主图相同
resultImage := image.NewRGBA(mainImageBounds)

// 将主图复制到新图像中
draw.Draw(resultImage, mainImageBounds, mainImage, mainImageBounds.Min, draw.Src)

2.1.7 缩放水印图像

根据前面计算的缩放比例调整水印图像的大小。我们可以使用golang.org/x/image/draw包中的draw.CatmullRom.Scale方法来进行高质量缩放。

// 创建一个用于存放缩放后水印的新图像
resizedWatermarkImage := image.NewRGBA(image.Rect(0, 0, watermarkWidth, watermarkHeight))

// 使用高质量缩放算法缩放水印图像
draw.CatmullRom.Scale(resizedWatermarkImage, resizedWatermarkImage.Bounds(), watermarkImage, watermarkImageBounds, draw.Over, nil)

2.1.8 确定水印位置

根据用户提供的参数确定水印应该放置的位置,例如左上角、右上角等。对于每个预设的位置,我们计算出相应的坐标点。这里仅给出右下角的例子:

// 引入 position 变量,并赋值为一个有效的水印位置常量
position := "left_top" // 假设使用 "left_top" 作为示例

// 计算水印放置的位置
var watermarkX, watermarkY int
switch position {
case "left_top":
	watermarkX = int(float64(mainImageBounds.Max.X) * 0.02) // 2% of the width
	watermarkY = int(float64(mainImageBounds.Max.Y) * 0.02) // 2% of the height
case "right_top":
	watermarkX = int(float64(mainImageBounds.Max.X)*0.98) - watermarkWidth // 98% of the width minus watermark width
	watermarkY = int(float64(mainImageBounds.Max.Y) * 0.02)                // 2% of the height
case "left_bottom":
	watermarkX = int(float64(mainImageBounds.Max.X) * 0.02)                 // 2% of the width
	watermarkY = int(float64(mainImageBounds.Max.Y)*0.98) - watermarkHeight // 98% of the height minus watermark height
case "right_bottom":
	watermarkX = int(float64(mainImageBounds.Max.X)*0.98) - watermarkWidth  // 98% of the width minus watermark width
	watermarkY = int(float64(mainImageBounds.Max.Y)*0.98) - watermarkHeight // 98% of the height minus watermark height
default:
	log.Fatalf("Invalid watermark position: %v", position)
}

2.1.9 绘制水印

最后,使用draw.Draw方法将调整后的水印绘制到新图像的指定位置。

// 将水印绘制到新图像的指定位置
draw.Draw(resultImage, image.Rectangle{
	Min: image.Point{X: watermarkX, Y: watermarkY},
	Max: image.Point{X: watermarkX + watermarkWidth, Y: watermarkY + watermarkHeight},
}, resizedWatermarkImage, image.Point{X: 0, Y: 0}, draw.Over)

2.1.10 绘制旋转水印(可选)

为了让水印更加多样化,可以引入旋转或倾斜的效果。这可以通过创建一个仿射变换矩阵并应用于文字图像来完成。以下是实现旋转功能的代码片段:

// 创建一个新的图像,大小与水印相同
rotatedWatermarkImage := image.NewRGBA(resizedWatermarkImage.Bounds())

// 引入 rotation 变量,并赋值为一个有效的旋转角度(度数)
rotation := 45.0 // 假设使用 45.0 度作为示例

// 计算旋转角度的弧度
radians := rotation * math.Pi / 180.0

// 计算旋转后的中心点
centerX := float64(watermarkWidth) / 2.0
centerY := float64(watermarkHeight) / 2.0

// 遍历每个像素点并应用旋转
for y := 0; y < watermarkHeight; y++ {
	for x := 0; x < watermarkWidth; x++ {
		// 将像素点转换为相对于中心点的坐标
		relX := float64(x) - centerX
		relY := float64(y) - centerY

		// 应用旋转矩阵
		newX := relX*math.Cos(radians) - relY*math.Sin(radians)
		newY := relX*math.Sin(radians) + relY*math.Cos(radians)

		// 将旋转后的坐标转换回图像坐标
		newX += centerX
		newY += centerY

		// 如果旋转后的坐标在图像范围内,则绘制像素
		if newX >= 0 && newX < float64(watermarkWidth) && newY >= 0 && newY < float64(watermarkHeight) {
			rotatedWatermarkImage.Set(int(newX), int(newY), resizedWatermarkImage.At(x, y))
		}
	}
}

// 将旋转后的水印绘制到新图像的指定位置
draw.Draw(resultImage, image.Rectangle{Min: image.Point{X: watermarkX, Y: watermarkY}, Max: image.Point{X: watermarkX + watermarkWidth, Y: watermarkY + watermarkHeight}}, rotatedWatermarkImage, image.Point{X: 0, Y: 0}, draw.Over)

2.1.11 保存结果图像

根据原始图像的格式(如PNG或JPEG),将带有水印的新图像编码并保存到内存中的缓冲区,然后再写入磁盘。

// 保存结果图像到内存
var buffer bytes.Buffer
switch fileExtension {
case ".png":
	err = png.Encode(&buffer, resultImage)
case ".jpg", ".jpeg":
	err = jpeg.Encode(&buffer, resultImage, nil)
default:
	log.Fatalf("Unsupported file extension: %v", fileExtension)
}
if err != nil {
	log.Fatalf("Failed to encode image: %v", err)
}

// 保存结果图像到文件
outputFileName := "output" + fileExtension
outputFile, err := os.Create(outputFileName)
if err != nil {
	log.Fatalf("Failed to create output file: %v", err)
}
defer outputFile.Close()

// 将内存中的图像数据写入文件
_, err = buffer.WriteTo(outputFile)
if err != nil {
	log.Fatalf("Failed to write to output file: %v", err)
}

2.1.12 完整代码和效果

package main

import (
	"bytes"
	"golang.org/x/image/draw"
	"image"
	"image/jpeg"
	"image/png"
	"log"
	"os"
	"path/filepath"
)

func main() {
	// 打开主图文件
	mainImageFile, err := os.Open("main.png")
	if err != nil {
		log.Fatalf("Failed to open main image: %v", err)
	}
	defer mainImageFile.Close()

	// 获取文件扩展名
	fileExtension := filepath.Ext(mainImageFile.Name())

	// 解码主图
	mainImage, _, err := image.Decode(mainImageFile)
	if err != nil {
		log.Fatalf("Failed to decode main image: %v", err)
	}

	// 打开水印图片
	watermarkImageFile, err := os.Open("logo.png") // 你可以将 "logo.png" 替换为 "logo.jpg" 或其他图片文件名
	if err != nil {
		log.Fatalf("Failed to open watermark image: %v", err)
	}
	defer watermarkImageFile.Close()

	// 解码水印
	watermarkImage, _, err := image.Decode(watermarkImageFile)
	if err != nil {
		log.Fatalf("Failed to decode watermark image: %v", err)
	}

	// 获取主图和水印的边界矩形
	mainImageBounds := mainImage.Bounds()
	watermarkImageBounds := watermarkImage.Bounds()

	// 计算水印的最大尺寸
	maxWatermarkWidth := int(float64(mainImageBounds.Max.X) * 0.20)  // 你可以将 "0.20" 替换为 "0.15" 或其他值
	maxWatermarkHeight := int(float64(mainImageBounds.Max.Y) * 0.20) // 你可以将 "0.20" 替换为 "0.15" 或其他值

	// 计算水印的缩放比例
	watermarkWidth := watermarkImageBounds.Max.X
	watermarkHeight := watermarkImageBounds.Max.Y

	// 计算缩放比例
	scale := 1.0
	if watermarkWidth > maxWatermarkWidth {
		scale = float64(maxWatermarkWidth) / float64(watermarkWidth)
	}
	if watermarkHeight > maxWatermarkHeight {
		if scale > float64(maxWatermarkHeight)/float64(watermarkHeight) {
			scale = float64(maxWatermarkHeight) / float64(watermarkHeight)
		}
	}

	// 应用缩放比例
	watermarkWidth = int(float64(watermarkWidth) * scale)
	watermarkHeight = int(float64(watermarkHeight) * scale)

	// 创建一个新的图像,大小与主图相同
	resultImage := image.NewRGBA(mainImageBounds)

	// 将主图复制到新图像中
	draw.Draw(resultImage, mainImageBounds, mainImage, mainImageBounds.Min, draw.Src)

	// 缩放水印图像
	resizedWatermarkImage := image.NewRGBA(image.Rect(0, 0, watermarkWidth, watermarkHeight))
	draw.NearestNeighbor.Scale(resizedWatermarkImage, resizedWatermarkImage.Bounds(), watermarkImage, watermarkImageBounds, draw.Over, nil)

	// 引入 position 变量,并赋值为一个有效的水印位置常量
	position := "left_top" // 假设使用 "left_top" 作为示例

	// 计算水印放置的位置
	var watermarkX, watermarkY int
	switch position {
	case "left_top":
		watermarkX = int(float64(mainImageBounds.Max.X) * 0.02) // 宽度的2%
		watermarkY = int(float64(mainImageBounds.Max.Y) * 0.02) // 高度的2%
	case "right_top":
		watermarkX = int(float64(mainImageBounds.Max.X)*0.98) - watermarkWidth // 宽度的98%减去水印宽度
		watermarkY = int(float64(mainImageBounds.Max.Y) * 0.02)                // 高度的2%
	case "left_bottom":
		watermarkX = int(float64(mainImageBounds.Max.X) * 0.02)                 // 宽度的2%
		watermarkY = int(float64(mainImageBounds.Max.Y)*0.98) - watermarkHeight // 高度的98%减去水印高度
	case "right_bottom":
		watermarkX = int(float64(mainImageBounds.Max.X)*0.98) - watermarkWidth  // 宽度的98%减去水印宽度
		watermarkY = int(float64(mainImageBounds.Max.Y)*0.98) - watermarkHeight // 高度的98%减去水印高度
	default:
		log.Fatalf("Invalid watermark position: %v", position)
	}

	// 将水印绘制到新图像的指定位置
	draw.Draw(resultImage, image.Rectangle{Min: image.Point{X: watermarkX, Y: watermarkY}, Max: image.Point{X: watermarkX + watermarkWidth, Y: watermarkY + watermarkHeight}}, resizedWatermarkImage, image.Point{X: 0, Y: 0}, draw.Over)

	// 保存结果图像到内存
	var buffer bytes.Buffer
	switch fileExtension {
	case ".png":
		err = png.Encode(&buffer, resultImage)
	case ".jpg", ".jpeg":
		err = jpeg.Encode(&buffer, resultImage, nil)
	default:
		log.Fatalf("Unsupported file extension: %v", fileExtension)
	}
	if err != nil {
		log.Fatalf("Failed to encode image: %v", err)
	}

	// 保存结果图像到文件
	outputFile, err := os.Create("output" + fileExtension)
	if err != nil {
		log.Fatalf("Failed to create output file: %v", err)
	}
	defer outputFile.Close() // 添加文件关闭操作

	// 将内存中的图像数据写入文件
	_, err = buffer.WriteTo(outputFile)
	if err != nil {
		log.Fatalf("Failed to write to output file: %v", err)
	}
}

2.2 文字水印

敬请期待!!!

总结

通过以上步骤,我们不仅完成了在图片上添加静态图片水印的功能实现,还增加了旋转、倾斜的水印功能,使得生成的水印更加多样化和个性化。您可以根据自己的需求进一步优化代码,比如支持更多的水印位置选项,或者允许用户上传自定义水印图片。希望这篇文章能帮助您理解和实现这一常见但非常有用的功能。如果您有任何问题或遇到困难,请随时查阅相关文档或寻求社区的帮助。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:/a/951641.html

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

国产编辑器EverEdit - 扩展脚本:关闭所有未修改文档

1 扩展脚本&#xff1a;关闭所有未修改文档 1.1 应用场景 当用户打开过多文档时&#xff0c;部分文档已经修改&#xff0c;而大部分没有修改&#xff0c;为了减少在众多已打开文档中来回跳转的不便&#xff0c;可以将没有修改的文档全部关闭&#xff0c;但目前提供的快速关闭窗…

Java Web开发进阶——Spring Security基础与应用

Spring Security是Spring框架的核心模块之一&#xff0c;用于保护Web应用程序和微服务的安全。它提供强大的认证和授权功能&#xff0c;并与Spring生态系统无缝集成。本节将详细介绍Spring Security的基础知识及其在实际项目中的应用。 1. Spring Security概述与功能 1.1 什么…

WebSocket介绍与使用

1.简介 在我们平时写的web项目中&#xff0c;大多是使用http协议&#xff0c;但是http协议是典型的一问一答的模式&#xff0c;只能由客户端向服务器发送请求&#xff0c;再由服务器返回响应&#xff0c;但实际开发中&#xff0c;很多场景都需要服务器主动发送消息给服务端&am…

PyCharm+RobotFramework框架实现UDS自动化测试——(二)RobotFramework环境配置

从0开始学习CANoe使用 从0开始学习车载测试 相信时间的力量 星光不负赶路者&#xff0c;时光不负有心人。 文章目录 1.环境准配2.Pycharm中相关配置2.1. 安装Hyper RobotFramework Support 3.脚本执行环境3.1 执行单条的配置3.2 执行全部用例配置 4.工程运行4.1 单条用例运行4.…

wireshark排除私接小路由

1.wireshark打开&#xff0c;发现了可疑地址&#xff0c;合法的地址段DHCP是192.168.100.0段的&#xff0c;打开后查看发现可疑地址段&#xff0c;分别是&#xff0c;192.168.0.1 192.168.1.174 192.168.1.1。查找到它对应的MAC地址。 ip.src192.168.1.1 2.通过show fdb p…

视频编辑最新SOTA!港中文Adobe等发布统一视频生成传播框架——GenProp

文章链接&#xff1a;https://arxiv.org/pdf/2412.19761 项目链接&#xff1a;https://genprop.github.io 亮点直击 定义了一个新的生成视频传播问题&#xff0c;目标是利用 I2V 模型的生成能力&#xff0c;将视频第一帧的各种变化传播到整个视频中。 精心设计了模型 GenProp&…

git merge与rebase区别以及实际应用

在 Git 中&#xff0c;merge 和 rebase 是两种将分支的更改合并到一起的常用方法。虽然它们都可以实现类似的目标&#xff0c;但它们的工作方式和效果有所不同。 1. Git Merge 定义&#xff1a;git merge 是将两个分支的历史合并在一起的一种操作。当你执行 git merge 时&…

HTML实战课堂之简单的拜年程序

一、目录&#xff1a; &#xfffc;&#xfffc; 一、目录&#xff1a; 二、祝福 三&#xff1a;代码讲解 &#xff08;1&#xff09;详细解释&#xff1a; 1.HTML部分 2. CSS部分 三、运行效果&#xff08;随机截图&#xff09;&#xff1a; 四、完整代码&#xff1a; 二、祝福…

Postman接口测试03|执行接口测试、全局变量和环境变量、接口关联、动态参数、断言

目录 七、Postman 1、安装 2、postman的界面介绍 八、Postman执行接口测试 1、请求页签 3、响应页签 九、Postman的环境变量和全局变量 1、创建环境变量和全局变量可以解决的问题 2、postman中的操作-全局变量 1️⃣手动设置 2️⃣代码设置 3️⃣界面获取 4️⃣代…

Linux第二课:LinuxC高级 学习记录day01

0、大纲 0.1、Linux 软件安装&#xff0c;用户管理&#xff0c;进程管理&#xff0c;shell 命令&#xff0c;硬链接和软连接&#xff0c;解压和压缩&#xff0c;功能性语句&#xff0c;结构性语句&#xff0c;分文件&#xff0c;make工具&#xff0c;shell脚本 0.2、C高级 …

python学opencv|读取图像(二十九)使用cv2.getRotationMatrix2D()函数旋转缩放图像

【1】引言 前序已经学习了如何平移图像&#xff0c;相关文章链接为&#xff1a; python学opencv|读取图像&#xff08;二十七&#xff09;使用cv2.warpAffine&#xff08;&#xff09;函数平移图像-CSDN博客 在此基础上&#xff0c;我们尝试旋转图像的同时缩放图像。 【2】…

logback日志

一、使用两个以上spring环境变量做三目操作 <springProperty name"application_name" scope"context" source"spring.application.name"/><springProperty name"trace_app_name" scope"context" source"sprin…

计算机网络 (34)可靠传输的工作原理

前言 计算机网络可靠传输的工作原理主要依赖于一系列协议和机制&#xff0c;以确保数据在传输过程中能够准确无误地到达目的地。 一、基本概念 可靠传输指的是数据链路层的发送端发送什么&#xff0c;在接收端就收到什么&#xff0c;即保证数据的完整性、正确性和顺序性。由于网…

如何用通俗易懂的方式解释大模型中的SFT,SFT过程需要大量标记的prompt和response吗?

想象你在培训一个超级助理 假设你新买了一个智能管家机器人&#xff0c;它已经看过海量的书籍和资料&#xff08;这就是预训练过程&#xff09;。但是呢&#xff0c;它还不太懂得"做人的艺术"——不知道该用什么语气说话、怎么回应你的需求。 现在你要训练它成为一…

istio-proxy oom问题排查步骤

1. 查看cluster数量 cluster数量太多会导致istio-proxy占用比较大的内存&#xff0c;此时需检查是否dr资源的host设置有配置为* 2. 查看链路数据采样率 若采样率设置过高&#xff0c;在压测时需要很大的内存来维护链路数据。可以调低采样率或增大istio-proxy内存。 检查iop中…

科研绘图系列:R语言绘制分组箱线图(boxplot)

禁止商业或二改转载,仅供自学使用,侵权必究,如需截取部分内容请后台联系作者! 文章目录 介绍加载R包数据下载导入数据数据预处理画图输出系统信息介绍 科研绘图系列:R语言绘制分组箱线图(boxplot) 加载R包 library(ggpubr) library(ggplot2) library(tidyverse) # dev…

【SpringAOP】Spring AOP 底层逻辑:切点表达式与原理简明阐述

前言 &#x1f31f;&#x1f31f;本期讲解关于spring aop的切面表达式和自身实现原理介绍~~~ &#x1f308;感兴趣的小伙伴看一看小编主页&#xff1a;GGBondlctrl-CSDN博客 &#x1f525; 你的点赞就是小编不断更新的最大动力 &am…

IOS HTTPS代理抓包工具使用教程

打开抓包软件 在设备列表中选择要抓包的 设备&#xff0c;然后选择功能区域中的 HTTPS代理抓包。根据弹出的提示按照配置文件和设置手机代理。如果是本机则会自动配置&#xff0c;只需要按照提醒操作即可。 iOS 抓包准备 通过 USB 将 iOS 设备连接到电脑&#xff0c;设备需解…

Elasticsearch:使用 Playground 与你的 PDF 聊天

LLMs作者&#xff1a;来自 Elastic Toms Mura 了解如何将 PDF 文件上传到 Kibana 并使用 Elastic Playground 与它们交互。本博客展示了在 Playground 中与 PDF 聊天的实用示例。 Elasticsearch 8.16 具有一项新功能&#xff0c;可让你将 PDF 文件直接上传到 Kibana 并使用 Pla…

ClickHouse vs StarRocks 选型对比

一、面向列存的 DBMS 新的选择 Hadoop 从诞生已经十三年了&#xff0c;Hadoop 的供应商争先恐后的为 Hadoop 贡献各种开源插件&#xff0c;发明各种的解决方案技术栈&#xff0c;一方面确实帮助很多用户解决了问题&#xff0c;但另一方面因为繁杂的技术栈与高昂的维护成本&…