Skip to content

Commit

Permalink
图片元数据
Browse files Browse the repository at this point in the history
  • Loading branch information
movsb committed Jul 2, 2024
1 parent f0d40e3 commit c705051
Show file tree
Hide file tree
Showing 12 changed files with 229 additions and 2 deletions.
1 change: 1 addition & 0 deletions Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ RUN apk add ca-certificates
RUN apk add sqlite
# for /etc/mime.types
RUN apk add mailcap
RUN apk add exiftool

WORKDIR /workspace
COPY --from=katex /katex/a.out katex
Expand Down
4 changes: 3 additions & 1 deletion gateway/handlers/features/FEATURES.md
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@
* 格式:SVG
* 样式:日间&夜间自动切换
* 数学公式
* 服务端渲染
* 自定义分割线🧵
* 话题 #标签
* 任务列表(可完成/取消完成任务)
Expand All @@ -28,7 +29,8 @@
***** 图片
***** iframes
**** 代码:服务端渲染
**** 保留 HTML 列表 Markers 样式
* 图片
* 元数据
** 评论
*** 发表
Expand Down
15 changes: 15 additions & 0 deletions modules/utils/generic.go
Original file line number Diff line number Diff line change
Expand Up @@ -107,3 +107,18 @@ func Map[T any, S []E, E any](s S, mapper func(e E) T) []T {
}
return t
}

// https://yourbasic.org/golang/formatting-byte-size-to-human-readable-format/
func ByteCountIEC(b int64) string {
const unit = 1024
if b < unit {
return fmt.Sprintf("%d B", b)
}
div, exp := int64(unit), 0
for n := b / unit; n >= unit; n /= unit {
div *= unit
exp++
}
return fmt.Sprintf("%.1f %ciB",
float64(b)/float64(div), "KMGTPE"[exp])
}
3 changes: 3 additions & 0 deletions service/modules/renderers/exif/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
# 图片元数据

用于从图片中提取元数据(时间、地点、相机等)信息用于展示。
71 changes: 71 additions & 0 deletions service/modules/renderers/exif/exif.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
package exif

import (
"context"
"encoding/json"
"io"
"log"
"os/exec"
"path/filepath"

"github.com/PuerkitoBio/goquery"
"github.com/movsb/taoblog/modules/utils"
gold_utils "github.com/movsb/taoblog/service/modules/renderers/goldutils"
)

type Exif struct {
fs gold_utils.WebFileSystem
}

func New(fs gold_utils.WebFileSystem) *Exif {
return &Exif{
fs: fs,
}
}

func (m *Exif) TransformHtml(doc *goquery.Document) error {
doc.Find(`img`).Each(func(i int, s *goquery.Selection) {
url := s.AttrOr(`src`, ``)
if url == "" {
return
}
fp, err := m.fs.OpenURL(url)
if err != nil {
log.Println(err)
return
}
defer fp.Close()
stat, err := fp.Stat()
if err != nil {
log.Println(err)
return
}
info, err := extract(fp)
if err != nil {
log.Println(err)
return
}
info.FileName = filepath.Base(stat.Name())
info.FileSize = utils.ByteCountIEC(stat.Size())
s.SetAttr(`data-metadata`, string(utils.DropLast1(json.Marshal(info.String()))))
})
return nil
}

// TODO 直接传文件?否则文件大小只能读完才知道。
func extract(r io.Reader) (*Metadata, error) {
cmd := exec.CommandContext(context.TODO(), `exiftool`, `-G`, `-s`, `-json`, `-`)
cmd.Stdin = r
output, err := cmd.Output()
if err != nil {
return nil, err
}
var md []*Metadata
if err := json.Unmarshal(output, &md); err != nil {
return nil, err
}
if len(md) <= 0 {
return nil, nil
}
return md[0], nil
}
88 changes: 88 additions & 0 deletions service/modules/renderers/exif/metadata.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
package exif

import (
"fmt"
"log"
"strings"
"time"
)

// exiftool -G -s -json test_data/exif.avif
type Metadata struct {
MimeType string `json:"File:MIMEType"` // 类型:image/avif
FileName string `json:"File:FileName"` // 文件名字
FileSize string `json:"File:FileSize"` // 文件大小
ImageSize string `json:"Composite:ImageSize"` // 尺寸
Model string `json:"EXIF:Model"` // 设置型号
Make string `json:"EXIF:Make"` // 设置制造商
FNumber float32 `json:"EXIF:FNumber"` // 光圈数
FocalLength string `json:"EXIF:FocalLength"` // 焦距
ExposureTime string `json:"EXIF:ExposureTime"` // 曝光时间
GPSPosition string `json:"Composite:GPSPosition"` // 坐标
GPSAltitude string `json:"Composite:GPSAltitude"` // 海拔
CreateDate string `json:"EXIF:CreateDate"` // 创建日期/时间
OffsetTime string `json:"EXIF:OffsetTime"` // 时区
}

func (m *Metadata) CreationDateTime() time.Time {
if m.CreateDate == "" {
return time.Time{}
}
timeZone := time.Now()
if m.OffsetTime != "" {
t, err := time.Parse(`-07:00`, m.OffsetTime)
if err != nil {
log.Println(`failed to parse timezone for exif:`, m.OffsetTime)
return time.Time{}
}
timeZone = t
}
layout := `2006:01:02 15:04:05`
t, err := time.ParseInLocation(layout, m.CreateDate, timeZone.Location())
if err != nil {
log.Println(`failed to parse time:`, m.CreateDate)
return time.Time{}
}
return t
}

func (m *Metadata) String() []string {
var pairs []string

add := func(value string, name string) {
if value != "" {
pairs = append(pairs, name, value)
}
}

add(m.FileName, `名字`)
add(m.FileSize, `大小`)
add(m.ImageSize, `尺寸`)
add(m.MimeType, `类型`)

if t := m.CreationDateTime(); !t.IsZero() {
f := t.Format(time.RFC3339)
add(f, `时间`)
}

if m.Make != "" && m.Model != "" {
add(m.Make+` / `+m.Model, `设备`)
}

lenInfo := []string{}
if m.FNumber > 0 {
lenInfo = append(lenInfo, fmt.Sprintf(`f/%v`, m.FNumber))
}
if m.FocalLength != "" {
lenInfo = append(lenInfo, m.FocalLength)
}
if m.ExposureTime != "" {
lenInfo = append(lenInfo, m.ExposureTime)
}
add(strings.Join(lenInfo, `, `), `镜头`)

add(m.GPSPosition, `位置`)
add(m.GPSAltitude, `海拔`)

return pairs
}
3 changes: 3 additions & 0 deletions service/modules/renderers/exif/test_data/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
# README

exif.avif 中的元数据是从 DJI_0759.JPG 里面通过 `exiftool -tagsFromFile DJI_0759.JPG exif.avif` 拷贝得到的。
Binary file not shown.
4 changes: 4 additions & 0 deletions service/modules/renderers/goldutils/utils.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import (
"bytes"
"errors"
"io/fs"
"log"
"net/url"
"slices"
"strings"
Expand Down Expand Up @@ -73,6 +74,9 @@ func (fs *_WebFileSystem) OpenURL(url_ string) (fs.File, error) {
}
// 即使 base 不包含 host 也满足。
if !strings.EqualFold(fs.base.Host, ref.Host) {
if ref.Scheme != `data` {
log.Println(`fs: url:`, url_)
}
return nil, ErrCrossOrigin
}
// fs.FS 不能以 / 开头。
Expand Down
2 changes: 2 additions & 0 deletions service/post.go
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ import (
"github.com/movsb/taoblog/service/models"
"github.com/movsb/taoblog/service/modules/renderers"
"github.com/movsb/taoblog/service/modules/renderers/custom_break"
"github.com/movsb/taoblog/service/modules/renderers/exif"
"github.com/movsb/taoblog/service/modules/renderers/friends"
gold_utils "github.com/movsb/taoblog/service/modules/renderers/goldutils"
"github.com/movsb/taoblog/service/modules/renderers/highlight"
Expand Down Expand Up @@ -298,6 +299,7 @@ func (s *Service) renderMarkdown(secure bool, postId, commentId int64, sourceTyp
renderers.WithLazyLoadingFrames(),
friends.New(),
katex.New(),
exif.New(s.OpenAsset(postId)),

// 其它选项可能会插入链接,所以放后面。
// BUG: 放在 html 的最后执行,不然无效,对 hashtags。
Expand Down
33 changes: 33 additions & 0 deletions theme/blog/statics/scripts/image-view.js
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,7 @@ class ImageViewUI {
this.root.appendChild(this.obj);
this.obj.src = img.src;
this.ref = img;
this.initMetadata(img.dataset.metadata);
}
}

Expand Down Expand Up @@ -301,6 +302,38 @@ class ImageViewUI {
_onDivClick(e) {
this.show(false);
}
initMetadata(metadata) {
let md;
try {
md = JSON.parse(metadata);
if (md.length <= 0 || md.length&1 > 0) {
return;
}
} catch {
return;
}
let title = '';
for(let i=0; i<md.length; i+=2) {
title += `${md[i+0]}${md[i+1]}\n`;
}
this.obj.title = title;

/*
let table = document.createElement('table');
table.classList.add('metadata');
for(let i=0; i<md.length; i+=2) {
let tr = document.createElement('tr');
let td1 = document.createElement('td');
td1.innerText = md[i+0];
let td2 = document.createElement('td');
td2.innerText = md[i+1];
tr.appendChild(td1);
tr.appendChild(td2);
table.appendChild(tr);
}
this.root.appendChild(table);
*/
}
}

class ImageView {
Expand Down
7 changes: 6 additions & 1 deletion theme/blog/styles/toolbar.scss
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,12 @@
svg text {
cursor: text;
}


// table.metadata {
// position: absolute;
// right: 0;
// bottom: 0;
// }
}

body#error {
Expand Down

0 comments on commit c705051

Please sign in to comment.