golang实现图片格式转换为webp,CDN费用直接降低70%

131 阅读3分钟

由于win平台对cgo支持不是很友好,所以笔者将转换功能单独写成一个服务。可以通过docker进行快速运行,避免环境问题(PS:如果对您有帮助,欢迎star)

先看收益

由于用户在上传图片的时候不会进行压缩,所以会有一些大图片导致加载速度很慢

image.png

我们可以在上传的时候先进行图片格式转换,转换为webp格式,理论上能压缩50%-80%的体积,下面是压缩后首屏的对比。可以看到小图片基本体积减少50%,对于2MB以上的大图片,体积减少85%左右

image.png

成本收益

image.png

如何在项目中使用

转换服务image2webp使用很简单,可以通过docker进行运行

$ make help
Available commands:
  make build          - Build the Go binary
  make run            - Run the application locally
  make docker-build   - Build Docker image with default settings
  make docker-build-env - Build Docker image using .env variables
  make docker-run     - Run Docker container

# run in docker
make docker-run

# test
$ curl -X POST http://localhost:10080/v1/upload   -F "image=@test.png"
Warning: Binary output can mess up your terminal. Use "--output -" to tell 
Warning: curl to output it to your terminal anyway, or consider "--output 
Warning: <FILE>" to save to a file.

在项目中,笔者使用oss上传图片,只需要简单修改,在上传前进行压缩即可

func (*OssUploadStrategy) PutFromWeb(ctx jet.Ctx) (string, error) {
	var (
		logger = ctx.Logger()
	)

	// 获取上传的文件
	file, err := ctx.FormFile("file")
	if err != nil {
		logger.Error("Error retrieving the file", fasthttp.StatusBadRequest)
		return "", err
	}

	// 打开上传的文件
	src, err := file.Open()
	if err != nil {
		logger.Error("Error opening the file", fasthttp.StatusInternalServerError)
		return "", err
	}
	defer src.Close()

	var finalFile io.Reader

	if ossCfg.Image2WebpURI != "" {
		// 启用压缩功能
		// 先压缩图片到WebP格式
		compressedImage, err := compressImageToWebP(src, file.Filename)
		if err != nil {
			logger.Errorf("Failed to compress image: %v", err)
			// 如果压缩失败,回退到使用原始文件
			if seeker, ok := src.(io.Seeker); ok {
				seeker.Seek(0, io.SeekStart)
			}
			compressedImage = src
			file.Filename = strings.TrimSuffix(file.Filename, filepath.Ext(file.Filename)) + ".webp"
		} else {
			defer compressedImage.Close()
			file.Filename = strings.TrimSuffix(file.Filename, filepath.Ext(file.Filename)) + ".webp"
		}

		finalFile = compressedImage
	} else {
		finalFile = src
	}

	filePath := fmt.Sprintf("%s/%s", ossCfg.StoragePath, utils.GenerateFileName(file.Filename))

	request := &oss.AppendObjectRequest{
		Bucket:   oss.Ptr(ossCfg.Bucket),
		Key:      oss.Ptr(filePath),
		Position: oss.Ptr(int64(0)),
		Body:     finalFile,
	}

	result, err := client.AppendObject(todoCtx, request)
	if err != nil {
		logger.Errorf("AppendObject ERROR:%v", err)
		return "", err
	}

	logger.Infof("append object result:%#v\n", result)

	return BuildURL(filePath), nil
}

var (
	compressImageLogger = xlog.NewWith("compressImageLogger")
)

// compressImageToWebP 调用WebP转换服务压缩图片
func compressImageToWebP(src multipart.File, filename string) (io.ReadCloser, error) {
	defer utils.TraceElapsedWithPrefix(compressImageLogger, "compressImageLogger")()
	// 重置文件读取位置
	if seeker, ok := src.(io.Seeker); ok {
		seeker.Seek(0, io.SeekStart)
	}

	// 读取文件内容到内存
	fileData, err := io.ReadAll(src)
	if err != nil {
		return nil, fmt.Errorf("failed to read file: %w", err)
	}

	// 创建multipart表单数据
	body := &bytes.Buffer{}
	writer := multipart.NewWriter(body)

	// 创建文件字段
	part, err := writer.CreateFormFile("image", filename)
	if err != nil {
		return nil, fmt.Errorf("failed to create form file: %w", err)
	}

	// 写入文件数据
	if _, err := part.Write(fileData); err != nil {
		return nil, fmt.Errorf("failed to write file data: %w", err)
	}

	// 关闭writer
	if err := writer.Close(); err != nil {
		return nil, fmt.Errorf("failed to close writer: %w", err)
	}

	// 创建HTTP请求
	req, err := http.NewRequest("POST", ossCfg.Image2WebpURI, body)
	if err != nil {
		return nil, fmt.Errorf("failed to create request: %w", err)
	}

	// 设置请求头
	req.Header.Set("Content-Type", writer.FormDataContentType())
	req.Header.Set("Accept", "*/*")
	req.Header.Set("Connection", "keep-alive")

	// 设置超时
	httpClient := &http.Client{
		Timeout: 30 * time.Second,
	}

	// 发送请求
	resp, err := httpClient.Do(req)
	if err != nil {
		return nil, fmt.Errorf("failed to send request to WebP service: %w", err)
	}

	// 检查响应状态
	if resp.StatusCode != http.StatusOK {
		resp.Body.Close()
		return nil, fmt.Errorf("WebP service returned non-200 status: %d", resp.StatusCode)
	}

	// 返回响应体(WebP图片数据)
	return resp.Body, nil
}

常见图片格式分析

常见的图片格式有:jpg、png、gif、psd、tif、bmp等格式

当然现在也出现了一些新的流行的格式,例如:WebP(2010年发布)、avif(2019年发布)

其中webp是现在支持性最好且压缩率最高的图片格式,常见对比如下:

对比如下:

  • webp对比gif格式能节省大约88%以上的空间
  • 相比png可以减少98%以上的空间
  • 相比jpg可以减少35%的空间
  • 平均减少 70%

如果相对webp格式更加了解的可以看下面的文章

image.png