go-Typora-Sqoosh-图像压缩-Github-图床
go-Typora-Sqoosh-图像压缩-Github-图床
- Squoosh
- GoogleChromeLabs/squoosh: Make images smaller using best-in-class codecs, right in the browser. (github.com)
- https://squoosh-desktop.vercel.app
- Squoosh-CLI - 批量图片压缩、批量图片格式转换、批量图片剪裁 - 小众软件 (appinn.com)
- 免费开源图片压缩工具 Squoosh 离线版 - 小众软件 (appinn.com)
- 仓库内容 - GitHub Docs
概述
这是一个使用情景
当在Typora中插入图片时:
- Sqoosh-图像压缩(压缩后的图片保存在用户的upPic目录下,按照日期归档)
- Github-上传图片(使用加速:cdn.staticaly.com)
- 最后返回图片的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 格式:
然后就会在原文件夹的 output 目录里,获得压缩后的 jpg 文件了。去掉 -d 参数会保存压缩后的文件至原目录。
进阶转换
Squoosh.app 其实直接提供了复制命令行的按钮,只需要先调整好参数,然后按下这个按钮,就可以了:
注意:这里有个坑,复制出来的参数直接用会报错,需要把 ' 和 " 给全部去掉才可以。
指定图片压缩
# 指定图片压缩
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:请求头
accept
string:Setting toapplication/vnd.github+json
is recommended.
Path parameters:URL请求参数
owner
stringRequired:The account owner of the repository. The name is not case sensitive.repo
stringRequired:The name of the repository. The name is not case sensitive.path
stringRequired:path parameter
Body parameters:请求体
message
stringRequired:The commit message.content
stringRequired:The new file content, using Base64 encoding.sha
string:Required if you are updating a file. The blob SHA of the file being replaced.branch
string:The branch name. Default: the repository’s default branch (usuallymaster
)committer
object:The person that committed the file. Default: the authenticated user.- Properties of
committer
name
stringRequired:The name of the author or committer of the commit. You'll receive a422
status code ifname
is omitted.email
stringRequired:The email of the author or committer of the commit. You'll receive a422
status code ifemail
is omitted.date
string
author
object:The author of the file. Default: Thecommitter
or the authenticated user if you omitcommitter
.- Properties of
author
name
stringRequired:The name of the author or committer of the commit. You'll receive a422
status code ifname
is omitted.email
stringRequired:The email of the author or committer of the commit. You'll receive a422
status code ifemail
is omitted.date
string
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
- 上传服务:Custom Command
- 命令:D:\GoProject\upPic\upPic.exe ghp_2Yq9EAIGRPc63EsnVF1EN7FOqB1vv14QPnTP https://cdn.staticaly.com/gh/ want-u pictures master post-test
验证图片上传
...