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

Commit

Permalink
First working version (#1)
Browse files Browse the repository at this point in the history
* use pipereader + pipewriter

* test updates

* testing visitor function

* hey it works

* pngs working

* rename license
  • Loading branch information
tsmethurst authored Jan 22, 2022
1 parent e9d92a5 commit 70aa283
Show file tree
Hide file tree
Showing 13 changed files with 1,155 additions and 46 deletions.
661 changes: 661 additions & 0 deletions LICENSE

Large diffs are not rendered by default.

70 changes: 70 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
# exif-terminator

`exif-terminator` removes exif data from images (jpeg and png currently supported) in a streaming manner. All you need to do is provide a reader of the image in, and exif-terminator will provide a reader of the image out.

Hasta la vista, baby!

```text
Expand Down Expand Up @@ -44,3 +46,71 @@ Hasta la vista, baby!
.;,;:;...,::. .;lokXKKNMMMWNOc,;;;,::;'...lOKNWNKkol:,..cKdcO0do
.:;... .. .,:okO0KNN0:.',,''''. ':xNMWKkxxOKXd,.cNk,:l:o
```

## Why?

Exif removal is a pain in the arse. Most other libraries seem to parse the whole image into memory, then remove the exif data, then encode the image again.

`exif-terminator` differs in that it removes exif data *while scanning through the image bytes*, and it doesn't do any reencoding of the image. Bytes of exif data are simply all set to 0, and the image data is piped back out again into the returned reader.

## Example

```go
package test

import (
"io"
"os"

terminator "github.com/superseriousbusiness/exif-terminator"
)

func main() {
// open a file
sloth, err := os.Open("./images/sloth.jpg")
if err != nil {
panic(err)
}

// get the length of the file
stat, err := sloth.Stat()
if err != nil {
panic(err)
}

// terminate!
out, err := terminator.Terminate(sloth, int(stat.Size()), "jpeg")
if err != nil {
panic(err)
}

// read the bytes from the reader
b, err := io.ReadAll(out)
if err != nil {
panic(err)
}

// save the file somewhere
if err := os.WriteFile("./images/sloth-clean.jpg", b, 0666); err != nil {
panic(err)
}
}
```

## Credits

### Libraries

`exif-terminator` borrows heavily from the two [`dsoprea`](https://github.com/dsoprea) libraries credited below. In fact, it's basically a hack on top of those libraries. Thanks `dsoprea`!

- [dsoprea/go-jpeg-image-structure](https://github.com/dsoprea/go-jpeg-image-structure): jpeg structure parsing. [MIT License](https://spdx.org/licenses/MIT.html).
- [dsoprea/go-png-image-structure](https://github.com/dsoprea/go-png-image-structure): png structure parsing. [MIT License](https://spdx.org/licenses/MIT.html).
- [stretchr/testify](https://github.com/stretchr/testify); test framework. [MIT License](https://spdx.org/licenses/MIT.html).

## License

![the gnu AGPL logo](https://www.gnu.org/graphics/agplv3-155x51.png)

`exif-terminator` is free software, licensed under the [GNU AGPL v3 LICENSE](LICENSE).

Copyright (C) 2022 SuperSeriousBusiness.
9 changes: 6 additions & 3 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -2,20 +2,23 @@ module github.com/superseriousbusiness/exif-terminator

go 1.17

require (
github.com/dsoprea/go-jpeg-image-structure/v2 v2.0.0-20210512043942-b434301c6836
github.com/dsoprea/go-png-image-structure/v2 v2.0.0-20210512210324-29b889a6093d
github.com/stretchr/testify v1.7.0
)

require (
github.com/davecgh/go-spew v1.1.0 // indirect
github.com/dsoprea/go-exif/v3 v3.0.0-20210625224831-a6301f85c82b // indirect
github.com/dsoprea/go-iptc v0.0.0-20200609062250-162ae6b44feb // indirect
github.com/dsoprea/go-jpeg-image-structure/v2 v2.0.0-20210512043942-b434301c6836 // indirect
github.com/dsoprea/go-logging v0.0.0-20200710184922-b02d349568dd // indirect
github.com/dsoprea/go-photoshop-info-format v0.0.0-20200609050348-3db9b63b202c // indirect
github.com/dsoprea/go-utility/v2 v2.0.0-20200717064901-2fccff4aa15e // indirect
github.com/go-errors/errors v1.1.1 // indirect
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
github.com/stretchr/objx v0.1.0 // indirect
github.com/stretchr/testify v1.7.0 // indirect
golang.org/x/net v0.0.0-20200707034311-ab3426394381 // indirect
gopkg.in/yaml.v2 v2.3.0 // indirect
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c // indirect
Expand Down
7 changes: 4 additions & 3 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -10,12 +10,13 @@ github.com/dsoprea/go-iptc v0.0.0-20200609062250-162ae6b44feb/go.mod h1:kYIdx9N9
github.com/dsoprea/go-jpeg-image-structure/v2 v2.0.0-20210512043942-b434301c6836 h1:KGCiMMWxODEMmI3+9Ms04l73efoqFVNKKKPbVyOvKrU=
github.com/dsoprea/go-jpeg-image-structure/v2 v2.0.0-20210512043942-b434301c6836/go.mod h1:WaARaUjQuSuDCDFAiU/GwzfxMTJBulfEhqEA2Tx6B4Y=
github.com/dsoprea/go-logging v0.0.0-20190624164917-c4f10aab7696/go.mod h1:Nm/x2ZUNRW6Fe5C3LxdY1PyZY5wmDv/s5dkPJ/VB3iA=
github.com/dsoprea/go-logging v0.0.0-20200517223158-a10564966e9d h1:F/7L5wr/fP/SKeO5HuMlNEX9Ipyx2MbH2rV9G4zJRpk=
github.com/dsoprea/go-logging v0.0.0-20200517223158-a10564966e9d/go.mod h1:7I+3Pe2o/YSU88W0hWlm9S22W7XI1JFNJ86U0zPKMf8=
github.com/dsoprea/go-logging v0.0.0-20200710184922-b02d349568dd h1:l+vLbuxptsC6VQyQsfD7NnEC8BZuFpz45PgY+pH8YTg=
github.com/dsoprea/go-logging v0.0.0-20200710184922-b02d349568dd/go.mod h1:7I+3Pe2o/YSU88W0hWlm9S22W7XI1JFNJ86U0zPKMf8=
github.com/dsoprea/go-photoshop-info-format v0.0.0-20200609050348-3db9b63b202c h1:7j5aWACOzROpr+dvMtu8GnI97g9ShLWD72XIELMgn+c=
github.com/dsoprea/go-photoshop-info-format v0.0.0-20200609050348-3db9b63b202c/go.mod h1:pqKB+ijp27cEcrHxhXVgUUMlSDRuGJJp1E+20Lj5H0E=
github.com/dsoprea/go-png-image-structure/v2 v2.0.0-20210512210324-29b889a6093d h1:2zNIgrJTspLxUKoJGl0Ln24+hufPKSjP3cu4++5MeSE=
github.com/dsoprea/go-png-image-structure/v2 v2.0.0-20210512210324-29b889a6093d/go.mod h1:scnx0wQSM7UiCMK66dSdiPZvL2hl6iF5DvpZ7uT59MY=
github.com/dsoprea/go-utility v0.0.0-20200711062821-fab8125e9bdf h1:/w4QxepU4AHh3AuO6/g8y/YIIHH5+aKP3Bj8sg5cqhU=
github.com/dsoprea/go-utility v0.0.0-20200711062821-fab8125e9bdf/go.mod h1:95+K3z2L0mqsVYd6yveIv1lmtT3tcQQ3dVakPySffW8=
github.com/dsoprea/go-utility/v2 v2.0.0-20200717064901-2fccff4aa15e h1:IxIbA7VbCNrwumIYjDoMOdf4KOSkMC6NJE4s8oRbE7E=
Expand All @@ -32,7 +33,6 @@ github.com/golang/geo v0.0.0-20200319012246-673a6f80352d/go.mod h1:QZ0nwyI2jOfgR
github.com/jessevdk/go-flags v1.4.0/go.mod h1:4FA24M0QyGHXBuZZK/XkWh8h0e1EYbRYJSGM75WSRxI=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/stretchr/objx v0.1.0 h1:4G4v2dO3VZwixGIRoQ5Lfboy6nUhCyYzaqnIAPPhYs4=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/testify v1.7.0 h1:nwc3DEeHmmLAfoZucVR881uASk0Mfjw8xYJ99tb5CcY=
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
Expand All @@ -42,14 +42,15 @@ golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn
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 h1:EZ3cVSzKOlJxAd8e8YAJ7no8nNypTxexh/YE/xW3ZEY=
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/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/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
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=
gopkg.in/yaml.v2 v2.3.0 h1:clyUAQHOM3G0M3f5vQj7LuJrETvjVot3Z5el9nffUtU=
Expand Down
Binary file added images/comic-clean.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added images/comic.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added images/kitten-clean.jpg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added images/sloth-clean.jpg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
138 changes: 138 additions & 0 deletions jpeg.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,138 @@
/*
exif-terminator
Copyright (C) 2022 SuperSeriousBusiness admin@gotosocial.org
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"
"fmt"
"io"

jpegstructure "github.com/dsoprea/go-jpeg-image-structure/v2"
)

var markerLen = map[byte]int{
0x00: 0,
0x01: 0,
0xd0: 0,
0xd1: 0,
0xd2: 0,
0xd3: 0,
0xd4: 0,
0xd5: 0,
0xd6: 0,
0xd7: 0,
0xd8: 0,
0xd9: 0,
0xda: 0,

// J2C
0x30: 0,
0x31: 0,
0x32: 0,
0x33: 0,
0x34: 0,
0x35: 0,
0x36: 0,
0x37: 0,
0x38: 0,
0x39: 0,
0x3a: 0,
0x3b: 0,
0x3c: 0,
0x3d: 0,
0x3e: 0,
0x3f: 0,
0x4f: 0,
0x92: 0,
0x93: 0,

// J2C extensions
0x74: 4,
0x75: 4,
0x77: 4,
}

type jpegVisitor struct {
js *jpegstructure.JpegSplitter
writer io.Writer
}

// HandleSegment satisfies the visitor interface{} of the jpegstructure library.
//
// We don't really care about any of the parameters, since all we're interested
// in here is the very last segment that was scanned.
func (v *jpegVisitor) HandleSegment(_ byte, _ string, _ int, _ bool) error {
// all we want to do here is get the last segment that was scanned, and then manipulate it
segmentList := v.js.Segments()
segments := segmentList.Segments()
lastSegment := segments[len(segments)-1]
return v.writeSegment(lastSegment)
}

func (v *jpegVisitor) writeSegment(s *jpegstructure.Segment) error {
w := v.writer

defer func() {
// whatever happens, when we finished then evict data from the segment;
// once we've written it we don't want it in memory anymore
s.Data = s.Data[:0]
}()

// The scan-data will have a marker-ID of (0) because it doesn't have a marker-ID or length.
if s.MarkerId != 0 {
if _, err := w.Write([]byte{0xff, s.MarkerId}); err != nil {
return err
}

sizeLen, found := markerLen[s.MarkerId]
if !found || sizeLen == 2 {
sizeLen = 2
l := uint16(len(s.Data) + sizeLen)

if err := binary.Write(w, binary.BigEndian, &l); err != nil {
return err
}

} else if sizeLen == 4 {
l := uint32(len(s.Data) + sizeLen)

if err := binary.Write(w, binary.BigEndian, &l); err != nil {
return err
}

} else if sizeLen != 0 {
return fmt.Errorf("not a supported marker-size: MARKER-ID=(0x%02x) MARKER-SIZE-LEN=(%d)", s.MarkerId, sizeLen)
}
}

if s.IsExif() {
// if this segment is exif data, write blank bytes
blank := make([]byte, len(s.Data))
if _, err := w.Write(blank); err != nil {
return err
}
} else {
// otherwise write the data
if _, err := w.Write(s.Data); err != nil {
return err
}
}

return nil
}
93 changes: 93 additions & 0 deletions png.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,93 @@
/*
exif-terminator
Copyright (C) 2022 SuperSeriousBusiness admin@gotosocial.org
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"
"io"

pngstructure "github.com/dsoprea/go-png-image-structure/v2"
)

type pngVisitor struct {
ps *pngstructure.PngSplitter
writer io.Writer
lastWrittenChunk int
}

func (v *pngVisitor) split(data []byte, atEOF bool) (int, []byte, error) {
// execute the ps split function to read in data
advance, token, err := v.ps.Split(data, atEOF)
if err != nil {
return advance, token, err
}

// if we haven't written anything at all yet, then write the png header back into the writer first
if v.lastWrittenChunk == -1 {
if _, err := v.writer.Write(pngstructure.PngSignature[:]); err != nil {
return advance, token, err
}
}

// check if the splitter has any new chunks in it that we haven't written yet
chunkSlice := v.ps.Chunks()
chunks := chunkSlice.Chunks()
for i, chunk := range chunks {
// look through all the chunks in the splitter
if i > v.lastWrittenChunk {
// we've got a chunk we haven't written yet! write it...
if err := v.writeChunk(chunk); err != nil {
return advance, token, err
}
// then remove the data
chunk.Data = chunk.Data[:0]
// and update
v.lastWrittenChunk = i
}
}

return advance, token, err
}

func (v *pngVisitor) writeChunk(chunk *pngstructure.Chunk) error {
if err := binary.Write(v.writer, binary.BigEndian, chunk.Length); err != nil {
return err
}

if _, err := v.writer.Write([]byte(chunk.Type)); err != nil {
return err
}

if chunk.Type == pngstructure.EXifChunkType {
blank := make([]byte, len(chunk.Data))
if _, err := v.writer.Write(blank); err != nil {
return err
}
} else {
if _, err := v.writer.Write(chunk.Data); err != nil {
return err
}
}

if err := binary.Write(v.writer, binary.BigEndian, chunk.Crc); err != nil {
return err
}

return nil
}
Loading

0 comments on commit 70aa283

Please sign in to comment.