11package imageutil
22
33import (
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