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

news2025/1/12 0:55:44

前言

在数字内容日益重要的今天,保护版权和标识来源变得关键。为图片添加水印有助于声明所有权、提升品牌认知度,并防止未经授权的使用。本文将介绍如何用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 文字水印

敬请期待!!!

总结

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

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

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

PyQt5 UI混合开发,控件的提升

PromoteLabelTest.py 提升的类 import sys from PyQt5.QtWidgets import QApplication, QWidget,QVBoxLayout,QTextEdit,QPushButton,QHBoxLayout,QFileDialog,QLabelclass PromoteLabel(QLabel):def __init__(self,parent None):super().__init__(parent)self.setText("…

CI/CD 流水线

CI/CD 流水线 CI 与 CD 的边界CI 持续集成CD&#xff08;持续交付/持续部署&#xff09;自动化流程示例&#xff1a; Jenkins 引入到 CI/CD 流程在本地或服务器上安装 Jenkins。配置 Jenkins 环境流程设计CI 阶段&#xff1a;Jenkins 流水线实现CD 阶段&#xff1a;Jenkins 流水…

ROS核心概念解析:从Node到Master,再到roslaunch的全面指南

Node 在ROS中&#xff0c;最小的进程单元就是节点&#xff08;node&#xff09;。一个软件包里可以有多个可执行文件&#xff0c;可执行文件在运行之后就成了一个进程(process)&#xff0c;这个进程在ROS中就叫做节点。 从程序角度来说&#xff0c;node就是一个可执行文件&…

深入Android架构(从线程到AIDL)_22 IPC的Proxy-Stub设计模式04

目录 5、 谁来写Proxy及Stub类呢? 如何考虑人的分工 IA接口知识取得的难题 在编程上&#xff0c;有什么技术可以实现这个方法&#xff1f; 范例 5、 谁来写Proxy及Stub类呢? -- 强龙提供AIDL工具&#xff0c;给地头蛇产出Proxy和Stub类 如何考虑人的分工 由框架开发者…

风水算命系统架构与功能分析

系统架构 服务端&#xff1a;Java&#xff08;最低JDK1.8&#xff0c;支持JDK11以及JDK17&#xff09;数据库&#xff1a;MySQL数据库&#xff08;标配5.7版本&#xff0c;支持MySQL8&#xff09;ORM框架&#xff1a;Mybatis&#xff08;集成通用tk-mapper&#xff0c;支持myb…

551 灌溉

常规解法&#xff1a; #include<bits/stdc.h> using namespace std; int n,m,k,t; const int N105; bool a[N][N],b[N][N]; int cnt; //设置滚动数组来存贮当前和下一状态的条件 //处理传播扩散问题非常有效int main() {cin>>n>>m>>t;for(int i1;i&l…

HDFS编程 - 使用HDFS Java API进行文件操作

文章目录 前言一、创建hdfs-demo项目1. 在idea上创建maven项目2. 导入hadoop相关依赖 二、常用 HDFS Java API1. 简介2. 获取文件系统实例3. 创建目录4. 创建文件4.1 创建文件并写入数据4.2 创建新空白文件 5. 查看文件内容6. 查看目录下的文件或目录信息6.1 查看指定目录下的文…

Java面试题~~

深拷贝和浅拷贝区别了解吗?什么是引用拷贝? 关于深拷贝和浅拷贝区别&#xff0c;我这里先给结论&#xff1a; 浅拷贝&#xff1a;浅拷贝会在堆上创建一个新的对象&#xff08;区别于引用拷贝的一点&#xff09;&#xff0c;不过&#xff0c;如果原对象内部的属性是引用类型的…

el-table 自定义表头颜色

第一种方法&#xff1a;计算属性 <template><div><el-table:data"formData.detail"border stripehighlight-current-row:cell-style"{ text-align: center }":header-cell-style"headerCellStyle"><el-table-column fixed…

MySQL笔记大总结20250108

Day2 1.where (1)关系运算符 select * from info where id>1; select * from info where id1; select * from info where id>1; select * from info where id!1;(2)逻辑运算符 select * from info where name"吴佩奇" and age19; select * from info wh…

精选2款.NET开源的博客系统

前言 博客系统是一个便于用户创建、管理和分享博客内容的在线平台&#xff0c;今天大姚给大家分享2款.NET开源的博客系统。 StarBlog StarBlog是一个支持Markdown导入的开源博客系统&#xff0c;后端基于最新的.Net6和Asp.Net Core框架&#xff0c;遵循RESTFul接口规范&…

SEO内容优化:如何通过用户需求赢得搜索引擎青睐?

在谷歌SEO优化中&#xff0c;内容一直是最重要的因素之一。但要想让内容真正发挥作用&#xff0c;关键在于满足用户需求&#xff0c;而不是简单地堆砌关键词。谷歌的算法越来越智能化&#xff0c;更注重用户体验和内容的实用性。 了解目标用户的需求。通过工具如Google Trends…

Spring——自动装配

假设一个场景&#xff1a; 一个人&#xff08;Person&#xff09;有一条狗&#xff08;Dog&#xff09;和一只猫(Cat)&#xff0c;狗和猫都会叫&#xff0c;狗叫是“汪汪”&#xff0c;猫叫是“喵喵”&#xff0c;同时人还有一个自己的名字。 将上述场景 抽象出三个实体类&…

计算机网络(三)——局域网和广域网

一、局域网 特点&#xff1a;覆盖较小的地理范围&#xff1b;具有较低的时延和误码率&#xff1b;使用双绞线、同轴电缆、光纤传输&#xff0c;传输效率高&#xff1b;局域网内各节点之间采用以帧为单位的数据传输&#xff1b;支持单播、广播和多播&#xff08;单播指点对点通信…

错误的类文件: *** 类文件具有错误的版本 61.0, 应为 52.0 请删除该文件或确保该文件位于正确的类路径子目录中

一、问题 用maven对一个开源项目打包时&#xff0c;遇到了“错误的类文件: *** 类文件具有错误的版本 61.0, 应为 52.0 请删除该文件或确保该文件位于正确的类路径子目录中。”&#xff1a; 二、原因 原因是当前java环境是Java 8&#xff08;版本52.0&#xff09;&#xff0c;但…

【大模型入门指南 07】量化技术浅析

【大模型入门指南】系列文章&#xff1a; 【大模型入门指南 01】深度学习入门【大模型入门指南 02】LLM大模型基础知识【大模型入门指南 03】提示词工程【大模型入门指南 04】Transformer结构【大模型入门指南 05】LLM技术选型【大模型入门指南 06】LLM数据预处理【大模型入门…

在线工具箱源码优化版

在线工具箱 前言效果图部分源码源码下载部署教程下期更新 前言 来自缤纷彩虹天地优化后的我爱工具网源码&#xff0c;百度基本全站收录&#xff0c;更能基本都比较全&#xff0c;个人使用或是建站都不错&#xff0c;挑过很多工具箱&#xff0c;这个比较简洁&#xff0c;非常实…

@LocalBuilder装饰器: 维持组件父子关系

一、前言 当开发者使用Builder做引用数据传递时&#xff0c;会考虑组件的父子关系&#xff0c;使用了bind(this)之后&#xff0c;组件的父子关系和状态管理的父子关系并不一致。为了解决组件的父子关系和状态管理的父子关系保持一致的问题&#xff0c;引入LocalBuilder装饰器。…

C 语言内存探秘:数据存储的字节密码

文章目录 一、数据在内存中的存储1、基本数据类型存储2、数组存储3、结构体存储1、基本存储规则2、举例说明3、查看结构体大小和成员偏移量的方法 二、大小端字节序三、字节序的判断 一、数据在内存中的存储 1、基本数据类型存储 整型&#xff1a;如int类型&#xff0c;通常在…

双因素身份验证技术在NPI区域邮件安全管控上的解决思路

在制造业中&#xff0c;NPI&#xff08;New Product Introduction&#xff0c;新产品导入&#xff09;区域是指专门负责新产品从概念到市场推出全过程的部门或团队。NPI 的目标是确保新产品能够高效、高质量地投入生产&#xff0c;并顺利满足市场需求。在支撑企业持续创新和竞争…