这比预期的要困难得多。我参考了这个资源,它解释了JPEG的一般结构,其中流段的唯一例外是"熵编码段"(ECS),其中包含了实际的图像数据。
你的方法存在问题
我的想法是从标签源文件中复制所有内容,直到第一个ffdb
(不包括该标记),然后从图像源文件中复制所有内容(没有标签),从第一个ffdb
(包括该标记)开始。结果文件损坏(缺少SOS标记)。
这对JPEG文件做出了非常强烈的假设,而这些假设并不成立。首先,ffdb
很可能会出现在段内的某个位置。段的顺序也非常松散,所以无法保证ffdb
之前或之后会出现什么(定义量化表的段)。即使这种方法在大多数情况下可能奏效,它仍然是一种非常脆弱、不可靠的解决方案。
正确的方法
正确的方法是遍历所有片段,从提供元数据的文件中仅复制元数据片段,从提供图像数据的文件中仅复制非元数据片段。
复杂之处在于由于某种原因,ECS(扫描开始)不遵循片段约定。因此,在读取SOS(扫描开始)后,我们需要通过找到下一个片段标记:0xFF后跳转到ECS的末尾,该标记可能既不是数据(零),也不是“重启标记”(0xD0-0xD7)。
为了测试,我使用了具有EXIF元数据的此图像。我的测试命令如下:
cp exif.jpg exif_stripped.jpg && exiftool -All= exif_stripped.jpg && go run main.go exif.jpg exif_stripped.jpg
我使用了
exiftool
来去除EXIF元数据,然后通过重新读取它来测试Go程序。使用
exiftool exif_stripped.jpg
(或您选择的图像查看器)查看元数据,并与
exiftool exif.jpg
的输出进行比较(附注:您可能完全可以通过使用
exiftool
来使这个Go程序过时)。
我编写的程序替换了EXIF元数据、注释和版权声明。我为测试添加了一个简单的命令行界面。如果您只想保留EXIF元数据,请将
isMetaTagType
函数更改为
func isMetaTagType(tagType byte) bool { return tagType == exif }
完整计划
package main
import (
"os"
"io"
"bufio"
"errors"
)
const (
soi = 0xD8
eoi = 0xD9
sos = 0xDA
exif = 0xE1
copyright = 0xEE
comment = 0xFE
)
func isMetaTagType(tagType byte) bool {
return tagType == exif || tagType == copyright || tagType == comment
}
func copySegments(dst *bufio.Writer, src *bufio.Reader, filterSegment func(tagType byte) bool) error {
var buf [2]byte
_, err := io.ReadFull(src, buf[:])
if err != nil { return err }
if buf != [2]byte{0xFF, soi} {
return errors.New("expected SOI")
}
for {
_, err := io.ReadFull(src, buf[:])
if err != nil { return err }
if buf[0] != 0xFF {
return errors.New("invalid tag type")
}
if buf[1] == eoi {
n, err := src.Read(buf[:1])
if err != nil && err != io.EOF { return err }
if n > 0 {
return errors.New("EOF expected after EOI")
}
return nil
}
sos := buf[1] == 0xDA
filter := filterSegment(buf[1])
if filter {
_, err = dst.Write(buf[:])
if err != nil { return err }
}
_, err = io.ReadFull(src, buf[:])
if err != nil { return err }
if filter {
_, err = dst.Write(buf[:])
if err != nil { return err }
}
tagLength := ((uint16(buf[0]) << 8) | uint16(buf[1])) - 2
if filter {
_, err = io.CopyN(dst, src, int64(tagLength))
} else {
_, err = src.Discard(int(tagLength))
}
if err != nil { return err }
if sos {
for {
bytes, err := src.Peek(2)
if err != nil { return err }
if bytes[0] == 0xFF {
data, rstMrk := bytes[1] == 0, bytes[1] >= 0xD0 && bytes[1] <= 0xD7
if !data && !rstMrk {
break
}
}
if filter {
err = dst.WriteByte(bytes[0])
if err != nil { return err }
}
_, err = src.Discard(1)
if err != nil { return err }
}
}
}
}
func copyMetadata(outImagePath, imagePath, metadataImagePath string) error {
outFile, err := os.Create(outImagePath)
if err != nil { return err }
defer outFile.Close()
writer := bufio.NewWriter(outFile)
imageFile, err := os.Open(imagePath)
if err != nil { return err }
defer imageFile.Close()
imageReader := bufio.NewReader(imageFile)
metaFile, err := os.Open(metadataImagePath)
if err != nil { return err }
defer metaFile.Close()
metaReader := bufio.NewReader(metaFile)
_, err = writer.Write([]byte{0xFF, soi})
if err != nil { return err }
{
err = copySegments(writer, metaReader, isMetaTagType)
if err != nil { return err }
err = copySegments(writer, imageReader, func(tagType byte) bool {
return !isMetaTagType(tagType)
})
if err != nil { return err }
}
_, err = writer.Write([]byte{0xFF, eoi})
if err != nil { return err }
return writer.Flush()
}
func replaceMetadata(toPath, fromPath string) error {
copyPath := toPath + "~"
err := os.Rename(toPath, copyPath)
if err != nil { return err }
defer os.Remove(copyPath)
return copyMetadata(toPath, copyPath, fromPath)
}
func main() {
if len(os.Args) < 3 {
println("args: FROM TO")
return
}
err := replaceMetadata(os.Args[2], os.Args[1])
if err != nil {
println("replacing metadata failed: " + err.Error())
}
}