Loading

go-Typora-Sqoosh-图像压缩-Github-图床

go-Typora-Sqoosh-图像压缩-Github-图床

概述

这是一个使用情景

当在Typora中插入图片时:

  1. Sqoosh-图像压缩(压缩后的图片保存在用户的upPic目录下,按照日期归档)
  2. Github-上传图片(使用加速:cdn.staticaly.com)
  3. 最后返回图片的url

PS:Typora上传图片的程序需要支持多图片上传,但是实际使用中当一次复制n张图片到Typora时,它会同时起n个进程去做上传;但是这里选择的是上传Github,需要保证一张图片上传完成后再继续后续动作,So...

Squoosh

Squoosh 是谷歌推出的一款开源的,专门用来压缩图片的在线服务,支持 JPG、PNG、WebP 等格式的极限压缩,以及格式转换剪裁等功能,非常适合青小蛙这样经常一张一张处理图片的用户。但默认提供的网页版本并不支持批量处理,不过好在 Squoosh 拥有 CLI 命令行版本(支持 Windows、macOS、Linux),可以非常方便的批量处理图片。

正常情况来讲,直接打开 https://squoosh.app/ 然后将图片拖拽上去,就能下载压缩好的图片了。但如果需要批量,就需要命令行版本了。

Squoosh-CLI安装

Squoosh-CLI(https://github.com/GoogleChromeLabs/squoosh/tree/dev/cli) 是 Squoosh 的命令行版本,需要先在电脑上安装 Node.js,然后再安装 Squoosh-CLI:

Node.js

下载并安装 Node.js:https://nodejs.org/zh-cn/

配置国内源

# 查看registry配置,默认是这个 https://registry.npmjs.org/
npm config get registry 
https://registry.npmjs.org/

# 换上 阿里巴巴开源镜像站-OPSX镜像站里的淘宝 NPM 镜像
npm config set registry https://registry.npmmirror.com

# 查看是否更换
npm config get registry
https://registry.npmmirror.com/

# 起飞

Squoosh-CLI

安装 Squoosh-CLI:

# 需要在终端,或者命令提示符中使用
npm i -g @squoosh/cli

然后,就能正常使用了~

C:\Users\luoxian>squoosh-cli --help
Usage: squoosh-cli [options] <files...>

Options:
  -d, --output-dir <dir>                                 Output directory (default: ".")
  -s, --suffix <suffix>                                  Append suffix to output files (default: "")
  --max-optimizer-rounds <rounds>                        Maximum number of compressions to use for auto optimizations (default: "6")
  --optimizer-butteraugli-target <butteraugli distance>  Target Butteraugli distance for auto optimizer (default: "1.4")
  --resize [config]                                      Resize the image before compressing
  --quant [config]                                       Reduce the number of colors used (aka. paletting)
  --rotate [config]                                      Rotate image
  --mozjpeg [config]                                     Use MozJPEG to generate a .jpg file with the given configuration
  --webp [config]                                        Use WebP to generate a .webp file with the given configuration
  --avif [config]                                        Use AVIF to generate a .avif file with the given configuration
  --jxl [config]                                         Use JPEG-XL to generate a .jxl file with the given configuration
  --wp2 [config]                                         Use WebP2 to generate a .wp2 file with the given configuration
  --oxipng [config]                                      Use OxiPNG to generate a .png file with the given configuration
  -h, --help                                             display help for command

Squoosh-CLI使用

简单使用

同样要在终端,或者命令提示符中使用:

squoosh-cli --mozjpeg auto ./ -d ./output

上述的意思是使用 MozJPEG 的自动优化功能来压缩图片,并转换为 jpg 格式:

Squoosh-CLI - 批量图片压缩、批量图片格式转换、批量图片剪裁

然后就会在原文件夹的 output 目录里,获得压缩后的 jpg 文件了。去掉 -d 参数会保存压缩后的文件至原目录。

进阶转换

Squoosh.app 其实直接提供了复制命令行的按钮,只需要先调整好参数,然后按下这个按钮,就可以了:

Squoosh-CLI - 批量图片压缩、批量图片格式转换、批量图片剪裁

注意:这里有个坑,复制出来的参数直接用会报错,需要把 ' 和 " 给全部去掉才可以。

指定图片压缩

# 指定图片压缩
squoosh-cli --mozjpeg {quality:75,baseline:false,arithmetic:false,progressive:true,optimize_coding:true,smoothing:0,color_space:3,quant_table:3,trellis_multipass:false,trellis_opt_zero:false,trellis_opt_table:false,trellis_loops:1,auto_subsample:true,chroma_subsample:2,separate_chroma_quality:false,chroma_quality:80} "C:\Users\luoxian\Pictures\Snipaste_2022-11-28_00-40-09.png" -d "D:\Squoosh\pictures"

当前目录批量压缩

# 目录批量压缩
squoosh-cli --mozjpeg {quality:75,baseline:false,arithmetic:false,progressive:true,optimize_coding:true,smoothing:0,color_space:3,quant_table:3,trellis_multipass:false,trellis_opt_zero:false,trellis_opt_table:false,trellis_loops:1,auto_subsample:true,chroma_subsample:2,separate_chroma_quality:false,chroma_quality:75} ./ -d ./output

在命令的最后面添加:./ 意思是压缩当前路径下的所有图片,如:

npx @squoosh/cli --resize '{"enabled":true,"width":1238,"height":841,"method":"lanczos3","fitMethod":"stretch","premultiply":true,"linearRGB":true}' --mozjpeg '{"quality":75,"baseline":false,"arithmetic":false,"progressive":true,"optimize_coding":true,"smoothing":0,"color_space":3,"quant_table":3,"trellis_multipass":false,"trellis_opt_zero":false,"trellis_opt_table":false,"trellis_loops":1,"auto_subsample":true,"chroma_subsample":2,"separate_chroma_quality":false,"chroma_quality":75}' ./

在 macOS、Linux 下可以使用 *.jpg 来单独处理某一类型的图片,但 Windows 下对于 * 的处理有问题,目前仍未解决,只能使用 ./

然后就愉快的去转换图片吧。目前小众软件的图片压缩均由 Squoosh 处理。

Github-API

读取图片路径 => 文件内容转换成base64 => 上传文件

测试API-Create or update file contents

Creates a new file or replaces an existing file in a repository. You must authenticate using an access token with the workflow scope to use this endpoint.

Note: If you use this endpoint and the "Delete a file" endpoint in parallel, the concurrent requests will conflict and you will receive errors. You must use these endpoints serially instead.

相关参数

Headers:请求头

  • acceptstring:Setting to application/vnd.github+json is recommended.

Path parameters:URL请求参数

  • ownerstringRequired:The account owner of the repository. The name is not case sensitive.
  • repostringRequired:The name of the repository. The name is not case sensitive.
  • pathstringRequired:path parameter

Body parameters:请求体

  • messagestringRequired:The commit message.
  • contentstringRequired:The new file content, using Base64 encoding.
  • shastring:Required if you are updating a file. The blob SHA of the file being replaced.
  • branchstring:The branch name. Default: the repository’s default branch (usually master)
  • committerobject:The person that committed the file. Default: the authenticated user.
  • Properties of committer
    • namestringRequired:The name of the author or committer of the commit. You'll receive a 422 status code if name is omitted.
    • emailstringRequired:The email of the author or committer of the commit. You'll receive a 422 status code if email is omitted.
    • datestring
  • authorobject:The author of the file. Default: The committer or the authenticated user if you omit committer.
  • Properties of author
    • namestringRequired:The name of the author or committer of the commit. You'll receive a 422 status code if name is omitted.
    • emailstringRequired:The email of the author or committer of the commit. You'll receive a 422 status code if email is omitted.
    • datestring

HTTP 响应状态代码

状态代码 说明
200 OK
302 Found
403 Forbidden
404 Resource not found

请求示例

Request

PUT` `/repos/{owner}/{repo}/contents/{path}
curl \
  -X PUT \
  -H "Accept: application/vnd.github+json" \
  -H "Authorization: Bearer <YOUR-TOKEN>" \
  https://api.github.com/repos/OWNER/REPO/contents/PATH \
  -d '{"message":"my commit message","committer":{"name":"Monalisa Octocat","email":"octocat@github.com"},"content":"bXkgbmV3IGZpbGUgY29udGVudHM="}'

Response

Status: 201
{
  "content": {
    "name": "hello.txt",
    "path": "notes/hello.txt",
    "sha": "95b966ae1c166bd92f8ae7d1c313e738c731dfc3",
    "size": 9,
    "url": "https://api.github.com/repos/octocat/Hello-World/contents/notes/hello.txt",
    "html_url": "https://github.com/octocat/Hello-World/blob/master/notes/hello.txt",
    "git_url": "https://api.github.com/repos/octocat/Hello-World/git/blobs/95b966ae1c166bd92f8ae7d1c313e738c731dfc3",
    "download_url": "https://raw.githubusercontent.com/octocat/HelloWorld/master/notes/hello.txt",
    "type": "file",
    "_links": {
      "self": "https://api.github.com/repos/octocat/Hello-World/contents/notes/hello.txt",
      "git": "https://api.github.com/repos/octocat/Hello-World/git/blobs/95b966ae1c166bd92f8ae7d1c313e738c731dfc3",
      "html": "https://github.com/octocat/Hello-World/blob/master/notes/hello.txt"
    }
  },
  "commit": {
    "sha": "7638417db6d59f3c431d3e1f261cc637155684cd",
    "node_id": "MDY6Q29tbWl0NzYzODQxN2RiNmQ1OWYzYzQzMWQzZTFmMjYxY2M2MzcxNTU2ODRjZA==",
    "url": "https://api.github.com/repos/octocat/Hello-World/git/commits/7638417db6d59f3c431d3e1f261cc637155684cd",
    "html_url": "https://github.com/octocat/Hello-World/git/commit/7638417db6d59f3c431d3e1f261cc637155684cd",
    "author": {
      "date": "2014-11-07T22:01:45Z",
      "name": "Monalisa Octocat",
      "email": "octocat@github.com"
    },
    "committer": {
      "date": "2014-11-07T22:01:45Z",
      "name": "Monalisa Octocat",
      "email": "octocat@github.com"
    },
    "message": "my commit message",
    "tree": {
      "url": "https://api.github.com/repos/octocat/Hello-World/git/trees/691272480426f78a0138979dd3ce63b77f706feb",
      "sha": "691272480426f78a0138979dd3ce63b77f706feb"
    },
    "parents": [
      {
        "url": "https://api.github.com/repos/octocat/Hello-World/git/commits/1acc419d4d6a9ce985db7be48c6349a0475975b5",
        "html_url": "https://github.com/octocat/Hello-World/git/commit/1acc419d4d6a9ce985db7be48c6349a0475975b5",
        "sha": "1acc419d4d6a9ce985db7be48c6349a0475975b5"
      }
    ],
    "verification": {
      "verified": false,
      "reason": "unsigned",
      "signature": null,
      "payload": null
    }
  }
}

代码-go-Typora-squoosh-cli-github

upPic.exe:https://luoxian.lanzoum.com/ivUaa0j2lkng

Usage: upPic.exe token domain owner repo branch repoPath file...
Examp: upPic.exe xxxxxxxxx https://cdn.staticaly.com/gh/ want-u pictures master post-test test1.jpg
Examp: upPic.exe xxxxxxxxx https://cdn.staticaly.com/gh/ want-u pictures master post-test test1.jpg test2.jpg

由于编译的时候去掉CMD窗口了,所以日志需要到log文件去看:

Win + r运行窗口:%USERPROFILE%\upPic

package main

import (
	"bytes"
	"encoding/base64"
	"encoding/json"
	"fmt"
	"io"
	"log"
	"net/http"
	"os"
	"os/exec"
	"path/filepath"
	"strings"
	"syscall"
	"time"
)

// 定义全局变量
var (
	GithubApiUrl = "https://api.github.com/repos/"          // Github api
	Timestemp    = time.Now().Format("2006-01-02 15:04:05") // 时间
	DatePath     = time.Now().Format("2006-01-02")          // 日期路径

	UpPicDirPath = createUpPicDir()
	UpPicPidPath = UpPicDirPath + "upPic.pid"                  // pid文件
	UpPicLogPath = UpPicDirPath + "upPic.log"                  // 日志路径
	SquooshPATH  = UpPicDirPath + "pictures/" + DatePath + "/" // 压缩图片路径

	Client = &http.Client{}                                                  // 创建一个客户端
	Logger = log.New(os.Stdout, "[LOG]", log.Lshortfile|log.Ldate|log.Ltime) // 日志格式
)

// 用户家目录 - 返回创建的upPic目录
func createUpPicDir() (upPicDir string) {
	s, e := os.UserHomeDir()
	if e != nil {
		Logger.Println("os.UserHomeDir() err:", e)
		return
	}
	upPicDir = s + "/upPic/"
	e = os.MkdirAll(upPicDir, 0755)
	if e != nil {
		Logger.Println("os.MkdirAll(upPicDir, 0755)", e)
		return
	}
	return
}

// 定义github相关参数
type GitConf struct {
	apiUrl   string // Request URL "https://api.github.com/repos/"
	owner    string // 账户
	repo     string // 仓库
	branch   string // branch
	repoPath string // 仓库路径
	domain   string // cdn域名
	token    string // token
}

// 定义github请求的结构体
type GitRequest struct {
	Committer map[string]string `json:"committer"` // 提交用户信息
	Branch    string            `json:"branch"`    // 提交分支
	Message   string            `json:"message"`   // 提交信息
	Content   string            `json:"content"`   // 文件内容, 要用 base64 编码
}

// 定义GitResponse中Content的一个字段 path(文件上传的路径)
type C struct {
	Path string `json:"path"` // content-path
}

// 定义github的返回结构体
type GitResponse struct {
	Message string                 `json:"message"` // 请求失败的消息
	Content C                      `json:"content"` // 请求成功的内容
	Commit  map[string]interface{} `json:"commit"`  // 请求成功的commit
}

// 遍历文件列表,上传文件,返回文件url
func (gf *GitConf) putPics(picSlice []string) {
	// 遍历文件列表
	for _, v := range picSlice {
		// 压缩图片
		if strings.HasSuffix(v, ".jpg") || strings.HasSuffix(v, ".png") || strings.HasSuffix(v, ".webp") {
			e := gf.squoosh(&v)
			if e != nil {
				Logger.Println(" squoosh压缩失败,尝试原图上传")
			} else {
				v = SquooshPATH + strings.Replace(filepath.Base(v), filepath.Ext(v), ".jpg", -1)
				Logger.Println(" squoosh压缩成功,压缩后路径:", v)
			}
		}
		// 上传文件,返回文件url
		gf.putOne(&v)
	}
}

// squoosh 压缩
func (gf *GitConf) squoosh(pic *string) (err error) {
	args := "{quality:75,baseline:false,arithmetic:false,progressive:true,optimize_coding:true,smoothing:0,color_space:3,quant_table:3,trellis_multipass:false,trellis_opt_zero:false,trellis_opt_table:false,trellis_loops:1,auto_subsample:true,chroma_subsample:2,separate_chroma_quality:false,chroma_quality:80}"
	cmd := exec.Command("cmd")
	cmd.SysProcAttr = &syscall.SysProcAttr{CmdLine: fmt.Sprintf(`/c "squoosh-cli --mozjpeg %q %q -d %q"`, args, *pic, SquooshPATH), HideWindow: true}
	if err = cmd.Run(); err != nil {
		fmt.Println("squoosh(pic *string) err:", err)
		return
	}
	return
}

// 上传文件函数
func (gf *GitConf) putOne(pic *string) {
	// 读取文件
	fileByte, err := os.ReadFile(*pic)
	if err != nil {
		Logger.Println("打开文件失败:", err)
		return
	}

	// 发起请求
	code, body, _ := gf.putRequest(fileByte, pic)
	// Logger.Println(string(body))
	// 序列化响应体
	var giteeResponse GitResponse
	err = json.Unmarshal(body, &giteeResponse)
	if err != nil {
		Logger.Println("序列化响应体失败:", err)
		return
	}
	if code == 422 { // 422 ==> Validation failed, or the endpoint has been spammed.
		Logger.Println("请求失败! 文件已存在或验证失败,响应码:", code, giteeResponse.Message)
		fmt.Println("请求失败! 文件已存在或验证失败,响应码:", code, giteeResponse.Message)
		return
	}
	if code != 201 { // 如果状态码不是201,就是响应错误 201 ==> Created
		Logger.Println("请求失败! 响应码:", code, giteeResponse.Message)
		fmt.Println("请求失败! 响应码:", code, giteeResponse.Message)
		return
	}
	// https://gitee.com/luoxian1011/pictures/raw/master/pic.test
	path := giteeResponse.Content.Path
	if !strings.HasSuffix(gf.domain, "/") {
		gf.domain += "/"
	}
	ImageUrl := gf.domain + gf.owner + "/" + gf.repo + "/" + gf.branch + "/" + path
	Logger.Println("Upload Success:", ImageUrl)
	// 输出文件url
	fmt.Println(ImageUrl)
}

// PUT请求
func (gf *GitConf) putRequest(fileByte []byte, pic *string) (code int, body []byte, err error) {
	// 创建GitRequest
	g := &GitRequest{}
	g.Branch = gf.branch
	g.Committer = map[string]string{
		"name":  "upPic",
		"email": "luoxian1011@163.com",
	}
	// content "base64编码后的字符串"
	g.Content = base64.StdEncoding.EncodeToString(fileByte)
	// message "Upload 文件名 by upPic" - 添加日期为前缀路径
	path := DatePath + "/" + filepath.Base(*pic)
	g.Message = "Upload " + filepath.Base(*pic) + " by upPic - " + Timestemp
	// url "https://api.github.com/repos/OWNER/REPO/contents/PATH"
	postUrl := gf.apiUrl + gf.owner + "/" + gf.repo + "/contents/" + strings.Trim(gf.repoPath, "/") + "/" + path
	Logger.Println("postUrl:", postUrl)
	// 序列化请求参数
	data, err := json.Marshal(g)
	if err != nil {
		Logger.Println("请求数据序列化失败:", err)
		return
	}
	// 构造请求
	req, err := http.NewRequest("PUT", postUrl, bytes.NewReader(data))
	if err != nil {
		Logger.Println("http.NewRequest err:", err)
		return
	}
	// 添加请求头
	req.Header.Add("Accept", "application/vnd.github+json")
	req.Header.Add("Authorization", "Bearer "+gf.token)
	// 开始上传文件
	// response, err := client.Post(postUrl, contentType, bytes.NewReader(data))
	response, err := Client.Do(req)
	if err != nil {
		Logger.Println("上传文件失败:", err)
		return
	}
	defer response.Body.Close() // 关闭
	code = response.StatusCode
	body, err = io.ReadAll(response.Body)
	if err != nil {
		Logger.Println("读取响应失败! 响应码:", response.StatusCode, err)
		return
	}
	return
}

// 初始化日志,并在程序启动时检查PID文件
func init() {
	// 日志输出文件路径 - ./main.log
	logFile, err := os.OpenFile(UpPicLogPath, os.O_CREATE|os.O_WRONLY|os.O_APPEND, 0775)
	if err != nil {
		Logger.Println("open log file failed, err:", err)
		return
	}
	Logger.SetOutput(logFile)

	for i := 0; i < 100; i++ {
		// 检查pid文件
		_, err = os.Stat(UpPicPidPath)
		if os.IsNotExist(err) { // 文件不存在在创建并跳出检查
			// 创建PID文件,如果已存则继续睡
			f, err := os.OpenFile(UpPicPidPath, os.O_CREATE|os.O_EXCL, 0666)
			if err != nil {
				Logger.Println(" os.Create(PidPaht) err:", err)
				time.Sleep(time.Second)
				continue
			}
			Logger.Println("os.Create(PidPaht) ok, started.")
			defer f.Close()
			break
		} else {
			// 文件存在则睡眠1s继续检查
			Logger.Println(" PID文件已存在,等待中...")
			time.Sleep(time.Second)
		}
	}
}

func main() {
	// upPic

	// 延时删除pid文件
	defer func() {
		err := os.Remove(UpPicPidPath)
		if err != nil {
			Logger.Println("os.Remove(PidPaht) err:", err)
		} else {
			Logger.Println("os.Remove(PidPaht) ok, done.")
		}
	}()

	// 命令行参数: 从第7个参数开始传入文件路径
	argsLen := len(os.Args)
	reqArg := 7
	if argsLen <= reqArg {
		Logger.Println("参数输入有误:")
		Logger.Println("Usage: upPic.exe token domain owner repo branch repoPath file...")
		Logger.Println("Examp: upPic.exe xxxxxxxxx https://cdn.staticaly.com/gh/ want-u pictures master post-test test1.jpg")
		Logger.Println("Examp: upPic.exe xxxxxxxxx https://cdn.staticaly.com/gh/ want-u pictures master post-test test1.jpg test2.jpg")
		return
	}

	// 创建请求结构体
	gitConf := &GitConf{
		token:    os.Args[1],
		domain:   os.Args[2],
		owner:    os.Args[3],
		repo:     os.Args[4],
		branch:   os.Args[5],
		repoPath: os.Args[6],
		apiUrl:   GithubApiUrl,
	}

	// 拿到文件路径切片
	picSlice := os.Args[reqArg:]

	// 上传图片
	gitConf.putPics(picSlice)
}

执行命令

D:\GoProject>go run "d:\GoProject\upPic\upPic-v2.go" ghp_2Yq9EAIGRPc63EsnVF1EN7FOqB1vv14QPnTP https://cdn.staticaly.com/gh/ want-u pictures master post-test "C:\Users\luoxian\Pictures\City Layer.png"
https://cdn.staticaly.com/gh/want-u/pictures/master/post-test/2022-11-28/City Layer.jpg

typora-Custom Command

image-20221121021251418

  • 上传服务:Custom Command
  • 命令:D:\GoProject\upPic\upPic.exe ghp_2Yq9EAIGRPc63EsnVF1EN7FOqB1vv14QPnTP https://cdn.staticaly.com/gh/ want-u pictures master post-test

验证图片上传

...

posted @ 2022-12-20 17:08  luoxian  阅读(314)  评论(0编辑  收藏  举报