Skip to content
This repository has been archived by the owner on Jan 15, 2024. It is now read-only.

Commit

Permalink
[feature] add webp support (#4)
Browse files Browse the repository at this point in the history
  • Loading branch information
ftrvxmtrx authored Dec 1, 2022
1 parent 79f2ed3 commit 3914082
Show file tree
Hide file tree
Showing 8 changed files with 178 additions and 3 deletions.
3 changes: 2 additions & 1 deletion go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ require (
github.com/dsoprea/go-png-image-structure/v2 v2.0.0-20210512210324-29b889a6093d
github.com/stretchr/testify v1.7.0
github.com/superseriousbusiness/go-jpeg-image-structure/v2 v2.0.0-20220321154430-d89a106fdabe
golang.org/x/image v0.1.0
)

require (
Expand All @@ -19,7 +20,7 @@ require (
github.com/go-xmlfmt/xmlfmt v0.0.0-20191208150333-d5b6f63a941b // indirect
github.com/golang/geo v0.0.0-20200319012246-673a6f80352d // indirect
github.com/pmezard/go-difflib v1.0.0 // indirect
golang.org/x/net v0.0.0-20200707034311-ab3426394381 // indirect
golang.org/x/net v0.0.0-20220722155237-a158d28d115b // indirect
gopkg.in/yaml.v2 v2.3.0 // indirect
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c // indirect
)
25 changes: 24 additions & 1 deletion go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -36,20 +36,43 @@ github.com/stretchr/testify v1.7.0 h1:nwc3DEeHmmLAfoZucVR881uASk0Mfjw8xYJ99tb5Cc
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/superseriousbusiness/go-jpeg-image-structure/v2 v2.0.0-20220321154430-d89a106fdabe h1:ksl2oCx/Qo8sNDc3Grb8WGKBM9nkvhCm25uvlT86azE=
github.com/superseriousbusiness/go-jpeg-image-structure/v2 v2.0.0-20220321154430-d89a106fdabe/go.mod h1:gH4P6gN1V+wmIw5o97KGaa1RgXB/tVpC2UNzijhg3E4=
github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
golang.org/x/image v0.1.0 h1:r8Oj8ZA2Xy12/b5KZYj3tuv7NG/fBz3TwQVvpJ9l8Rk=
golang.org/x/image v0.1.0/go.mod h1:iyPr49SD/G/TBxYVB/9RRtGUT5eNbo2u4NamWeQcD5c=
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20191209160850-c0dbc17a3553/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20200320220750-118fecf932d8/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20200501053045-e0ff5e5a1de5/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
golang.org/x/net v0.0.0-20200513185701-a91f0712d120/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
golang.org/x/net v0.0.0-20200520182314-0ba52f642ac2/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
golang.org/x/net v0.0.0-20200707034311-ab3426394381 h1:VXak5I6aEWmAXeQjA+QSZzlgNrpq9mjcfDemuexIKsU=
golang.org/x/net v0.0.0-20200707034311-ab3426394381/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA=
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
golang.org/x/net v0.0.0-20220722155237-a158d28d115b h1:PxfKdU9lEEDYjdIzOtC4qFWgkU2rGHdKlKowJSMN9h0=
golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
golang.org/x/text v0.4.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/yaml.v2 v2.2.7/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
Expand Down
Binary file added images/pjw-clean.webp
Binary file not shown.
Binary file added images/pjw.webp
Binary file not shown.
2 changes: 1 addition & 1 deletion jpeg.go
Original file line number Diff line number Diff line change
Expand Up @@ -91,7 +91,7 @@ func (v *jpegVisitor) HandleSegment(segmentMarker byte, _ string, _ int, _ bool)
if segmentMarker == jpegstructure.MARKER_EOI {
// take account of the last 2 bytes taken up by the EOI
eoiLength := 2

// this is the total file size we will
// have written including the EOI
willHaveWritten := v.writtenTotalBytes + eoiLength
Expand Down
14 changes: 14 additions & 0 deletions terminator.go
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,8 @@ func Terminate(in io.Reader, fileSize int, mediaType string) (io.Reader, error)
switch mediaType {
case "image/jpeg", "jpeg", "jpg":
err = terminateJpeg(scanner, pipeWriter, fileSize)
case "image/webp", "webp":
err = terminateWebp(scanner, pipeWriter)
case "image/png", "png":
// for pngs we need to skip the header bytes, so read them in
// and check we're really dealing with a png here
Expand Down Expand Up @@ -86,6 +88,18 @@ func terminateJpeg(scanner *bufio.Scanner, writer io.WriteCloser, expectedFileSi
return nil
}

func terminateWebp(scanner *bufio.Scanner, writer io.WriteCloser) error {
v := &webpVisitor{
writer: writer,
}

// use the webp visitor's 'split' function, which satisfies the bufio.SplitFunc interface
scanner.Split(v.split)

scanAndClose(scanner, writer)
return nil
}

func terminatePng(scanner *bufio.Scanner, writer io.WriteCloser) error {
ps := pngstructure.NewPngSplitter()

Expand Down
36 changes: 36 additions & 0 deletions terminator_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ import (

"github.com/stretchr/testify/suite"
terminator "github.com/superseriousbusiness/exif-terminator"
"golang.org/x/image/webp"
)

type TerminatorTestSuite struct {
Expand Down Expand Up @@ -174,6 +175,41 @@ func (suite *TerminatorTestSuite) TestTerminateTurnip() {
suite.EqualValues(turnipClean, b)
}

func (suite *TerminatorTestSuite) TestTerminatePJW() {
pjw, err := os.Open("./images/pjw.webp")
if err != nil {
panic(err)
}
defer pjw.Close()

stat, err := pjw.Stat()
if err != nil {
panic(err)
}

originalSize := int(stat.Size())

out, err := terminator.Terminate(pjw, originalSize, "webp")
suite.NoError(err)

// we should be able to get some bytes back from the returned reader
b, err := io.ReadAll(out)
suite.NoError(err)
suite.NotEmpty(b)

// the processed image should have the same size as the initial image
suite.EqualValues(originalSize, len(b))

// should be decodable as a webp
_, err = webp.Decode(bytes.NewBuffer(b))
suite.NoError(err)

// bytes should be the same as the clean image
pjwClean, err := os.ReadFile("./images/pjw-clean.webp")
suite.NoError(err)
suite.EqualValues(pjwClean, b)
}

func (suite *TerminatorTestSuite) TestTerminatePanorama() {
panorama, err := os.Open("./images/exif-panorama.jpg")
if err != nil {
Expand Down
101 changes: 101 additions & 0 deletions webp.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,101 @@
/*
exif-terminator
Copyright (C) 2022 SuperSeriousBusiness [email protected]
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU Affero General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU Affero General Public License for more details.
You should have received a copy of the GNU Affero General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>.
*/

package terminator

import (
"encoding/binary"
"errors"
"io"
)

const (
riffHeaderSize = 4 * 3
)

var (
riffHeader = [4]byte{'R', 'I', 'F', 'F'}
webpHeader = [4]byte{'W', 'E', 'B', 'P'}
exifFourcc = [4]byte{'E', 'X', 'I', 'F'}
xmpFourcc = [4]byte{'X', 'M', 'P', ' '}

errNoRiffHeader = errors.New("no RIFF header")
errNoWebpHeader = errors.New("not a WEBP file")
)

type webpVisitor struct {
writer io.Writer
doneHeader bool
}

func fourCC(b []byte) [4]byte {
return [4]byte{b[0], b[1], b[2], b[3]}
}

func (v *webpVisitor) split(data []byte, atEOF bool) (advance int, token []byte, err error) {
// parse/write the header first
if !v.doneHeader {
if len(data) < riffHeaderSize {
// need the full header
return
}
if fourCC(data) != riffHeader {
err = errNoRiffHeader
return
}
if fourCC(data[8:]) != webpHeader {
err = errNoWebpHeader
return
}
if _, err = v.writer.Write(data[:riffHeaderSize]); err != nil {
return
}
advance += riffHeaderSize
data = data[riffHeaderSize:]
v.doneHeader = true
}

// need enough for fourcc and size
if len(data) < 8 {
return
}
size := int64(binary.LittleEndian.Uint32(data[4:]))
if (size & 1) != 0 {
// odd chunk size - extra padding byte
size++
}
// wait until there is enough
if int64(len(data)-8) < size {
return
}

fourcc := fourCC(data)
rawChunkData := data[8 : 8+size]
if fourcc == exifFourcc || fourcc == xmpFourcc {
// replace exif/xmp with blank
rawChunkData = make([]byte, size)
}

if _, err = v.writer.Write(data[:8]); err == nil {
if _, err = v.writer.Write(rawChunkData); err == nil {
advance += 8 + int(size)
}
}

return
}

0 comments on commit 3914082

Please sign in to comment.