Skip to content

Commit 02bd953

Browse files
committed
feat: image/imageutil: add EncodeJPEGWithExif(), SOIFilterWriter{}
1 parent 01e8f10 commit 02bd953

File tree

2 files changed

+489
-16
lines changed

2 files changed

+489
-16
lines changed

image/imageutil/write.go

Lines changed: 133 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,9 @@
11
package imageutil
22

33
import (
4+
"bytes"
45
"errors"
6+
"fmt"
57
"image/gif"
68
"image/jpeg"
79
"io"
@@ -10,8 +12,9 @@ import (
1012
"regexp"
1113
"strings"
1214

15+
"image"
16+
1317
"github.com/grokify/mogo/errors/errorsutil"
14-
"github.com/grokify/mogo/io/ioutil"
1518
"github.com/grokify/mogo/os/osutil"
1619
)
1720

@@ -140,35 +143,149 @@ var JPEGEncodeOptionsQualityMax = &JPEGEncodeOptions{
140143
Options: &jpeg.Options{
141144
Quality: JPEGQualityMax}}
142145

146+
// SOIFilterWriter is a writer that filters out the SOI marker (0xFF 0xD8) from JPEG data.
147+
type SOIFilterWriter struct {
148+
w io.Writer
149+
state int // 0: initial, 1: saw 0xFF, 2: saw 0xD8
150+
}
151+
152+
func NewSOIFilterWriter(w io.Writer) *SOIFilterWriter {
153+
return &SOIFilterWriter{w: w}
154+
}
155+
156+
func (s *SOIFilterWriter) Write(p []byte) (n int, err error) {
157+
if len(p) == 0 {
158+
return 0, nil
159+
}
160+
161+
// If we're in state 2, we've already seen the SOI marker, write everything
162+
if s.state == 2 {
163+
return s.w.Write(p)
164+
}
165+
166+
// Process the data byte by byte
167+
for i := 0; i < len(p); i++ {
168+
switch s.state {
169+
case 0: // initial state
170+
if p[i] == JPEGMarkerPrefix {
171+
s.state = 1
172+
} else {
173+
if _, err := s.w.Write(p[i : i+1]); err != nil {
174+
return n, err
175+
}
176+
n++
177+
}
178+
case 1: // saw 0xFF
179+
if p[i] == JPEGMarkerSOI {
180+
s.state = 2
181+
} else {
182+
// Write the 0xFF we saw earlier
183+
if _, err := s.w.Write([]byte{JPEGMarkerPrefix}); err != nil {
184+
return n, err
185+
}
186+
n++
187+
// Write current byte if it's not 0xFF
188+
if p[i] != JPEGMarkerPrefix {
189+
if _, err := s.w.Write(p[i : i+1]); err != nil {
190+
return n, err
191+
}
192+
n++
193+
}
194+
s.state = 0
195+
}
196+
}
197+
}
198+
199+
// If we're still in state 1 at the end of the buffer, write the 0xFF
200+
if s.state == 1 {
201+
if _, err := s.w.Write([]byte{JPEGMarkerPrefix}); err != nil {
202+
return n, err
203+
}
204+
n++
205+
s.state = 0
206+
}
207+
208+
return n, nil
209+
}
210+
211+
// Close implements io.Closer
212+
func (s *SOIFilterWriter) Close() error {
213+
// If we're in state 1, write the final 0xFF
214+
if s.state == 1 {
215+
if _, err := s.w.Write([]byte{JPEGMarkerPrefix}); err != nil {
216+
return err
217+
}
218+
}
219+
return nil
220+
}
221+
143222
// newWriterExif is used to write Exif to an `io.Writer` before calling `jpeg.Encode()`.
144223
// It is used with `jpeg.Encode()` to remove the Start of Image (SOI) marker after adding
145224
// SOI and Exif.
146-
func newWriterExif(w io.Writer, exif []byte) (io.Writer, error) {
225+
func newWriterExif(w io.Writer, exif []byte) (io.WriteCloser, error) {
147226
// Adapted from the following under MIT license: https://github.com/jdeng/goheif/blob/a0d6a8b3e68f9d613abd9ae1db63c72ba33abd14/heic2jpg/main.go
148227
// See more here: https://en.wikipedia.org/wiki/JPEG_File_Interchange_Format
149228
// https://www.codeproject.com/Articles/47486/Understanding-and-Reading-Exif-Data
150-
wExif := ioutil.NewSkipWriter(w, 2)
151229

152-
if _, err := w.Write(JPEGMarker(JPEGMarkerSOI)); err != nil {
230+
// Create a buffer to hold the header
231+
header := &bytes.Buffer{}
232+
233+
// Write SOI marker
234+
if _, err := header.Write(JPEGMarker(JPEGMarkerSOI)); err != nil {
153235
return nil, err
154236
}
155237

156-
if exif == nil {
157-
return wExif, nil
238+
if exif != nil {
239+
// Write Exif marker and data
240+
markerLen := 2 + len(exif)
241+
if markerLen > 0xFFFF {
242+
return nil, fmt.Errorf("exif data too large: %d bytes", markerLen)
243+
}
244+
marker := []byte{
245+
JPEGMarkerPrefix,
246+
JPEGMarkerExif,
247+
byte(markerLen >> 8), // High byte
248+
byte(markerLen & 0xFF)} // Low byte
249+
if _, err := header.Write(marker); err != nil {
250+
return nil, err
251+
}
252+
if _, err := header.Write(exif); err != nil {
253+
return nil, err
254+
}
158255
}
159256

160-
markerLen := 2 + len(exif)
161-
marker := []byte{
162-
JPEGMarkerPrefix,
163-
JPEGMarkerExif,
164-
uint8(markerLen >> 8),
165-
uint8(markerLen & JPEGMarkerPrefix)}
166-
if _, err := w.Write(marker); err != nil {
257+
// Write the header to the underlying writer
258+
if _, err := w.Write(header.Bytes()); err != nil {
167259
return nil, err
168260
}
169261

170-
if _, err := w.Write(exif); err != nil {
171-
return nil, err
262+
// Return a filter writer to handle JPEG encoder output
263+
return NewSOIFilterWriter(w), nil
264+
}
265+
266+
// EncodeJPEGWithExif encodes a JPEG image, inserts Exif data after the SOI marker, and writes to w.
267+
func EncodeJPEGWithExif(w io.Writer, img image.Image, opts *jpeg.Options, exif []byte) error {
268+
buf := &bytes.Buffer{}
269+
if err := jpeg.Encode(buf, img, opts); err != nil {
270+
return err
271+
}
272+
jpegData := buf.Bytes()
273+
if len(jpegData) < 2 || jpegData[0] != 0xFF || jpegData[1] != 0xD8 {
274+
return fmt.Errorf("not a valid JPEG SOI")
275+
}
276+
if exif == nil || len(exif) == 0 {
277+
_, err := w.Write(jpegData)
278+
return err
279+
}
280+
markerLen := 2 + len(exif)
281+
if markerLen > 0xFFFF {
282+
return fmt.Errorf("exif too large")
172283
}
173-
return wExif, nil
284+
exifSegment := []byte{0xFF, 0xE1, byte(markerLen >> 8), byte(markerLen & 0xFF)}
285+
exifSegment = append(exifSegment, exif...)
286+
final := append([]byte{}, jpegData[:2]...)
287+
final = append(final, exifSegment...)
288+
final = append(final, jpegData[2:]...)
289+
_, err := w.Write(final)
290+
return err
174291
}

0 commit comments

Comments
 (0)