├── .MODULE_ROOT ├── .travis.yml ├── LICENSE ├── README.md ├── assets ├── 20180428_212314.jpg ├── FUJI.jpg ├── IMG_6691_Multiple_EOIs.jpg ├── NDM_8901.jpg ├── NDM_8901.jpg.just_exif └── NDM_8901.jpg.no_exif ├── command ├── js_dump │ ├── main.go │ └── main_test.go ├── js_exif │ ├── main.go │ └── main_test.go └── js_exif_drop │ ├── main.go │ └── main_test.go.ignore ├── go.mod ├── go.sum ├── markers.go ├── media_parser.go ├── media_parser_test.go ├── segment.go ├── segment_list.go ├── segment_list_test.go ├── segment_test.go ├── splitter.go ├── splitter_test.go ├── testing_common.go ├── utility.go └── v2 ├── .MODULE_ROOT ├── LICENSE ├── README.md ├── assets ├── 20180428_212314.jpg ├── IMG_6691_Multiple_EOIs.jpg ├── NDM_8901.jpg ├── NDM_8901.jpg.just_exif └── NDM_8901.jpg.no_exif ├── command ├── js_dump │ ├── main.go │ └── main_test.go ├── js_exif │ ├── main.go │ └── main_test.go └── js_exif_drop │ ├── main.go │ └── main_test.go.ignore ├── go.mod ├── go.sum ├── markers.go ├── media_parser.go ├── media_parser_test.go ├── segment.go ├── segment_list.go ├── segment_list_test.go ├── segment_test.go ├── splitter.go ├── splitter_test.go ├── testing_common.go └── utility.go /.MODULE_ROOT: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dsoprea/go-jpeg-image-structure/4f3f7e93410239bde7f71860b8b0af69ec77274d/.MODULE_ROOT -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: go 2 | go: 3 | - master 4 | - stable 5 | - "1.14" 6 | - "1.13" 7 | - "1.12" 8 | env: 9 | - GO111MODULE=on 10 | install: 11 | - go get -t ./... 12 | script: 13 | # v1 14 | - go test -v . 15 | # v2 16 | - cd v2 17 | - go test -v ./... -coverprofile=coverage.txt -covermode=atomic 18 | - cd .. 19 | after_success: 20 | - cd v2 21 | - curl -s https://codecov.io/bash | bash 22 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT LICENSE 2 | 3 | Copyright 2020 Dustin Oprea 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 6 | 7 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 8 | 9 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 10 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | [![Build Status](https://travis-ci.org/dsoprea/go-jpeg-image-structure.svg?branch=master)](https://travis-ci.org/dsoprea/go-jpeg-image-structure) 2 | [![codecov](https://codecov.io/gh/dsoprea/go-jpeg-image-structure/branch/master/graph/badge.svg?token=Twxyx7kpAa)](https://codecov.io/gh/dsoprea/go-jpeg-image-structure) 3 | [![Go Report Card](https://goreportcard.com/badge/github.com/dsoprea/go-jpeg-image-structure/v2)](https://goreportcard.com/report/github.com/dsoprea/go-jpeg-image-structure/v2) 4 | [![GoDoc](https://godoc.org/github.com/dsoprea/go-jpeg-image-structure/v2?status.svg)](https://godoc.org/github.com/dsoprea/go-jpeg-image-structure/v2) 5 | 6 | ## Overview 7 | 8 | Parse raw JPEG data into individual segments of data. You can print or export this data, including hash digests for each. You can also parse/modify the EXIF data and write an updated image. 9 | 10 | EXIF, XMP, and IPTC data can also be extracted. The provided CLI tool can print this data as well. 11 | -------------------------------------------------------------------------------- /assets/20180428_212314.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dsoprea/go-jpeg-image-structure/4f3f7e93410239bde7f71860b8b0af69ec77274d/assets/20180428_212314.jpg -------------------------------------------------------------------------------- /assets/FUJI.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dsoprea/go-jpeg-image-structure/4f3f7e93410239bde7f71860b8b0af69ec77274d/assets/FUJI.jpg -------------------------------------------------------------------------------- /assets/IMG_6691_Multiple_EOIs.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dsoprea/go-jpeg-image-structure/4f3f7e93410239bde7f71860b8b0af69ec77274d/assets/IMG_6691_Multiple_EOIs.jpg -------------------------------------------------------------------------------- /assets/NDM_8901.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dsoprea/go-jpeg-image-structure/4f3f7e93410239bde7f71860b8b0af69ec77274d/assets/NDM_8901.jpg -------------------------------------------------------------------------------- /assets/NDM_8901.jpg.just_exif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dsoprea/go-jpeg-image-structure/4f3f7e93410239bde7f71860b8b0af69ec77274d/assets/NDM_8901.jpg.just_exif -------------------------------------------------------------------------------- /assets/NDM_8901.jpg.no_exif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dsoprea/go-jpeg-image-structure/4f3f7e93410239bde7f71860b8b0af69ec77274d/assets/NDM_8901.jpg.no_exif -------------------------------------------------------------------------------- /command/js_dump/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | "os" 6 | 7 | "encoding/json" 8 | "io/ioutil" 9 | 10 | "github.com/dsoprea/go-iptc" 11 | "github.com/dsoprea/go-logging" 12 | "github.com/jessevdk/go-flags" 13 | 14 | "github.com/dsoprea/go-jpeg-image-structure" 15 | ) 16 | 17 | // TODO(dustin): Add comments to all of these structs. 18 | 19 | var ( 20 | options = &struct { 21 | Filepath string `short:"f" long:"filepath" required:"true" description:"File-path of JPEG image ('-' for STDIN)"` 22 | JsonAsList bool `short:"l" long:"json-list" description:"Print segments as a JSON list"` 23 | JsonAsObject bool `short:"o" long:"json-object" description:"Print segments as a JSON object"` 24 | IncludeData bool `short:"d" long:"data" description:"Include actual JPEG data (only with JSON)"` 25 | Verbose bool `short:"v" long:"verbose" description:"Enable logging verbosity"` 26 | JustXmp bool `short:"x" long:"just-xmp" description:"Just print raw XMP XML. Fails if not present."` 27 | JustFullIptc bool `short:"i" long:"just-full-iptc" description:"Just print raw IPTC data. Fails if not present."` 28 | JustSimpleIptc bool `short:"s" long:"just-simple-iptc" description:"Just print raw IPTC data. Omit non-standard tags, omit non-human-readable text, omit repeated tags). Fails if not present."` 29 | }{} 30 | ) 31 | 32 | type segmentResult struct { 33 | MarkerId byte `json:"marker_id"` 34 | MarkerName string `json:"marker_name"` 35 | Offset int `json:"offset"` 36 | Data []byte `json:"data"` 37 | Length int `json:"length"` 38 | } 39 | 40 | type segmentIndexItem struct { 41 | Offset int `json:"offset"` 42 | Data []byte `json:"data"` 43 | Length int `json:"length"` 44 | } 45 | 46 | func main() { 47 | _, err := flags.Parse(options) 48 | if err != nil { 49 | os.Exit(-1) 50 | } 51 | 52 | if options.Verbose == true { 53 | scp := log.NewStaticConfigurationProvider() 54 | scp.SetLevelName(log.LevelNameDebug) 55 | 56 | log.LoadConfiguration(scp) 57 | 58 | cla := log.NewConsoleLogAdapter() 59 | log.AddAdapter("console", cla) 60 | } 61 | 62 | if options.JsonAsList == true && options.JsonAsObject == true { 63 | fmt.Println("Only -jsonlist *or* -jsonobject can be chosen.") 64 | os.Exit(-2) 65 | } 66 | 67 | var data []byte 68 | if options.Filepath == "-" { 69 | var err error 70 | data, err = ioutil.ReadAll(os.Stdin) 71 | log.PanicIf(err) 72 | } else { 73 | var err error 74 | data, err = ioutil.ReadFile(options.Filepath) 75 | log.PanicIf(err) 76 | } 77 | 78 | jmp := jpegstructure.NewJpegMediaParser() 79 | 80 | intfc, parseErr := jmp.ParseBytes(data) 81 | 82 | // If there was an error *and* we got back some segments, print the segments 83 | // before panicing. 84 | if intfc == nil && parseErr != nil { 85 | log.Panic(parseErr) 86 | } 87 | 88 | sl := intfc.(*jpegstructure.SegmentList) 89 | 90 | if options.JustXmp == true { 91 | _, s, err := sl.FindXmp() 92 | log.PanicIf(err) 93 | 94 | xml, err := s.FormattedXmp() 95 | log.PanicIf(err) 96 | 97 | fmt.Println(xml) 98 | 99 | os.Exit(0) 100 | } 101 | 102 | if options.JustSimpleIptc == true { 103 | tags, err := sl.Iptc() 104 | log.PanicIf(err) 105 | 106 | distilled := iptc.GetSimpleDictionaryFromParsedTags(tags) 107 | sorted := jpegstructure.SortStringStringMap(distilled) 108 | 109 | for _, pair := range sorted { 110 | fmt.Printf("%s: %s\n", pair[0], pair[1]) 111 | } 112 | 113 | os.Exit(0) 114 | } else if options.JustFullIptc == true { 115 | tags, err := sl.Iptc() 116 | log.PanicIf(err) 117 | 118 | distilled := iptc.GetDictionaryFromParsedTags(tags) 119 | sorted := jpegstructure.SortStringStringMap(distilled) 120 | 121 | for _, pair := range sorted { 122 | fmt.Printf("%s: %s\n", pair[0], pair[1]) 123 | } 124 | 125 | os.Exit(0) 126 | } 127 | 128 | segments := make([]segmentResult, len(sl.Segments())) 129 | segmentIndex := make(map[string][]segmentIndexItem) 130 | 131 | for i, s := range sl.Segments() { 132 | var data []byte 133 | if (options.JsonAsList == true || options.JsonAsObject == true) && options.IncludeData == true { 134 | data = s.Data 135 | } 136 | 137 | segments[i] = segmentResult{ 138 | MarkerId: s.MarkerId, 139 | MarkerName: s.MarkerName, 140 | Offset: s.Offset, 141 | Length: len(s.Data), 142 | Data: data, 143 | } 144 | 145 | sii := segmentIndexItem{ 146 | Offset: s.Offset, 147 | Length: len(s.Data), 148 | Data: data, 149 | } 150 | 151 | if grouped, found := segmentIndex[s.MarkerName]; found == true { 152 | segmentIndex[s.MarkerName] = append(grouped, sii) 153 | } else { 154 | segmentIndex[s.MarkerName] = []segmentIndexItem{sii} 155 | } 156 | } 157 | 158 | if parseErr != nil { 159 | fmt.Printf("JPEG Segments (incomplete due to error):\n") 160 | fmt.Printf("\n") 161 | 162 | sl.Print() 163 | 164 | fmt.Printf("\n") 165 | 166 | log.Panic(parseErr) 167 | } 168 | 169 | if options.JsonAsList == true { 170 | raw, err := json.MarshalIndent(segments, "", " ") 171 | log.PanicIf(err) 172 | 173 | fmt.Println(string(raw)) 174 | } else if options.JsonAsObject == true { 175 | raw, err := json.MarshalIndent(segmentIndex, "", " ") 176 | log.PanicIf(err) 177 | 178 | fmt.Println(string(raw)) 179 | } else { 180 | fmt.Printf("JPEG Segments:\n") 181 | fmt.Printf("\n") 182 | 183 | sl.Print() 184 | 185 | sl.FindXmp() 186 | } 187 | } 188 | -------------------------------------------------------------------------------- /command/js_dump/main_test.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "bytes" 5 | "fmt" 6 | "path" 7 | "testing" 8 | 9 | "encoding/json" 10 | "os/exec" 11 | 12 | "github.com/dsoprea/go-logging" 13 | 14 | "github.com/dsoprea/go-jpeg-image-structure" 15 | ) 16 | 17 | type JsonResultJpegSegmentListItem struct { 18 | MarkerId byte `json:"marker_id"` 19 | MarkerName string `json:"market_name"` 20 | Offset int `json:"offset"` 21 | Data []byte `json:"data"` 22 | } 23 | 24 | type JsonResultJpegSegmentIndexItem struct { 25 | MarkerName string `json:"marker_name"` 26 | Offset int `json:"offset"` 27 | Data []byte `json:"data"` 28 | } 29 | 30 | func TestMain_Plain(t *testing.T) { 31 | imageFilepath := jpegstructure.GetTestImageFilepath() 32 | appFilepath := getAppFilepath() 33 | 34 | cmd := exec.Command( 35 | "go", "run", appFilepath, 36 | "--filepath", imageFilepath) 37 | 38 | b := new(bytes.Buffer) 39 | cmd.Stdout = b 40 | cmd.Stderr = b 41 | 42 | err := cmd.Run() 43 | actual := b.String() 44 | 45 | if err != nil { 46 | fmt.Printf(actual) 47 | panic(err) 48 | } 49 | 50 | expected := 51 | `JPEG Segments: 52 | 53 | 0: OFFSET=(0x00000000 0) ID=(0xd8) NAME=[SOI ] SIZE=( 0) SHA1=[da39a3ee5e6b4b0d3255bfef95601890afd80709] 54 | 1: OFFSET=(0x00000002 2) ID=(0xe1) NAME=[APP1 ] SIZE=( 32942) SHA1=[81dce16a2abe2232049b5aa430ccf4095d240071] [EXIF] 55 | 2: OFFSET=(0x000080b4 32948) ID=(0xe1) NAME=[APP1 ] SIZE=( 2558) SHA1=[b56f13aa6bc3410a7d866302ef51c8b9798113af] [XMP] 56 | 3: OFFSET=(0x00008ab6 35510) ID=(0xdb) NAME=[DQT ] SIZE=( 130) SHA1=[40441c843ce4c8027cbd3dbdc174ac13d7555aec] 57 | 4: OFFSET=(0x00008b3c 35644) ID=(0xc0) NAME=[SOF0 ] SIZE=( 15) SHA1=[2458a7e3cf26aed68a0becb123a0a022c03d1243] 58 | 5: OFFSET=(0x00008b4f 35663) ID=(0xc4) NAME=[DHT ] SIZE=( 416) SHA1=[41b700bdd457862ce170bec95c9dac272e415470] 59 | 6: OFFSET=(0x00008cf3 36083) ID=(0xda) NAME=[SOS ] SIZE=( 0) SHA1=[da39a3ee5e6b4b0d3255bfef95601890afd80709] 60 | 7: OFFSET=(0x00008cf5 36085) ID=(0x00) NAME=[ ] SIZE=( 5554296) SHA1=[16e7465a831a075b096dbd7f2d6f2c931e509edd] 61 | 8: OFFSET=(0x00554d6d 5590381) ID=(0xd9) NAME=[EOI ] SIZE=( 0) SHA1=[da39a3ee5e6b4b0d3255bfef95601890afd80709] 62 | ` 63 | 64 | if actual != expected { 65 | fmt.Printf("ACTUAL:\n%s\n", actual) 66 | fmt.Printf("EXPECTED:\n%s\n", expected) 67 | 68 | t.Fatalf("Output not expected.") 69 | } 70 | } 71 | 72 | func TestMain_Json_NoData(t *testing.T) { 73 | defer func() { 74 | if state := recover(); state != nil { 75 | err := log.Wrap(state.(error)) 76 | log.PrintErrorf(err, "Test failure.") 77 | } 78 | }() 79 | 80 | imageFilepath := jpegstructure.GetTestImageFilepath() 81 | appFilepath := getAppFilepath() 82 | 83 | cmd := exec.Command( 84 | "go", "run", appFilepath, 85 | "--json-list", 86 | "--filepath", imageFilepath) 87 | 88 | b := new(bytes.Buffer) 89 | cmd.Stdout = b 90 | cmd.Stderr = b 91 | 92 | err := cmd.Run() 93 | actual := b.String() 94 | 95 | if err != nil { 96 | fmt.Println(actual) 97 | panic(err) 98 | } 99 | 100 | expected := `[ 101 | { 102 | "marker_id": 216, 103 | "marker_name": "SOI", 104 | "offset": 0, 105 | "data": null, 106 | "length": 0 107 | }, 108 | { 109 | "marker_id": 225, 110 | "marker_name": "APP1", 111 | "offset": 2, 112 | "data": null, 113 | "length": 32942 114 | }, 115 | { 116 | "marker_id": 225, 117 | "marker_name": "APP1", 118 | "offset": 32948, 119 | "data": null, 120 | "length": 2558 121 | }, 122 | { 123 | "marker_id": 219, 124 | "marker_name": "DQT", 125 | "offset": 35510, 126 | "data": null, 127 | "length": 130 128 | }, 129 | { 130 | "marker_id": 192, 131 | "marker_name": "SOF0", 132 | "offset": 35644, 133 | "data": null, 134 | "length": 15 135 | }, 136 | { 137 | "marker_id": 196, 138 | "marker_name": "DHT", 139 | "offset": 35663, 140 | "data": null, 141 | "length": 416 142 | }, 143 | { 144 | "marker_id": 218, 145 | "marker_name": "SOS", 146 | "offset": 36083, 147 | "data": null, 148 | "length": 0 149 | }, 150 | { 151 | "marker_id": 0, 152 | "marker_name": "!SCANDATA", 153 | "offset": 36085, 154 | "data": null, 155 | "length": 5554296 156 | }, 157 | { 158 | "marker_id": 217, 159 | "marker_name": "EOI", 160 | "offset": 5590381, 161 | "data": null, 162 | "length": 0 163 | } 164 | ] 165 | ` 166 | 167 | if actual != expected { 168 | fmt.Printf("ACTUAL:\n%s\n\nEXPECTED:\n%s\n", actual, expected) 169 | 170 | t.Fatalf("output not expected.") 171 | } 172 | } 173 | 174 | func TestMain_Json_NoData_SegmentIndex(t *testing.T) { 175 | imageFilepath := jpegstructure.GetTestImageFilepath() 176 | appFilepath := getAppFilepath() 177 | 178 | cmd := exec.Command( 179 | "go", "run", appFilepath, 180 | "--json-object", 181 | "--filepath", imageFilepath) 182 | 183 | b := new(bytes.Buffer) 184 | cmd.Stdout = b 185 | cmd.Stderr = b 186 | 187 | err := cmd.Run() 188 | actual := b.String() 189 | 190 | if err != nil { 191 | fmt.Println(actual) 192 | panic(err) 193 | } 194 | 195 | expected := `{ 196 | "!SCANDATA": [ 197 | { 198 | "offset": 36085, 199 | "data": null, 200 | "length": 5554296 201 | } 202 | ], 203 | "APP1": [ 204 | { 205 | "offset": 2, 206 | "data": null, 207 | "length": 32942 208 | }, 209 | { 210 | "offset": 32948, 211 | "data": null, 212 | "length": 2558 213 | } 214 | ], 215 | "DHT": [ 216 | { 217 | "offset": 35663, 218 | "data": null, 219 | "length": 416 220 | } 221 | ], 222 | "DQT": [ 223 | { 224 | "offset": 35510, 225 | "data": null, 226 | "length": 130 227 | } 228 | ], 229 | "EOI": [ 230 | { 231 | "offset": 5590381, 232 | "data": null, 233 | "length": 0 234 | } 235 | ], 236 | "SOF0": [ 237 | { 238 | "offset": 35644, 239 | "data": null, 240 | "length": 15 241 | } 242 | ], 243 | "SOI": [ 244 | { 245 | "offset": 0, 246 | "data": null, 247 | "length": 0 248 | } 249 | ], 250 | "SOS": [ 251 | { 252 | "offset": 36083, 253 | "data": null, 254 | "length": 0 255 | } 256 | ] 257 | } 258 | ` 259 | 260 | if actual != expected { 261 | fmt.Printf("ACTUAL:\n%s\n\nEXPECTED:\n%s\n", actual, expected) 262 | 263 | t.Fatalf("output not expected.") 264 | } 265 | } 266 | 267 | func TestMain_Json_Data(t *testing.T) { 268 | imageFilepath := jpegstructure.GetTestImageFilepath() 269 | appFilepath := getAppFilepath() 270 | 271 | cmd := exec.Command( 272 | "go", "run", appFilepath, 273 | "--json-list", 274 | "--data", 275 | "--filepath", imageFilepath) 276 | 277 | b := new(bytes.Buffer) 278 | cmd.Stdout = b 279 | cmd.Stderr = b 280 | 281 | err := cmd.Run() 282 | raw := b.Bytes() 283 | 284 | if err != nil { 285 | fmt.Printf(string(raw)) 286 | panic(err) 287 | } 288 | 289 | result := make([]JsonResultJpegSegmentListItem, 0) 290 | 291 | err = json.Unmarshal(raw, &result) 292 | log.PanicIf(err) 293 | 294 | if len(result) != 9 { 295 | t.Fatalf("JPEG segment count not correct: (%d)", len(result)) 296 | } 297 | 298 | hasData := false 299 | for _, s := range result { 300 | if s.Data != nil { 301 | hasData = true 302 | break 303 | } 304 | } 305 | 306 | if hasData != true { 307 | t.Fatalf("No segments have data but were expected to.") 308 | } 309 | } 310 | 311 | func getAppFilepath() string { 312 | moduleRootPath := jpegstructure.GetModuleRootPath() 313 | return path.Join(moduleRootPath, "command", "js_dump", "main.go") 314 | } 315 | -------------------------------------------------------------------------------- /command/js_exif/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | "os" 6 | 7 | "encoding/json" 8 | "io/ioutil" 9 | 10 | "github.com/dsoprea/go-exif/v2" 11 | "github.com/dsoprea/go-jpeg-image-structure" 12 | "github.com/dsoprea/go-logging" 13 | "github.com/jessevdk/go-flags" 14 | ) 15 | 16 | var ( 17 | options = &struct { 18 | Filepath string `short:"f" long:"filepath" required:"true" description:"File-path of JPEG image ('-' for STDIN)"` 19 | Json bool `short:"j" long:"json" description:"Print as JSON"` 20 | DoPrintVerbose bool `short:"v" long:"verbose" description:"Print logging"` 21 | }{} 22 | ) 23 | 24 | func main() { 25 | defer func() { 26 | if errRaw := recover(); errRaw != nil { 27 | err := errRaw.(error) 28 | log.PrintError(err) 29 | 30 | os.Exit(-2) 31 | } 32 | }() 33 | 34 | _, err := flags.Parse(options) 35 | if err != nil { 36 | os.Exit(-1) 37 | } 38 | 39 | if options.DoPrintVerbose == true { 40 | cla := log.NewConsoleLogAdapter() 41 | log.AddAdapter("console", cla) 42 | 43 | scp := log.NewStaticConfigurationProvider() 44 | scp.SetLevelName(log.LevelNameDebug) 45 | 46 | log.LoadConfiguration(scp) 47 | } 48 | 49 | var data []byte 50 | if options.Filepath == "-" { 51 | var err error 52 | data, err = ioutil.ReadAll(os.Stdin) 53 | log.PanicIf(err) 54 | } else { 55 | var err error 56 | data, err = ioutil.ReadFile(options.Filepath) 57 | log.PanicIf(err) 58 | } 59 | 60 | jmp := jpegstructure.NewJpegMediaParser() 61 | 62 | intfc, parseErr := jmp.ParseBytes(data) 63 | 64 | var et []exif.ExifTag 65 | if intfc != nil { 66 | // If the parse failed, we should always still get all of the segments 67 | // that we've encountered so far. It should never be empty, and it 68 | // should be impossible for it to be `nil`. So, if the parse failed but 69 | // we still found EXIF data, just ignore the failure and proceed. We had 70 | // still got what we needed. 71 | 72 | sl := intfc.(*jpegstructure.SegmentList) 73 | 74 | var err error 75 | _, _, et, err = sl.DumpExif() 76 | 77 | // There was a parse error and we couldn't find/parse EXIF data. If the 78 | // extraction had already failed above and we were just trying for a 79 | // contingency, fail with that error first. 80 | if err != nil { 81 | if parseErr != nil { 82 | log.Panic(parseErr) 83 | } else { 84 | log.Panic(err) 85 | } 86 | } 87 | } else if parseErr == nil { 88 | // We should never get a `nil` `intfc` value back *and* a `nil` 89 | // `parseErr`. 90 | log.Panicf("could not parse JPEG even partially") 91 | } else { 92 | log.Panic(parseErr) 93 | } 94 | 95 | // If we get here, we either parsed the JPEG file well or at least parsed 96 | // enough to find EXIF data. 97 | 98 | if et == nil { 99 | // The JPEG image parsed fine (if it didn't and we haven't yet 100 | // terminated, we already extracted the EXIF tags above). 101 | 102 | sl := intfc.(*jpegstructure.SegmentList) 103 | 104 | var err error 105 | 106 | _, _, et, err = sl.DumpExif() 107 | if err != nil { 108 | if err == exif.ErrNoExif { 109 | fmt.Printf("No EXIF.\n") 110 | os.Exit(10) 111 | } 112 | 113 | log.Panic(err) 114 | } 115 | } 116 | 117 | if options.Json == true { 118 | raw, err := json.MarshalIndent(et, " ", " ") 119 | log.PanicIf(err) 120 | 121 | fmt.Println(string(raw)) 122 | } else { 123 | if len(et) == 0 { 124 | fmt.Printf("EXIF data is present but empty.\n") 125 | } else { 126 | for i, tag := range et { 127 | // Since we dump the complete value, the thumbnails introduce 128 | // too much noise. 129 | if (tag.TagId == exif.ThumbnailOffsetTagId || tag.TagId == exif.ThumbnailSizeTagId) && tag.IfdPath == exif.ThumbnailFqIfdPath { 130 | continue 131 | } 132 | 133 | fmt.Printf("%2d: IFD-PATH=[%s] ID=(0x%04x) NAME=[%s] TYPE=(%d):[%s] VALUE=[%v]", i, tag.IfdPath, tag.TagId, tag.TagName, tag.TagTypeId, tag.TagTypeName, tag.FormattedFirst) 134 | 135 | if tag.ChildIfdPath != "" { 136 | fmt.Printf(" CHILD-IFD-PATH=[%s]", tag.ChildIfdPath) 137 | } 138 | 139 | fmt.Printf("\n") 140 | } 141 | } 142 | } 143 | } 144 | -------------------------------------------------------------------------------- /command/js_exif/main_test.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "bytes" 5 | "fmt" 6 | "path" 7 | "testing" 8 | 9 | "encoding/json" 10 | "os/exec" 11 | 12 | "github.com/dsoprea/go-logging" 13 | 14 | "github.com/dsoprea/go-jpeg-image-structure" 15 | ) 16 | 17 | var ( 18 | assetsPath = "" 19 | appFilepath = "" 20 | ) 21 | 22 | type JsonResultExifTag struct { 23 | MarkerId byte `json:"marker_id"` 24 | MarkerName string `json:"market_name"` 25 | Offset int `json:"offset"` 26 | Data []byte `json:"data"` 27 | } 28 | 29 | func TestMain_Plain_Exif(t *testing.T) { 30 | appFilepath := getAppFilepath() 31 | imageFilepath := jpegstructure.GetTestImageFilepath() 32 | 33 | cmd := exec.Command( 34 | "go", "run", appFilepath, 35 | "--filepath", imageFilepath) 36 | 37 | b := new(bytes.Buffer) 38 | cmd.Stdout = b 39 | cmd.Stderr = b 40 | 41 | err := cmd.Run() 42 | actual := b.String() 43 | 44 | if err != nil { 45 | fmt.Printf(actual) 46 | panic(err) 47 | } 48 | 49 | expected := 50 | ` 0: IFD-PATH=[IFD] ID=(0x010f) NAME=[Make] TYPE=(2):[ASCII] VALUE=[Canon] 51 | 1: IFD-PATH=[IFD] ID=(0x0110) NAME=[Model] TYPE=(2):[ASCII] VALUE=[Canon EOS 5D Mark III] 52 | 2: IFD-PATH=[IFD] ID=(0x0112) NAME=[Orientation] TYPE=(3):[SHORT] VALUE=[1] 53 | 3: IFD-PATH=[IFD] ID=(0x011a) NAME=[XResolution] TYPE=(5):[RATIONAL] VALUE=[72/1] 54 | 4: IFD-PATH=[IFD] ID=(0x011b) NAME=[YResolution] TYPE=(5):[RATIONAL] VALUE=[72/1] 55 | 5: IFD-PATH=[IFD] ID=(0x0128) NAME=[ResolutionUnit] TYPE=(3):[SHORT] VALUE=[2] 56 | 6: IFD-PATH=[IFD] ID=(0x0132) NAME=[DateTime] TYPE=(2):[ASCII] VALUE=[2017:12:02 08:18:50] 57 | 7: IFD-PATH=[IFD] ID=(0x013b) NAME=[Artist] TYPE=(2):[ASCII] VALUE=[] 58 | 8: IFD-PATH=[IFD] ID=(0x0213) NAME=[YCbCrPositioning] TYPE=(3):[SHORT] VALUE=[2] 59 | 9: IFD-PATH=[IFD] ID=(0x8298) NAME=[Copyright] TYPE=(2):[ASCII] VALUE=[] 60 | 10: IFD-PATH=[IFD] ID=(0x8769) NAME=[ExifTag] TYPE=(4):[LONG] VALUE=[360] CHILD-IFD-PATH=[IFD/Exif] 61 | 11: IFD-PATH=[IFD/Exif] ID=(0x829a) NAME=[ExposureTime] TYPE=(5):[RATIONAL] VALUE=[1/640] 62 | 12: IFD-PATH=[IFD/Exif] ID=(0x829d) NAME=[FNumber] TYPE=(5):[RATIONAL] VALUE=[4/1] 63 | 13: IFD-PATH=[IFD/Exif] ID=(0x8822) NAME=[ExposureProgram] TYPE=(3):[SHORT] VALUE=[4] 64 | 14: IFD-PATH=[IFD/Exif] ID=(0x8827) NAME=[ISOSpeedRatings] TYPE=(3):[SHORT] VALUE=[1600] 65 | 15: IFD-PATH=[IFD/Exif] ID=(0x8830) NAME=[SensitivityType] TYPE=(3):[SHORT] VALUE=[2] 66 | 16: IFD-PATH=[IFD/Exif] ID=(0x8832) NAME=[RecommendedExposureIndex] TYPE=(4):[LONG] VALUE=[1600] 67 | 17: IFD-PATH=[IFD/Exif] ID=(0x9000) NAME=[ExifVersion] TYPE=(7):[UNDEFINED] VALUE=[0230] 68 | 18: IFD-PATH=[IFD/Exif] ID=(0x9003) NAME=[DateTimeOriginal] TYPE=(2):[ASCII] VALUE=[2017:12:02 08:18:50] 69 | 19: IFD-PATH=[IFD/Exif] ID=(0x9004) NAME=[DateTimeDigitized] TYPE=(2):[ASCII] VALUE=[2017:12:02 08:18:50] 70 | 20: IFD-PATH=[IFD/Exif] ID=(0x9101) NAME=[ComponentsConfiguration] TYPE=(7):[UNDEFINED] VALUE=[Exif9101ComponentsConfiguration] 71 | 21: IFD-PATH=[IFD/Exif] ID=(0x9201) NAME=[ShutterSpeedValue] TYPE=(10):[SRATIONAL] VALUE=[614400/65536] 72 | 22: IFD-PATH=[IFD/Exif] ID=(0x9202) NAME=[ApertureValue] TYPE=(5):[RATIONAL] VALUE=[262144/65536] 73 | 23: IFD-PATH=[IFD/Exif] ID=(0x9204) NAME=[ExposureBiasValue] TYPE=(10):[SRATIONAL] VALUE=[0/1] 74 | 24: IFD-PATH=[IFD/Exif] ID=(0x9207) NAME=[MeteringMode] TYPE=(3):[SHORT] VALUE=[5] 75 | 25: IFD-PATH=[IFD/Exif] ID=(0x9209) NAME=[Flash] TYPE=(3):[SHORT] VALUE=[16] 76 | 26: IFD-PATH=[IFD/Exif] ID=(0x920a) NAME=[FocalLength] TYPE=(5):[RATIONAL] VALUE=[16/1] 77 | 27: IFD-PATH=[IFD/Exif] ID=(0x927c) NAME=[MakerNote] TYPE=(7):[UNDEFINED] VALUE=[MakerNote] 78 | 28: IFD-PATH=[IFD/Exif] ID=(0x9286) NAME=[UserComment] TYPE=(7):[UNDEFINED] VALUE=[UserComment] 79 | 29: IFD-PATH=[IFD/Exif] ID=(0x9290) NAME=[SubSecTime] TYPE=(2):[ASCII] VALUE=[00] 80 | 30: IFD-PATH=[IFD/Exif] ID=(0x9291) NAME=[SubSecTimeOriginal] TYPE=(2):[ASCII] VALUE=[00] 81 | 31: IFD-PATH=[IFD/Exif] ID=(0x9292) NAME=[SubSecTimeDigitized] TYPE=(2):[ASCII] VALUE=[00] 82 | 32: IFD-PATH=[IFD/Exif] ID=(0xa000) NAME=[FlashpixVersion] TYPE=(7):[UNDEFINED] VALUE=[0100] 83 | 33: IFD-PATH=[IFD/Exif] ID=(0xa001) NAME=[ColorSpace] TYPE=(3):[SHORT] VALUE=[1] 84 | 34: IFD-PATH=[IFD/Exif] ID=(0xa002) NAME=[PixelXDimension] TYPE=(3):[SHORT] VALUE=[3840] 85 | 35: IFD-PATH=[IFD/Exif] ID=(0xa003) NAME=[PixelYDimension] TYPE=(3):[SHORT] VALUE=[2560] 86 | 36: IFD-PATH=[IFD/Exif] ID=(0xa005) NAME=[InteroperabilityTag] TYPE=(4):[LONG] VALUE=[9326] CHILD-IFD-PATH=[IFD/Exif/Iop] 87 | 37: IFD-PATH=[IFD/Exif/Iop] ID=(0x0001) NAME=[InteroperabilityIndex] TYPE=(2):[ASCII] VALUE=[R98] 88 | 38: IFD-PATH=[IFD/Exif/Iop] ID=(0x0002) NAME=[InteroperabilityVersion] TYPE=(7):[UNDEFINED] VALUE=[0100] 89 | 39: IFD-PATH=[IFD/Exif] ID=(0xa20e) NAME=[FocalPlaneXResolution] TYPE=(5):[RATIONAL] VALUE=[3840000/1461] 90 | 40: IFD-PATH=[IFD/Exif] ID=(0xa20f) NAME=[FocalPlaneYResolution] TYPE=(5):[RATIONAL] VALUE=[2560000/972] 91 | 41: IFD-PATH=[IFD/Exif] ID=(0xa210) NAME=[FocalPlaneResolutionUnit] TYPE=(3):[SHORT] VALUE=[2] 92 | 42: IFD-PATH=[IFD/Exif] ID=(0xa401) NAME=[CustomRendered] TYPE=(3):[SHORT] VALUE=[0] 93 | 43: IFD-PATH=[IFD/Exif] ID=(0xa402) NAME=[ExposureMode] TYPE=(3):[SHORT] VALUE=[0] 94 | 44: IFD-PATH=[IFD/Exif] ID=(0xa403) NAME=[WhiteBalance] TYPE=(3):[SHORT] VALUE=[0] 95 | 45: IFD-PATH=[IFD/Exif] ID=(0xa406) NAME=[SceneCaptureType] TYPE=(3):[SHORT] VALUE=[0] 96 | 46: IFD-PATH=[IFD/Exif] ID=(0xa430) NAME=[CameraOwnerName] TYPE=(2):[ASCII] VALUE=[] 97 | 47: IFD-PATH=[IFD/Exif] ID=(0xa431) NAME=[BodySerialNumber] TYPE=(2):[ASCII] VALUE=[063024020097] 98 | 48: IFD-PATH=[IFD/Exif] ID=(0xa432) NAME=[LensSpecification] TYPE=(5):[RATIONAL] VALUE=[16/1...] 99 | 49: IFD-PATH=[IFD/Exif] ID=(0xa434) NAME=[LensModel] TYPE=(2):[ASCII] VALUE=[EF16-35mm f/4L IS USM] 100 | 50: IFD-PATH=[IFD/Exif] ID=(0xa435) NAME=[LensSerialNumber] TYPE=(2):[ASCII] VALUE=[2400001068] 101 | 51: IFD-PATH=[IFD] ID=(0x8825) NAME=[GPSTag] TYPE=(4):[LONG] VALUE=[9554] CHILD-IFD-PATH=[IFD/GPSInfo] 102 | 52: IFD-PATH=[IFD/GPSInfo] ID=(0x0000) NAME=[GPSVersionID] TYPE=(1):[BYTE] VALUE=[02 03 00 00] 103 | 53: IFD-PATH=[IFD1] ID=(0x0103) NAME=[Compression] TYPE=(3):[SHORT] VALUE=[6] 104 | 54: IFD-PATH=[IFD1] ID=(0x011a) NAME=[XResolution] TYPE=(5):[RATIONAL] VALUE=[72/1] 105 | 55: IFD-PATH=[IFD1] ID=(0x011b) NAME=[YResolution] TYPE=(5):[RATIONAL] VALUE=[72/1] 106 | 56: IFD-PATH=[IFD1] ID=(0x0128) NAME=[ResolutionUnit] TYPE=(3):[SHORT] VALUE=[2] 107 | ` 108 | 109 | if actual != expected { 110 | fmt.Printf("ACTUAL:\n%s\n", actual) 111 | fmt.Printf("EXPECTED:\n%s\n", expected) 112 | 113 | t.Fatalf("Output not expected.") 114 | } 115 | } 116 | 117 | func TestMain_Json_Exif(t *testing.T) { 118 | appFilepath := getAppFilepath() 119 | imageFilepath := jpegstructure.GetTestImageFilepath() 120 | 121 | cmd := exec.Command( 122 | "go", "run", appFilepath, 123 | "--json", 124 | "--filepath", imageFilepath) 125 | 126 | b := new(bytes.Buffer) 127 | cmd.Stdout = b 128 | cmd.Stderr = b 129 | 130 | err := cmd.Run() 131 | raw := b.Bytes() 132 | 133 | if err != nil { 134 | fmt.Printf(string(raw)) 135 | panic(err) 136 | } 137 | 138 | result := make([]JsonResultExifTag, 0) 139 | 140 | err = json.Unmarshal(raw, &result) 141 | log.PanicIf(err) 142 | 143 | // TODO(dustin): !! Store the expected JSON in a file. 144 | 145 | if len(result) != 59 { 146 | t.Fatalf("Exif tag-count not correct: (%d)", len(result)) 147 | } 148 | } 149 | 150 | func getAppFilepath() string { 151 | moduleRootPath := jpegstructure.GetModuleRootPath() 152 | return path.Join(moduleRootPath, "command", "js_exif", "main.go") 153 | } 154 | -------------------------------------------------------------------------------- /command/js_exif_drop/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | "os" 6 | 7 | "io/ioutil" 8 | 9 | "github.com/dsoprea/go-jpeg-image-structure" 10 | "github.com/dsoprea/go-logging" 11 | "github.com/jessevdk/go-flags" 12 | ) 13 | 14 | var ( 15 | options = &struct { 16 | InputFilepath string `short:"f" long:"input-filepath" required:"true" description:"File-path of JPEG image to read"` 17 | OutputFilepath string `short:"o" long:"output-filepath" description:"File-path of JPEG image to write (if not provided, then the input JPEG will be used)"` 18 | }{} 19 | ) 20 | 21 | func main() { 22 | _, err := flags.Parse(options) 23 | if err != nil { 24 | os.Exit(-1) 25 | } 26 | 27 | data, err := ioutil.ReadFile(options.InputFilepath) 28 | log.PanicIf(err) 29 | 30 | jmp := jpegstructure.NewJpegMediaParser() 31 | 32 | intfc, err := jmp.ParseBytes(data) 33 | log.PanicIf(err) 34 | 35 | sl := intfc.(*jpegstructure.SegmentList) 36 | 37 | wasDropped, err := sl.DropExif() 38 | log.PanicIf(err) 39 | 40 | fmt.Printf("%v\n", wasDropped) 41 | 42 | if wasDropped == false { 43 | os.Exit(10) 44 | } 45 | 46 | outputFilepath := options.OutputFilepath 47 | if outputFilepath == "" { 48 | outputFilepath = options.InputFilepath 49 | } 50 | 51 | f, err := os.OpenFile(outputFilepath, os.O_CREATE|os.O_WRONLY, 0644) 52 | log.PanicIf(err) 53 | 54 | defer f.Close() 55 | 56 | err = sl.Write(f) 57 | log.PanicIf(err) 58 | } 59 | -------------------------------------------------------------------------------- /command/js_exif_drop/main_test.go.ignore: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "bytes" 5 | "fmt" 6 | "os" 7 | "path" 8 | "testing" 9 | 10 | "encoding/json" 11 | "os/exec" 12 | 13 | "github.com/dsoprea/go-logging" 14 | ) 15 | 16 | var ( 17 | assetsPath = "" 18 | appFilepath = "" 19 | ) 20 | 21 | type JsonResultExifTag struct { 22 | MarkerId byte `json:"marker_id"` 23 | MarkerName string `json:"market_name"` 24 | Offset int `json:"offset"` 25 | Data []byte `json:"data"` 26 | } 27 | 28 | func TestMain_Plain_Exif(t *testing.T) { 29 | appFilepath := getAppFilepath() 30 | imageFilepath := getTestImageFilepath() 31 | 32 | cmd := exec.Command( 33 | "go", "run", appFilepath, 34 | "--filepath", imageFilepath) 35 | 36 | b := new(bytes.Buffer) 37 | cmd.Stdout = b 38 | cmd.Stderr = b 39 | 40 | err := cmd.Run() 41 | actual := b.String() 42 | 43 | if err != nil { 44 | fmt.Printf(actual) 45 | panic(err) 46 | } 47 | 48 | expected := 49 | ` 0: IFD-PATH=[IFD] ID=(0x10f) NAME=[Make] TYPE=(2):[ASCII] VALUE=[Canon] 50 | 1: IFD-PATH=[IFD] ID=(0x110) NAME=[Model] TYPE=(2):[ASCII] VALUE=[Canon EOS 5D Mark III] 51 | 2: IFD-PATH=[IFD] ID=(0x112) NAME=[Orientation] TYPE=(3):[SHORT] VALUE=[[1]] 52 | 3: IFD-PATH=[IFD] ID=(0x11a) NAME=[XResolution] TYPE=(5):[RATIONAL] VALUE=[[{72 1}]] 53 | 4: IFD-PATH=[IFD] ID=(0x11b) NAME=[YResolution] TYPE=(5):[RATIONAL] VALUE=[[{72 1}]] 54 | 5: IFD-PATH=[IFD] ID=(0x128) NAME=[ResolutionUnit] TYPE=(3):[SHORT] VALUE=[[2]] 55 | 6: IFD-PATH=[IFD] ID=(0x132) NAME=[DateTime] TYPE=(2):[ASCII] VALUE=[2017:12:02 08:18:50] 56 | 7: IFD-PATH=[IFD] ID=(0x13b) NAME=[Artist] TYPE=(2):[ASCII] VALUE=[] 57 | 8: IFD-PATH=[IFD] ID=(0x213) NAME=[YCbCrPositioning] TYPE=(3):[SHORT] VALUE=[[2]] 58 | 9: IFD-PATH=[IFD] ID=(0x8298) NAME=[Copyright] TYPE=(2):[ASCII] VALUE=[] 59 | 10: IFD-PATH=[IFD] ID=(0x8769) NAME=[ExifTag] TYPE=(4):[LONG] VALUE=[[360]] CHILD-IFD-PATH=[IFD/Exif] 60 | 11: IFD-PATH=[IFD] ID=(0x8825) NAME=[GPSTag] TYPE=(4):[LONG] VALUE=[[9554]] CHILD-IFD-PATH=[IFD/GPSInfo] 61 | 12: IFD-PATH=[IFD/Exif] ID=(0x829a) NAME=[ExposureTime] TYPE=(5):[RATIONAL] VALUE=[[{1 640}]] 62 | 13: IFD-PATH=[IFD/Exif] ID=(0x829d) NAME=[FNumber] TYPE=(5):[RATIONAL] VALUE=[[{4 1}]] 63 | 14: IFD-PATH=[IFD/Exif] ID=(0x8822) NAME=[ExposureProgram] TYPE=(3):[SHORT] VALUE=[[4]] 64 | 15: IFD-PATH=[IFD/Exif] ID=(0x8827) NAME=[ISOSpeedRatings] TYPE=(3):[SHORT] VALUE=[[1600]] 65 | 16: IFD-PATH=[IFD/Exif] ID=(0x8830) NAME=[SensitivityType] TYPE=(3):[SHORT] VALUE=[[2]] 66 | 17: IFD-PATH=[IFD/Exif] ID=(0x8832) NAME=[RecommendedExposureIndex] TYPE=(4):[LONG] VALUE=[[1600]] 67 | 18: IFD-PATH=[IFD/Exif] ID=(0x9000) NAME=[ExifVersion] TYPE=(7):[UNDEFINED] VALUE=[0230] 68 | 19: IFD-PATH=[IFD/Exif] ID=(0x9003) NAME=[DateTimeOriginal] TYPE=(2):[ASCII] VALUE=[2017:12:02 08:18:50] 69 | 20: IFD-PATH=[IFD/Exif] ID=(0x9004) NAME=[DateTimeDigitized] TYPE=(2):[ASCII] VALUE=[2017:12:02 08:18:50] 70 | 21: IFD-PATH=[IFD/Exif] ID=(0x9101) NAME=[ComponentsConfiguration] TYPE=(7):[UNDEFINED] VALUE=[ComponentsConfiguration] 71 | 22: IFD-PATH=[IFD/Exif] ID=(0x9201) NAME=[ShutterSpeedValue] TYPE=(10):[SRATIONAL] VALUE=[[{614400 65536}]] 72 | 23: IFD-PATH=[IFD/Exif] ID=(0x9202) NAME=[ApertureValue] TYPE=(5):[RATIONAL] VALUE=[[{262144 65536}]] 73 | 24: IFD-PATH=[IFD/Exif] ID=(0x9204) NAME=[ExposureBiasValue] TYPE=(10):[SRATIONAL] VALUE=[[{0 1}]] 74 | 25: IFD-PATH=[IFD/Exif] ID=(0x9207) NAME=[MeteringMode] TYPE=(3):[SHORT] VALUE=[[5]] 75 | 26: IFD-PATH=[IFD/Exif] ID=(0x9209) NAME=[Flash] TYPE=(3):[SHORT] VALUE=[[16]] 76 | 27: IFD-PATH=[IFD/Exif] ID=(0x920a) NAME=[FocalLength] TYPE=(5):[RATIONAL] VALUE=[[{16 1}]] 77 | 28: IFD-PATH=[IFD/Exif] ID=(0x927c) NAME=[MakerNote] TYPE=(7):[UNDEFINED] VALUE=[MakerNote] 78 | 29: IFD-PATH=[IFD/Exif] ID=(0x9286) NAME=[UserComment] TYPE=(7):[UNDEFINED] VALUE=[UserComment] 79 | 30: IFD-PATH=[IFD/Exif] ID=(0x9290) NAME=[SubSecTime] TYPE=(2):[ASCII] VALUE=[00] 80 | 31: IFD-PATH=[IFD/Exif] ID=(0x9291) NAME=[SubSecTimeOriginal] TYPE=(2):[ASCII] VALUE=[00] 81 | 32: IFD-PATH=[IFD/Exif] ID=(0x9292) NAME=[SubSecTimeDigitized] TYPE=(2):[ASCII] VALUE=[00] 82 | 33: IFD-PATH=[IFD/Exif] ID=(0xa000) NAME=[FlashpixVersion] TYPE=(7):[UNDEFINED] VALUE=[0100] 83 | 34: IFD-PATH=[IFD/Exif] ID=(0xa001) NAME=[ColorSpace] TYPE=(3):[SHORT] VALUE=[[1]] 84 | 35: IFD-PATH=[IFD/Exif] ID=(0xa002) NAME=[PixelXDimension] TYPE=(3):[SHORT] VALUE=[[3840]] 85 | 36: IFD-PATH=[IFD/Exif] ID=(0xa003) NAME=[PixelYDimension] TYPE=(3):[SHORT] VALUE=[[2560]] 86 | 37: IFD-PATH=[IFD/Exif] ID=(0xa005) NAME=[InteroperabilityTag] TYPE=(4):[LONG] VALUE=[[9326]] CHILD-IFD-PATH=[IFD/Exif/Iop] 87 | 38: IFD-PATH=[IFD/Exif] ID=(0xa20e) NAME=[FocalPlaneXResolution] TYPE=(5):[RATIONAL] VALUE=[[{3840000 1461}]] 88 | 39: IFD-PATH=[IFD/Exif] ID=(0xa20f) NAME=[FocalPlaneYResolution] TYPE=(5):[RATIONAL] VALUE=[[{2560000 972}]] 89 | 40: IFD-PATH=[IFD/Exif] ID=(0xa210) NAME=[FocalPlaneResolutionUnit] TYPE=(3):[SHORT] VALUE=[[2]] 90 | 41: IFD-PATH=[IFD/Exif] ID=(0xa401) NAME=[CustomRendered] TYPE=(3):[SHORT] VALUE=[[0]] 91 | 42: IFD-PATH=[IFD/Exif] ID=(0xa402) NAME=[ExposureMode] TYPE=(3):[SHORT] VALUE=[[0]] 92 | 43: IFD-PATH=[IFD/Exif] ID=(0xa403) NAME=[WhiteBalance] TYPE=(3):[SHORT] VALUE=[[0]] 93 | 44: IFD-PATH=[IFD/Exif] ID=(0xa406) NAME=[SceneCaptureType] TYPE=(3):[SHORT] VALUE=[[0]] 94 | 45: IFD-PATH=[IFD/Exif] ID=(0xa430) NAME=[CameraOwnerName] TYPE=(2):[ASCII] VALUE=[] 95 | 46: IFD-PATH=[IFD/Exif] ID=(0xa431) NAME=[BodySerialNumber] TYPE=(2):[ASCII] VALUE=[063024020097] 96 | 47: IFD-PATH=[IFD/Exif] ID=(0xa432) NAME=[LensSpecification] TYPE=(5):[RATIONAL] VALUE=[[{16 1} {35 1} {0 1} {0 1}]] 97 | 48: IFD-PATH=[IFD/Exif] ID=(0xa434) NAME=[LensModel] TYPE=(2):[ASCII] VALUE=[EF16-35mm f/4L IS USM] 98 | 49: IFD-PATH=[IFD/Exif] ID=(0xa435) NAME=[LensSerialNumber] TYPE=(2):[ASCII] VALUE=[2400001068] 99 | 50: IFD-PATH=[IFD/GPSInfo] ID=(0x00) NAME=[GPSVersionID] TYPE=(1):[BYTE] VALUE=[[2 3 0 0]] 100 | 51: IFD-PATH=[IFD] ID=(0x103) NAME=[Compression] TYPE=(3):[SHORT] VALUE=[[6]] 101 | 52: IFD-PATH=[IFD] ID=(0x11a) NAME=[XResolution] TYPE=(5):[RATIONAL] VALUE=[[{72 1}]] 102 | 53: IFD-PATH=[IFD] ID=(0x11b) NAME=[YResolution] TYPE=(5):[RATIONAL] VALUE=[[{72 1}]] 103 | 54: IFD-PATH=[IFD] ID=(0x128) NAME=[ResolutionUnit] TYPE=(3):[SHORT] VALUE=[[2]] 104 | 55: IFD-PATH=[IFD/Exif/Iop] ID=(0x01) NAME=[InteroperabilityIndex] TYPE=(2):[ASCII] VALUE=[R98] 105 | 56: IFD-PATH=[IFD/Exif/Iop] ID=(0x02) NAME=[InteroperabilityVersion] TYPE=(7):[UNDEFINED] VALUE=[0100] 106 | ` 107 | 108 | if actual != expected { 109 | fmt.Printf("ACTUAL:\n%s\n", actual) 110 | fmt.Printf("EXPECTED:\n%s\n", expected) 111 | 112 | t.Fatalf("Output not expected.") 113 | } 114 | } 115 | 116 | func TestMain_Json_Exif(t *testing.T) { 117 | appFilepath := getAppFilepath() 118 | imageFilepath := getTestImageFilepath() 119 | 120 | cmd := exec.Command( 121 | "go", "run", appFilepath, 122 | "--json", 123 | "--filepath", imageFilepath) 124 | 125 | b := new(bytes.Buffer) 126 | cmd.Stdout = b 127 | cmd.Stderr = b 128 | 129 | err := cmd.Run() 130 | raw := b.Bytes() 131 | 132 | if err != nil { 133 | fmt.Printf(string(raw)) 134 | panic(err) 135 | } 136 | 137 | result := make([]JsonResultExifTag, 0) 138 | 139 | err = json.Unmarshal(raw, &result) 140 | log.PanicIf(err) 141 | 142 | // TODO(dustin): !! Store the expected JSON in a file. 143 | 144 | if len(result) != 57 { 145 | t.Fatalf("Exif tag-count not correct: (%d)", len(result)) 146 | } 147 | } 148 | 149 | func getTestAssetsPath() string { 150 | if assetsPath == "" { 151 | moduleRootPath := GetModuleRootPath() 152 | assetsPath = path.Join(moduleRootPath, "assets") 153 | } 154 | 155 | return assetsPath 156 | } 157 | 158 | func getTestImageFilepath() string { 159 | assetsPath := getTestAssetsPath() 160 | return path.Join(assetsPath, "NDM_8901.jpg") 161 | } 162 | 163 | func getAppFilepath() string { 164 | moduleRootPath := GetModuleRootPath() 165 | return path.Join(moduleRootPath, "command", "js_exif", "main.go") 166 | } 167 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/dsoprea/go-jpeg-image-structure 2 | 3 | go 1.13 4 | 5 | // Development only 6 | // replace github.com/dsoprea/go-utility => ../go-utility 7 | // replace github.com/dsoprea/go-logging => ../go-logging 8 | // replace github.com/dsoprea/go-exif/v2 => ../go-exif/v2 9 | // replace github.com/dsoprea/go-photoshop-info-format => ../go-photoshop-info-format 10 | // replace github.com/dsoprea/go-iptc => ../go-iptc 11 | 12 | require ( 13 | github.com/dsoprea/go-exif/v2 v2.0.0-20200604193436-ca8584a0e1c4 14 | github.com/dsoprea/go-exif/v3 v3.0.0-20210512043655-120bcdb2a55e // indirect 15 | github.com/dsoprea/go-iptc v0.0.0-20200609062250-162ae6b44feb 16 | github.com/dsoprea/go-logging v0.0.0-20200517223158-a10564966e9d 17 | github.com/dsoprea/go-photoshop-info-format v0.0.0-20200609050348-3db9b63b202c 18 | github.com/dsoprea/go-utility v0.0.0-20200711062821-fab8125e9bdf 19 | github.com/go-xmlfmt/xmlfmt v0.0.0-20191208150333-d5b6f63a941b 20 | github.com/jessevdk/go-flags v1.4.0 21 | golang.org/x/net v0.0.0-20200520182314-0ba52f642ac2 // indirect 22 | ) 23 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | github.com/dsoprea/go-exif v0.0.0-20200502203340-6aea10b45f4c h1:PoW4xOq3wUrX8ghNGiJFzem7mwd+mY/Xkgo0Z8AwcNY= 2 | github.com/dsoprea/go-exif v0.0.0-20200516122116-a45cc7cfd55e h1:tTb1WdrhFs8VdnmxiADJEUpDJWKHFUFys0OUyLM9A6o= 3 | github.com/dsoprea/go-exif/v2 v2.0.0-20200113231207-0bbb7a3584f7 h1:+koSu4BOaLu+dy50WEj+ltzEjMzK5evzPawKxgIQerw= 4 | github.com/dsoprea/go-exif/v2 v2.0.0-20200113231207-0bbb7a3584f7/go.mod h1:Lm2lMM2zx8p4a34ZemkaUV95AnMl4ZvLbCUbwOvLC2E= 5 | github.com/dsoprea/go-exif/v2 v2.0.0-20200321225314-640175a69fe4/go.mod h1:Lm2lMM2zx8p4a34ZemkaUV95AnMl4ZvLbCUbwOvLC2E= 6 | github.com/dsoprea/go-exif/v2 v2.0.0-20200502203340-6aea10b45f4c h1:fQNBTLqL4u7yhl5AqW6dGG5RSxGuRhzXLnBVDR2uUuE= 7 | github.com/dsoprea/go-exif/v2 v2.0.0-20200502203340-6aea10b45f4c/go.mod h1:YXOyDqCYjBuHHRw4JIGPgOgMit0IDvVSjjhsqOAFTYQ= 8 | github.com/dsoprea/go-exif/v2 v2.0.0-20200516113213-42546383ce8f h1:WFrUfvt3uESgJ/NwPG/2Vjhp2uOE7X2wENldE+ch1yw= 9 | github.com/dsoprea/go-exif/v2 v2.0.0-20200516113213-42546383ce8f/go.mod h1:YXOyDqCYjBuHHRw4JIGPgOgMit0IDvVSjjhsqOAFTYQ= 10 | github.com/dsoprea/go-exif/v2 v2.0.0-20200516122116-a45cc7cfd55e h1:tPHXVRs63sg0ajoZjdmMa5aZuyjnSAt3Anwh2F4XsJM= 11 | github.com/dsoprea/go-exif/v2 v2.0.0-20200516122116-a45cc7cfd55e/go.mod h1:YXOyDqCYjBuHHRw4JIGPgOgMit0IDvVSjjhsqOAFTYQ= 12 | github.com/dsoprea/go-exif/v2 v2.0.0-20200516213102-7f6eb3d9f38c h1:92aud+9pN3bQjh/iw1+849uOBQfLuAcUC4LJwtfmRBo= 13 | github.com/dsoprea/go-exif/v2 v2.0.0-20200516213102-7f6eb3d9f38c/go.mod h1:YXOyDqCYjBuHHRw4JIGPgOgMit0IDvVSjjhsqOAFTYQ= 14 | github.com/dsoprea/go-exif/v2 v2.0.0-20200517080529-c9be4b30b064 h1:V7CH/kZImE6Lf27H4DS5PG7qzBkf774GIXUuM31vVNA= 15 | github.com/dsoprea/go-exif/v2 v2.0.0-20200517080529-c9be4b30b064/go.mod h1:YXOyDqCYjBuHHRw4JIGPgOgMit0IDvVSjjhsqOAFTYQ= 16 | github.com/dsoprea/go-exif/v2 v2.0.0-20200518001653-d0d0f14dea03 h1:r+aCxLEe6uGDC/NJCpA3WQJ+C7WJ0chzfHKgy173fug= 17 | github.com/dsoprea/go-exif/v2 v2.0.0-20200518001653-d0d0f14dea03/go.mod h1:STKu28lNwOeoO0bieAKJ3zQYkUbZ2hivI6qjjGVW0sc= 18 | github.com/dsoprea/go-exif/v2 v2.0.0-20200520183328-015129a9efd5 h1:iKMxnRjFqQQYKEpdsjFDMV2+VUAncTLT4ofcCiQpDvo= 19 | github.com/dsoprea/go-exif/v2 v2.0.0-20200520183328-015129a9efd5/go.mod h1:9EXlPeHfblFFnwu5UOqmP2eoZfJyAZ2Ri/Vki33ajO0= 20 | github.com/dsoprea/go-exif/v2 v2.0.0-20200527040709-fecb7e81f4be h1:iYHdwTUXN48h6wZd2QQHDyR4QsuWM08PX4wCJuzd7O0= 21 | github.com/dsoprea/go-exif/v2 v2.0.0-20200527040709-fecb7e81f4be/go.mod h1:9EXlPeHfblFFnwu5UOqmP2eoZfJyAZ2Ri/Vki33ajO0= 22 | github.com/dsoprea/go-exif/v2 v2.0.0-20200527042908-2a1e3f0fa19c h1:3uLJ1ub/I1sFM76IEzRi7RjqbhL1WfyPJeSko0tIYMI= 23 | github.com/dsoprea/go-exif/v2 v2.0.0-20200527042908-2a1e3f0fa19c/go.mod h1:9EXlPeHfblFFnwu5UOqmP2eoZfJyAZ2Ri/Vki33ajO0= 24 | github.com/dsoprea/go-exif/v2 v2.0.0-20200527165002-1a62daf3052a h1:Xk487H/DyhmIgYAnbJ5gvOrwI/eJ+FVXIO9Y22m44VI= 25 | github.com/dsoprea/go-exif/v2 v2.0.0-20200527165002-1a62daf3052a/go.mod h1:9EXlPeHfblFFnwu5UOqmP2eoZfJyAZ2Ri/Vki33ajO0= 26 | github.com/dsoprea/go-exif/v2 v2.0.0-20200604193436-ca8584a0e1c4 h1:Mg7pY7kxDQD2Bkvr1N+XW4BESSIQ7tTTR7Vv+Gi2CsM= 27 | github.com/dsoprea/go-exif/v2 v2.0.0-20200604193436-ca8584a0e1c4/go.mod h1:9EXlPeHfblFFnwu5UOqmP2eoZfJyAZ2Ri/Vki33ajO0= 28 | github.com/dsoprea/go-exif/v3 v3.0.0-20200717053412-08f1b6708903/go.mod h1:0nsO1ce0mh5czxGeLo4+OCZ/C6Eo6ZlMWsz7rH/Gxv8= 29 | github.com/dsoprea/go-exif/v3 v3.0.0-20210512043655-120bcdb2a55e h1:E4XTSQZF/JtOQWcSaJBJho7t+RNWfdO92W/5skg10Jk= 30 | github.com/dsoprea/go-exif/v3 v3.0.0-20210512043655-120bcdb2a55e/go.mod h1:cg5SNYKHMmzxsr9X6ZeLh/nfBRHHp5PngtEPcujONtk= 31 | github.com/dsoprea/go-iptc v0.0.0-20200609062250-162ae6b44feb h1:gwjJjUr6FY7zAWVEueFPrcRHhd9+IK81TcItbqw2du4= 32 | github.com/dsoprea/go-iptc v0.0.0-20200609062250-162ae6b44feb/go.mod h1:kYIdx9N9NaOyD7U6D+YtExN7QhRm+5kq7//yOsRXQtM= 33 | github.com/dsoprea/go-logging v0.0.0-20190624164917-c4f10aab7696 h1:VGFnZAcLwPpt1sHlAxml+pGLZz9A2s+K/s1YNhPC91Y= 34 | github.com/dsoprea/go-logging v0.0.0-20190624164917-c4f10aab7696/go.mod h1:Nm/x2ZUNRW6Fe5C3LxdY1PyZY5wmDv/s5dkPJ/VB3iA= 35 | github.com/dsoprea/go-logging v0.0.0-20200502201358-170ff607885f h1:FonKAuW3PmNtqk9tOR+Z7bnyQHytmnZBCmm5z1PQMss= 36 | github.com/dsoprea/go-logging v0.0.0-20200502201358-170ff607885f/go.mod h1:7I+3Pe2o/YSU88W0hWlm9S22W7XI1JFNJ86U0zPKMf8= 37 | github.com/dsoprea/go-logging v0.0.0-20200517222403-5742ce3fc1be h1:k3sHKay8cXGnGHeF8x6U7KtX8Lc7qAiQCNDRGEIPdnU= 38 | github.com/dsoprea/go-logging v0.0.0-20200517222403-5742ce3fc1be/go.mod h1:7I+3Pe2o/YSU88W0hWlm9S22W7XI1JFNJ86U0zPKMf8= 39 | github.com/dsoprea/go-logging v0.0.0-20200517223158-a10564966e9d h1:F/7L5wr/fP/SKeO5HuMlNEX9Ipyx2MbH2rV9G4zJRpk= 40 | github.com/dsoprea/go-logging v0.0.0-20200517223158-a10564966e9d/go.mod h1:7I+3Pe2o/YSU88W0hWlm9S22W7XI1JFNJ86U0zPKMf8= 41 | github.com/dsoprea/go-photoshop-info-format v0.0.0-20200609050348-3db9b63b202c h1:7j5aWACOzROpr+dvMtu8GnI97g9ShLWD72XIELMgn+c= 42 | github.com/dsoprea/go-photoshop-info-format v0.0.0-20200609050348-3db9b63b202c/go.mod h1:pqKB+ijp27cEcrHxhXVgUUMlSDRuGJJp1E+20Lj5H0E= 43 | github.com/dsoprea/go-utility v0.0.0-20200322055224-4dc0f716e7d0 h1:zFSboMDWXX2UX7/k/mCHBjZhHlaFMx0HmtUE37HABsA= 44 | github.com/dsoprea/go-utility v0.0.0-20200322055224-4dc0f716e7d0/go.mod h1:xv8CVgDmI/Shx/X+EUXyXELVnH5lSRUYRija52OHq7E= 45 | github.com/dsoprea/go-utility v0.0.0-20200322154813-27f0b0d142d7 h1:DJhSHW0odJrW5wR9MU6ry5S+PsxuRXA165KFaiB+cZo= 46 | github.com/dsoprea/go-utility v0.0.0-20200322154813-27f0b0d142d7/go.mod h1:xv8CVgDmI/Shx/X+EUXyXELVnH5lSRUYRija52OHq7E= 47 | github.com/dsoprea/go-utility v0.0.0-20200512094054-1abbbc781176 h1:CfXezFYb2STGOd1+n1HshvE191zVx+QX3A1nML5xxME= 48 | github.com/dsoprea/go-utility v0.0.0-20200512094054-1abbbc781176/go.mod h1:95+K3z2L0mqsVYd6yveIv1lmtT3tcQQ3dVakPySffW8= 49 | github.com/dsoprea/go-utility v0.0.0-20200711062821-fab8125e9bdf h1:/w4QxepU4AHh3AuO6/g8y/YIIHH5+aKP3Bj8sg5cqhU= 50 | github.com/dsoprea/go-utility v0.0.0-20200711062821-fab8125e9bdf/go.mod h1:95+K3z2L0mqsVYd6yveIv1lmtT3tcQQ3dVakPySffW8= 51 | github.com/dsoprea/go-utility/v2 v2.0.0-20200717064901-2fccff4aa15e h1:IxIbA7VbCNrwumIYjDoMOdf4KOSkMC6NJE4s8oRbE7E= 52 | github.com/dsoprea/go-utility/v2 v2.0.0-20200717064901-2fccff4aa15e/go.mod h1:uAzdkPTub5Y9yQwXe8W4m2XuP0tK4a9Q/dantD0+uaU= 53 | github.com/go-errors/errors v1.0.1 h1:LUHzmkK3GUKUrL/1gfBUxAHzcev3apQlezX/+O7ma6w= 54 | github.com/go-errors/errors v1.0.1/go.mod h1:f4zRHt4oKfwPJE5k8C9vpYG+aDHdBFUsgrm6/TyX73Q= 55 | github.com/go-errors/errors v1.0.2 h1:xMxH9j2fNg/L4hLn/4y3M0IUsn0M6Wbu/Uh9QlOfBh4= 56 | github.com/go-errors/errors v1.0.2/go.mod h1:psDX2osz5VnTOnFWbDeWwS7yejl+uV3FEWEp4lssFEs= 57 | github.com/go-errors/errors v1.1.1 h1:ljK/pL5ltg3qoN+OtN6yCv9HWSfMwxSx90GJCZQxYNg= 58 | github.com/go-errors/errors v1.1.1/go.mod h1:psDX2osz5VnTOnFWbDeWwS7yejl+uV3FEWEp4lssFEs= 59 | github.com/go-xmlfmt/xmlfmt v0.0.0-20191208150333-d5b6f63a941b h1:khEcpUM4yFcxg4/FHQWkvVRmgijNXRfzkIDHh23ggEo= 60 | github.com/go-xmlfmt/xmlfmt v0.0.0-20191208150333-d5b6f63a941b/go.mod h1:aUCEOzzezBEjDBbFBoSiya/gduyIiWYRP6CnSFIV8AM= 61 | github.com/golang/geo v0.0.0-20190916061304-5b978397cfec h1:lJwO/92dFXWeXOZdoGXgptLmNLwynMSHUmU6besqtiw= 62 | github.com/golang/geo v0.0.0-20190916061304-5b978397cfec/go.mod h1:QZ0nwyI2jOfgRAoBvP+ab5aRr7c9x7lhGEJrKvBwjWI= 63 | github.com/golang/geo v0.0.0-20200319012246-673a6f80352d h1:C/hKUcHT483btRbeGkrRjJz+Zbcj8audldIi9tRJDCc= 64 | github.com/golang/geo v0.0.0-20200319012246-673a6f80352d/go.mod h1:QZ0nwyI2jOfgRAoBvP+ab5aRr7c9x7lhGEJrKvBwjWI= 65 | github.com/jessevdk/go-flags v1.4.0 h1:4IU2WS7AumrZ/40jfhf4QVDMsQwqA7VEHozFRrGARJA= 66 | github.com/jessevdk/go-flags v1.4.0/go.mod h1:4FA24M0QyGHXBuZZK/XkWh8h0e1EYbRYJSGM75WSRxI= 67 | golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= 68 | golang.org/x/net v0.0.0-20191209160850-c0dbc17a3553 h1:efeOvDhwQ29Dj3SdAV/MJf8oukgn+8D8WgaCaRMchF8= 69 | golang.org/x/net v0.0.0-20191209160850-c0dbc17a3553/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= 70 | golang.org/x/net v0.0.0-20200320220750-118fecf932d8/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= 71 | golang.org/x/net v0.0.0-20200501053045-e0ff5e5a1de5 h1:WQ8q63x+f/zpC8Ac1s9wLElVoHhm32p6tudrU72n1QA= 72 | golang.org/x/net v0.0.0-20200501053045-e0ff5e5a1de5/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= 73 | golang.org/x/net v0.0.0-20200513185701-a91f0712d120 h1:EZ3cVSzKOlJxAd8e8YAJ7no8nNypTxexh/YE/xW3ZEY= 74 | golang.org/x/net v0.0.0-20200513185701-a91f0712d120/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= 75 | golang.org/x/net v0.0.0-20200520182314-0ba52f642ac2 h1:eDrdRpKgkcCqKZQwyZRyeFZgfqt37SL7Kv3tok06cKE= 76 | golang.org/x/net v0.0.0-20200520182314-0ba52f642ac2/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= 77 | golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 78 | golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 79 | golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= 80 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= 81 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 82 | gopkg.in/yaml.v2 v2.2.7 h1:VUgggvou5XRW9mHwD/yXxIYSMtY0zoKQf/v226p2nyo= 83 | gopkg.in/yaml.v2 v2.2.7/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= 84 | gopkg.in/yaml.v2 v2.3.0 h1:clyUAQHOM3G0M3f5vQj7LuJrETvjVot3Z5el9nffUtU= 85 | gopkg.in/yaml.v2 v2.3.0/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= 86 | -------------------------------------------------------------------------------- /markers.go: -------------------------------------------------------------------------------- 1 | package jpegstructure 2 | 3 | import ( 4 | "github.com/dsoprea/go-logging" 5 | ) 6 | 7 | const ( 8 | // MARKER_SOI marker 9 | MARKER_SOI = 0xd8 10 | 11 | // MARKER_EOI marker 12 | MARKER_EOI = 0xd9 13 | 14 | // MARKER_SOS marker 15 | MARKER_SOS = 0xda 16 | 17 | // MARKER_SOD marker 18 | MARKER_SOD = 0x93 19 | 20 | // MARKER_DQT marker 21 | MARKER_DQT = 0xdb 22 | 23 | // MARKER_APP0 marker 24 | MARKER_APP0 = 0xe0 25 | 26 | // MARKER_APP1 marker 27 | MARKER_APP1 = 0xe1 28 | 29 | // MARKER_APP2 marker 30 | MARKER_APP2 = 0xe2 31 | 32 | // MARKER_APP3 marker 33 | MARKER_APP3 = 0xe3 34 | 35 | // MARKER_APP4 marker 36 | MARKER_APP4 = 0xe4 37 | 38 | // MARKER_APP5 marker 39 | MARKER_APP5 = 0xe5 40 | 41 | // MARKER_APP6 marker 42 | MARKER_APP6 = 0xe6 43 | 44 | // MARKER_APP7 marker 45 | MARKER_APP7 = 0xe7 46 | 47 | // MARKER_APP8 marker 48 | MARKER_APP8 = 0xe8 49 | 50 | // MARKER_APP10 marker 51 | MARKER_APP10 = 0xea 52 | 53 | // MARKER_APP12 marker 54 | MARKER_APP12 = 0xec 55 | 56 | // MARKER_APP13 marker 57 | MARKER_APP13 = 0xed 58 | 59 | // MARKER_APP14 marker 60 | MARKER_APP14 = 0xee 61 | 62 | // MARKER_APP15 marker 63 | MARKER_APP15 = 0xef 64 | 65 | // MARKER_COM marker 66 | MARKER_COM = 0xfe 67 | 68 | // MARKER_CME marker 69 | MARKER_CME = 0x64 70 | 71 | // MARKER_SIZ marker 72 | MARKER_SIZ = 0x51 73 | 74 | // MARKER_DHT marker 75 | MARKER_DHT = 0xc4 76 | 77 | // MARKER_JPG marker 78 | MARKER_JPG = 0xc8 79 | 80 | // MARKER_DAC marker 81 | MARKER_DAC = 0xcc 82 | 83 | // MARKER_SOF0 marker 84 | MARKER_SOF0 = 0xc0 85 | 86 | // MARKER_SOF1 marker 87 | MARKER_SOF1 = 0xc1 88 | 89 | // MARKER_SOF2 marker 90 | MARKER_SOF2 = 0xc2 91 | 92 | // MARKER_SOF3 marker 93 | MARKER_SOF3 = 0xc3 94 | 95 | // MARKER_SOF5 marker 96 | MARKER_SOF5 = 0xc5 97 | 98 | // MARKER_SOF6 marker 99 | MARKER_SOF6 = 0xc6 100 | 101 | // MARKER_SOF7 marker 102 | MARKER_SOF7 = 0xc7 103 | 104 | // MARKER_SOF9 marker 105 | MARKER_SOF9 = 0xc9 106 | 107 | // MARKER_SOF10 marker 108 | MARKER_SOF10 = 0xca 109 | 110 | // MARKER_SOF11 marker 111 | MARKER_SOF11 = 0xcb 112 | 113 | // MARKER_SOF13 marker 114 | MARKER_SOF13 = 0xcd 115 | 116 | // MARKER_SOF14 marker 117 | MARKER_SOF14 = 0xce 118 | 119 | // MARKER_SOF15 marker 120 | MARKER_SOF15 = 0xcf 121 | ) 122 | 123 | var ( 124 | jpegLogger = log.NewLogger("jpegstructure.jpeg") 125 | jpegMagicStandard = []byte{0xff, MARKER_SOI, 0xff} 126 | jpegMagic2000 = []byte{0xff, 0x4f, 0xff} 127 | 128 | markerLen = map[byte]int{ 129 | 0x00: 0, 130 | 0x01: 0, 131 | 0xd0: 0, 132 | 0xd1: 0, 133 | 0xd2: 0, 134 | 0xd3: 0, 135 | 0xd4: 0, 136 | 0xd5: 0, 137 | 0xd6: 0, 138 | 0xd7: 0, 139 | 0xd8: 0, 140 | 0xd9: 0, 141 | 0xda: 0, 142 | 143 | // J2C 144 | 0x30: 0, 145 | 0x31: 0, 146 | 0x32: 0, 147 | 0x33: 0, 148 | 0x34: 0, 149 | 0x35: 0, 150 | 0x36: 0, 151 | 0x37: 0, 152 | 0x38: 0, 153 | 0x39: 0, 154 | 0x3a: 0, 155 | 0x3b: 0, 156 | 0x3c: 0, 157 | 0x3d: 0, 158 | 0x3e: 0, 159 | 0x3f: 0, 160 | 0x4f: 0, 161 | 0x92: 0, 162 | 0x93: 0, 163 | 164 | // J2C extensions 165 | 0x74: 4, 166 | 0x75: 4, 167 | 0x77: 4, 168 | } 169 | 170 | markerNames = map[byte]string{ 171 | MARKER_SOI: "SOI", 172 | MARKER_EOI: "EOI", 173 | MARKER_SOS: "SOS", 174 | MARKER_SOD: "SOD", 175 | MARKER_DQT: "DQT", 176 | MARKER_APP0: "APP0", 177 | MARKER_APP1: "APP1", 178 | MARKER_APP2: "APP2", 179 | MARKER_APP3: "APP3", 180 | MARKER_APP4: "APP4", 181 | MARKER_APP5: "APP5", 182 | MARKER_APP6: "APP6", 183 | MARKER_APP7: "APP7", 184 | MARKER_APP8: "APP8", 185 | MARKER_APP10: "APP10", 186 | MARKER_APP12: "APP12", 187 | MARKER_APP13: "APP13", 188 | MARKER_APP14: "APP14", 189 | MARKER_APP15: "APP15", 190 | MARKER_COM: "COM", 191 | MARKER_CME: "CME", 192 | MARKER_SIZ: "SIZ", 193 | 194 | MARKER_DHT: "DHT", 195 | MARKER_JPG: "JPG", 196 | MARKER_DAC: "DAC", 197 | 198 | MARKER_SOF0: "SOF0", 199 | MARKER_SOF1: "SOF1", 200 | MARKER_SOF2: "SOF2", 201 | MARKER_SOF3: "SOF3", 202 | MARKER_SOF5: "SOF5", 203 | MARKER_SOF6: "SOF6", 204 | MARKER_SOF7: "SOF7", 205 | MARKER_SOF9: "SOF9", 206 | MARKER_SOF10: "SOF10", 207 | MARKER_SOF11: "SOF11", 208 | MARKER_SOF13: "SOF13", 209 | MARKER_SOF14: "SOF14", 210 | MARKER_SOF15: "SOF15", 211 | } 212 | ) 213 | -------------------------------------------------------------------------------- /media_parser.go: -------------------------------------------------------------------------------- 1 | package jpegstructure 2 | 3 | import ( 4 | "bufio" 5 | "bytes" 6 | "io" 7 | "os" 8 | 9 | "github.com/dsoprea/go-logging" 10 | "github.com/dsoprea/go-utility/image" 11 | ) 12 | 13 | // JpegMediaParser is a `riimage.MediaParser` that knows how to parse JPEG 14 | // images. 15 | type JpegMediaParser struct { 16 | } 17 | 18 | // NewJpegMediaParser returns a new JpegMediaParser. 19 | func NewJpegMediaParser() *JpegMediaParser { 20 | 21 | // TODO(dustin): Add test 22 | 23 | return new(JpegMediaParser) 24 | } 25 | 26 | // Parse parses a JPEG uses an `io.ReadSeeker`. Even if it fails, it will return 27 | // the list of segments encountered prior to the failure. 28 | func (jmp *JpegMediaParser) Parse(rs io.ReadSeeker, size int) (ec riimage.MediaContext, err error) { 29 | defer func() { 30 | if state := recover(); state != nil { 31 | err = log.Wrap(state.(error)) 32 | } 33 | }() 34 | 35 | s := bufio.NewScanner(rs) 36 | 37 | // Since each segment can be any size, our buffer must allowed to grow as 38 | // large as the file. 39 | buffer := []byte{} 40 | s.Buffer(buffer, size) 41 | 42 | js := NewJpegSplitter(nil) 43 | s.Split(js.Split) 44 | 45 | for s.Scan() != false { 46 | } 47 | 48 | // Always return the segments that were parsed, at least until there was an 49 | // error. 50 | ec = js.Segments() 51 | 52 | log.PanicIf(s.Err()) 53 | 54 | return ec, nil 55 | } 56 | 57 | // ParseFile parses a JPEG file. Even if it fails, it will return the list of 58 | // segments encountered prior to the failure. 59 | func (jmp *JpegMediaParser) ParseFile(filepath string) (ec riimage.MediaContext, err error) { 60 | defer func() { 61 | if state := recover(); state != nil { 62 | err = log.Wrap(state.(error)) 63 | } 64 | }() 65 | 66 | // TODO(dustin): Add test 67 | 68 | f, err := os.Open(filepath) 69 | log.PanicIf(err) 70 | 71 | defer f.Close() 72 | 73 | stat, err := f.Stat() 74 | log.PanicIf(err) 75 | 76 | size := stat.Size() 77 | 78 | sl, err := jmp.Parse(f, int(size)) 79 | 80 | // Always return the segments that were parsed, at least until there was an 81 | // error. 82 | ec = sl 83 | 84 | log.PanicIf(err) 85 | 86 | return ec, nil 87 | } 88 | 89 | // ParseBytes parses a JPEG byte-slice. Even if it fails, it will return the 90 | // list of segments encountered prior to the failure. 91 | func (jmp *JpegMediaParser) ParseBytes(data []byte) (ec riimage.MediaContext, err error) { 92 | defer func() { 93 | if state := recover(); state != nil { 94 | err = log.Wrap(state.(error)) 95 | } 96 | }() 97 | 98 | br := bytes.NewReader(data) 99 | 100 | sl, err := jmp.Parse(br, len(data)) 101 | 102 | // Always return the segments that were parsed, at least until there was an 103 | // error. 104 | ec = sl 105 | 106 | log.PanicIf(err) 107 | 108 | return ec, nil 109 | } 110 | 111 | // LooksLikeFormat indicates whether the data looks like a JPEG image. 112 | func (jmp *JpegMediaParser) LooksLikeFormat(data []byte) bool { 113 | if len(data) < 4 { 114 | return false 115 | } 116 | 117 | // https://cs.opensource.google/go/go/+/master:src/net/http/sniff.go;l=126;drc=8b364451e2e2f2f816ed877a4639d9342279f299 118 | return bytes.HasPrefix(data, []byte("\xFF\xD8\xFF")) 119 | } 120 | 121 | var ( 122 | // Enforce interface conformance. 123 | _ riimage.MediaParser = new(JpegMediaParser) 124 | ) 125 | -------------------------------------------------------------------------------- /media_parser_test.go: -------------------------------------------------------------------------------- 1 | package jpegstructure 2 | 3 | import ( 4 | "fmt" 5 | "os" 6 | "path" 7 | "testing" 8 | 9 | "io/ioutil" 10 | 11 | "github.com/dsoprea/go-logging" 12 | ) 13 | 14 | func TestJpegMediaParser_Parse(t *testing.T) { 15 | filepath := GetTestImageFilepath() 16 | 17 | f, err := os.Open(filepath) 18 | log.PanicIf(err) 19 | 20 | defer f.Close() 21 | 22 | stat, err := f.Stat() 23 | log.PanicIf(err) 24 | 25 | size := stat.Size() 26 | 27 | jmp := NewJpegMediaParser() 28 | 29 | intfc, err := jmp.Parse(f, int(size)) 30 | log.PanicIf(err) 31 | 32 | sl := intfc.(*SegmentList) 33 | 34 | expected := []*Segment{ 35 | { 36 | MarkerId: 0xd8, 37 | Offset: 0x0, 38 | }, 39 | { 40 | MarkerId: 0xe1, 41 | Offset: 0x2, 42 | }, 43 | { 44 | MarkerId: 0xe1, 45 | Offset: 0x000080b4, 46 | }, 47 | { 48 | MarkerId: 0xdb, 49 | Offset: 0x8ab6, 50 | }, 51 | { 52 | MarkerId: 0xc0, 53 | Offset: 0x8b3c, 54 | }, 55 | { 56 | MarkerId: 0xc4, 57 | Offset: 0x8b4f, 58 | }, 59 | { 60 | MarkerId: 0xda, 61 | Offset: 0x8cf3, 62 | }, 63 | { 64 | MarkerId: 0x0, 65 | Offset: 0x8cf5, 66 | }, 67 | { 68 | MarkerId: 0xd9, 69 | Offset: 0x554d6d, 70 | }, 71 | } 72 | 73 | if len(sl.segments) != len(expected) { 74 | t.Fatalf("Number of segments is unexpected: (%d) != (%d)", len(sl.segments), len(expected)) 75 | } 76 | 77 | for i, s := range sl.segments { 78 | if s.MarkerId != expected[i].MarkerId { 79 | t.Fatalf("Segment (%d) marker-ID not correct: (0x%02x != 0x%02x)", i, s.MarkerId, expected[i].MarkerId) 80 | } else if s.Offset != expected[i].Offset { 81 | t.Fatalf("Segment (%d) offset not correct: (0x%08x != 0x%08x)", i, s.Offset, expected[i].Offset) 82 | } 83 | } 84 | } 85 | 86 | func TestJpegMediaParser_ParseBytes(t *testing.T) { 87 | filepath := GetTestImageFilepath() 88 | 89 | data, err := ioutil.ReadFile(filepath) 90 | log.PanicIf(err) 91 | 92 | jmp := NewJpegMediaParser() 93 | 94 | intfc, err := jmp.ParseBytes(data) 95 | log.PanicIf(err) 96 | 97 | sl := intfc.(*SegmentList) 98 | 99 | expectedSegments := []*Segment{ 100 | { 101 | MarkerId: 0xd8, 102 | Offset: 0x0, 103 | }, 104 | { 105 | MarkerId: 0xe1, 106 | Offset: 0x2, 107 | }, 108 | { 109 | MarkerId: 0xe1, 110 | Offset: 0x000080b4, 111 | }, 112 | { 113 | MarkerId: 0xdb, 114 | Offset: 0x8ab6, 115 | }, 116 | { 117 | MarkerId: 0xc0, 118 | Offset: 0x8b3c, 119 | }, 120 | { 121 | MarkerId: 0xc4, 122 | Offset: 0x8b4f, 123 | }, 124 | { 125 | MarkerId: 0xda, 126 | Offset: 0x8cf3, 127 | }, 128 | { 129 | MarkerId: 0x0, 130 | Offset: 0x8cf5, 131 | }, 132 | { 133 | MarkerId: 0xd9, 134 | Offset: 0x554d6d, 135 | }, 136 | } 137 | 138 | expectedSl := NewSegmentList(expectedSegments) 139 | 140 | if sl.OffsetsEqual(expectedSl) != true { 141 | t.Fatalf("Segments not expected") 142 | } 143 | } 144 | 145 | func TestJpegMediaParser_ParseBytes_Offsets(t *testing.T) { 146 | filepath := GetTestImageFilepath() 147 | 148 | data, err := ioutil.ReadFile(filepath) 149 | log.PanicIf(err) 150 | 151 | jmp := NewJpegMediaParser() 152 | 153 | intfc, err := jmp.ParseBytes(data) 154 | log.PanicIf(err) 155 | 156 | sl := intfc.(*SegmentList) 157 | 158 | err = sl.Validate(data) 159 | log.PanicIf(err) 160 | } 161 | 162 | func TestJpegMediaParser_ParseBytes_MultipleEois(t *testing.T) { 163 | defer func() { 164 | if state := recover(); state != nil { 165 | err := log.Wrap(state.(error)) 166 | log.PrintErrorf(err, "Test failure.") 167 | t.Fatalf("Test failure.") 168 | } 169 | }() 170 | 171 | assetsPath := GetTestAssetsPath() 172 | filepath := path.Join(assetsPath, "IMG_6691_Multiple_EOIs.jpg") 173 | 174 | data, err := ioutil.ReadFile(filepath) 175 | log.PanicIf(err) 176 | 177 | jmp := NewJpegMediaParser() 178 | 179 | intfc, err := jmp.ParseBytes(data) 180 | log.PanicIf(err) 181 | 182 | sl := intfc.(*SegmentList) 183 | 184 | expectedSegments := []*Segment{ 185 | { 186 | MarkerId: 0xd8, 187 | Offset: 0x0, 188 | }, 189 | { 190 | MarkerId: 0xe1, 191 | Offset: 0x00000002, 192 | }, 193 | { 194 | MarkerId: 0xe1, 195 | Offset: 0x00007002, 196 | }, 197 | { 198 | MarkerId: 0xe2, 199 | Offset: 0x00007fa0, 200 | }, 201 | { 202 | MarkerId: 0xdb, 203 | Offset: 0x00008002, 204 | }, 205 | { 206 | MarkerId: 0xc0, 207 | Offset: 0x00008088, 208 | }, 209 | { 210 | MarkerId: 0xc4, 211 | Offset: 0x0000809b, 212 | }, 213 | { 214 | MarkerId: 0xda, 215 | Offset: 0x0000823f, 216 | }, 217 | { 218 | MarkerId: 0x0, 219 | Offset: 0x00008241, 220 | }, 221 | { 222 | MarkerId: 0xd9, 223 | Offset: 0x003f24db, 224 | }, 225 | } 226 | 227 | expectedSl := NewSegmentList(expectedSegments) 228 | 229 | if sl.OffsetsEqual(expectedSl) != true { 230 | for i, segment := range sl.segments { 231 | fmt.Printf("%d: ACTUAL: MARKER=(%02x) OFF=(%10x)\n", i, segment.MarkerId, segment.Offset) 232 | } 233 | 234 | for i, segment := range expectedSl.segments { 235 | fmt.Printf("%d: EXPECTED: MARKER=(%02x) OFF=(%10x)\n", i, segment.MarkerId, segment.Offset) 236 | } 237 | 238 | t.Fatalf("Segments not expected") 239 | } 240 | } 241 | 242 | func TestJpegMediaParser_LooksLikeFormat(t *testing.T) { 243 | imgs := []string{ 244 | GetTestImageFilepath(), 245 | GetTestImageFujiFilepath(), 246 | } 247 | 248 | for _, filepath := range imgs { 249 | data, err := ioutil.ReadFile(filepath) 250 | log.PanicIf(err) 251 | 252 | jmp := NewJpegMediaParser() 253 | 254 | if jmp.LooksLikeFormat(data) != true { 255 | t.Fatalf("[%s] not detected as JPEG", filepath) 256 | } 257 | } 258 | } 259 | -------------------------------------------------------------------------------- /segment.go: -------------------------------------------------------------------------------- 1 | package jpegstructure 2 | 3 | import ( 4 | "bytes" 5 | "errors" 6 | "fmt" 7 | 8 | "crypto/sha1" 9 | "encoding/hex" 10 | 11 | "github.com/dsoprea/go-exif/v2" 12 | "github.com/dsoprea/go-iptc" 13 | "github.com/dsoprea/go-logging" 14 | "github.com/dsoprea/go-photoshop-info-format" 15 | "github.com/dsoprea/go-utility/image" 16 | ) 17 | 18 | const ( 19 | pirIptcImageResourceId = uint16(0x0404) 20 | ) 21 | 22 | var ( 23 | // exifPrefix is the prefix found at the top of an EXIF slice. This is JPEG- 24 | // specific. 25 | exifPrefix = []byte{'E', 'x', 'i', 'f', 0, 0} 26 | 27 | xmpPrefix = []byte("http://ns.adobe.com/xap/1.0/\000") 28 | 29 | ps30Prefix = []byte("Photoshop 3.0\000") 30 | ) 31 | 32 | var ( 33 | // ErrNoXmp is returned if XMP data was requested but not found. 34 | ErrNoXmp = errors.New("no XMP data") 35 | 36 | // ErrNoIptc is returned if IPTC data was requested but not found. 37 | ErrNoIptc = errors.New("no IPTC data") 38 | 39 | // ErrNoPhotoshopData is returned if Photoshop info was requested but not 40 | // found. 41 | ErrNoPhotoshopData = errors.New("no photoshop data") 42 | ) 43 | 44 | // SofSegment has info read from a SOF segment. 45 | type SofSegment struct { 46 | // BitsPerSample is the bits-per-sample. 47 | BitsPerSample byte 48 | 49 | // Width is the image width. 50 | Width uint16 51 | 52 | // Height is the image height. 53 | Height uint16 54 | 55 | // ComponentCount is the number of color components. 56 | ComponentCount byte 57 | } 58 | 59 | // String returns a string representation of the SOF segment. 60 | func (ss SofSegment) String() string { 61 | 62 | // TODO(dustin): Add test 63 | 64 | return fmt.Sprintf("SOF", ss.BitsPerSample, ss.Width, ss.Height, ss.ComponentCount) 65 | } 66 | 67 | // SegmentVisitor describes a segment-visitor struct. 68 | type SegmentVisitor interface { 69 | // HandleSegment is triggered for each segment encountered as well as the 70 | // scan-data. 71 | HandleSegment(markerId byte, markerName string, counter int, lastIsScanData bool) error 72 | } 73 | 74 | // SofSegmentVisitor describes a visitor that is only called for each SOF 75 | // segment. 76 | type SofSegmentVisitor interface { 77 | // HandleSof is called for each encountered SOF segment. 78 | HandleSof(sof *SofSegment) error 79 | } 80 | 81 | // Segment describes a single segment. 82 | type Segment struct { 83 | MarkerId byte 84 | MarkerName string 85 | Offset int 86 | Data []byte 87 | 88 | photoshopInfo map[uint16]photoshopinfo.Photoshop30InfoRecord 89 | iptcTags map[iptc.StreamTagKey][]iptc.TagData 90 | } 91 | 92 | // SetExif encodes and sets EXIF data into this segment. 93 | func (s *Segment) SetExif(ib *exif.IfdBuilder) (err error) { 94 | defer func() { 95 | if state := recover(); state != nil { 96 | err = log.Wrap(state.(error)) 97 | } 98 | }() 99 | 100 | ibe := exif.NewIfdByteEncoder() 101 | 102 | exifData, err := ibe.EncodeToExif(ib) 103 | log.PanicIf(err) 104 | 105 | l := len(exifPrefix) 106 | 107 | s.Data = make([]byte, l+len(exifData)) 108 | copy(s.Data[0:], exifPrefix) 109 | copy(s.Data[l:], exifData) 110 | 111 | return nil 112 | } 113 | 114 | // Exif returns an `exif.Ifd` instance for the EXIF data we currently have. 115 | func (s *Segment) Exif() (rootIfd *exif.Ifd, data []byte, err error) { 116 | defer func() { 117 | if state := recover(); state != nil { 118 | err = log.Wrap(state.(error)) 119 | } 120 | }() 121 | 122 | l := len(exifPrefix) 123 | 124 | rawExif := s.Data[l:] 125 | 126 | jpegLogger.Debugf(nil, "Attempting to parse (%d) byte EXIF blob (Exif).", len(rawExif)) 127 | 128 | im := exif.NewIfdMappingWithStandard() 129 | ti := exif.NewTagIndex() 130 | 131 | _, index, err := exif.Collect(im, ti, rawExif) 132 | log.PanicIf(err) 133 | 134 | return index.RootIfd, rawExif, nil 135 | } 136 | 137 | // FlatExif parses the EXIF data and just returns a list of tags. 138 | func (s *Segment) FlatExif() (exifTags []exif.ExifTag, err error) { 139 | defer func() { 140 | if state := recover(); state != nil { 141 | err = log.Wrap(state.(error)) 142 | } 143 | }() 144 | 145 | // TODO(dustin): Add test 146 | 147 | l := len(exifPrefix) 148 | 149 | rawExif := s.Data[l:] 150 | 151 | jpegLogger.Debugf(nil, "Attempting to parse (%d) byte EXIF blob (FlatExif).", len(rawExif)) 152 | 153 | exifTags, err = exif.GetFlatExifData(rawExif) 154 | log.PanicIf(err) 155 | 156 | return exifTags, nil 157 | } 158 | 159 | // EmbeddedString returns a string of properties that can be embedded into an 160 | // longer string of properties. 161 | func (s *Segment) EmbeddedString() string { 162 | h := sha1.New() 163 | h.Write(s.Data) 164 | 165 | // TODO(dustin): Add test 166 | 167 | digestString := hex.EncodeToString(h.Sum(nil)) 168 | 169 | return fmt.Sprintf("OFFSET=(0x%08x %10d) ID=(0x%02x) NAME=[%-5s] SIZE=(%10d) SHA1=[%s]", s.Offset, s.Offset, s.MarkerId, markerNames[s.MarkerId], len(s.Data), digestString) 170 | } 171 | 172 | // String returns a descriptive string. 173 | func (s *Segment) String() string { 174 | 175 | // TODO(dustin): Add test 176 | 177 | return fmt.Sprintf("Segment<%s>", s.EmbeddedString()) 178 | } 179 | 180 | // IsExif returns true if EXIF data. 181 | func (s *Segment) IsExif() bool { 182 | if s.MarkerId != MARKER_APP1 { 183 | return false 184 | } 185 | 186 | // TODO(dustin): Add test 187 | 188 | l := len(exifPrefix) 189 | 190 | if len(s.Data) < l { 191 | return false 192 | } 193 | 194 | if bytes.Equal(s.Data[:l], exifPrefix) == false { 195 | return false 196 | } 197 | 198 | return true 199 | } 200 | 201 | // IsXmp returns true if XMP data. 202 | func (s *Segment) IsXmp() bool { 203 | if s.MarkerId != MARKER_APP1 { 204 | return false 205 | } 206 | 207 | // TODO(dustin): Add test 208 | 209 | l := len(xmpPrefix) 210 | 211 | if len(s.Data) < l { 212 | return false 213 | } 214 | 215 | if bytes.Equal(s.Data[:l], xmpPrefix) == false { 216 | return false 217 | } 218 | 219 | return true 220 | } 221 | 222 | // FormattedXmp returns a formatted XML string. This only makes sense for a 223 | // segment comprised of XML data (like XMP). 224 | func (s *Segment) FormattedXmp() (formatted string, err error) { 225 | defer func() { 226 | if state := recover(); state != nil { 227 | err = log.Wrap(state.(error)) 228 | } 229 | }() 230 | 231 | // TODO(dustin): Add test 232 | 233 | if s.IsXmp() != true { 234 | log.Panicf("not an XMP segment") 235 | } 236 | 237 | l := len(xmpPrefix) 238 | 239 | raw := string(s.Data[l:]) 240 | 241 | formatted, err = FormatXml(raw) 242 | log.PanicIf(err) 243 | 244 | return formatted, nil 245 | } 246 | 247 | func (s *Segment) parsePhotoshopInfo() (photoshopInfo map[uint16]photoshopinfo.Photoshop30InfoRecord, err error) { 248 | defer func() { 249 | if state := recover(); state != nil { 250 | err = log.Wrap(state.(error)) 251 | } 252 | }() 253 | 254 | if s.photoshopInfo != nil { 255 | return s.photoshopInfo, nil 256 | } 257 | 258 | if s.MarkerId != MARKER_APP13 { 259 | return nil, ErrNoPhotoshopData 260 | } 261 | 262 | l := len(ps30Prefix) 263 | 264 | if len(s.Data) < l { 265 | return nil, ErrNoPhotoshopData 266 | } 267 | 268 | if bytes.Equal(s.Data[:l], ps30Prefix) == false { 269 | return nil, ErrNoPhotoshopData 270 | } 271 | 272 | data := s.Data[l:] 273 | b := bytes.NewBuffer(data) 274 | 275 | // Parse it. 276 | 277 | pirIndex, err := photoshopinfo.ReadPhotoshop30Info(b) 278 | log.PanicIf(err) 279 | 280 | s.photoshopInfo = pirIndex 281 | 282 | return s.photoshopInfo, nil 283 | } 284 | 285 | // IsIptc returns true if XMP data. 286 | func (s *Segment) IsIptc() bool { 287 | // TODO(dustin): Add test 288 | 289 | // There's a cost to determining if there's IPTC data, so we won't do it 290 | // more than once. 291 | if s.iptcTags != nil { 292 | return true 293 | } 294 | 295 | photoshopInfo, err := s.parsePhotoshopInfo() 296 | if err != nil { 297 | if err == ErrNoPhotoshopData { 298 | return false 299 | } 300 | 301 | log.Panic(err) 302 | } 303 | 304 | // Bail if the Photoshop info doesn't have IPTC data. 305 | 306 | _, found := photoshopInfo[pirIptcImageResourceId] 307 | if found == false { 308 | return false 309 | } 310 | 311 | return true 312 | } 313 | 314 | // Iptc parses Photoshop info (if present) and then parses the IPTC info inside 315 | // it (if present). 316 | func (s *Segment) Iptc() (tags map[iptc.StreamTagKey][]iptc.TagData, err error) { 317 | defer func() { 318 | if state := recover(); state != nil { 319 | err = log.Wrap(state.(error)) 320 | } 321 | }() 322 | 323 | // Cache the parse. 324 | if s.iptcTags != nil { 325 | return s.iptcTags, nil 326 | } 327 | 328 | photoshopInfo, err := s.parsePhotoshopInfo() 329 | log.PanicIf(err) 330 | 331 | iptcPir, found := photoshopInfo[pirIptcImageResourceId] 332 | if found == false { 333 | return nil, ErrNoIptc 334 | } 335 | 336 | b := bytes.NewBuffer(iptcPir.Data) 337 | 338 | tags, err = iptc.ParseStream(b) 339 | log.PanicIf(err) 340 | 341 | s.iptcTags = tags 342 | 343 | return tags, nil 344 | } 345 | 346 | var ( 347 | // Enforce interface conformance. 348 | _ riimage.MediaContext = new(Segment) 349 | ) 350 | -------------------------------------------------------------------------------- /segment_list.go: -------------------------------------------------------------------------------- 1 | package jpegstructure 2 | 3 | import ( 4 | "bytes" 5 | "fmt" 6 | "io" 7 | 8 | "crypto/sha1" 9 | "encoding/binary" 10 | 11 | "github.com/dsoprea/go-exif/v2" 12 | "github.com/dsoprea/go-iptc" 13 | "github.com/dsoprea/go-logging" 14 | ) 15 | 16 | // SegmentList contains a slice of segments. 17 | type SegmentList struct { 18 | segments []*Segment 19 | } 20 | 21 | // NewSegmentList returns a new SegmentList struct. 22 | func NewSegmentList(segments []*Segment) (sl *SegmentList) { 23 | if segments == nil { 24 | segments = make([]*Segment, 0) 25 | } 26 | 27 | return &SegmentList{ 28 | segments: segments, 29 | } 30 | } 31 | 32 | // OffsetsEqual returns true is all segments have the same marker-IDs and were 33 | // found at the same offsets. 34 | func (sl *SegmentList) OffsetsEqual(o *SegmentList) bool { 35 | if len(o.segments) != len(sl.segments) { 36 | return false 37 | } 38 | 39 | for i, s := range o.segments { 40 | if s.MarkerId != sl.segments[i].MarkerId || s.Offset != sl.segments[i].Offset { 41 | return false 42 | } 43 | } 44 | 45 | return true 46 | } 47 | 48 | // Segments returns the underlying slice of segments. 49 | func (sl *SegmentList) Segments() []*Segment { 50 | return sl.segments 51 | } 52 | 53 | // Add adds another segment. 54 | func (sl *SegmentList) Add(s *Segment) { 55 | sl.segments = append(sl.segments, s) 56 | } 57 | 58 | // Print prints segment info. 59 | func (sl *SegmentList) Print() { 60 | if len(sl.segments) == 0 { 61 | fmt.Printf("No segments.\n") 62 | } else { 63 | exifIndex, _, err := sl.FindExif() 64 | if err != nil { 65 | if err == exif.ErrNoExif { 66 | exifIndex = -1 67 | } else { 68 | log.Panic(err) 69 | } 70 | } 71 | 72 | xmpIndex, _, err := sl.FindXmp() 73 | if err != nil { 74 | if err == ErrNoXmp { 75 | xmpIndex = -1 76 | } else { 77 | log.Panic(err) 78 | } 79 | } 80 | 81 | iptcIndex, _, err := sl.FindIptc() 82 | if err != nil { 83 | if err == ErrNoIptc { 84 | iptcIndex = -1 85 | } else { 86 | log.Panic(err) 87 | } 88 | } 89 | 90 | for i, s := range sl.segments { 91 | fmt.Printf("%2d: %s", i, s.EmbeddedString()) 92 | 93 | if i == exifIndex { 94 | fmt.Printf(" [EXIF]") 95 | } else if i == xmpIndex { 96 | fmt.Printf(" [XMP]") 97 | } else if i == iptcIndex { 98 | fmt.Printf(" [IPTC]") 99 | } 100 | 101 | fmt.Printf("\n") 102 | } 103 | } 104 | } 105 | 106 | // Validate checks that all of the markers are actually located at all of the 107 | // recorded offsets. 108 | func (sl *SegmentList) Validate(data []byte) (err error) { 109 | defer func() { 110 | if state := recover(); state != nil { 111 | err = log.Wrap(state.(error)) 112 | } 113 | }() 114 | 115 | if len(sl.segments) < 2 { 116 | log.Panicf("minimum segments not found") 117 | } 118 | 119 | if sl.segments[0].MarkerId != MARKER_SOI { 120 | log.Panicf("first segment not SOI") 121 | } else if sl.segments[len(sl.segments)-1].MarkerId != MARKER_EOI { 122 | log.Panicf("last segment not EOI") 123 | } 124 | 125 | lastOffset := 0 126 | for i, s := range sl.segments { 127 | if lastOffset != 0 && s.Offset <= lastOffset { 128 | log.Panicf("segment offset not greater than the last: SEGMENT=(%d) (0x%08x) <= (0x%08x)", i, s.Offset, lastOffset) 129 | } 130 | 131 | // The scan-data doesn't start with a marker. 132 | if s.MarkerId == 0x0 { 133 | continue 134 | } 135 | 136 | o := s.Offset 137 | if bytes.Compare(data[o:o+2], []byte{0xff, s.MarkerId}) != 0 { 138 | log.Panicf("segment offset does not point to the start of a segment: SEGMENT=(%d) (0x%08x)", i, s.Offset) 139 | } 140 | 141 | lastOffset = o 142 | } 143 | 144 | return nil 145 | } 146 | 147 | // FindExif returns the the segment that hosts the EXIF data (if present). 148 | func (sl *SegmentList) FindExif() (index int, segment *Segment, err error) { 149 | defer func() { 150 | if state := recover(); state != nil { 151 | err = log.Wrap(state.(error)) 152 | } 153 | }() 154 | 155 | for i, s := range sl.segments { 156 | if s.IsExif() == true { 157 | return i, s, nil 158 | } 159 | } 160 | 161 | return -1, nil, exif.ErrNoExif 162 | } 163 | 164 | // FindXmp returns the the segment that hosts the XMP data (if present). 165 | func (sl *SegmentList) FindXmp() (index int, segment *Segment, err error) { 166 | defer func() { 167 | if state := recover(); state != nil { 168 | err = log.Wrap(state.(error)) 169 | } 170 | }() 171 | 172 | for i, s := range sl.segments { 173 | if s.IsXmp() == true { 174 | return i, s, nil 175 | } 176 | } 177 | 178 | return -1, nil, ErrNoXmp 179 | } 180 | 181 | // FindIptc returns the the segment that hosts the IPTC data (if present). 182 | func (sl *SegmentList) FindIptc() (index int, segment *Segment, err error) { 183 | defer func() { 184 | if state := recover(); state != nil { 185 | err = log.Wrap(state.(error)) 186 | } 187 | }() 188 | 189 | for i, s := range sl.segments { 190 | if s.IsIptc() == true { 191 | return i, s, nil 192 | } 193 | } 194 | 195 | return -1, nil, ErrNoIptc 196 | } 197 | 198 | // Exif returns an `exif.Ifd` instance for the EXIF data we currently have. 199 | func (sl *SegmentList) Exif() (rootIfd *exif.Ifd, rawExif []byte, err error) { 200 | defer func() { 201 | if state := recover(); state != nil { 202 | err = log.Wrap(state.(error)) 203 | } 204 | }() 205 | 206 | _, s, err := sl.FindExif() 207 | log.PanicIf(err) 208 | 209 | rootIfd, rawExif, err = s.Exif() 210 | log.PanicIf(err) 211 | 212 | return rootIfd, rawExif, nil 213 | } 214 | 215 | // Iptc returns embedded IPTC data if present. 216 | func (sl *SegmentList) Iptc() (tags map[iptc.StreamTagKey][]iptc.TagData, err error) { 217 | defer func() { 218 | if state := recover(); state != nil { 219 | err = log.Wrap(state.(error)) 220 | } 221 | }() 222 | 223 | // TODO(dustin): Add comment and return data. 224 | 225 | _, s, err := sl.FindIptc() 226 | log.PanicIf(err) 227 | 228 | tags, err = s.Iptc() 229 | log.PanicIf(err) 230 | 231 | return tags, nil 232 | } 233 | 234 | // ConstructExifBuilder returns an `exif.IfdBuilder` instance (needed for 235 | // modifying) preloaded with all existing tags. 236 | func (sl *SegmentList) ConstructExifBuilder() (rootIb *exif.IfdBuilder, err error) { 237 | defer func() { 238 | if state := recover(); state != nil { 239 | err = log.Wrap(state.(error)) 240 | } 241 | }() 242 | 243 | rootIfd, _, err := sl.Exif() 244 | log.PanicIf(err) 245 | 246 | ib := exif.NewIfdBuilderFromExistingChain(rootIfd) 247 | 248 | return ib, nil 249 | } 250 | 251 | // DumpExif returns an unstructured list of tags (useful when just reviewing). 252 | func (sl *SegmentList) DumpExif() (segmentIndex int, segment *Segment, exifTags []exif.ExifTag, err error) { 253 | defer func() { 254 | if state := recover(); state != nil { 255 | err = log.Wrap(state.(error)) 256 | } 257 | }() 258 | 259 | segmentIndex, s, err := sl.FindExif() 260 | if err != nil { 261 | if err == exif.ErrNoExif { 262 | return 0, nil, nil, err 263 | } 264 | 265 | log.Panic(err) 266 | } 267 | 268 | exifTags, err = s.FlatExif() 269 | log.PanicIf(err) 270 | 271 | return segmentIndex, s, exifTags, nil 272 | } 273 | 274 | func makeEmptyExifSegment() (s *Segment) { 275 | 276 | // TODO(dustin): Add test 277 | 278 | return &Segment{ 279 | MarkerId: MARKER_APP1, 280 | } 281 | } 282 | 283 | // SetExif encodes and sets EXIF data into the given segment. If `index` is -1, 284 | // append a new segment. 285 | func (sl *SegmentList) SetExif(ib *exif.IfdBuilder) (err error) { 286 | defer func() { 287 | if state := recover(); state != nil { 288 | err = log.Wrap(state.(error)) 289 | } 290 | }() 291 | 292 | _, s, err := sl.FindExif() 293 | if err != nil { 294 | if log.Is(err, exif.ErrNoExif) == false { 295 | log.Panic(err) 296 | } 297 | 298 | s = makeEmptyExifSegment() 299 | 300 | prefix := sl.segments[:1] 301 | 302 | // Install it near the beginning where we know it's safe. We can't 303 | // insert it after the EOI segment, and there might be more than one 304 | // depending on implementation and/or lax adherence to the standard. 305 | tail := append([]*Segment{s}, sl.segments[1:]...) 306 | 307 | sl.segments = append(prefix, tail...) 308 | } 309 | 310 | err = s.SetExif(ib) 311 | log.PanicIf(err) 312 | 313 | return nil 314 | } 315 | 316 | // DropExif will drop the EXIF data if present. 317 | func (sl *SegmentList) DropExif() (wasDropped bool, err error) { 318 | defer func() { 319 | if state := recover(); state != nil { 320 | err = log.Wrap(state.(error)) 321 | } 322 | }() 323 | 324 | // TODO(dustin): Add test 325 | 326 | i, _, err := sl.FindExif() 327 | if err == nil { 328 | // Found. 329 | sl.segments = append(sl.segments[:i], sl.segments[i+1:]...) 330 | 331 | return true, nil 332 | } else if log.Is(err, exif.ErrNoExif) == false { 333 | log.Panic(err) 334 | } 335 | 336 | // Not found. 337 | return false, nil 338 | } 339 | 340 | // Write writes the segment data to the given `io.Writer`. 341 | func (sl *SegmentList) Write(w io.Writer) (err error) { 342 | defer func() { 343 | if state := recover(); state != nil { 344 | err = log.Wrap(state.(error)) 345 | } 346 | }() 347 | 348 | offset := 0 349 | 350 | for i, s := range sl.segments { 351 | h := sha1.New() 352 | h.Write(s.Data) 353 | 354 | // The scan-data will have a marker-ID of (0) because it doesn't have a 355 | // marker-ID or length. 356 | if s.MarkerId != 0 { 357 | _, err := w.Write([]byte{0xff}) 358 | log.PanicIf(err) 359 | 360 | offset++ 361 | 362 | _, err = w.Write([]byte{s.MarkerId}) 363 | log.PanicIf(err) 364 | 365 | offset++ 366 | 367 | sizeLen, found := markerLen[s.MarkerId] 368 | if found == false || sizeLen == 2 { 369 | sizeLen = 2 370 | l := uint16(len(s.Data) + sizeLen) 371 | 372 | err = binary.Write(w, binary.BigEndian, &l) 373 | log.PanicIf(err) 374 | 375 | offset += 2 376 | } else if sizeLen == 4 { 377 | l := uint32(len(s.Data) + sizeLen) 378 | 379 | err = binary.Write(w, binary.BigEndian, &l) 380 | log.PanicIf(err) 381 | 382 | offset += 4 383 | } else if sizeLen != 0 { 384 | log.Panicf("not a supported marker-size: SEGMENT-INDEX=(%d) MARKER-ID=(0x%02x) MARKER-SIZE-LEN=(%d)", i, s.MarkerId, sizeLen) 385 | } 386 | } 387 | 388 | _, err := w.Write(s.Data) 389 | log.PanicIf(err) 390 | 391 | offset += len(s.Data) 392 | } 393 | 394 | return nil 395 | } 396 | -------------------------------------------------------------------------------- /segment_test.go: -------------------------------------------------------------------------------- 1 | package jpegstructure 2 | 3 | import ( 4 | "bytes" 5 | "fmt" 6 | "strings" 7 | "testing" 8 | 9 | "io/ioutil" 10 | 11 | "github.com/dsoprea/go-exif/v2" 12 | "github.com/dsoprea/go-exif/v2/common" 13 | "github.com/dsoprea/go-exif/v2/undefined" 14 | "github.com/dsoprea/go-logging" 15 | ) 16 | 17 | func TestSegment_SetExif_Update(t *testing.T) { 18 | defer func() { 19 | if state := recover(); state != nil { 20 | err := log.Wrap(state.(error)) 21 | log.PrintErrorf(err, "Test failure.") 22 | t.Fatalf("Test failure.") 23 | } 24 | }() 25 | 26 | filepath := GetTestImageFilepath() 27 | 28 | // TODO(dustin): !! Might want to test a reconstruction without actually modifying anything. This is also useful. Everything will still be reallocated and this will help us determine if we're having parsing/encoding problems versions problems with an individual tag's value. 29 | // TODO(dustin): !! Use native/third-party EXIF support to test? 30 | 31 | // Parse the image. 32 | 33 | jmp := NewJpegMediaParser() 34 | 35 | intfc, err := jmp.ParseFile(filepath) 36 | log.PanicIf(err) 37 | 38 | sl := intfc.(*SegmentList) 39 | 40 | // Update the UserComment tag. 41 | 42 | rootIb, err := sl.ConstructExifBuilder() 43 | log.PanicIf(err) 44 | 45 | i, err := rootIb.Find(exifcommon.IfdExifStandardIfdIdentity.TagId()) 46 | log.PanicIf(err) 47 | 48 | exifBt := rootIb.Tags()[i] 49 | exifIb := exifBt.Value().Ib() 50 | 51 | uc := exifundefined.Tag9286UserComment{ 52 | EncodingType: exifundefined.TagUndefinedType_9286_UserComment_Encoding_ASCII, 53 | EncodingBytes: []byte("TEST COMMENT"), 54 | } 55 | 56 | err = exifIb.SetStandardWithName("UserComment", uc) 57 | log.PanicIf(err) 58 | 59 | // Update the exif segment. 60 | 61 | err = sl.SetExif(rootIb) 62 | log.PanicIf(err) 63 | 64 | b := new(bytes.Buffer) 65 | 66 | err = sl.Write(b) 67 | log.PanicIf(err) 68 | 69 | recoveredBytes := b.Bytes() 70 | 71 | // Parse the re-encoded JPEG data and validate. 72 | 73 | recoveredIntfc, err := jmp.ParseBytes(recoveredBytes) 74 | log.PanicIf(err) 75 | 76 | recoveredSl := recoveredIntfc.(*SegmentList) 77 | 78 | rootIfd, _, err := recoveredSl.Exif() 79 | log.PanicIf(err) 80 | 81 | exifIfd, err := rootIfd.ChildWithIfdPath(exifcommon.IfdExifStandardIfdIdentity) 82 | log.PanicIf(err) 83 | 84 | results, err := exifIfd.FindTagWithName("UserComment") 85 | log.PanicIf(err) 86 | 87 | ucIte := results[0] 88 | 89 | if ucIte.TagId() != 0x9286 { 90 | t.Fatalf("tag-ID not correct") 91 | } 92 | 93 | recoveredValueBytes, err := ucIte.GetRawBytes() 94 | log.PanicIf(err) 95 | 96 | expectedValueBytes := make([]byte, 0) 97 | 98 | expectedValueBytes = append(expectedValueBytes, []byte{'A', 'S', 'C', 'I', 'I', 0, 0, 0}...) 99 | expectedValueBytes = append(expectedValueBytes, []byte("TEST COMMENT")...) 100 | 101 | if bytes.Compare(recoveredValueBytes, expectedValueBytes) != 0 { 102 | t.Fatalf("Recovered UserComment does not have the right value: %v != %v", recoveredValueBytes, expectedValueBytes) 103 | } 104 | } 105 | 106 | func TestSegment_SetExif_FromScratch(t *testing.T) { 107 | defer func() { 108 | if state := recover(); state != nil { 109 | err := log.Wrap(state.(error)) 110 | log.PrintErrorf(err, "Test failure.") 111 | t.Fatalf("Test failure.") 112 | } 113 | }() 114 | 115 | // Create the IB. 116 | 117 | im := exif.NewIfdMappingWithStandard() 118 | ti := exif.NewTagIndex() 119 | 120 | err := exif.LoadStandardTags(ti) 121 | log.PanicIf(err) 122 | 123 | rootIb := exif.NewIfdBuilder(im, ti, exifcommon.IfdStandardIfdIdentity, exifcommon.EncodeDefaultByteOrder) 124 | 125 | err = rootIb.AddStandardWithName("ProcessingSoftware", "some software") 126 | log.PanicIf(err) 127 | 128 | // Encode. 129 | 130 | s := makeEmptyExifSegment() 131 | 132 | err = s.SetExif(rootIb) 133 | log.PanicIf(err) 134 | 135 | // Decode. 136 | 137 | rootIfd, _, err := s.Exif() 138 | log.PanicIf(err) 139 | 140 | results, err := rootIfd.FindTagWithName("ProcessingSoftware") 141 | log.PanicIf(err) 142 | 143 | ucIte := results[0] 144 | 145 | if ucIte.TagId() != 0x000b { 146 | t.Fatalf("tag-ID not correct") 147 | } 148 | 149 | recoveredValueRaw, err := ucIte.Value() 150 | log.PanicIf(err) 151 | 152 | recoveredValue := recoveredValueRaw.(string) 153 | if recoveredValue != "some software" { 154 | t.Fatalf("Value of tag not correct: [%s]", recoveredValue) 155 | } 156 | } 157 | 158 | func TestSegment_Exif(t *testing.T) { 159 | defer func() { 160 | if state := recover(); state != nil { 161 | err := log.Wrap(state.(error)) 162 | log.PrintErrorf(err, "Test failure.") 163 | t.Fatalf("Test failure.") 164 | } 165 | }() 166 | 167 | imageFilepath := GetTestImageFilepath() 168 | 169 | // Parse the image. 170 | 171 | jmp := NewJpegMediaParser() 172 | 173 | intfc, err := jmp.ParseFile(imageFilepath) 174 | log.PanicIf(err) 175 | 176 | sl := intfc.(*SegmentList) 177 | 178 | _, s, err := sl.FindExif() 179 | log.PanicIf(err) 180 | 181 | rootIfd, data, err := s.Exif() 182 | log.PanicIf(err) 183 | 184 | if rootIfd.IfdIdentity().Equals(exifcommon.IfdStandardIfdIdentity) != true { 185 | t.Fatalf("root IFD does not have correct identity") 186 | } 187 | 188 | exifFilepath := fmt.Sprintf("%s.just_exif", imageFilepath) 189 | 190 | expectedExifBytes, err := ioutil.ReadFile(exifFilepath) 191 | log.PanicIf(err) 192 | 193 | if bytes.Compare(data, expectedExifBytes) != 0 { 194 | t.Fatalf("exif data not correct") 195 | } 196 | } 197 | 198 | func TestSegment_IsExif_Hit(t *testing.T) { 199 | defer func() { 200 | if state := recover(); state != nil { 201 | err := log.Wrap(state.(error)) 202 | log.PrintErrorf(err, "Test failure.") 203 | t.Fatalf("Test failure.") 204 | } 205 | }() 206 | 207 | imageFilepath := GetTestImageFilepath() 208 | 209 | // Parse the image. 210 | 211 | jmp := NewJpegMediaParser() 212 | 213 | intfc, err := jmp.ParseFile(imageFilepath) 214 | log.PanicIf(err) 215 | 216 | sl := intfc.(*SegmentList) 217 | 218 | _, s, err := sl.FindExif() 219 | log.PanicIf(err) 220 | 221 | if s.IsExif() != true { 222 | t.Fatalf("Did not return true.") 223 | } 224 | } 225 | 226 | func TestSegment_IsExif_Miss(t *testing.T) { 227 | defer func() { 228 | if state := recover(); state != nil { 229 | err := log.Wrap(state.(error)) 230 | log.PrintErrorf(err, "Test failure.") 231 | t.Fatalf("Test failure.") 232 | } 233 | }() 234 | 235 | imageFilepath := GetTestImageFilepath() 236 | 237 | // Parse the image. 238 | 239 | jmp := NewJpegMediaParser() 240 | 241 | intfc, err := jmp.ParseFile(imageFilepath) 242 | log.PanicIf(err) 243 | 244 | sl := intfc.(*SegmentList) 245 | 246 | if sl.Segments()[4].IsExif() != false { 247 | t.Fatalf("Did not return false.") 248 | } 249 | } 250 | 251 | func TestSegment_IsXmp_Hit(t *testing.T) { 252 | defer func() { 253 | if state := recover(); state != nil { 254 | err := log.Wrap(state.(error)) 255 | log.PrintErrorf(err, "Test failure.") 256 | t.Fatalf("Test failure.") 257 | } 258 | }() 259 | 260 | imageFilepath := GetTestImageFilepath() 261 | 262 | // Parse the image. 263 | 264 | jmp := NewJpegMediaParser() 265 | 266 | intfc, err := jmp.ParseFile(imageFilepath) 267 | log.PanicIf(err) 268 | 269 | sl := intfc.(*SegmentList) 270 | 271 | _, s, err := sl.FindXmp() 272 | log.PanicIf(err) 273 | 274 | if s.IsXmp() != true { 275 | t.Fatalf("Did not return true.") 276 | } 277 | } 278 | 279 | func TestSegment_IsXmp_Miss(t *testing.T) { 280 | defer func() { 281 | if state := recover(); state != nil { 282 | err := log.Wrap(state.(error)) 283 | log.PrintErrorf(err, "Test failure.") 284 | t.Fatalf("Test failure.") 285 | } 286 | }() 287 | 288 | imageFilepath := GetTestImageFilepath() 289 | 290 | // Parse the image. 291 | 292 | jmp := NewJpegMediaParser() 293 | 294 | intfc, err := jmp.ParseFile(imageFilepath) 295 | log.PanicIf(err) 296 | 297 | sl := intfc.(*SegmentList) 298 | 299 | if sl.Segments()[4].IsXmp() != false { 300 | t.Fatalf("Did not return false.") 301 | } 302 | } 303 | 304 | func TestSegment_FormattedXmp(t *testing.T) { 305 | defer func() { 306 | if state := recover(); state != nil { 307 | err := log.Wrap(state.(error)) 308 | log.PrintErrorf(err, "Test failure.") 309 | t.Fatalf("Test failure.") 310 | } 311 | }() 312 | 313 | imageFilepath := GetTestImageFilepath() 314 | 315 | // Parse the image. 316 | 317 | jmp := NewJpegMediaParser() 318 | 319 | intfc, err := jmp.ParseFile(imageFilepath) 320 | log.PanicIf(err) 321 | 322 | sl := intfc.(*SegmentList) 323 | 324 | _, s, err := sl.FindXmp() 325 | log.PanicIf(err) 326 | 327 | actualData, err := s.FormattedXmp() 328 | log.PanicIf(err) 329 | 330 | // Filter out the Unicode BOM character since this would add unnecessary 331 | // complexity to the test. 332 | actualData = strings.ReplaceAll(actualData, "\ufeff", "") 333 | 334 | // Replace Windows-style newlines to Unix. 335 | actualData = strings.ReplaceAll(actualData, "\r\n", "\n") 336 | 337 | expectedData := ` 338 | 339 | 340 | 341 | 0 342 | 343 | 344 | 345 | 346 | ` 347 | 348 | if actualData != expectedData { 349 | t.Fatalf("XMP data is not correct:\nACTUAL:\n>>>%s<<<\n\nEXPECTED:\n>>>%s<<<\n", actualData, expectedData) 350 | } 351 | } 352 | -------------------------------------------------------------------------------- /splitter.go: -------------------------------------------------------------------------------- 1 | package jpegstructure 2 | 3 | import ( 4 | "bufio" 5 | "bytes" 6 | "io" 7 | 8 | "encoding/binary" 9 | 10 | "github.com/dsoprea/go-logging" 11 | ) 12 | 13 | // JpegSplitter uses the Go stream splitter to divide the JPEG stream into 14 | // segments. 15 | type JpegSplitter struct { 16 | lastMarkerId byte 17 | lastMarkerName string 18 | counter int 19 | lastIsScanData bool 20 | visitor interface{} 21 | 22 | currentOffset int 23 | segments *SegmentList 24 | 25 | scandataOffset int 26 | } 27 | 28 | // NewJpegSplitter returns a new JpegSplitter. 29 | func NewJpegSplitter(visitor interface{}) *JpegSplitter { 30 | return &JpegSplitter{ 31 | segments: NewSegmentList(nil), 32 | visitor: visitor, 33 | } 34 | } 35 | 36 | // Segments returns all found segments. 37 | func (js *JpegSplitter) Segments() *SegmentList { 38 | return js.segments 39 | } 40 | 41 | // MarkerId returns the ID of the last processed marker. 42 | func (js *JpegSplitter) MarkerId() byte { 43 | return js.lastMarkerId 44 | } 45 | 46 | // MarkerName returns the name of the last-processed marker. 47 | func (js *JpegSplitter) MarkerName() string { 48 | return js.lastMarkerName 49 | } 50 | 51 | // Counter returns the number of processed segments. 52 | func (js *JpegSplitter) Counter() int { 53 | return js.counter 54 | } 55 | 56 | // IsScanData returns whether the last processed segment was scan-data. 57 | func (js *JpegSplitter) IsScanData() bool { 58 | return js.lastIsScanData 59 | } 60 | 61 | func (js *JpegSplitter) processScanData(data []byte) (advanceBytes int, err error) { 62 | defer func() { 63 | if state := recover(); state != nil { 64 | err = log.Wrap(state.(error)) 65 | } 66 | }() 67 | 68 | // Search through the segment, past all 0xff's therein, until we encounter 69 | // the EOI segment. 70 | 71 | dataLength := -1 72 | for i := js.scandataOffset; i < len(data); i++ { 73 | thisByte := data[i] 74 | 75 | if i == 0 { 76 | continue 77 | } 78 | 79 | lastByte := data[i-1] 80 | if lastByte != 0xff { 81 | continue 82 | } 83 | 84 | if thisByte == 0x00 || thisByte >= 0xd0 && thisByte <= 0xd8 { 85 | continue 86 | } 87 | 88 | // After all of the other checks, this means that we're on the EOF 89 | // segment. 90 | if thisByte != MARKER_EOI { 91 | continue 92 | } 93 | 94 | dataLength = i - 1 95 | break 96 | } 97 | 98 | if dataLength == -1 { 99 | // On the next pass, start on the last byte of this pass, just in case 100 | // the first byte of the two-byte sequence is here. 101 | js.scandataOffset = len(data) - 1 102 | 103 | jpegLogger.Debugf(nil, "Scan-data not fully available (%d).", len(data)) 104 | return 0, nil 105 | } 106 | 107 | js.lastIsScanData = true 108 | js.lastMarkerId = 0 109 | js.lastMarkerName = "" 110 | 111 | // Note that we don't increment the counter since this isn't an actual 112 | // segment. 113 | 114 | jpegLogger.Debugf(nil, "End of scan-data.") 115 | 116 | err = js.handleSegment(0x0, "!SCANDATA", 0x0, data[:dataLength]) 117 | log.PanicIf(err) 118 | 119 | return dataLength, nil 120 | } 121 | 122 | func (js *JpegSplitter) readSegment(data []byte) (count int, err error) { 123 | defer func() { 124 | if state := recover(); state != nil { 125 | err = log.Wrap(state.(error)) 126 | } 127 | }() 128 | 129 | if js.counter == 0 { 130 | // Verify magic bytes. 131 | 132 | if len(data) < 3 { 133 | jpegLogger.Debugf(nil, "Not enough (1)") 134 | return 0, nil 135 | } 136 | 137 | if data[0] == jpegMagic2000[0] && data[1] == jpegMagic2000[1] && data[2] == jpegMagic2000[2] { 138 | // TODO(dustin): Revisit JPEG2000 support. 139 | log.Panicf("JPEG2000 not supported") 140 | } 141 | 142 | if data[0] != jpegMagicStandard[0] || data[1] != jpegMagicStandard[1] || data[2] != jpegMagicStandard[2] { 143 | log.Panicf("file does not look like a JPEG: (%02x) (%02x) (%02x)", data[0], data[1], data[2]) 144 | } 145 | } 146 | 147 | chunkLength := len(data) 148 | 149 | jpegLogger.Debugf(nil, "SPLIT: LEN=(%d) COUNTER=(%d)", chunkLength, js.counter) 150 | 151 | if js.scanDataIsNext() == true { 152 | // If the last segment was the SOS, we're currently sitting on scan data. 153 | // Search for the EOI marker afterward in order to know how much data 154 | // there is. Return this as its own token. 155 | // 156 | // REF: https://stackoverflow.com/questions/26715684/parsing-jpeg-sos-marker 157 | 158 | advanceBytes, err := js.processScanData(data) 159 | log.PanicIf(err) 160 | 161 | // This will either return 0 and implicitly request that we need more 162 | // data and then need to run again or will return an actual byte count 163 | // to progress by. 164 | 165 | return advanceBytes, nil 166 | } else if js.lastMarkerId == MARKER_EOI { 167 | // We have more data following the EOI, which is unexpected. There 168 | // might be non-standard cruft at the end of the file. Terminate the 169 | // parse because the file-structure is, technically, complete at this 170 | // point. 171 | 172 | return 0, io.EOF 173 | } else { 174 | js.lastIsScanData = false 175 | } 176 | 177 | // If we're here, we're supposed to be sitting on the 0xff bytes at the 178 | // beginning of a segment (just before the marker). 179 | 180 | if data[0] != 0xff { 181 | log.Panicf("not on new segment marker @ (%d): (%02X)", js.currentOffset, data[0]) 182 | } 183 | 184 | i := 0 185 | found := false 186 | for ; i < chunkLength; i++ { 187 | jpegLogger.Debugf(nil, "Prefix check: (%d) %02X", i, data[i]) 188 | 189 | if data[i] != 0xff { 190 | found = true 191 | break 192 | } 193 | } 194 | 195 | jpegLogger.Debugf(nil, "Skipped over leading 0xFF bytes: (%d)", i) 196 | 197 | if found == false || i >= chunkLength { 198 | jpegLogger.Debugf(nil, "Not enough (3)") 199 | return 0, nil 200 | } 201 | 202 | markerId := data[i] 203 | 204 | js.lastMarkerName = markerNames[markerId] 205 | 206 | sizeLen, found := markerLen[markerId] 207 | jpegLogger.Debugf(nil, "MARKER-ID=%x SIZELEN=%v FOUND=%v", markerId, sizeLen, found) 208 | 209 | i++ 210 | 211 | b := bytes.NewBuffer(data[i:]) 212 | payloadLength := 0 213 | 214 | // marker-ID + size => 2 + 215 | headerSize := 2 + sizeLen 216 | 217 | if found == false { 218 | 219 | // It's not one of the static-length markers. Read the length. 220 | // 221 | // The length is an unsigned 16-bit network/big-endian. 222 | 223 | // marker-ID + size => 2 + 2 224 | headerSize = 2 + 2 225 | 226 | if i+2 >= chunkLength { 227 | jpegLogger.Debugf(nil, "Not enough (4)") 228 | return 0, nil 229 | } 230 | 231 | l := uint16(0) 232 | err = binary.Read(b, binary.BigEndian, &l) 233 | log.PanicIf(err) 234 | 235 | if l <= 2 { 236 | log.Panicf("length of size read for non-special marker (%02x) is unexpectedly not more than two.", markerId) 237 | } 238 | 239 | // (l includes the bytes of the length itself.) 240 | payloadLength = int(l) - 2 241 | jpegLogger.Debugf(nil, "DataLength (dynamically-sized segment): (%d)", payloadLength) 242 | 243 | i += 2 244 | } else if sizeLen > 0 { 245 | 246 | // Accommodates the non-zero markers in our marker index, which only 247 | // represent J2C extensions. 248 | // 249 | // The length is an unsigned 32-bit network/big-endian. 250 | 251 | // TODO(dustin): !! This needs to be tested, but we need an image. 252 | 253 | if sizeLen != 4 { 254 | log.Panicf("known non-zero marker is not four bytes, which is not currently handled: M=(%x)", markerId) 255 | } 256 | 257 | if i+4 >= chunkLength { 258 | jpegLogger.Debugf(nil, "Not enough (5)") 259 | return 0, nil 260 | } 261 | 262 | l := uint32(0) 263 | err = binary.Read(b, binary.BigEndian, &l) 264 | log.PanicIf(err) 265 | 266 | payloadLength = int(l) - 4 267 | jpegLogger.Debugf(nil, "DataLength (four-byte-length segment): (%u)", l) 268 | 269 | i += 4 270 | } 271 | 272 | jpegLogger.Debugf(nil, "PAYLOAD-LENGTH: %d", payloadLength) 273 | 274 | payload := data[i:] 275 | 276 | if payloadLength < 0 { 277 | log.Panicf("payload length less than zero: (%d)", payloadLength) 278 | } 279 | 280 | i += int(payloadLength) 281 | 282 | if i > chunkLength { 283 | jpegLogger.Debugf(nil, "Not enough (6)") 284 | return 0, nil 285 | } 286 | 287 | jpegLogger.Debugf(nil, "Found whole segment.") 288 | 289 | js.lastMarkerId = markerId 290 | 291 | payloadWindow := payload[:payloadLength] 292 | err = js.handleSegment(markerId, js.lastMarkerName, headerSize, payloadWindow) 293 | log.PanicIf(err) 294 | 295 | js.counter++ 296 | 297 | jpegLogger.Debugf(nil, "Returning advance of (%d)", i) 298 | 299 | return i, nil 300 | } 301 | 302 | func (js *JpegSplitter) scanDataIsNext() bool { 303 | return js.lastMarkerId == MARKER_SOS 304 | } 305 | 306 | // Split is the base splitting function that satisfies `bufio.SplitFunc`. 307 | func (js *JpegSplitter) Split(data []byte, atEOF bool) (advance int, token []byte, err error) { 308 | defer func() { 309 | if state := recover(); state != nil { 310 | err = log.Wrap(state.(error)) 311 | } 312 | }() 313 | 314 | for len(data) > 0 { 315 | currentAdvance, err := js.readSegment(data) 316 | if err != nil { 317 | if err == io.EOF { 318 | // We've encountered an EOI marker. 319 | return 0, nil, err 320 | } 321 | 322 | log.Panic(err) 323 | } 324 | 325 | if currentAdvance == 0 { 326 | if len(data) > 0 && atEOF == true { 327 | // Provide a little context in the error message. 328 | 329 | if js.scanDataIsNext() == true { 330 | // Yes, we've ran into this. 331 | 332 | log.Panicf("scan-data is unbounded; EOI not encountered before EOF") 333 | } else { 334 | log.Panicf("partial segment data encountered before scan-data") 335 | } 336 | } 337 | 338 | // We don't have enough data for another segment. 339 | break 340 | } 341 | 342 | data = data[currentAdvance:] 343 | advance += currentAdvance 344 | } 345 | 346 | return advance, nil, nil 347 | } 348 | 349 | func (js *JpegSplitter) parseSof(data []byte) (sof *SofSegment, err error) { 350 | defer func() { 351 | if state := recover(); state != nil { 352 | err = log.Wrap(state.(error)) 353 | } 354 | }() 355 | 356 | stream := bytes.NewBuffer(data) 357 | buffer := bufio.NewReader(stream) 358 | 359 | bitsPerSample, err := buffer.ReadByte() 360 | log.PanicIf(err) 361 | 362 | height := uint16(0) 363 | err = binary.Read(buffer, binary.BigEndian, &height) 364 | log.PanicIf(err) 365 | 366 | width := uint16(0) 367 | err = binary.Read(buffer, binary.BigEndian, &width) 368 | log.PanicIf(err) 369 | 370 | componentCount, err := buffer.ReadByte() 371 | log.PanicIf(err) 372 | 373 | sof = &SofSegment{ 374 | BitsPerSample: bitsPerSample, 375 | Width: width, 376 | Height: height, 377 | ComponentCount: componentCount, 378 | } 379 | 380 | return sof, nil 381 | } 382 | 383 | func (js *JpegSplitter) parseAppData(markerId byte, data []byte) (err error) { 384 | defer func() { 385 | if state := recover(); state != nil { 386 | err = log.Wrap(state.(error)) 387 | } 388 | }() 389 | 390 | return nil 391 | } 392 | 393 | func (js *JpegSplitter) handleSegment(markerId byte, markerName string, headerSize int, payload []byte) (err error) { 394 | defer func() { 395 | if state := recover(); state != nil { 396 | err = log.Wrap(state.(error)) 397 | } 398 | }() 399 | 400 | cloned := make([]byte, len(payload)) 401 | copy(cloned, payload) 402 | 403 | s := &Segment{ 404 | MarkerId: markerId, 405 | MarkerName: markerName, 406 | Offset: js.currentOffset, 407 | Data: cloned, 408 | } 409 | 410 | jpegLogger.Debugf(nil, "Encountered marker (0x%02x) [%s] at offset (%d)", markerId, markerName, js.currentOffset) 411 | 412 | js.currentOffset += headerSize + len(payload) 413 | 414 | js.segments.Add(s) 415 | 416 | sv, ok := js.visitor.(SegmentVisitor) 417 | if ok == true { 418 | err = sv.HandleSegment(js.lastMarkerId, js.lastMarkerName, js.counter, js.lastIsScanData) 419 | log.PanicIf(err) 420 | } 421 | 422 | if markerId >= MARKER_SOF0 && markerId <= MARKER_SOF15 { 423 | ssv, ok := js.visitor.(SofSegmentVisitor) 424 | if ok == true { 425 | sof, err := js.parseSof(payload) 426 | log.PanicIf(err) 427 | 428 | err = ssv.HandleSof(sof) 429 | log.PanicIf(err) 430 | } 431 | } else if markerId >= MARKER_APP0 && markerId <= MARKER_APP15 { 432 | err := js.parseAppData(markerId, payload) 433 | log.PanicIf(err) 434 | } 435 | 436 | return nil 437 | } 438 | -------------------------------------------------------------------------------- /splitter_test.go: -------------------------------------------------------------------------------- 1 | package jpegstructure 2 | 3 | import ( 4 | "bufio" 5 | "bytes" 6 | "os" 7 | "reflect" 8 | "testing" 9 | 10 | "github.com/dsoprea/go-logging" 11 | ) 12 | 13 | type collectorVisitor struct { 14 | markerList []byte 15 | sofList []SofSegment 16 | } 17 | 18 | func (v *collectorVisitor) HandleSegment(lastMarkerId byte, lastMarkerName string, counter int, lastIsScanData bool) (err error) { 19 | defer func() { 20 | if state := recover(); state != nil { 21 | err = log.Wrap(state.(error)) 22 | } 23 | }() 24 | 25 | v.markerList = append(v.markerList, lastMarkerId) 26 | 27 | return nil 28 | } 29 | 30 | func (v *collectorVisitor) HandleSof(sof *SofSegment) (err error) { 31 | defer func() { 32 | if state := recover(); state != nil { 33 | err = log.Wrap(state.(error)) 34 | } 35 | }() 36 | 37 | v.sofList = append(v.sofList, *sof) 38 | 39 | return nil 40 | } 41 | 42 | func Test_JpegSplitter_Split(t *testing.T) { 43 | defer func() { 44 | if state := recover(); state != nil { 45 | err := log.Wrap(state.(error)) 46 | log.PrintErrorf(err, "Test failure.") 47 | t.Fatalf("Test failure.") 48 | } 49 | }() 50 | 51 | filepath := GetTestImageFilepath() 52 | 53 | f, err := os.Open(filepath) 54 | log.PanicIf(err) 55 | 56 | defer f.Close() 57 | 58 | stat, err := f.Stat() 59 | log.PanicIf(err) 60 | 61 | size := stat.Size() 62 | 63 | v := new(collectorVisitor) 64 | js := NewJpegSplitter(v) 65 | 66 | s := bufio.NewScanner(f) 67 | 68 | // Since each segment can be any size, our buffer must allowed to grow as 69 | // large as the file. 70 | buffer := []byte{} 71 | s.Buffer(buffer, int(size)) 72 | 73 | s.Split(js.Split) 74 | 75 | for s.Scan() != false { 76 | } 77 | 78 | if s.Err() != nil { 79 | log.PrintError(s.Err()) 80 | t.Fatalf("error while scanning: %v", s.Err()) 81 | } 82 | 83 | expectedMarkers := []byte{0xd8, 0xe1, 0xe1, 0xdb, 0xc0, 0xc4, 0xda, 0x00, 0xd9} 84 | 85 | if bytes.Compare(v.markerList, expectedMarkers) != 0 { 86 | t.Fatalf("Markers found are not correct: %v\n", DumpBytesToString(v.markerList)) 87 | } 88 | 89 | expectedSofList := []SofSegment{ 90 | { 91 | BitsPerSample: 8, 92 | Width: 3840, 93 | Height: 2560, 94 | ComponentCount: 3, 95 | }, 96 | { 97 | BitsPerSample: 0, 98 | Width: 1281, 99 | Height: 1, 100 | ComponentCount: 1, 101 | }, 102 | } 103 | 104 | if reflect.DeepEqual(v.sofList, expectedSofList) == false { 105 | t.Fatalf("SOF segments not equal: %v\n", v.sofList) 106 | } 107 | } 108 | -------------------------------------------------------------------------------- /testing_common.go: -------------------------------------------------------------------------------- 1 | package jpegstructure 2 | 3 | import ( 4 | "os" 5 | "path" 6 | 7 | "github.com/dsoprea/go-logging" 8 | ) 9 | 10 | var ( 11 | testImageRelFilepath = "NDM_8901.jpg" 12 | testImageFujiFilepath = "FUJI.jpg" 13 | ) 14 | 15 | var ( 16 | moduleRootPath = "" 17 | assetsPath = "" 18 | ) 19 | 20 | // GetModuleRootPath returns the root-path of the module. 21 | func GetModuleRootPath() string { 22 | if moduleRootPath == "" { 23 | moduleRootPath = os.Getenv("JPEG_MODULE_ROOT_PATH") 24 | if moduleRootPath != "" { 25 | return moduleRootPath 26 | } 27 | 28 | currentWd, err := os.Getwd() 29 | log.PanicIf(err) 30 | 31 | currentPath := currentWd 32 | visited := make([]string, 0) 33 | 34 | for { 35 | tryStampFilepath := path.Join(currentPath, ".MODULE_ROOT") 36 | 37 | _, err := os.Stat(tryStampFilepath) 38 | if err != nil && os.IsNotExist(err) != true { 39 | log.Panic(err) 40 | } else if err == nil { 41 | break 42 | } 43 | 44 | visited = append(visited, tryStampFilepath) 45 | 46 | currentPath = path.Dir(currentPath) 47 | if currentPath == "/" { 48 | log.Panicf("could not find module-root: %v", visited) 49 | } 50 | } 51 | 52 | moduleRootPath = currentPath 53 | } 54 | 55 | return moduleRootPath 56 | } 57 | 58 | // GetTestAssetsPath returns the path of the test-assets. 59 | func GetTestAssetsPath() string { 60 | if assetsPath == "" { 61 | moduleRootPath := GetModuleRootPath() 62 | assetsPath = path.Join(moduleRootPath, "assets") 63 | } 64 | 65 | return assetsPath 66 | } 67 | 68 | // GetTestImageFilepath returns the file-path of the common test-image. 69 | func GetTestImageFilepath() string { 70 | assetsPath := GetTestAssetsPath() 71 | filepath := path.Join(assetsPath, testImageRelFilepath) 72 | 73 | return filepath 74 | } 75 | 76 | // GetTestImageFujiFilepath returns the file-path of the fuji camera test image. 77 | func GetTestImageFujiFilepath() string { 78 | assetsPath := GetTestAssetsPath() 79 | filepath := path.Join(assetsPath, testImageFujiFilepath) 80 | 81 | return filepath 82 | } 83 | -------------------------------------------------------------------------------- /utility.go: -------------------------------------------------------------------------------- 1 | package jpegstructure 2 | 3 | import ( 4 | "bytes" 5 | "fmt" 6 | "sort" 7 | "strings" 8 | 9 | "github.com/dsoprea/go-logging" 10 | "github.com/go-xmlfmt/xmlfmt" 11 | ) 12 | 13 | // DumpBytes prints the hex for a given byte-slice. 14 | func DumpBytes(data []byte) { 15 | fmt.Printf("DUMP: ") 16 | for _, x := range data { 17 | fmt.Printf("%02x ", x) 18 | } 19 | 20 | fmt.Printf("\n") 21 | } 22 | 23 | // DumpBytesClause prints a Go-formatted byte-slice expression. 24 | func DumpBytesClause(data []byte) { 25 | fmt.Printf("DUMP: ") 26 | 27 | fmt.Printf("[]byte { ") 28 | 29 | for i, x := range data { 30 | fmt.Printf("0x%02x", x) 31 | 32 | if i < len(data)-1 { 33 | fmt.Printf(", ") 34 | } 35 | } 36 | 37 | fmt.Printf(" }\n") 38 | } 39 | 40 | // DumpBytesToString returns a string of hex-encoded bytes. 41 | func DumpBytesToString(data []byte) string { 42 | b := new(bytes.Buffer) 43 | 44 | for i, x := range data { 45 | _, err := b.WriteString(fmt.Sprintf("%02x", x)) 46 | log.PanicIf(err) 47 | 48 | if i < len(data)-1 { 49 | _, err := b.WriteRune(' ') 50 | log.PanicIf(err) 51 | } 52 | } 53 | 54 | return b.String() 55 | } 56 | 57 | // DumpBytesClauseToString returns a string of Go-formatted byte values. 58 | func DumpBytesClauseToString(data []byte) string { 59 | b := new(bytes.Buffer) 60 | 61 | for i, x := range data { 62 | _, err := b.WriteString(fmt.Sprintf("0x%02x", x)) 63 | log.PanicIf(err) 64 | 65 | if i < len(data)-1 { 66 | _, err := b.WriteString(", ") 67 | log.PanicIf(err) 68 | } 69 | } 70 | 71 | return b.String() 72 | } 73 | 74 | // FormatXml prettifies XML data. 75 | func FormatXml(raw string) (formatted string, err error) { 76 | defer func() { 77 | if state := recover(); state != nil { 78 | err = log.Wrap(state.(error)) 79 | } 80 | }() 81 | 82 | formatted = xmlfmt.FormatXML(raw, " ", " ") 83 | formatted = strings.TrimSpace(formatted) 84 | 85 | return formatted, nil 86 | } 87 | 88 | // SortStringStringMap sorts a string-string dictionary and returns it as a list 89 | // of 2-tuples. 90 | func SortStringStringMap(data map[string]string) (sorted [][2]string) { 91 | // Sort keys. 92 | 93 | sortedKeys := make([]string, len(data)) 94 | i := 0 95 | for key := range data { 96 | sortedKeys[i] = key 97 | i++ 98 | } 99 | 100 | sort.Strings(sortedKeys) 101 | 102 | // Build result. 103 | 104 | sorted = make([][2]string, len(sortedKeys)) 105 | for i, key := range sortedKeys { 106 | sorted[i] = [2]string{key, data[key]} 107 | } 108 | 109 | return sorted 110 | } 111 | -------------------------------------------------------------------------------- /v2/.MODULE_ROOT: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dsoprea/go-jpeg-image-structure/4f3f7e93410239bde7f71860b8b0af69ec77274d/v2/.MODULE_ROOT -------------------------------------------------------------------------------- /v2/LICENSE: -------------------------------------------------------------------------------- 1 | MIT LICENSE 2 | 3 | Copyright 2020 Dustin Oprea 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 6 | 7 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 8 | 9 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 10 | -------------------------------------------------------------------------------- /v2/README.md: -------------------------------------------------------------------------------- 1 | [![Build Status](https://travis-ci.org/dsoprea/go-jpeg-image-structure/v2.svg?branch=master)](https://travis-ci.org/dsoprea/go-jpeg-image-structure/v2) 2 | [![codecov](https://codecov.io/gh/dsoprea/go-jpeg-image-structure/branch/master/graph/badge.svg)](https://codecov.io/gh/dsoprea/go-jpeg-image-structure) 3 | [![Go Report Card](https://goreportcard.com/badge/github.com/dsoprea/go-jpeg-image-structure/v2)](https://goreportcard.com/report/github.com/dsoprea/go-jpeg-image-structure/v2) 4 | [![GoDoc](https://godoc.org/github.com/dsoprea/go-jpeg-image-structure/v2?status.svg)](https://godoc.org/github.com/dsoprea/go-jpeg-image-structure/v2) 5 | 6 | ## Overview 7 | 8 | Parse raw JPEG data into individual segments of data. You can print or export this data, including hash digests for each. You can also parse/modify the EXIF data and write an updated image. 9 | 10 | EXIF, XMP, and IPTC data can also be extracted. The provided CLI tool can print this data as well. 11 | -------------------------------------------------------------------------------- /v2/assets/20180428_212314.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dsoprea/go-jpeg-image-structure/4f3f7e93410239bde7f71860b8b0af69ec77274d/v2/assets/20180428_212314.jpg -------------------------------------------------------------------------------- /v2/assets/IMG_6691_Multiple_EOIs.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dsoprea/go-jpeg-image-structure/4f3f7e93410239bde7f71860b8b0af69ec77274d/v2/assets/IMG_6691_Multiple_EOIs.jpg -------------------------------------------------------------------------------- /v2/assets/NDM_8901.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dsoprea/go-jpeg-image-structure/4f3f7e93410239bde7f71860b8b0af69ec77274d/v2/assets/NDM_8901.jpg -------------------------------------------------------------------------------- /v2/assets/NDM_8901.jpg.just_exif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dsoprea/go-jpeg-image-structure/4f3f7e93410239bde7f71860b8b0af69ec77274d/v2/assets/NDM_8901.jpg.just_exif -------------------------------------------------------------------------------- /v2/assets/NDM_8901.jpg.no_exif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dsoprea/go-jpeg-image-structure/4f3f7e93410239bde7f71860b8b0af69ec77274d/v2/assets/NDM_8901.jpg.no_exif -------------------------------------------------------------------------------- /v2/command/js_dump/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | "os" 6 | 7 | "encoding/json" 8 | "io/ioutil" 9 | 10 | "github.com/dsoprea/go-iptc" 11 | "github.com/dsoprea/go-logging" 12 | "github.com/jessevdk/go-flags" 13 | 14 | "github.com/dsoprea/go-jpeg-image-structure/v2" 15 | ) 16 | 17 | // TODO(dustin): Add comments to all of these structs. 18 | 19 | var ( 20 | options = &struct { 21 | Filepath string `short:"f" long:"filepath" required:"true" description:"File-path of JPEG image ('-' for STDIN)"` 22 | JsonAsList bool `short:"l" long:"json-list" description:"Print segments as a JSON list"` 23 | JsonAsObject bool `short:"o" long:"json-object" description:"Print segments as a JSON object"` 24 | IncludeData bool `short:"d" long:"data" description:"Include actual JPEG data (only with JSON)"` 25 | Verbose bool `short:"v" long:"verbose" description:"Enable logging verbosity"` 26 | JustXmp bool `short:"x" long:"just-xmp" description:"Just print raw XMP XML. Fails if not present."` 27 | JustFullIptc bool `short:"i" long:"just-full-iptc" description:"Just print raw IPTC data. Fails if not present."` 28 | JustSimpleIptc bool `short:"s" long:"just-simple-iptc" description:"Just print raw IPTC data. Omit non-standard tags, omit non-human-readable text, omit repeated tags). Fails if not present."` 29 | }{} 30 | ) 31 | 32 | type segmentResult struct { 33 | MarkerId byte `json:"marker_id"` 34 | MarkerName string `json:"marker_name"` 35 | Offset int `json:"offset"` 36 | Data []byte `json:"data"` 37 | Length int `json:"length"` 38 | } 39 | 40 | type segmentIndexItem struct { 41 | Offset int `json:"offset"` 42 | Data []byte `json:"data"` 43 | Length int `json:"length"` 44 | } 45 | 46 | func main() { 47 | _, err := flags.Parse(options) 48 | if err != nil { 49 | os.Exit(-1) 50 | } 51 | 52 | if options.Verbose == true { 53 | scp := log.NewStaticConfigurationProvider() 54 | scp.SetLevelName(log.LevelNameDebug) 55 | 56 | log.LoadConfiguration(scp) 57 | 58 | cla := log.NewConsoleLogAdapter() 59 | log.AddAdapter("console", cla) 60 | } 61 | 62 | if options.JsonAsList == true && options.JsonAsObject == true { 63 | fmt.Println("Only -jsonlist *or* -jsonobject can be chosen.") 64 | os.Exit(-2) 65 | } 66 | 67 | var data []byte 68 | if options.Filepath == "-" { 69 | var err error 70 | data, err = ioutil.ReadAll(os.Stdin) 71 | log.PanicIf(err) 72 | } else { 73 | var err error 74 | data, err = ioutil.ReadFile(options.Filepath) 75 | log.PanicIf(err) 76 | } 77 | 78 | jmp := jpegstructure.NewJpegMediaParser() 79 | 80 | intfc, parseErr := jmp.ParseBytes(data) 81 | 82 | // If there was an error *and* we got back some segments, print the segments 83 | // before panicing. 84 | if intfc == nil && parseErr != nil { 85 | log.Panic(parseErr) 86 | } 87 | 88 | sl := intfc.(*jpegstructure.SegmentList) 89 | 90 | if options.JustXmp == true { 91 | _, s, err := sl.FindXmp() 92 | log.PanicIf(err) 93 | 94 | xml, err := s.FormattedXmp() 95 | log.PanicIf(err) 96 | 97 | fmt.Println(xml) 98 | 99 | os.Exit(0) 100 | } 101 | 102 | if options.JustSimpleIptc == true { 103 | tags, err := sl.Iptc() 104 | log.PanicIf(err) 105 | 106 | distilled := iptc.GetSimpleDictionaryFromParsedTags(tags) 107 | sorted := jpegstructure.SortStringStringMap(distilled) 108 | 109 | for _, pair := range sorted { 110 | fmt.Printf("%s: %s\n", pair[0], pair[1]) 111 | } 112 | 113 | os.Exit(0) 114 | } else if options.JustFullIptc == true { 115 | tags, err := sl.Iptc() 116 | log.PanicIf(err) 117 | 118 | distilled := iptc.GetDictionaryFromParsedTags(tags) 119 | sorted := jpegstructure.SortStringStringMap(distilled) 120 | 121 | for _, pair := range sorted { 122 | fmt.Printf("%s: %s\n", pair[0], pair[1]) 123 | } 124 | 125 | os.Exit(0) 126 | } 127 | 128 | segments := make([]segmentResult, len(sl.Segments())) 129 | segmentIndex := make(map[string][]segmentIndexItem) 130 | 131 | for i, s := range sl.Segments() { 132 | var data []byte 133 | if (options.JsonAsList == true || options.JsonAsObject == true) && options.IncludeData == true { 134 | data = s.Data 135 | } 136 | 137 | segments[i] = segmentResult{ 138 | MarkerId: s.MarkerId, 139 | MarkerName: s.MarkerName, 140 | Offset: s.Offset, 141 | Length: len(s.Data), 142 | Data: data, 143 | } 144 | 145 | sii := segmentIndexItem{ 146 | Offset: s.Offset, 147 | Length: len(s.Data), 148 | Data: data, 149 | } 150 | 151 | if grouped, found := segmentIndex[s.MarkerName]; found == true { 152 | segmentIndex[s.MarkerName] = append(grouped, sii) 153 | } else { 154 | segmentIndex[s.MarkerName] = []segmentIndexItem{sii} 155 | } 156 | } 157 | 158 | if parseErr != nil { 159 | fmt.Printf("JPEG Segments (incomplete due to error):\n") 160 | fmt.Printf("\n") 161 | 162 | sl.Print() 163 | 164 | fmt.Printf("\n") 165 | 166 | log.Panic(parseErr) 167 | } 168 | 169 | if options.JsonAsList == true { 170 | raw, err := json.MarshalIndent(segments, "", " ") 171 | log.PanicIf(err) 172 | 173 | fmt.Println(string(raw)) 174 | } else if options.JsonAsObject == true { 175 | raw, err := json.MarshalIndent(segmentIndex, "", " ") 176 | log.PanicIf(err) 177 | 178 | fmt.Println(string(raw)) 179 | } else { 180 | fmt.Printf("JPEG Segments:\n") 181 | fmt.Printf("\n") 182 | 183 | sl.Print() 184 | 185 | sl.FindXmp() 186 | } 187 | } 188 | -------------------------------------------------------------------------------- /v2/command/js_dump/main_test.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "bytes" 5 | "fmt" 6 | "path" 7 | "testing" 8 | 9 | "encoding/json" 10 | "os/exec" 11 | 12 | "github.com/dsoprea/go-logging" 13 | 14 | "github.com/dsoprea/go-jpeg-image-structure/v2" 15 | ) 16 | 17 | type JsonResultJpegSegmentListItem struct { 18 | MarkerId byte `json:"marker_id"` 19 | MarkerName string `json:"market_name"` 20 | Offset int `json:"offset"` 21 | Data []byte `json:"data"` 22 | } 23 | 24 | type JsonResultJpegSegmentIndexItem struct { 25 | MarkerName string `json:"marker_name"` 26 | Offset int `json:"offset"` 27 | Data []byte `json:"data"` 28 | } 29 | 30 | func TestMain_Plain(t *testing.T) { 31 | imageFilepath := jpegstructure.GetTestImageFilepath() 32 | appFilepath := getAppFilepath() 33 | 34 | cmd := exec.Command( 35 | "go", "run", appFilepath, 36 | "--filepath", imageFilepath) 37 | 38 | b := new(bytes.Buffer) 39 | cmd.Stdout = b 40 | cmd.Stderr = b 41 | 42 | err := cmd.Run() 43 | actual := b.String() 44 | 45 | if err != nil { 46 | fmt.Printf(actual) 47 | panic(err) 48 | } 49 | 50 | expected := 51 | `JPEG Segments: 52 | 53 | 0: OFFSET=(0x00000000 0) ID=(0xd8) NAME=[SOI ] SIZE=( 0) SHA1=[da39a3ee5e6b4b0d3255bfef95601890afd80709] 54 | 1: OFFSET=(0x00000002 2) ID=(0xe1) NAME=[APP1 ] SIZE=( 32942) SHA1=[81dce16a2abe2232049b5aa430ccf4095d240071] [EXIF] 55 | 2: OFFSET=(0x000080b4 32948) ID=(0xe1) NAME=[APP1 ] SIZE=( 2558) SHA1=[b56f13aa6bc3410a7d866302ef51c8b9798113af] [XMP] 56 | 3: OFFSET=(0x00008ab6 35510) ID=(0xdb) NAME=[DQT ] SIZE=( 130) SHA1=[40441c843ce4c8027cbd3dbdc174ac13d7555aec] 57 | 4: OFFSET=(0x00008b3c 35644) ID=(0xc0) NAME=[SOF0 ] SIZE=( 15) SHA1=[2458a7e3cf26aed68a0becb123a0a022c03d1243] 58 | 5: OFFSET=(0x00008b4f 35663) ID=(0xc4) NAME=[DHT ] SIZE=( 416) SHA1=[41b700bdd457862ce170bec95c9dac272e415470] 59 | 6: OFFSET=(0x00008cf3 36083) ID=(0xda) NAME=[SOS ] SIZE=( 0) SHA1=[da39a3ee5e6b4b0d3255bfef95601890afd80709] 60 | 7: OFFSET=(0x00008cf5 36085) ID=(0x00) NAME=[ ] SIZE=( 5554296) SHA1=[16e7465a831a075b096dbd7f2d6f2c931e509edd] 61 | 8: OFFSET=(0x00554d6d 5590381) ID=(0xd9) NAME=[EOI ] SIZE=( 0) SHA1=[da39a3ee5e6b4b0d3255bfef95601890afd80709] 62 | ` 63 | 64 | if actual != expected { 65 | fmt.Printf("ACTUAL:\n%s\n", actual) 66 | fmt.Printf("EXPECTED:\n%s\n", expected) 67 | 68 | t.Fatalf("Output not expected.") 69 | } 70 | } 71 | 72 | func TestMain_Json_NoData(t *testing.T) { 73 | defer func() { 74 | if state := recover(); state != nil { 75 | err := log.Wrap(state.(error)) 76 | log.PrintErrorf(err, "Test failure.") 77 | } 78 | }() 79 | 80 | imageFilepath := jpegstructure.GetTestImageFilepath() 81 | appFilepath := getAppFilepath() 82 | 83 | cmd := exec.Command( 84 | "go", "run", appFilepath, 85 | "--json-list", 86 | "--filepath", imageFilepath) 87 | 88 | b := new(bytes.Buffer) 89 | cmd.Stdout = b 90 | cmd.Stderr = b 91 | 92 | err := cmd.Run() 93 | actual := b.String() 94 | 95 | if err != nil { 96 | fmt.Println(actual) 97 | panic(err) 98 | } 99 | 100 | expected := `[ 101 | { 102 | "marker_id": 216, 103 | "marker_name": "SOI", 104 | "offset": 0, 105 | "data": null, 106 | "length": 0 107 | }, 108 | { 109 | "marker_id": 225, 110 | "marker_name": "APP1", 111 | "offset": 2, 112 | "data": null, 113 | "length": 32942 114 | }, 115 | { 116 | "marker_id": 225, 117 | "marker_name": "APP1", 118 | "offset": 32948, 119 | "data": null, 120 | "length": 2558 121 | }, 122 | { 123 | "marker_id": 219, 124 | "marker_name": "DQT", 125 | "offset": 35510, 126 | "data": null, 127 | "length": 130 128 | }, 129 | { 130 | "marker_id": 192, 131 | "marker_name": "SOF0", 132 | "offset": 35644, 133 | "data": null, 134 | "length": 15 135 | }, 136 | { 137 | "marker_id": 196, 138 | "marker_name": "DHT", 139 | "offset": 35663, 140 | "data": null, 141 | "length": 416 142 | }, 143 | { 144 | "marker_id": 218, 145 | "marker_name": "SOS", 146 | "offset": 36083, 147 | "data": null, 148 | "length": 0 149 | }, 150 | { 151 | "marker_id": 0, 152 | "marker_name": "!SCANDATA", 153 | "offset": 36085, 154 | "data": null, 155 | "length": 5554296 156 | }, 157 | { 158 | "marker_id": 217, 159 | "marker_name": "EOI", 160 | "offset": 5590381, 161 | "data": null, 162 | "length": 0 163 | } 164 | ] 165 | ` 166 | 167 | if actual != expected { 168 | fmt.Printf("ACTUAL:\n%s\n\nEXPECTED:\n%s\n", actual, expected) 169 | 170 | t.Fatalf("output not expected.") 171 | } 172 | } 173 | 174 | func TestMain_Json_NoData_SegmentIndex(t *testing.T) { 175 | imageFilepath := jpegstructure.GetTestImageFilepath() 176 | appFilepath := getAppFilepath() 177 | 178 | cmd := exec.Command( 179 | "go", "run", appFilepath, 180 | "--json-object", 181 | "--filepath", imageFilepath) 182 | 183 | b := new(bytes.Buffer) 184 | cmd.Stdout = b 185 | cmd.Stderr = b 186 | 187 | err := cmd.Run() 188 | actual := b.String() 189 | 190 | if err != nil { 191 | fmt.Println(actual) 192 | panic(err) 193 | } 194 | 195 | expected := `{ 196 | "!SCANDATA": [ 197 | { 198 | "offset": 36085, 199 | "data": null, 200 | "length": 5554296 201 | } 202 | ], 203 | "APP1": [ 204 | { 205 | "offset": 2, 206 | "data": null, 207 | "length": 32942 208 | }, 209 | { 210 | "offset": 32948, 211 | "data": null, 212 | "length": 2558 213 | } 214 | ], 215 | "DHT": [ 216 | { 217 | "offset": 35663, 218 | "data": null, 219 | "length": 416 220 | } 221 | ], 222 | "DQT": [ 223 | { 224 | "offset": 35510, 225 | "data": null, 226 | "length": 130 227 | } 228 | ], 229 | "EOI": [ 230 | { 231 | "offset": 5590381, 232 | "data": null, 233 | "length": 0 234 | } 235 | ], 236 | "SOF0": [ 237 | { 238 | "offset": 35644, 239 | "data": null, 240 | "length": 15 241 | } 242 | ], 243 | "SOI": [ 244 | { 245 | "offset": 0, 246 | "data": null, 247 | "length": 0 248 | } 249 | ], 250 | "SOS": [ 251 | { 252 | "offset": 36083, 253 | "data": null, 254 | "length": 0 255 | } 256 | ] 257 | } 258 | ` 259 | 260 | if actual != expected { 261 | fmt.Printf("ACTUAL:\n%s\n\nEXPECTED:\n%s\n", actual, expected) 262 | 263 | t.Fatalf("output not expected.") 264 | } 265 | } 266 | 267 | func TestMain_Json_Data(t *testing.T) { 268 | imageFilepath := jpegstructure.GetTestImageFilepath() 269 | appFilepath := getAppFilepath() 270 | 271 | cmd := exec.Command( 272 | "go", "run", appFilepath, 273 | "--json-list", 274 | "--data", 275 | "--filepath", imageFilepath) 276 | 277 | b := new(bytes.Buffer) 278 | cmd.Stdout = b 279 | cmd.Stderr = b 280 | 281 | err := cmd.Run() 282 | raw := b.Bytes() 283 | 284 | if err != nil { 285 | fmt.Printf(string(raw)) 286 | panic(err) 287 | } 288 | 289 | result := make([]JsonResultJpegSegmentListItem, 0) 290 | 291 | err = json.Unmarshal(raw, &result) 292 | log.PanicIf(err) 293 | 294 | if len(result) != 9 { 295 | t.Fatalf("JPEG segment count not correct: (%d)", len(result)) 296 | } 297 | 298 | hasData := false 299 | for _, s := range result { 300 | if s.Data != nil { 301 | hasData = true 302 | break 303 | } 304 | } 305 | 306 | if hasData != true { 307 | t.Fatalf("No segments have data but were expected to.") 308 | } 309 | } 310 | 311 | func getAppFilepath() string { 312 | moduleRootPath := jpegstructure.GetModuleRootPath() 313 | return path.Join(moduleRootPath, "command", "js_dump", "main.go") 314 | } 315 | -------------------------------------------------------------------------------- /v2/command/js_exif/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | "os" 6 | 7 | "encoding/json" 8 | "io/ioutil" 9 | 10 | "github.com/dsoprea/go-exif/v3" 11 | "github.com/dsoprea/go-jpeg-image-structure/v2" 12 | "github.com/dsoprea/go-logging" 13 | "github.com/jessevdk/go-flags" 14 | ) 15 | 16 | var ( 17 | options = &struct { 18 | Filepath string `short:"f" long:"filepath" required:"true" description:"File-path of JPEG image ('-' for STDIN)"` 19 | Json bool `short:"j" long:"json" description:"Print as JSON"` 20 | DoPrintVerbose bool `short:"v" long:"verbose" description:"Print logging"` 21 | }{} 22 | ) 23 | 24 | func main() { 25 | defer func() { 26 | if errRaw := recover(); errRaw != nil { 27 | err := errRaw.(error) 28 | log.PrintError(err) 29 | 30 | os.Exit(-2) 31 | } 32 | }() 33 | 34 | _, err := flags.Parse(options) 35 | if err != nil { 36 | os.Exit(-1) 37 | } 38 | 39 | if options.DoPrintVerbose == true { 40 | cla := log.NewConsoleLogAdapter() 41 | log.AddAdapter("console", cla) 42 | 43 | scp := log.NewStaticConfigurationProvider() 44 | scp.SetLevelName(log.LevelNameDebug) 45 | 46 | log.LoadConfiguration(scp) 47 | } 48 | 49 | var data []byte 50 | if options.Filepath == "-" { 51 | var err error 52 | data, err = ioutil.ReadAll(os.Stdin) 53 | log.PanicIf(err) 54 | } else { 55 | var err error 56 | data, err = ioutil.ReadFile(options.Filepath) 57 | log.PanicIf(err) 58 | } 59 | 60 | jmp := jpegstructure.NewJpegMediaParser() 61 | 62 | intfc, parseErr := jmp.ParseBytes(data) 63 | 64 | var et []exif.ExifTag 65 | if intfc != nil { 66 | // If the parse failed, we should always still get all of the segments 67 | // that we've encountered so far. It should never be empty, and it 68 | // should be impossible for it to be `nil`. So, if the parse failed but 69 | // we still found EXIF data, just ignore the failure and proceed. We had 70 | // still got what we needed. 71 | 72 | sl := intfc.(*jpegstructure.SegmentList) 73 | 74 | var err error 75 | _, _, et, err = sl.DumpExif() 76 | 77 | // There was a parse error and we couldn't find/parse EXIF data. If the 78 | // extraction had already failed above and we were just trying for a 79 | // contingency, fail with that error first. 80 | if err != nil { 81 | if parseErr != nil { 82 | log.Panic(parseErr) 83 | } else { 84 | log.Panic(err) 85 | } 86 | } 87 | } else if parseErr == nil { 88 | // We should never get a `nil` `intfc` value back *and* a `nil` 89 | // `parseErr`. 90 | log.Panicf("could not parse JPEG even partially") 91 | } else { 92 | log.Panic(parseErr) 93 | } 94 | 95 | // If we get here, we either parsed the JPEG file well or at least parsed 96 | // enough to find EXIF data. 97 | 98 | if et == nil { 99 | // The JPEG image parsed fine (if it didn't and we haven't yet 100 | // terminated, we already extracted the EXIF tags above). 101 | 102 | sl := intfc.(*jpegstructure.SegmentList) 103 | 104 | var err error 105 | 106 | _, _, et, err = sl.DumpExif() 107 | if err != nil { 108 | if err == exif.ErrNoExif { 109 | fmt.Printf("No EXIF.\n") 110 | os.Exit(10) 111 | } 112 | 113 | log.Panic(err) 114 | } 115 | } 116 | 117 | if options.Json == true { 118 | raw, err := json.MarshalIndent(et, " ", " ") 119 | log.PanicIf(err) 120 | 121 | fmt.Println(string(raw)) 122 | } else { 123 | if len(et) == 0 { 124 | fmt.Printf("EXIF data is present but empty.\n") 125 | } else { 126 | for i, tag := range et { 127 | // Since we dump the complete value, the thumbnails introduce 128 | // too much noise. 129 | if (tag.TagId == exif.ThumbnailOffsetTagId || tag.TagId == exif.ThumbnailSizeTagId) && tag.IfdPath == exif.ThumbnailFqIfdPath { 130 | continue 131 | } 132 | 133 | fmt.Printf("%2d: IFD-PATH=[%s] ID=(0x%04x) NAME=[%s] TYPE=(%d):[%s] VALUE=[%v]", i, tag.IfdPath, tag.TagId, tag.TagName, tag.TagTypeId, tag.TagTypeName, tag.FormattedFirst) 134 | 135 | if tag.ChildIfdPath != "" { 136 | fmt.Printf(" CHILD-IFD-PATH=[%s]", tag.ChildIfdPath) 137 | } 138 | 139 | fmt.Printf("\n") 140 | } 141 | } 142 | } 143 | } 144 | -------------------------------------------------------------------------------- /v2/command/js_exif/main_test.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "bytes" 5 | "fmt" 6 | "path" 7 | "testing" 8 | 9 | "encoding/json" 10 | "os/exec" 11 | 12 | "github.com/dsoprea/go-logging" 13 | 14 | "github.com/dsoprea/go-jpeg-image-structure/v2" 15 | ) 16 | 17 | var ( 18 | assetsPath = "" 19 | appFilepath = "" 20 | ) 21 | 22 | type JsonResultExifTag struct { 23 | MarkerId byte `json:"marker_id"` 24 | MarkerName string `json:"market_name"` 25 | Offset int `json:"offset"` 26 | Data []byte `json:"data"` 27 | } 28 | 29 | func TestMain_Plain_Exif(t *testing.T) { 30 | appFilepath := getAppFilepath() 31 | imageFilepath := jpegstructure.GetTestImageFilepath() 32 | 33 | cmd := exec.Command( 34 | "go", "run", appFilepath, 35 | "--filepath", imageFilepath) 36 | 37 | b := new(bytes.Buffer) 38 | cmd.Stdout = b 39 | cmd.Stderr = b 40 | 41 | err := cmd.Run() 42 | actual := b.String() 43 | 44 | if err != nil { 45 | fmt.Printf(actual) 46 | panic(err) 47 | } 48 | 49 | expected := 50 | ` 0: IFD-PATH=[IFD] ID=(0x010f) NAME=[Make] TYPE=(2):[ASCII] VALUE=[Canon] 51 | 1: IFD-PATH=[IFD] ID=(0x0110) NAME=[Model] TYPE=(2):[ASCII] VALUE=[Canon EOS 5D Mark III] 52 | 2: IFD-PATH=[IFD] ID=(0x0112) NAME=[Orientation] TYPE=(3):[SHORT] VALUE=[1] 53 | 3: IFD-PATH=[IFD] ID=(0x011a) NAME=[XResolution] TYPE=(5):[RATIONAL] VALUE=[72/1] 54 | 4: IFD-PATH=[IFD] ID=(0x011b) NAME=[YResolution] TYPE=(5):[RATIONAL] VALUE=[72/1] 55 | 5: IFD-PATH=[IFD] ID=(0x0128) NAME=[ResolutionUnit] TYPE=(3):[SHORT] VALUE=[2] 56 | 6: IFD-PATH=[IFD] ID=(0x0132) NAME=[DateTime] TYPE=(2):[ASCII] VALUE=[2017:12:02 08:18:50] 57 | 7: IFD-PATH=[IFD] ID=(0x013b) NAME=[Artist] TYPE=(2):[ASCII] VALUE=[] 58 | 8: IFD-PATH=[IFD] ID=(0x0213) NAME=[YCbCrPositioning] TYPE=(3):[SHORT] VALUE=[2] 59 | 9: IFD-PATH=[IFD] ID=(0x8298) NAME=[Copyright] TYPE=(2):[ASCII] VALUE=[] 60 | 10: IFD-PATH=[IFD] ID=(0x8769) NAME=[ExifTag] TYPE=(4):[LONG] VALUE=[360] CHILD-IFD-PATH=[IFD/Exif] 61 | 11: IFD-PATH=[IFD/Exif] ID=(0x829a) NAME=[ExposureTime] TYPE=(5):[RATIONAL] VALUE=[1/640] 62 | 12: IFD-PATH=[IFD/Exif] ID=(0x829d) NAME=[FNumber] TYPE=(5):[RATIONAL] VALUE=[4/1] 63 | 13: IFD-PATH=[IFD/Exif] ID=(0x8822) NAME=[ExposureProgram] TYPE=(3):[SHORT] VALUE=[4] 64 | 14: IFD-PATH=[IFD/Exif] ID=(0x8827) NAME=[ISOSpeedRatings] TYPE=(3):[SHORT] VALUE=[1600] 65 | 15: IFD-PATH=[IFD/Exif] ID=(0x8830) NAME=[SensitivityType] TYPE=(3):[SHORT] VALUE=[2] 66 | 16: IFD-PATH=[IFD/Exif] ID=(0x8832) NAME=[RecommendedExposureIndex] TYPE=(4):[LONG] VALUE=[1600] 67 | 17: IFD-PATH=[IFD/Exif] ID=(0x9000) NAME=[ExifVersion] TYPE=(7):[UNDEFINED] VALUE=[0230] 68 | 18: IFD-PATH=[IFD/Exif] ID=(0x9003) NAME=[DateTimeOriginal] TYPE=(2):[ASCII] VALUE=[2017:12:02 08:18:50] 69 | 19: IFD-PATH=[IFD/Exif] ID=(0x9004) NAME=[DateTimeDigitized] TYPE=(2):[ASCII] VALUE=[2017:12:02 08:18:50] 70 | 20: IFD-PATH=[IFD/Exif] ID=(0x9101) NAME=[ComponentsConfiguration] TYPE=(7):[UNDEFINED] VALUE=[Exif9101ComponentsConfiguration] 71 | 21: IFD-PATH=[IFD/Exif] ID=(0x9201) NAME=[ShutterSpeedValue] TYPE=(10):[SRATIONAL] VALUE=[614400/65536] 72 | 22: IFD-PATH=[IFD/Exif] ID=(0x9202) NAME=[ApertureValue] TYPE=(5):[RATIONAL] VALUE=[262144/65536] 73 | 23: IFD-PATH=[IFD/Exif] ID=(0x9204) NAME=[ExposureBiasValue] TYPE=(10):[SRATIONAL] VALUE=[0/1] 74 | 24: IFD-PATH=[IFD/Exif] ID=(0x9207) NAME=[MeteringMode] TYPE=(3):[SHORT] VALUE=[5] 75 | 25: IFD-PATH=[IFD/Exif] ID=(0x9209) NAME=[Flash] TYPE=(3):[SHORT] VALUE=[16] 76 | 26: IFD-PATH=[IFD/Exif] ID=(0x920a) NAME=[FocalLength] TYPE=(5):[RATIONAL] VALUE=[16/1] 77 | 27: IFD-PATH=[IFD/Exif] ID=(0x927c) NAME=[MakerNote] TYPE=(7):[UNDEFINED] VALUE=[MakerNote] 78 | 28: IFD-PATH=[IFD/Exif] ID=(0x9286) NAME=[UserComment] TYPE=(7):[UNDEFINED] VALUE=[UserComment] 79 | 29: IFD-PATH=[IFD/Exif] ID=(0x9290) NAME=[SubSecTime] TYPE=(2):[ASCII] VALUE=[00] 80 | 30: IFD-PATH=[IFD/Exif] ID=(0x9291) NAME=[SubSecTimeOriginal] TYPE=(2):[ASCII] VALUE=[00] 81 | 31: IFD-PATH=[IFD/Exif] ID=(0x9292) NAME=[SubSecTimeDigitized] TYPE=(2):[ASCII] VALUE=[00] 82 | 32: IFD-PATH=[IFD/Exif] ID=(0xa000) NAME=[FlashpixVersion] TYPE=(7):[UNDEFINED] VALUE=[0100] 83 | 33: IFD-PATH=[IFD/Exif] ID=(0xa001) NAME=[ColorSpace] TYPE=(3):[SHORT] VALUE=[1] 84 | 34: IFD-PATH=[IFD/Exif] ID=(0xa002) NAME=[PixelXDimension] TYPE=(3):[SHORT] VALUE=[3840] 85 | 35: IFD-PATH=[IFD/Exif] ID=(0xa003) NAME=[PixelYDimension] TYPE=(3):[SHORT] VALUE=[2560] 86 | 36: IFD-PATH=[IFD/Exif] ID=(0xa005) NAME=[InteroperabilityTag] TYPE=(4):[LONG] VALUE=[9326] CHILD-IFD-PATH=[IFD/Exif/Iop] 87 | 37: IFD-PATH=[IFD/Exif/Iop] ID=(0x0001) NAME=[InteroperabilityIndex] TYPE=(2):[ASCII] VALUE=[R98] 88 | 38: IFD-PATH=[IFD/Exif/Iop] ID=(0x0002) NAME=[InteroperabilityVersion] TYPE=(7):[UNDEFINED] VALUE=[0100] 89 | 39: IFD-PATH=[IFD/Exif] ID=(0xa20e) NAME=[FocalPlaneXResolution] TYPE=(5):[RATIONAL] VALUE=[3840000/1461] 90 | 40: IFD-PATH=[IFD/Exif] ID=(0xa20f) NAME=[FocalPlaneYResolution] TYPE=(5):[RATIONAL] VALUE=[2560000/972] 91 | 41: IFD-PATH=[IFD/Exif] ID=(0xa210) NAME=[FocalPlaneResolutionUnit] TYPE=(3):[SHORT] VALUE=[2] 92 | 42: IFD-PATH=[IFD/Exif] ID=(0xa401) NAME=[CustomRendered] TYPE=(3):[SHORT] VALUE=[0] 93 | 43: IFD-PATH=[IFD/Exif] ID=(0xa402) NAME=[ExposureMode] TYPE=(3):[SHORT] VALUE=[0] 94 | 44: IFD-PATH=[IFD/Exif] ID=(0xa403) NAME=[WhiteBalance] TYPE=(3):[SHORT] VALUE=[0] 95 | 45: IFD-PATH=[IFD/Exif] ID=(0xa406) NAME=[SceneCaptureType] TYPE=(3):[SHORT] VALUE=[0] 96 | 46: IFD-PATH=[IFD/Exif] ID=(0xa430) NAME=[CameraOwnerName] TYPE=(2):[ASCII] VALUE=[] 97 | 47: IFD-PATH=[IFD/Exif] ID=(0xa431) NAME=[BodySerialNumber] TYPE=(2):[ASCII] VALUE=[063024020097] 98 | 48: IFD-PATH=[IFD/Exif] ID=(0xa432) NAME=[LensSpecification] TYPE=(5):[RATIONAL] VALUE=[16/1...] 99 | 49: IFD-PATH=[IFD/Exif] ID=(0xa434) NAME=[LensModel] TYPE=(2):[ASCII] VALUE=[EF16-35mm f/4L IS USM] 100 | 50: IFD-PATH=[IFD/Exif] ID=(0xa435) NAME=[LensSerialNumber] TYPE=(2):[ASCII] VALUE=[2400001068] 101 | 51: IFD-PATH=[IFD] ID=(0x8825) NAME=[GPSTag] TYPE=(4):[LONG] VALUE=[9554] CHILD-IFD-PATH=[IFD/GPSInfo] 102 | 52: IFD-PATH=[IFD/GPSInfo] ID=(0x0000) NAME=[GPSVersionID] TYPE=(1):[BYTE] VALUE=[02 03 00 00] 103 | 53: IFD-PATH=[IFD1] ID=(0x0103) NAME=[Compression] TYPE=(3):[SHORT] VALUE=[6] 104 | 54: IFD-PATH=[IFD1] ID=(0x011a) NAME=[XResolution] TYPE=(5):[RATIONAL] VALUE=[72/1] 105 | 55: IFD-PATH=[IFD1] ID=(0x011b) NAME=[YResolution] TYPE=(5):[RATIONAL] VALUE=[72/1] 106 | 56: IFD-PATH=[IFD1] ID=(0x0128) NAME=[ResolutionUnit] TYPE=(3):[SHORT] VALUE=[2] 107 | ` 108 | 109 | if actual != expected { 110 | fmt.Printf("ACTUAL:\n%s\n", actual) 111 | fmt.Printf("EXPECTED:\n%s\n", expected) 112 | 113 | t.Fatalf("Output not expected.") 114 | } 115 | } 116 | 117 | func TestMain_Json_Exif(t *testing.T) { 118 | appFilepath := getAppFilepath() 119 | imageFilepath := jpegstructure.GetTestImageFilepath() 120 | 121 | cmd := exec.Command( 122 | "go", "run", appFilepath, 123 | "--json", 124 | "--filepath", imageFilepath) 125 | 126 | b := new(bytes.Buffer) 127 | cmd.Stdout = b 128 | cmd.Stderr = b 129 | 130 | err := cmd.Run() 131 | raw := b.Bytes() 132 | 133 | if err != nil { 134 | fmt.Printf(string(raw)) 135 | panic(err) 136 | } 137 | 138 | result := make([]JsonResultExifTag, 0) 139 | 140 | err = json.Unmarshal(raw, &result) 141 | log.PanicIf(err) 142 | 143 | // TODO(dustin): !! Store the expected JSON in a file. 144 | 145 | if len(result) != 59 { 146 | t.Fatalf("Exif tag-count not correct: (%d)", len(result)) 147 | } 148 | } 149 | 150 | func getAppFilepath() string { 151 | moduleRootPath := jpegstructure.GetModuleRootPath() 152 | return path.Join(moduleRootPath, "command", "js_exif", "main.go") 153 | } 154 | -------------------------------------------------------------------------------- /v2/command/js_exif_drop/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | "os" 6 | 7 | "io/ioutil" 8 | 9 | "github.com/dsoprea/go-jpeg-image-structure/v2" 10 | "github.com/dsoprea/go-logging" 11 | "github.com/jessevdk/go-flags" 12 | ) 13 | 14 | var ( 15 | options = &struct { 16 | InputFilepath string `short:"f" long:"input-filepath" required:"true" description:"File-path of JPEG image to read"` 17 | OutputFilepath string `short:"o" long:"output-filepath" description:"File-path of JPEG image to write (if not provided, then the input JPEG will be used)"` 18 | }{} 19 | ) 20 | 21 | func main() { 22 | _, err := flags.Parse(options) 23 | if err != nil { 24 | os.Exit(-1) 25 | } 26 | 27 | data, err := ioutil.ReadFile(options.InputFilepath) 28 | log.PanicIf(err) 29 | 30 | jmp := jpegstructure.NewJpegMediaParser() 31 | 32 | intfc, err := jmp.ParseBytes(data) 33 | log.PanicIf(err) 34 | 35 | sl := intfc.(*jpegstructure.SegmentList) 36 | 37 | wasDropped, err := sl.DropExif() 38 | log.PanicIf(err) 39 | 40 | fmt.Printf("%v\n", wasDropped) 41 | 42 | if wasDropped == false { 43 | os.Exit(10) 44 | } 45 | 46 | outputFilepath := options.OutputFilepath 47 | if outputFilepath == "" { 48 | outputFilepath = options.InputFilepath 49 | } 50 | 51 | f, err := os.OpenFile(outputFilepath, os.O_CREATE|os.O_WRONLY, 0644) 52 | log.PanicIf(err) 53 | 54 | defer f.Close() 55 | 56 | err = sl.Write(f) 57 | log.PanicIf(err) 58 | } 59 | -------------------------------------------------------------------------------- /v2/command/js_exif_drop/main_test.go.ignore: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "bytes" 5 | "fmt" 6 | "os" 7 | "path" 8 | "testing" 9 | 10 | "encoding/json" 11 | "os/exec" 12 | 13 | "github.com/dsoprea/go-logging" 14 | ) 15 | 16 | var ( 17 | assetsPath = "" 18 | appFilepath = "" 19 | ) 20 | 21 | type JsonResultExifTag struct { 22 | MarkerId byte `json:"marker_id"` 23 | MarkerName string `json:"market_name"` 24 | Offset int `json:"offset"` 25 | Data []byte `json:"data"` 26 | } 27 | 28 | func TestMain_Plain_Exif(t *testing.T) { 29 | appFilepath := getAppFilepath() 30 | imageFilepath := getTestImageFilepath() 31 | 32 | cmd := exec.Command( 33 | "go", "run", appFilepath, 34 | "--filepath", imageFilepath) 35 | 36 | b := new(bytes.Buffer) 37 | cmd.Stdout = b 38 | cmd.Stderr = b 39 | 40 | err := cmd.Run() 41 | actual := b.String() 42 | 43 | if err != nil { 44 | fmt.Printf(actual) 45 | panic(err) 46 | } 47 | 48 | expected := 49 | ` 0: IFD-PATH=[IFD] ID=(0x10f) NAME=[Make] TYPE=(2):[ASCII] VALUE=[Canon] 50 | 1: IFD-PATH=[IFD] ID=(0x110) NAME=[Model] TYPE=(2):[ASCII] VALUE=[Canon EOS 5D Mark III] 51 | 2: IFD-PATH=[IFD] ID=(0x112) NAME=[Orientation] TYPE=(3):[SHORT] VALUE=[[1]] 52 | 3: IFD-PATH=[IFD] ID=(0x11a) NAME=[XResolution] TYPE=(5):[RATIONAL] VALUE=[[{72 1}]] 53 | 4: IFD-PATH=[IFD] ID=(0x11b) NAME=[YResolution] TYPE=(5):[RATIONAL] VALUE=[[{72 1}]] 54 | 5: IFD-PATH=[IFD] ID=(0x128) NAME=[ResolutionUnit] TYPE=(3):[SHORT] VALUE=[[2]] 55 | 6: IFD-PATH=[IFD] ID=(0x132) NAME=[DateTime] TYPE=(2):[ASCII] VALUE=[2017:12:02 08:18:50] 56 | 7: IFD-PATH=[IFD] ID=(0x13b) NAME=[Artist] TYPE=(2):[ASCII] VALUE=[] 57 | 8: IFD-PATH=[IFD] ID=(0x213) NAME=[YCbCrPositioning] TYPE=(3):[SHORT] VALUE=[[2]] 58 | 9: IFD-PATH=[IFD] ID=(0x8298) NAME=[Copyright] TYPE=(2):[ASCII] VALUE=[] 59 | 10: IFD-PATH=[IFD] ID=(0x8769) NAME=[ExifTag] TYPE=(4):[LONG] VALUE=[[360]] CHILD-IFD-PATH=[IFD/Exif] 60 | 11: IFD-PATH=[IFD] ID=(0x8825) NAME=[GPSTag] TYPE=(4):[LONG] VALUE=[[9554]] CHILD-IFD-PATH=[IFD/GPSInfo] 61 | 12: IFD-PATH=[IFD/Exif] ID=(0x829a) NAME=[ExposureTime] TYPE=(5):[RATIONAL] VALUE=[[{1 640}]] 62 | 13: IFD-PATH=[IFD/Exif] ID=(0x829d) NAME=[FNumber] TYPE=(5):[RATIONAL] VALUE=[[{4 1}]] 63 | 14: IFD-PATH=[IFD/Exif] ID=(0x8822) NAME=[ExposureProgram] TYPE=(3):[SHORT] VALUE=[[4]] 64 | 15: IFD-PATH=[IFD/Exif] ID=(0x8827) NAME=[ISOSpeedRatings] TYPE=(3):[SHORT] VALUE=[[1600]] 65 | 16: IFD-PATH=[IFD/Exif] ID=(0x8830) NAME=[SensitivityType] TYPE=(3):[SHORT] VALUE=[[2]] 66 | 17: IFD-PATH=[IFD/Exif] ID=(0x8832) NAME=[RecommendedExposureIndex] TYPE=(4):[LONG] VALUE=[[1600]] 67 | 18: IFD-PATH=[IFD/Exif] ID=(0x9000) NAME=[ExifVersion] TYPE=(7):[UNDEFINED] VALUE=[0230] 68 | 19: IFD-PATH=[IFD/Exif] ID=(0x9003) NAME=[DateTimeOriginal] TYPE=(2):[ASCII] VALUE=[2017:12:02 08:18:50] 69 | 20: IFD-PATH=[IFD/Exif] ID=(0x9004) NAME=[DateTimeDigitized] TYPE=(2):[ASCII] VALUE=[2017:12:02 08:18:50] 70 | 21: IFD-PATH=[IFD/Exif] ID=(0x9101) NAME=[ComponentsConfiguration] TYPE=(7):[UNDEFINED] VALUE=[ComponentsConfiguration] 71 | 22: IFD-PATH=[IFD/Exif] ID=(0x9201) NAME=[ShutterSpeedValue] TYPE=(10):[SRATIONAL] VALUE=[[{614400 65536}]] 72 | 23: IFD-PATH=[IFD/Exif] ID=(0x9202) NAME=[ApertureValue] TYPE=(5):[RATIONAL] VALUE=[[{262144 65536}]] 73 | 24: IFD-PATH=[IFD/Exif] ID=(0x9204) NAME=[ExposureBiasValue] TYPE=(10):[SRATIONAL] VALUE=[[{0 1}]] 74 | 25: IFD-PATH=[IFD/Exif] ID=(0x9207) NAME=[MeteringMode] TYPE=(3):[SHORT] VALUE=[[5]] 75 | 26: IFD-PATH=[IFD/Exif] ID=(0x9209) NAME=[Flash] TYPE=(3):[SHORT] VALUE=[[16]] 76 | 27: IFD-PATH=[IFD/Exif] ID=(0x920a) NAME=[FocalLength] TYPE=(5):[RATIONAL] VALUE=[[{16 1}]] 77 | 28: IFD-PATH=[IFD/Exif] ID=(0x927c) NAME=[MakerNote] TYPE=(7):[UNDEFINED] VALUE=[MakerNote] 78 | 29: IFD-PATH=[IFD/Exif] ID=(0x9286) NAME=[UserComment] TYPE=(7):[UNDEFINED] VALUE=[UserComment] 79 | 30: IFD-PATH=[IFD/Exif] ID=(0x9290) NAME=[SubSecTime] TYPE=(2):[ASCII] VALUE=[00] 80 | 31: IFD-PATH=[IFD/Exif] ID=(0x9291) NAME=[SubSecTimeOriginal] TYPE=(2):[ASCII] VALUE=[00] 81 | 32: IFD-PATH=[IFD/Exif] ID=(0x9292) NAME=[SubSecTimeDigitized] TYPE=(2):[ASCII] VALUE=[00] 82 | 33: IFD-PATH=[IFD/Exif] ID=(0xa000) NAME=[FlashpixVersion] TYPE=(7):[UNDEFINED] VALUE=[0100] 83 | 34: IFD-PATH=[IFD/Exif] ID=(0xa001) NAME=[ColorSpace] TYPE=(3):[SHORT] VALUE=[[1]] 84 | 35: IFD-PATH=[IFD/Exif] ID=(0xa002) NAME=[PixelXDimension] TYPE=(3):[SHORT] VALUE=[[3840]] 85 | 36: IFD-PATH=[IFD/Exif] ID=(0xa003) NAME=[PixelYDimension] TYPE=(3):[SHORT] VALUE=[[2560]] 86 | 37: IFD-PATH=[IFD/Exif] ID=(0xa005) NAME=[InteroperabilityTag] TYPE=(4):[LONG] VALUE=[[9326]] CHILD-IFD-PATH=[IFD/Exif/Iop] 87 | 38: IFD-PATH=[IFD/Exif] ID=(0xa20e) NAME=[FocalPlaneXResolution] TYPE=(5):[RATIONAL] VALUE=[[{3840000 1461}]] 88 | 39: IFD-PATH=[IFD/Exif] ID=(0xa20f) NAME=[FocalPlaneYResolution] TYPE=(5):[RATIONAL] VALUE=[[{2560000 972}]] 89 | 40: IFD-PATH=[IFD/Exif] ID=(0xa210) NAME=[FocalPlaneResolutionUnit] TYPE=(3):[SHORT] VALUE=[[2]] 90 | 41: IFD-PATH=[IFD/Exif] ID=(0xa401) NAME=[CustomRendered] TYPE=(3):[SHORT] VALUE=[[0]] 91 | 42: IFD-PATH=[IFD/Exif] ID=(0xa402) NAME=[ExposureMode] TYPE=(3):[SHORT] VALUE=[[0]] 92 | 43: IFD-PATH=[IFD/Exif] ID=(0xa403) NAME=[WhiteBalance] TYPE=(3):[SHORT] VALUE=[[0]] 93 | 44: IFD-PATH=[IFD/Exif] ID=(0xa406) NAME=[SceneCaptureType] TYPE=(3):[SHORT] VALUE=[[0]] 94 | 45: IFD-PATH=[IFD/Exif] ID=(0xa430) NAME=[CameraOwnerName] TYPE=(2):[ASCII] VALUE=[] 95 | 46: IFD-PATH=[IFD/Exif] ID=(0xa431) NAME=[BodySerialNumber] TYPE=(2):[ASCII] VALUE=[063024020097] 96 | 47: IFD-PATH=[IFD/Exif] ID=(0xa432) NAME=[LensSpecification] TYPE=(5):[RATIONAL] VALUE=[[{16 1} {35 1} {0 1} {0 1}]] 97 | 48: IFD-PATH=[IFD/Exif] ID=(0xa434) NAME=[LensModel] TYPE=(2):[ASCII] VALUE=[EF16-35mm f/4L IS USM] 98 | 49: IFD-PATH=[IFD/Exif] ID=(0xa435) NAME=[LensSerialNumber] TYPE=(2):[ASCII] VALUE=[2400001068] 99 | 50: IFD-PATH=[IFD/GPSInfo] ID=(0x00) NAME=[GPSVersionID] TYPE=(1):[BYTE] VALUE=[[2 3 0 0]] 100 | 51: IFD-PATH=[IFD] ID=(0x103) NAME=[Compression] TYPE=(3):[SHORT] VALUE=[[6]] 101 | 52: IFD-PATH=[IFD] ID=(0x11a) NAME=[XResolution] TYPE=(5):[RATIONAL] VALUE=[[{72 1}]] 102 | 53: IFD-PATH=[IFD] ID=(0x11b) NAME=[YResolution] TYPE=(5):[RATIONAL] VALUE=[[{72 1}]] 103 | 54: IFD-PATH=[IFD] ID=(0x128) NAME=[ResolutionUnit] TYPE=(3):[SHORT] VALUE=[[2]] 104 | 55: IFD-PATH=[IFD/Exif/Iop] ID=(0x01) NAME=[InteroperabilityIndex] TYPE=(2):[ASCII] VALUE=[R98] 105 | 56: IFD-PATH=[IFD/Exif/Iop] ID=(0x02) NAME=[InteroperabilityVersion] TYPE=(7):[UNDEFINED] VALUE=[0100] 106 | ` 107 | 108 | if actual != expected { 109 | fmt.Printf("ACTUAL:\n%s\n", actual) 110 | fmt.Printf("EXPECTED:\n%s\n", expected) 111 | 112 | t.Fatalf("Output not expected.") 113 | } 114 | } 115 | 116 | func TestMain_Json_Exif(t *testing.T) { 117 | appFilepath := getAppFilepath() 118 | imageFilepath := getTestImageFilepath() 119 | 120 | cmd := exec.Command( 121 | "go", "run", appFilepath, 122 | "--json", 123 | "--filepath", imageFilepath) 124 | 125 | b := new(bytes.Buffer) 126 | cmd.Stdout = b 127 | cmd.Stderr = b 128 | 129 | err := cmd.Run() 130 | raw := b.Bytes() 131 | 132 | if err != nil { 133 | fmt.Printf(string(raw)) 134 | panic(err) 135 | } 136 | 137 | result := make([]JsonResultExifTag, 0) 138 | 139 | err = json.Unmarshal(raw, &result) 140 | log.PanicIf(err) 141 | 142 | // TODO(dustin): !! Store the expected JSON in a file. 143 | 144 | if len(result) != 57 { 145 | t.Fatalf("Exif tag-count not correct: (%d)", len(result)) 146 | } 147 | } 148 | 149 | func getTestAssetsPath() string { 150 | if assetsPath == "" { 151 | moduleRootPath := GetModuleRootPath() 152 | assetsPath = path.Join(moduleRootPath, "assets") 153 | } 154 | 155 | return assetsPath 156 | } 157 | 158 | func getTestImageFilepath() string { 159 | assetsPath := getTestAssetsPath() 160 | return path.Join(assetsPath, "NDM_8901.jpg") 161 | } 162 | 163 | func getAppFilepath() string { 164 | moduleRootPath := GetModuleRootPath() 165 | return path.Join(moduleRootPath, "command", "js_exif", "main.go") 166 | } 167 | -------------------------------------------------------------------------------- /v2/go.mod: -------------------------------------------------------------------------------- 1 | module github.com/dsoprea/go-jpeg-image-structure/v2 2 | 3 | go 1.12 4 | 5 | // Development only 6 | // replace github.com/dsoprea/go-utility/v2 => ../../go-utility/v2 7 | // replace github.com/dsoprea/go-logging => ../../go-logging 8 | // replace github.com/dsoprea/go-exif/v3 => ../../go-exif/v3 9 | // replace github.com/dsoprea/go-photoshop-info-format => ../../go-photoshop-info-format 10 | // replace github.com/dsoprea/go-iptc => ../../go-iptc 11 | 12 | require ( 13 | github.com/dsoprea/go-exif/v3 v3.0.0-20210428042052-dca55bf8ca15 14 | github.com/dsoprea/go-iptc v0.0.0-20200609062250-162ae6b44feb 15 | github.com/dsoprea/go-logging v0.0.0-20200710184922-b02d349568dd 16 | github.com/dsoprea/go-photoshop-info-format v0.0.0-20200609050348-3db9b63b202c 17 | github.com/dsoprea/go-utility/v2 v2.0.0-20200717064901-2fccff4aa15e 18 | github.com/go-xmlfmt/xmlfmt v0.0.0-20191208150333-d5b6f63a941b 19 | github.com/jessevdk/go-flags v1.4.0 20 | golang.org/x/net v0.0.0-20200707034311-ab3426394381 // indirect 21 | ) 22 | -------------------------------------------------------------------------------- /v2/go.sum: -------------------------------------------------------------------------------- 1 | github.com/dsoprea/go-exif/v2 v2.0.0-20200321225314-640175a69fe4/go.mod h1:Lm2lMM2zx8p4a34ZemkaUV95AnMl4ZvLbCUbwOvLC2E= 2 | github.com/dsoprea/go-exif/v3 v3.0.0-20200717053412-08f1b6708903/go.mod h1:0nsO1ce0mh5czxGeLo4+OCZ/C6Eo6ZlMWsz7rH/Gxv8= 3 | github.com/dsoprea/go-exif/v3 v3.0.0-20200717071058-9393e7afd446 h1:96yylb+JH415u6V7ykNtnEBLaZUwS1S31TnAezcvnNE= 4 | github.com/dsoprea/go-exif/v3 v3.0.0-20200717071058-9393e7afd446/go.mod h1:cg5SNYKHMmzxsr9X6ZeLh/nfBRHHp5PngtEPcujONtk= 5 | github.com/dsoprea/go-exif/v3 v3.0.0-20200722033536-33ee3a8313da h1:L/UYVj2DUQWlKl9ppghzcisZofMs5P1E/FawUaPMHwU= 6 | github.com/dsoprea/go-exif/v3 v3.0.0-20200722033536-33ee3a8313da/go.mod h1:cg5SNYKHMmzxsr9X6ZeLh/nfBRHHp5PngtEPcujONtk= 7 | github.com/dsoprea/go-exif/v3 v3.0.0-20200807075213-089aa48c91e6 h1:AWLaaemM6TvO4DVwMtXibJKpWWfyw+tiZwYUiueLPzE= 8 | github.com/dsoprea/go-exif/v3 v3.0.0-20200807075213-089aa48c91e6/go.mod h1:cg5SNYKHMmzxsr9X6ZeLh/nfBRHHp5PngtEPcujONtk= 9 | github.com/dsoprea/go-exif/v3 v3.0.0-20210428042052-dca55bf8ca15 h1:QQjMErNKRqrPUfRmdBpICftkac6holciY+B95S002fY= 10 | github.com/dsoprea/go-exif/v3 v3.0.0-20210428042052-dca55bf8ca15/go.mod h1:cg5SNYKHMmzxsr9X6ZeLh/nfBRHHp5PngtEPcujONtk= 11 | github.com/dsoprea/go-iptc v0.0.0-20200609062250-162ae6b44feb h1:gwjJjUr6FY7zAWVEueFPrcRHhd9+IK81TcItbqw2du4= 12 | github.com/dsoprea/go-iptc v0.0.0-20200609062250-162ae6b44feb/go.mod h1:kYIdx9N9NaOyD7U6D+YtExN7QhRm+5kq7//yOsRXQtM= 13 | github.com/dsoprea/go-logging v0.0.0-20190624164917-c4f10aab7696 h1:VGFnZAcLwPpt1sHlAxml+pGLZz9A2s+K/s1YNhPC91Y= 14 | github.com/dsoprea/go-logging v0.0.0-20190624164917-c4f10aab7696/go.mod h1:Nm/x2ZUNRW6Fe5C3LxdY1PyZY5wmDv/s5dkPJ/VB3iA= 15 | github.com/dsoprea/go-logging v0.0.0-20200502201358-170ff607885f h1:FonKAuW3PmNtqk9tOR+Z7bnyQHytmnZBCmm5z1PQMss= 16 | github.com/dsoprea/go-logging v0.0.0-20200502201358-170ff607885f/go.mod h1:7I+3Pe2o/YSU88W0hWlm9S22W7XI1JFNJ86U0zPKMf8= 17 | github.com/dsoprea/go-logging v0.0.0-20200517222403-5742ce3fc1be h1:k3sHKay8cXGnGHeF8x6U7KtX8Lc7qAiQCNDRGEIPdnU= 18 | github.com/dsoprea/go-logging v0.0.0-20200517222403-5742ce3fc1be/go.mod h1:7I+3Pe2o/YSU88W0hWlm9S22W7XI1JFNJ86U0zPKMf8= 19 | github.com/dsoprea/go-logging v0.0.0-20200517223158-a10564966e9d h1:F/7L5wr/fP/SKeO5HuMlNEX9Ipyx2MbH2rV9G4zJRpk= 20 | github.com/dsoprea/go-logging v0.0.0-20200517223158-a10564966e9d/go.mod h1:7I+3Pe2o/YSU88W0hWlm9S22W7XI1JFNJ86U0zPKMf8= 21 | github.com/dsoprea/go-logging v0.0.0-20200710184922-b02d349568dd h1:l+vLbuxptsC6VQyQsfD7NnEC8BZuFpz45PgY+pH8YTg= 22 | github.com/dsoprea/go-logging v0.0.0-20200710184922-b02d349568dd/go.mod h1:7I+3Pe2o/YSU88W0hWlm9S22W7XI1JFNJ86U0zPKMf8= 23 | github.com/dsoprea/go-photoshop-info-format v0.0.0-20200609050348-3db9b63b202c h1:7j5aWACOzROpr+dvMtu8GnI97g9ShLWD72XIELMgn+c= 24 | github.com/dsoprea/go-photoshop-info-format v0.0.0-20200609050348-3db9b63b202c/go.mod h1:pqKB+ijp27cEcrHxhXVgUUMlSDRuGJJp1E+20Lj5H0E= 25 | github.com/dsoprea/go-utility v0.0.0-20200711062821-fab8125e9bdf h1:/w4QxepU4AHh3AuO6/g8y/YIIHH5+aKP3Bj8sg5cqhU= 26 | github.com/dsoprea/go-utility v0.0.0-20200711062821-fab8125e9bdf/go.mod h1:95+K3z2L0mqsVYd6yveIv1lmtT3tcQQ3dVakPySffW8= 27 | github.com/dsoprea/go-utility/v2 v2.0.0-20200717064901-2fccff4aa15e h1:IxIbA7VbCNrwumIYjDoMOdf4KOSkMC6NJE4s8oRbE7E= 28 | github.com/dsoprea/go-utility/v2 v2.0.0-20200717064901-2fccff4aa15e/go.mod h1:uAzdkPTub5Y9yQwXe8W4m2XuP0tK4a9Q/dantD0+uaU= 29 | github.com/go-errors/errors v1.0.1 h1:LUHzmkK3GUKUrL/1gfBUxAHzcev3apQlezX/+O7ma6w= 30 | github.com/go-errors/errors v1.0.1/go.mod h1:f4zRHt4oKfwPJE5k8C9vpYG+aDHdBFUsgrm6/TyX73Q= 31 | github.com/go-errors/errors v1.0.2 h1:xMxH9j2fNg/L4hLn/4y3M0IUsn0M6Wbu/Uh9QlOfBh4= 32 | github.com/go-errors/errors v1.0.2/go.mod h1:psDX2osz5VnTOnFWbDeWwS7yejl+uV3FEWEp4lssFEs= 33 | github.com/go-errors/errors v1.1.1 h1:ljK/pL5ltg3qoN+OtN6yCv9HWSfMwxSx90GJCZQxYNg= 34 | github.com/go-errors/errors v1.1.1/go.mod h1:psDX2osz5VnTOnFWbDeWwS7yejl+uV3FEWEp4lssFEs= 35 | github.com/go-xmlfmt/xmlfmt v0.0.0-20191208150333-d5b6f63a941b h1:khEcpUM4yFcxg4/FHQWkvVRmgijNXRfzkIDHh23ggEo= 36 | github.com/go-xmlfmt/xmlfmt v0.0.0-20191208150333-d5b6f63a941b/go.mod h1:aUCEOzzezBEjDBbFBoSiya/gduyIiWYRP6CnSFIV8AM= 37 | github.com/golang/geo v0.0.0-20190916061304-5b978397cfec h1:lJwO/92dFXWeXOZdoGXgptLmNLwynMSHUmU6besqtiw= 38 | github.com/golang/geo v0.0.0-20190916061304-5b978397cfec/go.mod h1:QZ0nwyI2jOfgRAoBvP+ab5aRr7c9x7lhGEJrKvBwjWI= 39 | github.com/golang/geo v0.0.0-20200319012246-673a6f80352d h1:C/hKUcHT483btRbeGkrRjJz+Zbcj8audldIi9tRJDCc= 40 | github.com/golang/geo v0.0.0-20200319012246-673a6f80352d/go.mod h1:QZ0nwyI2jOfgRAoBvP+ab5aRr7c9x7lhGEJrKvBwjWI= 41 | github.com/jessevdk/go-flags v1.4.0 h1:4IU2WS7AumrZ/40jfhf4QVDMsQwqA7VEHozFRrGARJA= 42 | github.com/jessevdk/go-flags v1.4.0/go.mod h1:4FA24M0QyGHXBuZZK/XkWh8h0e1EYbRYJSGM75WSRxI= 43 | golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= 44 | golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= 45 | golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= 46 | golang.org/x/net v0.0.0-20191209160850-c0dbc17a3553 h1:efeOvDhwQ29Dj3SdAV/MJf8oukgn+8D8WgaCaRMchF8= 47 | golang.org/x/net v0.0.0-20191209160850-c0dbc17a3553/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= 48 | golang.org/x/net v0.0.0-20200320220750-118fecf932d8/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= 49 | golang.org/x/net v0.0.0-20200501053045-e0ff5e5a1de5 h1:WQ8q63x+f/zpC8Ac1s9wLElVoHhm32p6tudrU72n1QA= 50 | golang.org/x/net v0.0.0-20200501053045-e0ff5e5a1de5/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= 51 | golang.org/x/net v0.0.0-20200513185701-a91f0712d120 h1:EZ3cVSzKOlJxAd8e8YAJ7no8nNypTxexh/YE/xW3ZEY= 52 | golang.org/x/net v0.0.0-20200513185701-a91f0712d120/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= 53 | golang.org/x/net v0.0.0-20200520182314-0ba52f642ac2 h1:eDrdRpKgkcCqKZQwyZRyeFZgfqt37SL7Kv3tok06cKE= 54 | golang.org/x/net v0.0.0-20200520182314-0ba52f642ac2/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= 55 | golang.org/x/net v0.0.0-20200707034311-ab3426394381 h1:VXak5I6aEWmAXeQjA+QSZzlgNrpq9mjcfDemuexIKsU= 56 | golang.org/x/net v0.0.0-20200707034311-ab3426394381/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA= 57 | golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 58 | golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 59 | golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 60 | golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= 61 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= 62 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 63 | gopkg.in/yaml.v2 v2.2.7 h1:VUgggvou5XRW9mHwD/yXxIYSMtY0zoKQf/v226p2nyo= 64 | gopkg.in/yaml.v2 v2.2.7/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= 65 | gopkg.in/yaml.v2 v2.3.0 h1:clyUAQHOM3G0M3f5vQj7LuJrETvjVot3Z5el9nffUtU= 66 | gopkg.in/yaml.v2 v2.3.0/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= 67 | -------------------------------------------------------------------------------- /v2/markers.go: -------------------------------------------------------------------------------- 1 | package jpegstructure 2 | 3 | import ( 4 | "github.com/dsoprea/go-logging" 5 | ) 6 | 7 | const ( 8 | // MARKER_SOI marker 9 | MARKER_SOI = 0xd8 10 | 11 | // MARKER_EOI marker 12 | MARKER_EOI = 0xd9 13 | 14 | // MARKER_SOS marker 15 | MARKER_SOS = 0xda 16 | 17 | // MARKER_SOD marker 18 | MARKER_SOD = 0x93 19 | 20 | // MARKER_DQT marker 21 | MARKER_DQT = 0xdb 22 | 23 | // MARKER_APP0 marker 24 | MARKER_APP0 = 0xe0 25 | 26 | // MARKER_APP1 marker 27 | MARKER_APP1 = 0xe1 28 | 29 | // MARKER_APP2 marker 30 | MARKER_APP2 = 0xe2 31 | 32 | // MARKER_APP3 marker 33 | MARKER_APP3 = 0xe3 34 | 35 | // MARKER_APP4 marker 36 | MARKER_APP4 = 0xe4 37 | 38 | // MARKER_APP5 marker 39 | MARKER_APP5 = 0xe5 40 | 41 | // MARKER_APP6 marker 42 | MARKER_APP6 = 0xe6 43 | 44 | // MARKER_APP7 marker 45 | MARKER_APP7 = 0xe7 46 | 47 | // MARKER_APP8 marker 48 | MARKER_APP8 = 0xe8 49 | 50 | // MARKER_APP10 marker 51 | MARKER_APP10 = 0xea 52 | 53 | // MARKER_APP12 marker 54 | MARKER_APP12 = 0xec 55 | 56 | // MARKER_APP13 marker 57 | MARKER_APP13 = 0xed 58 | 59 | // MARKER_APP14 marker 60 | MARKER_APP14 = 0xee 61 | 62 | // MARKER_APP15 marker 63 | MARKER_APP15 = 0xef 64 | 65 | // MARKER_COM marker 66 | MARKER_COM = 0xfe 67 | 68 | // MARKER_CME marker 69 | MARKER_CME = 0x64 70 | 71 | // MARKER_SIZ marker 72 | MARKER_SIZ = 0x51 73 | 74 | // MARKER_DHT marker 75 | MARKER_DHT = 0xc4 76 | 77 | // MARKER_JPG marker 78 | MARKER_JPG = 0xc8 79 | 80 | // MARKER_DAC marker 81 | MARKER_DAC = 0xcc 82 | 83 | // MARKER_SOF0 marker 84 | MARKER_SOF0 = 0xc0 85 | 86 | // MARKER_SOF1 marker 87 | MARKER_SOF1 = 0xc1 88 | 89 | // MARKER_SOF2 marker 90 | MARKER_SOF2 = 0xc2 91 | 92 | // MARKER_SOF3 marker 93 | MARKER_SOF3 = 0xc3 94 | 95 | // MARKER_SOF5 marker 96 | MARKER_SOF5 = 0xc5 97 | 98 | // MARKER_SOF6 marker 99 | MARKER_SOF6 = 0xc6 100 | 101 | // MARKER_SOF7 marker 102 | MARKER_SOF7 = 0xc7 103 | 104 | // MARKER_SOF9 marker 105 | MARKER_SOF9 = 0xc9 106 | 107 | // MARKER_SOF10 marker 108 | MARKER_SOF10 = 0xca 109 | 110 | // MARKER_SOF11 marker 111 | MARKER_SOF11 = 0xcb 112 | 113 | // MARKER_SOF13 marker 114 | MARKER_SOF13 = 0xcd 115 | 116 | // MARKER_SOF14 marker 117 | MARKER_SOF14 = 0xce 118 | 119 | // MARKER_SOF15 marker 120 | MARKER_SOF15 = 0xcf 121 | ) 122 | 123 | var ( 124 | jpegLogger = log.NewLogger("jpegstructure.jpeg") 125 | jpegMagicStandard = []byte{0xff, MARKER_SOI, 0xff} 126 | jpegMagic2000 = []byte{0xff, 0x4f, 0xff} 127 | 128 | markerLen = map[byte]int{ 129 | 0x00: 0, 130 | 0x01: 0, 131 | 0xd0: 0, 132 | 0xd1: 0, 133 | 0xd2: 0, 134 | 0xd3: 0, 135 | 0xd4: 0, 136 | 0xd5: 0, 137 | 0xd6: 0, 138 | 0xd7: 0, 139 | 0xd8: 0, 140 | 0xd9: 0, 141 | 0xda: 0, 142 | 143 | // J2C 144 | 0x30: 0, 145 | 0x31: 0, 146 | 0x32: 0, 147 | 0x33: 0, 148 | 0x34: 0, 149 | 0x35: 0, 150 | 0x36: 0, 151 | 0x37: 0, 152 | 0x38: 0, 153 | 0x39: 0, 154 | 0x3a: 0, 155 | 0x3b: 0, 156 | 0x3c: 0, 157 | 0x3d: 0, 158 | 0x3e: 0, 159 | 0x3f: 0, 160 | 0x4f: 0, 161 | 0x92: 0, 162 | 0x93: 0, 163 | 164 | // J2C extensions 165 | 0x74: 4, 166 | 0x75: 4, 167 | 0x77: 4, 168 | } 169 | 170 | markerNames = map[byte]string{ 171 | MARKER_SOI: "SOI", 172 | MARKER_EOI: "EOI", 173 | MARKER_SOS: "SOS", 174 | MARKER_SOD: "SOD", 175 | MARKER_DQT: "DQT", 176 | MARKER_APP0: "APP0", 177 | MARKER_APP1: "APP1", 178 | MARKER_APP2: "APP2", 179 | MARKER_APP3: "APP3", 180 | MARKER_APP4: "APP4", 181 | MARKER_APP5: "APP5", 182 | MARKER_APP6: "APP6", 183 | MARKER_APP7: "APP7", 184 | MARKER_APP8: "APP8", 185 | MARKER_APP10: "APP10", 186 | MARKER_APP12: "APP12", 187 | MARKER_APP13: "APP13", 188 | MARKER_APP14: "APP14", 189 | MARKER_APP15: "APP15", 190 | MARKER_COM: "COM", 191 | MARKER_CME: "CME", 192 | MARKER_SIZ: "SIZ", 193 | 194 | MARKER_DHT: "DHT", 195 | MARKER_JPG: "JPG", 196 | MARKER_DAC: "DAC", 197 | 198 | MARKER_SOF0: "SOF0", 199 | MARKER_SOF1: "SOF1", 200 | MARKER_SOF2: "SOF2", 201 | MARKER_SOF3: "SOF3", 202 | MARKER_SOF5: "SOF5", 203 | MARKER_SOF6: "SOF6", 204 | MARKER_SOF7: "SOF7", 205 | MARKER_SOF9: "SOF9", 206 | MARKER_SOF10: "SOF10", 207 | MARKER_SOF11: "SOF11", 208 | MARKER_SOF13: "SOF13", 209 | MARKER_SOF14: "SOF14", 210 | MARKER_SOF15: "SOF15", 211 | } 212 | ) 213 | -------------------------------------------------------------------------------- /v2/media_parser.go: -------------------------------------------------------------------------------- 1 | package jpegstructure 2 | 3 | import ( 4 | "bufio" 5 | "bytes" 6 | "image" 7 | "io" 8 | "os" 9 | 10 | "image/jpeg" 11 | 12 | "github.com/dsoprea/go-logging" 13 | "github.com/dsoprea/go-utility/v2/image" 14 | ) 15 | 16 | // JpegMediaParser is a `riimage.MediaParser` that knows how to parse JPEG 17 | // images. 18 | type JpegMediaParser struct { 19 | } 20 | 21 | // NewJpegMediaParser returns a new JpegMediaParser. 22 | func NewJpegMediaParser() *JpegMediaParser { 23 | 24 | // TODO(dustin): Add test 25 | 26 | return new(JpegMediaParser) 27 | } 28 | 29 | // Parse parses a JPEG uses an `io.ReadSeeker`. Even if it fails, it will return 30 | // the list of segments encountered prior to the failure. 31 | func (jmp *JpegMediaParser) Parse(rs io.ReadSeeker, size int) (ec riimage.MediaContext, err error) { 32 | defer func() { 33 | if state := recover(); state != nil { 34 | err = log.Wrap(state.(error)) 35 | } 36 | }() 37 | 38 | s := bufio.NewScanner(rs) 39 | 40 | // Since each segment can be any size, our buffer must allowed to grow as 41 | // large as the file. 42 | buffer := []byte{} 43 | s.Buffer(buffer, size) 44 | 45 | js := NewJpegSplitter(nil) 46 | s.Split(js.Split) 47 | 48 | for s.Scan() != false { 49 | } 50 | 51 | // Always return the segments that were parsed, at least until there was an 52 | // error. 53 | ec = js.Segments() 54 | 55 | log.PanicIf(s.Err()) 56 | 57 | return ec, nil 58 | } 59 | 60 | // ParseFile parses a JPEG file. Even if it fails, it will return the list of 61 | // segments encountered prior to the failure. 62 | func (jmp *JpegMediaParser) ParseFile(filepath string) (ec riimage.MediaContext, err error) { 63 | defer func() { 64 | if state := recover(); state != nil { 65 | err = log.Wrap(state.(error)) 66 | } 67 | }() 68 | 69 | // TODO(dustin): Add test 70 | 71 | f, err := os.Open(filepath) 72 | log.PanicIf(err) 73 | 74 | defer f.Close() 75 | 76 | stat, err := f.Stat() 77 | log.PanicIf(err) 78 | 79 | size := stat.Size() 80 | 81 | sl, err := jmp.Parse(f, int(size)) 82 | 83 | // Always return the segments that were parsed, at least until there was an 84 | // error. 85 | ec = sl 86 | 87 | log.PanicIf(err) 88 | 89 | return ec, nil 90 | } 91 | 92 | // ParseBytes parses a JPEG byte-slice. Even if it fails, it will return the 93 | // list of segments encountered prior to the failure. 94 | func (jmp *JpegMediaParser) ParseBytes(data []byte) (ec riimage.MediaContext, err error) { 95 | defer func() { 96 | if state := recover(); state != nil { 97 | err = log.Wrap(state.(error)) 98 | } 99 | }() 100 | 101 | br := bytes.NewReader(data) 102 | 103 | sl, err := jmp.Parse(br, len(data)) 104 | 105 | // Always return the segments that were parsed, at least until there was an 106 | // error. 107 | ec = sl 108 | 109 | log.PanicIf(err) 110 | 111 | return ec, nil 112 | } 113 | 114 | // LooksLikeFormat indicates whether the data looks like a JPEG image. 115 | func (jmp *JpegMediaParser) LooksLikeFormat(data []byte) bool { 116 | if len(data) < 4 { 117 | return false 118 | } 119 | 120 | l := len(data) 121 | if data[0] != 0xff || data[1] != MARKER_SOI || data[l-2] != 0xff || data[l-1] != MARKER_EOI { 122 | return false 123 | } 124 | 125 | return true 126 | } 127 | 128 | // GetImage returns an image.Image-compatible struct. 129 | func (jmp *JpegMediaParser) GetImage(r io.Reader) (img image.Image, err error) { 130 | img, err = jpeg.Decode(r) 131 | log.PanicIf(err) 132 | 133 | return img, nil 134 | } 135 | 136 | var ( 137 | // Enforce interface conformance. 138 | _ riimage.MediaParser = new(JpegMediaParser) 139 | ) 140 | -------------------------------------------------------------------------------- /v2/media_parser_test.go: -------------------------------------------------------------------------------- 1 | package jpegstructure 2 | 3 | import ( 4 | "fmt" 5 | "os" 6 | "path" 7 | "testing" 8 | 9 | "io/ioutil" 10 | 11 | "github.com/dsoprea/go-logging" 12 | ) 13 | 14 | func TestJpegMediaParser_Parse(t *testing.T) { 15 | filepath := GetTestImageFilepath() 16 | 17 | f, err := os.Open(filepath) 18 | log.PanicIf(err) 19 | 20 | defer f.Close() 21 | 22 | stat, err := f.Stat() 23 | log.PanicIf(err) 24 | 25 | size := stat.Size() 26 | 27 | jmp := NewJpegMediaParser() 28 | 29 | intfc, err := jmp.Parse(f, int(size)) 30 | log.PanicIf(err) 31 | 32 | sl := intfc.(*SegmentList) 33 | 34 | expected := []*Segment{ 35 | { 36 | MarkerId: 0xd8, 37 | Offset: 0x0, 38 | }, 39 | { 40 | MarkerId: 0xe1, 41 | Offset: 0x2, 42 | }, 43 | { 44 | MarkerId: 0xe1, 45 | Offset: 0x000080b4, 46 | }, 47 | { 48 | MarkerId: 0xdb, 49 | Offset: 0x8ab6, 50 | }, 51 | { 52 | MarkerId: 0xc0, 53 | Offset: 0x8b3c, 54 | }, 55 | { 56 | MarkerId: 0xc4, 57 | Offset: 0x8b4f, 58 | }, 59 | { 60 | MarkerId: 0xda, 61 | Offset: 0x8cf3, 62 | }, 63 | { 64 | MarkerId: 0x0, 65 | Offset: 0x8cf5, 66 | }, 67 | { 68 | MarkerId: 0xd9, 69 | Offset: 0x554d6d, 70 | }, 71 | } 72 | 73 | if len(sl.segments) != len(expected) { 74 | t.Fatalf("Number of segments is unexpected: (%d) != (%d)", len(sl.segments), len(expected)) 75 | } 76 | 77 | for i, s := range sl.segments { 78 | if s.MarkerId != expected[i].MarkerId { 79 | t.Fatalf("Segment (%d) marker-ID not correct: (0x%02x != 0x%02x)", i, s.MarkerId, expected[i].MarkerId) 80 | } else if s.Offset != expected[i].Offset { 81 | t.Fatalf("Segment (%d) offset not correct: (0x%08x != 0x%08x)", i, s.Offset, expected[i].Offset) 82 | } 83 | } 84 | } 85 | 86 | func TestJpegMediaParser_ParseBytes(t *testing.T) { 87 | filepath := GetTestImageFilepath() 88 | 89 | data, err := ioutil.ReadFile(filepath) 90 | log.PanicIf(err) 91 | 92 | jmp := NewJpegMediaParser() 93 | 94 | intfc, err := jmp.ParseBytes(data) 95 | log.PanicIf(err) 96 | 97 | sl := intfc.(*SegmentList) 98 | 99 | expectedSegments := []*Segment{ 100 | { 101 | MarkerId: 0xd8, 102 | Offset: 0x0, 103 | }, 104 | { 105 | MarkerId: 0xe1, 106 | Offset: 0x2, 107 | }, 108 | { 109 | MarkerId: 0xe1, 110 | Offset: 0x000080b4, 111 | }, 112 | { 113 | MarkerId: 0xdb, 114 | Offset: 0x8ab6, 115 | }, 116 | { 117 | MarkerId: 0xc0, 118 | Offset: 0x8b3c, 119 | }, 120 | { 121 | MarkerId: 0xc4, 122 | Offset: 0x8b4f, 123 | }, 124 | { 125 | MarkerId: 0xda, 126 | Offset: 0x8cf3, 127 | }, 128 | { 129 | MarkerId: 0x0, 130 | Offset: 0x8cf5, 131 | }, 132 | { 133 | MarkerId: 0xd9, 134 | Offset: 0x554d6d, 135 | }, 136 | } 137 | 138 | expectedSl := NewSegmentList(expectedSegments) 139 | 140 | if sl.OffsetsEqual(expectedSl) != true { 141 | t.Fatalf("Segments not expected") 142 | } 143 | } 144 | 145 | func TestJpegMediaParser_ParseBytes_Offsets(t *testing.T) { 146 | filepath := GetTestImageFilepath() 147 | 148 | data, err := ioutil.ReadFile(filepath) 149 | log.PanicIf(err) 150 | 151 | jmp := NewJpegMediaParser() 152 | 153 | intfc, err := jmp.ParseBytes(data) 154 | log.PanicIf(err) 155 | 156 | sl := intfc.(*SegmentList) 157 | 158 | err = sl.Validate(data) 159 | log.PanicIf(err) 160 | } 161 | 162 | func TestJpegMediaParser_ParseBytes_MultipleEois(t *testing.T) { 163 | defer func() { 164 | if state := recover(); state != nil { 165 | err := log.Wrap(state.(error)) 166 | log.PrintErrorf(err, "Test failure.") 167 | t.Fatalf("Test failure.") 168 | } 169 | }() 170 | 171 | assetsPath := GetTestAssetsPath() 172 | filepath := path.Join(assetsPath, "IMG_6691_Multiple_EOIs.jpg") 173 | 174 | data, err := ioutil.ReadFile(filepath) 175 | log.PanicIf(err) 176 | 177 | jmp := NewJpegMediaParser() 178 | 179 | intfc, err := jmp.ParseBytes(data) 180 | log.PanicIf(err) 181 | 182 | sl := intfc.(*SegmentList) 183 | 184 | expectedSegments := []*Segment{ 185 | { 186 | MarkerId: 0xd8, 187 | Offset: 0x0, 188 | }, 189 | { 190 | MarkerId: 0xe1, 191 | Offset: 0x00000002, 192 | }, 193 | { 194 | MarkerId: 0xe1, 195 | Offset: 0x00007002, 196 | }, 197 | { 198 | MarkerId: 0xe2, 199 | Offset: 0x00007fa0, 200 | }, 201 | { 202 | MarkerId: 0xdb, 203 | Offset: 0x00008002, 204 | }, 205 | { 206 | MarkerId: 0xc0, 207 | Offset: 0x00008088, 208 | }, 209 | { 210 | MarkerId: 0xc4, 211 | Offset: 0x0000809b, 212 | }, 213 | { 214 | MarkerId: 0xda, 215 | Offset: 0x0000823f, 216 | }, 217 | { 218 | MarkerId: 0x0, 219 | Offset: 0x00008241, 220 | }, 221 | { 222 | MarkerId: 0xd9, 223 | Offset: 0x003f24db, 224 | }, 225 | } 226 | 227 | expectedSl := NewSegmentList(expectedSegments) 228 | 229 | if sl.OffsetsEqual(expectedSl) != true { 230 | for i, segment := range sl.segments { 231 | fmt.Printf("%d: ACTUAL: MARKER=(%02x) OFF=(%10x)\n", i, segment.MarkerId, segment.Offset) 232 | } 233 | 234 | for i, segment := range expectedSl.segments { 235 | fmt.Printf("%d: EXPECTED: MARKER=(%02x) OFF=(%10x)\n", i, segment.MarkerId, segment.Offset) 236 | } 237 | 238 | t.Fatalf("Segments not expected") 239 | } 240 | } 241 | 242 | func TestJpegMediaParser_LooksLikeFormat(t *testing.T) { 243 | filepath := GetTestImageFilepath() 244 | 245 | data, err := ioutil.ReadFile(filepath) 246 | log.PanicIf(err) 247 | 248 | jmp := NewJpegMediaParser() 249 | 250 | if jmp.LooksLikeFormat(data) != true { 251 | t.Fatalf("not detected as JPEG") 252 | } 253 | } 254 | -------------------------------------------------------------------------------- /v2/segment.go: -------------------------------------------------------------------------------- 1 | package jpegstructure 2 | 3 | import ( 4 | "bytes" 5 | "errors" 6 | "fmt" 7 | 8 | "crypto/sha1" 9 | "encoding/hex" 10 | 11 | "github.com/dsoprea/go-exif/v3" 12 | "github.com/dsoprea/go-exif/v3/common" 13 | "github.com/dsoprea/go-iptc" 14 | "github.com/dsoprea/go-logging" 15 | "github.com/dsoprea/go-photoshop-info-format" 16 | "github.com/dsoprea/go-utility/v2/image" 17 | ) 18 | 19 | const ( 20 | pirIptcImageResourceId = uint16(0x0404) 21 | ) 22 | 23 | var ( 24 | // exifPrefix is the prefix found at the top of an EXIF slice. This is JPEG- 25 | // specific. 26 | exifPrefix = []byte{'E', 'x', 'i', 'f', 0, 0} 27 | 28 | xmpPrefix = []byte("http://ns.adobe.com/xap/1.0/\000") 29 | 30 | ps30Prefix = []byte("Photoshop 3.0\000") 31 | ) 32 | 33 | var ( 34 | // ErrNoXmp is returned if XMP data was requested but not found. 35 | ErrNoXmp = errors.New("no XMP data") 36 | 37 | // ErrNoIptc is returned if IPTC data was requested but not found. 38 | ErrNoIptc = errors.New("no IPTC data") 39 | 40 | // ErrNoPhotoshopData is returned if Photoshop info was requested but not 41 | // found. 42 | ErrNoPhotoshopData = errors.New("no photoshop data") 43 | ) 44 | 45 | // SofSegment has info read from a SOF segment. 46 | type SofSegment struct { 47 | // BitsPerSample is the bits-per-sample. 48 | BitsPerSample byte 49 | 50 | // Width is the image width. 51 | Width uint16 52 | 53 | // Height is the image height. 54 | Height uint16 55 | 56 | // ComponentCount is the number of color components. 57 | ComponentCount byte 58 | } 59 | 60 | // String returns a string representation of the SOF segment. 61 | func (ss SofSegment) String() string { 62 | 63 | // TODO(dustin): Add test 64 | 65 | return fmt.Sprintf("SOF", ss.BitsPerSample, ss.Width, ss.Height, ss.ComponentCount) 66 | } 67 | 68 | // SegmentVisitor describes a segment-visitor struct. 69 | type SegmentVisitor interface { 70 | // HandleSegment is triggered for each segment encountered as well as the 71 | // scan-data. 72 | HandleSegment(markerId byte, markerName string, counter int, lastIsScanData bool) error 73 | } 74 | 75 | // SofSegmentVisitor describes a visitor that is only called for each SOF 76 | // segment. 77 | type SofSegmentVisitor interface { 78 | // HandleSof is called for each encountered SOF segment. 79 | HandleSof(sof *SofSegment) error 80 | } 81 | 82 | // Segment describes a single segment. 83 | type Segment struct { 84 | MarkerId byte 85 | MarkerName string 86 | Offset int 87 | Data []byte 88 | 89 | photoshopInfo map[uint16]photoshopinfo.Photoshop30InfoRecord 90 | iptcTags map[iptc.StreamTagKey][]iptc.TagData 91 | } 92 | 93 | // SetExif encodes and sets EXIF data into this segment. 94 | func (s *Segment) SetExif(ib *exif.IfdBuilder) (err error) { 95 | defer func() { 96 | if state := recover(); state != nil { 97 | err = log.Wrap(state.(error)) 98 | } 99 | }() 100 | 101 | ibe := exif.NewIfdByteEncoder() 102 | 103 | exifData, err := ibe.EncodeToExif(ib) 104 | log.PanicIf(err) 105 | 106 | l := len(exifPrefix) 107 | 108 | s.Data = make([]byte, l+len(exifData)) 109 | copy(s.Data[0:], exifPrefix) 110 | copy(s.Data[l:], exifData) 111 | 112 | return nil 113 | } 114 | 115 | // Exif returns an `exif.Ifd` instance for the EXIF data we currently have. 116 | func (s *Segment) Exif() (rootIfd *exif.Ifd, data []byte, err error) { 117 | defer func() { 118 | if state := recover(); state != nil { 119 | err = log.Wrap(state.(error)) 120 | } 121 | }() 122 | 123 | l := len(exifPrefix) 124 | 125 | rawExif := s.Data[l:] 126 | 127 | jpegLogger.Debugf(nil, "Attempting to parse (%d) byte EXIF blob (Exif).", len(rawExif)) 128 | 129 | im, err := exifcommon.NewIfdMappingWithStandard() 130 | log.PanicIf(err) 131 | 132 | ti := exif.NewTagIndex() 133 | 134 | _, index, err := exif.Collect(im, ti, rawExif) 135 | log.PanicIf(err) 136 | 137 | return index.RootIfd, rawExif, nil 138 | } 139 | 140 | // FlatExif parses the EXIF data and just returns a list of tags. 141 | func (s *Segment) FlatExif() (exifTags []exif.ExifTag, err error) { 142 | defer func() { 143 | if state := recover(); state != nil { 144 | err = log.Wrap(state.(error)) 145 | } 146 | }() 147 | 148 | // TODO(dustin): Add test 149 | 150 | l := len(exifPrefix) 151 | 152 | rawExif := s.Data[l:] 153 | 154 | jpegLogger.Debugf(nil, "Attempting to parse (%d) byte EXIF blob (FlatExif).", len(rawExif)) 155 | 156 | exifTags, _, err = exif.GetFlatExifData(rawExif, nil) 157 | log.PanicIf(err) 158 | 159 | return exifTags, nil 160 | } 161 | 162 | // EmbeddedString returns a string of properties that can be embedded into an 163 | // longer string of properties. 164 | func (s *Segment) EmbeddedString() string { 165 | h := sha1.New() 166 | h.Write(s.Data) 167 | 168 | // TODO(dustin): Add test 169 | 170 | digestString := hex.EncodeToString(h.Sum(nil)) 171 | 172 | return fmt.Sprintf("OFFSET=(0x%08x %10d) ID=(0x%02x) NAME=[%-5s] SIZE=(%10d) SHA1=[%s]", s.Offset, s.Offset, s.MarkerId, markerNames[s.MarkerId], len(s.Data), digestString) 173 | } 174 | 175 | // String returns a descriptive string. 176 | func (s *Segment) String() string { 177 | 178 | // TODO(dustin): Add test 179 | 180 | return fmt.Sprintf("Segment<%s>", s.EmbeddedString()) 181 | } 182 | 183 | // IsExif returns true if EXIF data. 184 | func (s *Segment) IsExif() bool { 185 | if s.MarkerId != MARKER_APP1 { 186 | return false 187 | } 188 | 189 | // TODO(dustin): Add test 190 | 191 | l := len(exifPrefix) 192 | 193 | if len(s.Data) < l { 194 | return false 195 | } 196 | 197 | if bytes.Equal(s.Data[:l], exifPrefix) == false { 198 | return false 199 | } 200 | 201 | return true 202 | } 203 | 204 | // IsXmp returns true if XMP data. 205 | func (s *Segment) IsXmp() bool { 206 | if s.MarkerId != MARKER_APP1 { 207 | return false 208 | } 209 | 210 | // TODO(dustin): Add test 211 | 212 | l := len(xmpPrefix) 213 | 214 | if len(s.Data) < l { 215 | return false 216 | } 217 | 218 | if bytes.Equal(s.Data[:l], xmpPrefix) == false { 219 | return false 220 | } 221 | 222 | return true 223 | } 224 | 225 | // FormattedXmp returns a formatted XML string. This only makes sense for a 226 | // segment comprised of XML data (like XMP). 227 | func (s *Segment) FormattedXmp() (formatted string, err error) { 228 | defer func() { 229 | if state := recover(); state != nil { 230 | err = log.Wrap(state.(error)) 231 | } 232 | }() 233 | 234 | // TODO(dustin): Add test 235 | 236 | if s.IsXmp() != true { 237 | log.Panicf("not an XMP segment") 238 | } 239 | 240 | l := len(xmpPrefix) 241 | 242 | raw := string(s.Data[l:]) 243 | 244 | formatted, err = FormatXml(raw) 245 | log.PanicIf(err) 246 | 247 | return formatted, nil 248 | } 249 | 250 | func (s *Segment) parsePhotoshopInfo() (photoshopInfo map[uint16]photoshopinfo.Photoshop30InfoRecord, err error) { 251 | defer func() { 252 | if state := recover(); state != nil { 253 | err = log.Wrap(state.(error)) 254 | } 255 | }() 256 | 257 | if s.photoshopInfo != nil { 258 | return s.photoshopInfo, nil 259 | } 260 | 261 | if s.MarkerId != MARKER_APP13 { 262 | return nil, ErrNoPhotoshopData 263 | } 264 | 265 | l := len(ps30Prefix) 266 | 267 | if len(s.Data) < l { 268 | return nil, ErrNoPhotoshopData 269 | } 270 | 271 | if bytes.Equal(s.Data[:l], ps30Prefix) == false { 272 | return nil, ErrNoPhotoshopData 273 | } 274 | 275 | data := s.Data[l:] 276 | b := bytes.NewBuffer(data) 277 | 278 | // Parse it. 279 | 280 | pirIndex, err := photoshopinfo.ReadPhotoshop30Info(b) 281 | log.PanicIf(err) 282 | 283 | s.photoshopInfo = pirIndex 284 | 285 | return s.photoshopInfo, nil 286 | } 287 | 288 | // IsIptc returns true if XMP data. 289 | func (s *Segment) IsIptc() bool { 290 | // TODO(dustin): Add test 291 | 292 | // There's a cost to determining if there's IPTC data, so we won't do it 293 | // more than once. 294 | if s.iptcTags != nil { 295 | return true 296 | } 297 | 298 | photoshopInfo, err := s.parsePhotoshopInfo() 299 | if err != nil { 300 | if err == ErrNoPhotoshopData { 301 | return false 302 | } 303 | 304 | log.Panic(err) 305 | } 306 | 307 | // Bail if the Photoshop info doesn't have IPTC data. 308 | 309 | _, found := photoshopInfo[pirIptcImageResourceId] 310 | if found == false { 311 | return false 312 | } 313 | 314 | return true 315 | } 316 | 317 | // Iptc parses Photoshop info (if present) and then parses the IPTC info inside 318 | // it (if present). 319 | func (s *Segment) Iptc() (tags map[iptc.StreamTagKey][]iptc.TagData, err error) { 320 | defer func() { 321 | if state := recover(); state != nil { 322 | err = log.Wrap(state.(error)) 323 | } 324 | }() 325 | 326 | // Cache the parse. 327 | if s.iptcTags != nil { 328 | return s.iptcTags, nil 329 | } 330 | 331 | photoshopInfo, err := s.parsePhotoshopInfo() 332 | log.PanicIf(err) 333 | 334 | iptcPir, found := photoshopInfo[pirIptcImageResourceId] 335 | if found == false { 336 | return nil, ErrNoIptc 337 | } 338 | 339 | b := bytes.NewBuffer(iptcPir.Data) 340 | 341 | tags, err = iptc.ParseStream(b) 342 | log.PanicIf(err) 343 | 344 | s.iptcTags = tags 345 | 346 | return tags, nil 347 | } 348 | 349 | var ( 350 | // Enforce interface conformance. 351 | _ riimage.MediaContext = new(Segment) 352 | ) 353 | -------------------------------------------------------------------------------- /v2/segment_list.go: -------------------------------------------------------------------------------- 1 | package jpegstructure 2 | 3 | import ( 4 | "bytes" 5 | "fmt" 6 | "io" 7 | 8 | "crypto/sha1" 9 | "encoding/binary" 10 | 11 | "github.com/dsoprea/go-exif/v3" 12 | "github.com/dsoprea/go-exif/v3/common" 13 | "github.com/dsoprea/go-iptc" 14 | "github.com/dsoprea/go-logging" 15 | ) 16 | 17 | // SegmentList contains a slice of segments. 18 | type SegmentList struct { 19 | segments []*Segment 20 | } 21 | 22 | // NewSegmentList returns a new SegmentList struct. 23 | func NewSegmentList(segments []*Segment) (sl *SegmentList) { 24 | if segments == nil { 25 | segments = make([]*Segment, 0) 26 | } 27 | 28 | return &SegmentList{ 29 | segments: segments, 30 | } 31 | } 32 | 33 | // OffsetsEqual returns true is all segments have the same marker-IDs and were 34 | // found at the same offsets. 35 | func (sl *SegmentList) OffsetsEqual(o *SegmentList) bool { 36 | if len(o.segments) != len(sl.segments) { 37 | return false 38 | } 39 | 40 | for i, s := range o.segments { 41 | if s.MarkerId != sl.segments[i].MarkerId || s.Offset != sl.segments[i].Offset { 42 | return false 43 | } 44 | } 45 | 46 | return true 47 | } 48 | 49 | // Segments returns the underlying slice of segments. 50 | func (sl *SegmentList) Segments() []*Segment { 51 | return sl.segments 52 | } 53 | 54 | // Add adds another segment. 55 | func (sl *SegmentList) Add(s *Segment) { 56 | sl.segments = append(sl.segments, s) 57 | } 58 | 59 | // Print prints segment info. 60 | func (sl *SegmentList) Print() { 61 | if len(sl.segments) == 0 { 62 | fmt.Printf("No segments.\n") 63 | } else { 64 | exifIndex, _, err := sl.FindExif() 65 | if err != nil { 66 | if err == exif.ErrNoExif { 67 | exifIndex = -1 68 | } else { 69 | log.Panic(err) 70 | } 71 | } 72 | 73 | xmpIndex, _, err := sl.FindXmp() 74 | if err != nil { 75 | if err == ErrNoXmp { 76 | xmpIndex = -1 77 | } else { 78 | log.Panic(err) 79 | } 80 | } 81 | 82 | iptcIndex, _, err := sl.FindIptc() 83 | if err != nil { 84 | if err == ErrNoIptc { 85 | iptcIndex = -1 86 | } else { 87 | log.Panic(err) 88 | } 89 | } 90 | 91 | for i, s := range sl.segments { 92 | fmt.Printf("%2d: %s", i, s.EmbeddedString()) 93 | 94 | if i == exifIndex { 95 | fmt.Printf(" [EXIF]") 96 | } else if i == xmpIndex { 97 | fmt.Printf(" [XMP]") 98 | } else if i == iptcIndex { 99 | fmt.Printf(" [IPTC]") 100 | } 101 | 102 | fmt.Printf("\n") 103 | } 104 | } 105 | } 106 | 107 | // Validate checks that all of the markers are actually located at all of the 108 | // recorded offsets. 109 | func (sl *SegmentList) Validate(data []byte) (err error) { 110 | defer func() { 111 | if state := recover(); state != nil { 112 | err = log.Wrap(state.(error)) 113 | } 114 | }() 115 | 116 | if len(sl.segments) < 2 { 117 | log.Panicf("minimum segments not found") 118 | } 119 | 120 | if sl.segments[0].MarkerId != MARKER_SOI { 121 | log.Panicf("first segment not SOI") 122 | } else if sl.segments[len(sl.segments)-1].MarkerId != MARKER_EOI { 123 | log.Panicf("last segment not EOI") 124 | } 125 | 126 | lastOffset := 0 127 | for i, s := range sl.segments { 128 | if lastOffset != 0 && s.Offset <= lastOffset { 129 | log.Panicf("segment offset not greater than the last: SEGMENT=(%d) (0x%08x) <= (0x%08x)", i, s.Offset, lastOffset) 130 | } 131 | 132 | // The scan-data doesn't start with a marker. 133 | if s.MarkerId == 0x0 { 134 | continue 135 | } 136 | 137 | o := s.Offset 138 | if bytes.Compare(data[o:o+2], []byte{0xff, s.MarkerId}) != 0 { 139 | log.Panicf("segment offset does not point to the start of a segment: SEGMENT=(%d) (0x%08x)", i, s.Offset) 140 | } 141 | 142 | lastOffset = o 143 | } 144 | 145 | return nil 146 | } 147 | 148 | // FindExif returns the the segment that hosts the EXIF data (if present). 149 | func (sl *SegmentList) FindExif() (index int, segment *Segment, err error) { 150 | defer func() { 151 | if state := recover(); state != nil { 152 | err = log.Wrap(state.(error)) 153 | } 154 | }() 155 | 156 | for i, s := range sl.segments { 157 | if s.IsExif() == true { 158 | return i, s, nil 159 | } 160 | } 161 | 162 | return -1, nil, exif.ErrNoExif 163 | } 164 | 165 | // FindXmp returns the the segment that hosts the XMP data (if present). 166 | func (sl *SegmentList) FindXmp() (index int, segment *Segment, err error) { 167 | defer func() { 168 | if state := recover(); state != nil { 169 | err = log.Wrap(state.(error)) 170 | } 171 | }() 172 | 173 | for i, s := range sl.segments { 174 | if s.IsXmp() == true { 175 | return i, s, nil 176 | } 177 | } 178 | 179 | return -1, nil, ErrNoXmp 180 | } 181 | 182 | // FindIptc returns the the segment that hosts the IPTC data (if present). 183 | func (sl *SegmentList) FindIptc() (index int, segment *Segment, err error) { 184 | defer func() { 185 | if state := recover(); state != nil { 186 | err = log.Wrap(state.(error)) 187 | } 188 | }() 189 | 190 | for i, s := range sl.segments { 191 | if s.IsIptc() == true { 192 | return i, s, nil 193 | } 194 | } 195 | 196 | return -1, nil, ErrNoIptc 197 | } 198 | 199 | // Exif returns an `exif.Ifd` instance for the EXIF data we currently have. 200 | func (sl *SegmentList) Exif() (rootIfd *exif.Ifd, rawExif []byte, err error) { 201 | defer func() { 202 | if state := recover(); state != nil { 203 | err = log.Wrap(state.(error)) 204 | } 205 | }() 206 | 207 | _, s, err := sl.FindExif() 208 | log.PanicIf(err) 209 | 210 | rootIfd, rawExif, err = s.Exif() 211 | log.PanicIf(err) 212 | 213 | return rootIfd, rawExif, nil 214 | } 215 | 216 | // Iptc returns embedded IPTC data if present. 217 | func (sl *SegmentList) Iptc() (tags map[iptc.StreamTagKey][]iptc.TagData, err error) { 218 | defer func() { 219 | if state := recover(); state != nil { 220 | err = log.Wrap(state.(error)) 221 | } 222 | }() 223 | 224 | // TODO(dustin): Add comment and return data. 225 | 226 | _, s, err := sl.FindIptc() 227 | log.PanicIf(err) 228 | 229 | tags, err = s.Iptc() 230 | log.PanicIf(err) 231 | 232 | return tags, nil 233 | } 234 | 235 | // ConstructExifBuilder returns an `exif.IfdBuilder` instance (needed for 236 | // modifying) preloaded with all existing tags. 237 | func (sl *SegmentList) ConstructExifBuilder() (rootIb *exif.IfdBuilder, err error) { 238 | defer func() { 239 | if state := recover(); state != nil { 240 | err = log.Wrap(state.(error)) 241 | } 242 | }() 243 | 244 | rootIfd, _, err := sl.Exif() 245 | if log.Is(err, exif.ErrNoExif) == true { 246 | // No EXIF. Just create a boilerplate builder. 247 | 248 | im := exifcommon.NewIfdMapping() 249 | 250 | err := exifcommon.LoadStandardIfds(im) 251 | log.PanicIf(err) 252 | 253 | ti := exif.NewTagIndex() 254 | 255 | rootIb := 256 | exif.NewIfdBuilder( 257 | im, 258 | ti, 259 | exifcommon.IfdStandardIfdIdentity, 260 | exifcommon.EncodeDefaultByteOrder) 261 | 262 | return rootIb, nil 263 | } else if err != nil { 264 | log.Panic(err) 265 | } 266 | 267 | rootIb = exif.NewIfdBuilderFromExistingChain(rootIfd) 268 | 269 | return rootIb, nil 270 | } 271 | 272 | // DumpExif returns an unstructured list of tags (useful when just reviewing). 273 | func (sl *SegmentList) DumpExif() (segmentIndex int, segment *Segment, exifTags []exif.ExifTag, err error) { 274 | defer func() { 275 | if state := recover(); state != nil { 276 | err = log.Wrap(state.(error)) 277 | } 278 | }() 279 | 280 | segmentIndex, s, err := sl.FindExif() 281 | if err != nil { 282 | if err == exif.ErrNoExif { 283 | return 0, nil, nil, err 284 | } 285 | 286 | log.Panic(err) 287 | } 288 | 289 | exifTags, err = s.FlatExif() 290 | log.PanicIf(err) 291 | 292 | return segmentIndex, s, exifTags, nil 293 | } 294 | 295 | func makeEmptyExifSegment() (s *Segment) { 296 | 297 | // TODO(dustin): Add test 298 | 299 | return &Segment{ 300 | MarkerId: MARKER_APP1, 301 | } 302 | } 303 | 304 | // SetExif encodes and sets EXIF data into the given segment. If `index` is -1, 305 | // append a new segment. 306 | func (sl *SegmentList) SetExif(ib *exif.IfdBuilder) (err error) { 307 | defer func() { 308 | if state := recover(); state != nil { 309 | err = log.Wrap(state.(error)) 310 | } 311 | }() 312 | 313 | _, s, err := sl.FindExif() 314 | if err != nil { 315 | if log.Is(err, exif.ErrNoExif) == false { 316 | log.Panic(err) 317 | } 318 | 319 | s = makeEmptyExifSegment() 320 | 321 | prefix := sl.segments[:1] 322 | 323 | // Install it near the beginning where we know it's safe. We can't 324 | // insert it after the EOI segment, and there might be more than one 325 | // depending on implementation and/or lax adherence to the standard. 326 | tail := append([]*Segment{s}, sl.segments[1:]...) 327 | 328 | sl.segments = append(prefix, tail...) 329 | } 330 | 331 | err = s.SetExif(ib) 332 | log.PanicIf(err) 333 | 334 | return nil 335 | } 336 | 337 | // DropExif will drop the EXIF data if present. 338 | func (sl *SegmentList) DropExif() (wasDropped bool, err error) { 339 | defer func() { 340 | if state := recover(); state != nil { 341 | err = log.Wrap(state.(error)) 342 | } 343 | }() 344 | 345 | // TODO(dustin): Add test 346 | 347 | i, _, err := sl.FindExif() 348 | if err == nil { 349 | // Found. 350 | sl.segments = append(sl.segments[:i], sl.segments[i+1:]...) 351 | 352 | return true, nil 353 | } else if log.Is(err, exif.ErrNoExif) == false { 354 | log.Panic(err) 355 | } 356 | 357 | // Not found. 358 | return false, nil 359 | } 360 | 361 | // Write writes the segment data to the given `io.Writer`. 362 | func (sl *SegmentList) Write(w io.Writer) (err error) { 363 | defer func() { 364 | if state := recover(); state != nil { 365 | err = log.Wrap(state.(error)) 366 | } 367 | }() 368 | 369 | offset := 0 370 | 371 | for i, s := range sl.segments { 372 | h := sha1.New() 373 | h.Write(s.Data) 374 | 375 | // The scan-data will have a marker-ID of (0) because it doesn't have a 376 | // marker-ID or length. 377 | if s.MarkerId != 0 { 378 | _, err := w.Write([]byte{0xff}) 379 | log.PanicIf(err) 380 | 381 | offset++ 382 | 383 | _, err = w.Write([]byte{s.MarkerId}) 384 | log.PanicIf(err) 385 | 386 | offset++ 387 | 388 | sizeLen, found := markerLen[s.MarkerId] 389 | if found == false || sizeLen == 2 { 390 | sizeLen = 2 391 | l := uint16(len(s.Data) + sizeLen) 392 | 393 | err = binary.Write(w, binary.BigEndian, &l) 394 | log.PanicIf(err) 395 | 396 | offset += 2 397 | } else if sizeLen == 4 { 398 | l := uint32(len(s.Data) + sizeLen) 399 | 400 | err = binary.Write(w, binary.BigEndian, &l) 401 | log.PanicIf(err) 402 | 403 | offset += 4 404 | } else if sizeLen != 0 { 405 | log.Panicf("not a supported marker-size: SEGMENT-INDEX=(%d) MARKER-ID=(0x%02x) MARKER-SIZE-LEN=(%d)", i, s.MarkerId, sizeLen) 406 | } 407 | } 408 | 409 | _, err := w.Write(s.Data) 410 | log.PanicIf(err) 411 | 412 | offset += len(s.Data) 413 | } 414 | 415 | return nil 416 | } 417 | -------------------------------------------------------------------------------- /v2/segment_test.go: -------------------------------------------------------------------------------- 1 | package jpegstructure 2 | 3 | import ( 4 | "bytes" 5 | "fmt" 6 | "strings" 7 | "testing" 8 | 9 | "io/ioutil" 10 | 11 | "github.com/dsoprea/go-exif/v3" 12 | "github.com/dsoprea/go-exif/v3/common" 13 | "github.com/dsoprea/go-exif/v3/undefined" 14 | "github.com/dsoprea/go-logging" 15 | ) 16 | 17 | func TestSegment_SetExif_Update(t *testing.T) { 18 | defer func() { 19 | if state := recover(); state != nil { 20 | err := log.Wrap(state.(error)) 21 | log.PrintErrorf(err, "Test failure.") 22 | t.Fatalf("Test failure.") 23 | } 24 | }() 25 | 26 | filepath := GetTestImageFilepath() 27 | 28 | // TODO(dustin): !! Might want to test a reconstruction without actually modifying anything. This is also useful. Everything will still be reallocated and this will help us determine if we're having parsing/encoding problems versions problems with an individual tag's value. 29 | // TODO(dustin): !! Use native/third-party EXIF support to test? 30 | 31 | // Parse the image. 32 | 33 | jmp := NewJpegMediaParser() 34 | 35 | intfc, err := jmp.ParseFile(filepath) 36 | log.PanicIf(err) 37 | 38 | sl := intfc.(*SegmentList) 39 | 40 | // Update the UserComment tag. 41 | 42 | rootIb, err := sl.ConstructExifBuilder() 43 | log.PanicIf(err) 44 | 45 | i, err := rootIb.Find(exifcommon.IfdExifStandardIfdIdentity.TagId()) 46 | log.PanicIf(err) 47 | 48 | exifBt := rootIb.Tags()[i] 49 | exifIb := exifBt.Value().Ib() 50 | 51 | uc := exifundefined.Tag9286UserComment{ 52 | EncodingType: exifundefined.TagUndefinedType_9286_UserComment_Encoding_ASCII, 53 | EncodingBytes: []byte("TEST COMMENT"), 54 | } 55 | 56 | err = exifIb.SetStandardWithName("UserComment", uc) 57 | log.PanicIf(err) 58 | 59 | // Update the exif segment. 60 | 61 | err = sl.SetExif(rootIb) 62 | log.PanicIf(err) 63 | 64 | b := new(bytes.Buffer) 65 | 66 | err = sl.Write(b) 67 | log.PanicIf(err) 68 | 69 | recoveredBytes := b.Bytes() 70 | 71 | // Parse the re-encoded JPEG data and validate. 72 | 73 | recoveredIntfc, err := jmp.ParseBytes(recoveredBytes) 74 | log.PanicIf(err) 75 | 76 | recoveredSl := recoveredIntfc.(*SegmentList) 77 | 78 | rootIfd, _, err := recoveredSl.Exif() 79 | log.PanicIf(err) 80 | 81 | exifIfd, err := rootIfd.ChildWithIfdPath(exifcommon.IfdExifStandardIfdIdentity) 82 | log.PanicIf(err) 83 | 84 | results, err := exifIfd.FindTagWithName("UserComment") 85 | log.PanicIf(err) 86 | 87 | ucIte := results[0] 88 | 89 | if ucIte.TagId() != 0x9286 { 90 | t.Fatalf("tag-ID not correct") 91 | } 92 | 93 | recoveredValueBytes, err := ucIte.GetRawBytes() 94 | log.PanicIf(err) 95 | 96 | expectedValueBytes := make([]byte, 0) 97 | 98 | expectedValueBytes = append(expectedValueBytes, []byte{'A', 'S', 'C', 'I', 'I', 0, 0, 0}...) 99 | expectedValueBytes = append(expectedValueBytes, []byte("TEST COMMENT")...) 100 | 101 | if bytes.Compare(recoveredValueBytes, expectedValueBytes) != 0 { 102 | t.Fatalf("Recovered UserComment does not have the right value: %v != %v", recoveredValueBytes, expectedValueBytes) 103 | } 104 | } 105 | 106 | func TestSegment_SetExif_FromScratch(t *testing.T) { 107 | defer func() { 108 | if state := recover(); state != nil { 109 | err := log.Wrap(state.(error)) 110 | log.PrintErrorf(err, "Test failure.") 111 | t.Fatalf("Test failure.") 112 | } 113 | }() 114 | 115 | // Create the IB. 116 | 117 | im, err := exifcommon.NewIfdMappingWithStandard() 118 | log.PanicIf(err) 119 | 120 | ti := exif.NewTagIndex() 121 | 122 | err = exif.LoadStandardTags(ti) 123 | log.PanicIf(err) 124 | 125 | rootIb := exif.NewIfdBuilder(im, ti, exifcommon.IfdStandardIfdIdentity, exifcommon.EncodeDefaultByteOrder) 126 | 127 | err = rootIb.AddStandardWithName("ProcessingSoftware", "some software") 128 | log.PanicIf(err) 129 | 130 | // Encode. 131 | 132 | s := makeEmptyExifSegment() 133 | 134 | err = s.SetExif(rootIb) 135 | log.PanicIf(err) 136 | 137 | // Decode. 138 | 139 | rootIfd, _, err := s.Exif() 140 | log.PanicIf(err) 141 | 142 | results, err := rootIfd.FindTagWithName("ProcessingSoftware") 143 | log.PanicIf(err) 144 | 145 | ucIte := results[0] 146 | 147 | if ucIte.TagId() != 0x000b { 148 | t.Fatalf("tag-ID not correct") 149 | } 150 | 151 | recoveredValueRaw, err := ucIte.Value() 152 | log.PanicIf(err) 153 | 154 | recoveredValue := recoveredValueRaw.(string) 155 | if recoveredValue != "some software" { 156 | t.Fatalf("Value of tag not correct: [%s]", recoveredValue) 157 | } 158 | } 159 | 160 | func TestSegment_Exif(t *testing.T) { 161 | defer func() { 162 | if state := recover(); state != nil { 163 | err := log.Wrap(state.(error)) 164 | log.PrintErrorf(err, "Test failure.") 165 | t.Fatalf("Test failure.") 166 | } 167 | }() 168 | 169 | imageFilepath := GetTestImageFilepath() 170 | 171 | // Parse the image. 172 | 173 | jmp := NewJpegMediaParser() 174 | 175 | intfc, err := jmp.ParseFile(imageFilepath) 176 | log.PanicIf(err) 177 | 178 | sl := intfc.(*SegmentList) 179 | 180 | _, s, err := sl.FindExif() 181 | log.PanicIf(err) 182 | 183 | rootIfd, data, err := s.Exif() 184 | log.PanicIf(err) 185 | 186 | if rootIfd.IfdIdentity().Equals(exifcommon.IfdStandardIfdIdentity) != true { 187 | t.Fatalf("root IFD does not have correct identity") 188 | } 189 | 190 | exifFilepath := fmt.Sprintf("%s.just_exif", imageFilepath) 191 | 192 | expectedExifBytes, err := ioutil.ReadFile(exifFilepath) 193 | log.PanicIf(err) 194 | 195 | if bytes.Compare(data, expectedExifBytes) != 0 { 196 | t.Fatalf("exif data not correct") 197 | } 198 | } 199 | 200 | func TestSegment_IsExif_Hit(t *testing.T) { 201 | defer func() { 202 | if state := recover(); state != nil { 203 | err := log.Wrap(state.(error)) 204 | log.PrintErrorf(err, "Test failure.") 205 | t.Fatalf("Test failure.") 206 | } 207 | }() 208 | 209 | imageFilepath := GetTestImageFilepath() 210 | 211 | // Parse the image. 212 | 213 | jmp := NewJpegMediaParser() 214 | 215 | intfc, err := jmp.ParseFile(imageFilepath) 216 | log.PanicIf(err) 217 | 218 | sl := intfc.(*SegmentList) 219 | 220 | _, s, err := sl.FindExif() 221 | log.PanicIf(err) 222 | 223 | if s.IsExif() != true { 224 | t.Fatalf("Did not return true.") 225 | } 226 | } 227 | 228 | func TestSegment_IsExif_Miss(t *testing.T) { 229 | defer func() { 230 | if state := recover(); state != nil { 231 | err := log.Wrap(state.(error)) 232 | log.PrintErrorf(err, "Test failure.") 233 | t.Fatalf("Test failure.") 234 | } 235 | }() 236 | 237 | imageFilepath := GetTestImageFilepath() 238 | 239 | // Parse the image. 240 | 241 | jmp := NewJpegMediaParser() 242 | 243 | intfc, err := jmp.ParseFile(imageFilepath) 244 | log.PanicIf(err) 245 | 246 | sl := intfc.(*SegmentList) 247 | 248 | if sl.Segments()[4].IsExif() != false { 249 | t.Fatalf("Did not return false.") 250 | } 251 | } 252 | 253 | func TestSegment_IsXmp_Hit(t *testing.T) { 254 | defer func() { 255 | if state := recover(); state != nil { 256 | err := log.Wrap(state.(error)) 257 | log.PrintErrorf(err, "Test failure.") 258 | t.Fatalf("Test failure.") 259 | } 260 | }() 261 | 262 | imageFilepath := GetTestImageFilepath() 263 | 264 | // Parse the image. 265 | 266 | jmp := NewJpegMediaParser() 267 | 268 | intfc, err := jmp.ParseFile(imageFilepath) 269 | log.PanicIf(err) 270 | 271 | sl := intfc.(*SegmentList) 272 | 273 | _, s, err := sl.FindXmp() 274 | log.PanicIf(err) 275 | 276 | if s.IsXmp() != true { 277 | t.Fatalf("Did not return true.") 278 | } 279 | } 280 | 281 | func TestSegment_IsXmp_Miss(t *testing.T) { 282 | defer func() { 283 | if state := recover(); state != nil { 284 | err := log.Wrap(state.(error)) 285 | log.PrintErrorf(err, "Test failure.") 286 | t.Fatalf("Test failure.") 287 | } 288 | }() 289 | 290 | imageFilepath := GetTestImageFilepath() 291 | 292 | // Parse the image. 293 | 294 | jmp := NewJpegMediaParser() 295 | 296 | intfc, err := jmp.ParseFile(imageFilepath) 297 | log.PanicIf(err) 298 | 299 | sl := intfc.(*SegmentList) 300 | 301 | if sl.Segments()[4].IsXmp() != false { 302 | t.Fatalf("Did not return false.") 303 | } 304 | } 305 | 306 | func TestSegment_FormattedXmp(t *testing.T) { 307 | defer func() { 308 | if state := recover(); state != nil { 309 | err := log.Wrap(state.(error)) 310 | log.PrintErrorf(err, "Test failure.") 311 | t.Fatalf("Test failure.") 312 | } 313 | }() 314 | 315 | imageFilepath := GetTestImageFilepath() 316 | 317 | // Parse the image. 318 | 319 | jmp := NewJpegMediaParser() 320 | 321 | intfc, err := jmp.ParseFile(imageFilepath) 322 | log.PanicIf(err) 323 | 324 | sl := intfc.(*SegmentList) 325 | 326 | _, s, err := sl.FindXmp() 327 | log.PanicIf(err) 328 | 329 | actualData, err := s.FormattedXmp() 330 | log.PanicIf(err) 331 | 332 | // Filter out the Unicode BOM character since this would add unnecessary 333 | // complexity to the test. 334 | actualData = strings.ReplaceAll(actualData, "\ufeff", "") 335 | 336 | // Replace Windows-style newlines to Unix. 337 | actualData = strings.ReplaceAll(actualData, "\r\n", "\n") 338 | 339 | expectedData := ` 340 | 341 | 342 | 343 | 0 344 | 345 | 346 | 347 | 348 | ` 349 | 350 | if actualData != expectedData { 351 | t.Fatalf("XMP data is not correct:\nACTUAL:\n>>>%s<<<\n\nEXPECTED:\n>>>%s<<<\n", actualData, expectedData) 352 | } 353 | } 354 | -------------------------------------------------------------------------------- /v2/splitter.go: -------------------------------------------------------------------------------- 1 | package jpegstructure 2 | 3 | import ( 4 | "bufio" 5 | "bytes" 6 | "io" 7 | 8 | "encoding/binary" 9 | 10 | "github.com/dsoprea/go-logging" 11 | ) 12 | 13 | // JpegSplitter uses the Go stream splitter to divide the JPEG stream into 14 | // segments. 15 | type JpegSplitter struct { 16 | lastMarkerId byte 17 | lastMarkerName string 18 | counter int 19 | lastIsScanData bool 20 | visitor interface{} 21 | 22 | currentOffset int 23 | segments *SegmentList 24 | 25 | scandataOffset int 26 | } 27 | 28 | // NewJpegSplitter returns a new JpegSplitter. 29 | func NewJpegSplitter(visitor interface{}) *JpegSplitter { 30 | return &JpegSplitter{ 31 | segments: NewSegmentList(nil), 32 | visitor: visitor, 33 | } 34 | } 35 | 36 | // Segments returns all found segments. 37 | func (js *JpegSplitter) Segments() *SegmentList { 38 | return js.segments 39 | } 40 | 41 | // MarkerId returns the ID of the last processed marker. 42 | func (js *JpegSplitter) MarkerId() byte { 43 | return js.lastMarkerId 44 | } 45 | 46 | // MarkerName returns the name of the last-processed marker. 47 | func (js *JpegSplitter) MarkerName() string { 48 | return js.lastMarkerName 49 | } 50 | 51 | // Counter returns the number of processed segments. 52 | func (js *JpegSplitter) Counter() int { 53 | return js.counter 54 | } 55 | 56 | // IsScanData returns whether the last processed segment was scan-data. 57 | func (js *JpegSplitter) IsScanData() bool { 58 | return js.lastIsScanData 59 | } 60 | 61 | func (js *JpegSplitter) processScanData(data []byte) (advanceBytes int, err error) { 62 | defer func() { 63 | if state := recover(); state != nil { 64 | err = log.Wrap(state.(error)) 65 | } 66 | }() 67 | 68 | // Search through the segment, past all 0xff's therein, until we encounter 69 | // the EOI segment. 70 | 71 | dataLength := -1 72 | for i := js.scandataOffset; i < len(data); i++ { 73 | thisByte := data[i] 74 | 75 | if i == 0 { 76 | continue 77 | } 78 | 79 | lastByte := data[i-1] 80 | if lastByte != 0xff { 81 | continue 82 | } 83 | 84 | if thisByte == 0x00 || thisByte >= 0xd0 && thisByte <= 0xd8 { 85 | continue 86 | } 87 | 88 | // After all of the other checks, this means that we're on the EOF 89 | // segment. 90 | if thisByte != MARKER_EOI { 91 | continue 92 | } 93 | 94 | dataLength = i - 1 95 | break 96 | } 97 | 98 | if dataLength == -1 { 99 | // On the next pass, start on the last byte of this pass, just in case 100 | // the first byte of the two-byte sequence is here. 101 | js.scandataOffset = len(data) - 1 102 | 103 | jpegLogger.Debugf(nil, "Scan-data not fully available (%d).", len(data)) 104 | return 0, nil 105 | } 106 | 107 | js.lastIsScanData = true 108 | js.lastMarkerId = 0 109 | js.lastMarkerName = "" 110 | 111 | // Note that we don't increment the counter since this isn't an actual 112 | // segment. 113 | 114 | jpegLogger.Debugf(nil, "End of scan-data.") 115 | 116 | err = js.handleSegment(0x0, "!SCANDATA", 0x0, data[:dataLength]) 117 | log.PanicIf(err) 118 | 119 | return dataLength, nil 120 | } 121 | 122 | func (js *JpegSplitter) readSegment(data []byte) (count int, err error) { 123 | defer func() { 124 | if state := recover(); state != nil { 125 | err = log.Wrap(state.(error)) 126 | } 127 | }() 128 | 129 | if js.counter == 0 { 130 | // Verify magic bytes. 131 | 132 | if len(data) < 3 { 133 | jpegLogger.Debugf(nil, "Not enough (1)") 134 | return 0, nil 135 | } 136 | 137 | if data[0] == jpegMagic2000[0] && data[1] == jpegMagic2000[1] && data[2] == jpegMagic2000[2] { 138 | // TODO(dustin): Revisit JPEG2000 support. 139 | log.Panicf("JPEG2000 not supported") 140 | } 141 | 142 | if data[0] != jpegMagicStandard[0] || data[1] != jpegMagicStandard[1] || data[2] != jpegMagicStandard[2] { 143 | log.Panicf("file does not look like a JPEG: (%02x) (%02x) (%02x)", data[0], data[1], data[2]) 144 | } 145 | } 146 | 147 | chunkLength := len(data) 148 | 149 | jpegLogger.Debugf(nil, "SPLIT: LEN=(%d) COUNTER=(%d)", chunkLength, js.counter) 150 | 151 | if js.scanDataIsNext() == true { 152 | // If the last segment was the SOS, we're currently sitting on scan data. 153 | // Search for the EOI marker afterward in order to know how much data 154 | // there is. Return this as its own token. 155 | // 156 | // REF: https://stackoverflow.com/questions/26715684/parsing-jpeg-sos-marker 157 | 158 | advanceBytes, err := js.processScanData(data) 159 | log.PanicIf(err) 160 | 161 | // This will either return 0 and implicitly request that we need more 162 | // data and then need to run again or will return an actual byte count 163 | // to progress by. 164 | 165 | return advanceBytes, nil 166 | } else if js.lastMarkerId == MARKER_EOI { 167 | // We have more data following the EOI, which is unexpected. There 168 | // might be non-standard cruft at the end of the file. Terminate the 169 | // parse because the file-structure is, technically, complete at this 170 | // point. 171 | 172 | return 0, io.EOF 173 | } else { 174 | js.lastIsScanData = false 175 | } 176 | 177 | // If we're here, we're supposed to be sitting on the 0xff bytes at the 178 | // beginning of a segment (just before the marker). 179 | 180 | if data[0] != 0xff { 181 | log.Panicf("not on new segment marker @ (%d): (%02X)", js.currentOffset, data[0]) 182 | } 183 | 184 | i := 0 185 | found := false 186 | for ; i < chunkLength; i++ { 187 | jpegLogger.Debugf(nil, "Prefix check: (%d) %02X", i, data[i]) 188 | 189 | if data[i] != 0xff { 190 | found = true 191 | break 192 | } 193 | } 194 | 195 | jpegLogger.Debugf(nil, "Skipped over leading 0xFF bytes: (%d)", i) 196 | 197 | if found == false || i >= chunkLength { 198 | jpegLogger.Debugf(nil, "Not enough (3)") 199 | return 0, nil 200 | } 201 | 202 | markerId := data[i] 203 | 204 | js.lastMarkerName = markerNames[markerId] 205 | 206 | sizeLen, found := markerLen[markerId] 207 | jpegLogger.Debugf(nil, "MARKER-ID=%x SIZELEN=%v FOUND=%v", markerId, sizeLen, found) 208 | 209 | i++ 210 | 211 | b := bytes.NewBuffer(data[i:]) 212 | payloadLength := 0 213 | 214 | // marker-ID + size => 2 + 215 | headerSize := 2 + sizeLen 216 | 217 | if found == false { 218 | 219 | // It's not one of the static-length markers. Read the length. 220 | // 221 | // The length is an unsigned 16-bit network/big-endian. 222 | 223 | // marker-ID + size => 2 + 2 224 | headerSize = 2 + 2 225 | 226 | if i+2 >= chunkLength { 227 | jpegLogger.Debugf(nil, "Not enough (4)") 228 | return 0, nil 229 | } 230 | 231 | l := uint16(0) 232 | err = binary.Read(b, binary.BigEndian, &l) 233 | log.PanicIf(err) 234 | 235 | if l <= 2 { 236 | log.Panicf("length of size read for non-special marker (%02x) is unexpectedly not more than two.", markerId) 237 | } 238 | 239 | // (l includes the bytes of the length itself.) 240 | payloadLength = int(l) - 2 241 | jpegLogger.Debugf(nil, "DataLength (dynamically-sized segment): (%d)", payloadLength) 242 | 243 | i += 2 244 | } else if sizeLen > 0 { 245 | 246 | // Accommodates the non-zero markers in our marker index, which only 247 | // represent J2C extensions. 248 | // 249 | // The length is an unsigned 32-bit network/big-endian. 250 | 251 | // TODO(dustin): !! This needs to be tested, but we need an image. 252 | 253 | if sizeLen != 4 { 254 | log.Panicf("known non-zero marker is not four bytes, which is not currently handled: M=(%x)", markerId) 255 | } 256 | 257 | if i+4 >= chunkLength { 258 | jpegLogger.Debugf(nil, "Not enough (5)") 259 | return 0, nil 260 | } 261 | 262 | l := uint32(0) 263 | err = binary.Read(b, binary.BigEndian, &l) 264 | log.PanicIf(err) 265 | 266 | payloadLength = int(l) - 4 267 | jpegLogger.Debugf(nil, "DataLength (four-byte-length segment): (%u)", l) 268 | 269 | i += 4 270 | } 271 | 272 | jpegLogger.Debugf(nil, "PAYLOAD-LENGTH: %d", payloadLength) 273 | 274 | payload := data[i:] 275 | 276 | if payloadLength < 0 { 277 | log.Panicf("payload length less than zero: (%d)", payloadLength) 278 | } 279 | 280 | i += int(payloadLength) 281 | 282 | if i > chunkLength { 283 | jpegLogger.Debugf(nil, "Not enough (6)") 284 | return 0, nil 285 | } 286 | 287 | jpegLogger.Debugf(nil, "Found whole segment.") 288 | 289 | js.lastMarkerId = markerId 290 | 291 | payloadWindow := payload[:payloadLength] 292 | err = js.handleSegment(markerId, js.lastMarkerName, headerSize, payloadWindow) 293 | log.PanicIf(err) 294 | 295 | js.counter++ 296 | 297 | jpegLogger.Debugf(nil, "Returning advance of (%d)", i) 298 | 299 | return i, nil 300 | } 301 | 302 | func (js *JpegSplitter) scanDataIsNext() bool { 303 | return js.lastMarkerId == MARKER_SOS 304 | } 305 | 306 | // Split is the base splitting function that satisfies `bufio.SplitFunc`. 307 | func (js *JpegSplitter) Split(data []byte, atEOF bool) (advance int, token []byte, err error) { 308 | defer func() { 309 | if state := recover(); state != nil { 310 | err = log.Wrap(state.(error)) 311 | } 312 | }() 313 | 314 | for len(data) > 0 { 315 | currentAdvance, err := js.readSegment(data) 316 | if err != nil { 317 | if err == io.EOF { 318 | // We've encountered an EOI marker. 319 | return 0, nil, err 320 | } 321 | 322 | log.Panic(err) 323 | } 324 | 325 | if currentAdvance == 0 { 326 | if len(data) > 0 && atEOF == true { 327 | // Provide a little context in the error message. 328 | 329 | if js.scanDataIsNext() == true { 330 | // Yes, we've ran into this. 331 | 332 | log.Panicf("scan-data is unbounded; EOI not encountered before EOF") 333 | } else { 334 | log.Panicf("partial segment data encountered before scan-data") 335 | } 336 | } 337 | 338 | // We don't have enough data for another segment. 339 | break 340 | } 341 | 342 | data = data[currentAdvance:] 343 | advance += currentAdvance 344 | } 345 | 346 | return advance, nil, nil 347 | } 348 | 349 | func (js *JpegSplitter) parseSof(data []byte) (sof *SofSegment, err error) { 350 | defer func() { 351 | if state := recover(); state != nil { 352 | err = log.Wrap(state.(error)) 353 | } 354 | }() 355 | 356 | stream := bytes.NewBuffer(data) 357 | buffer := bufio.NewReader(stream) 358 | 359 | bitsPerSample, err := buffer.ReadByte() 360 | log.PanicIf(err) 361 | 362 | height := uint16(0) 363 | err = binary.Read(buffer, binary.BigEndian, &height) 364 | log.PanicIf(err) 365 | 366 | width := uint16(0) 367 | err = binary.Read(buffer, binary.BigEndian, &width) 368 | log.PanicIf(err) 369 | 370 | componentCount, err := buffer.ReadByte() 371 | log.PanicIf(err) 372 | 373 | sof = &SofSegment{ 374 | BitsPerSample: bitsPerSample, 375 | Width: width, 376 | Height: height, 377 | ComponentCount: componentCount, 378 | } 379 | 380 | return sof, nil 381 | } 382 | 383 | func (js *JpegSplitter) parseAppData(markerId byte, data []byte) (err error) { 384 | defer func() { 385 | if state := recover(); state != nil { 386 | err = log.Wrap(state.(error)) 387 | } 388 | }() 389 | 390 | return nil 391 | } 392 | 393 | func (js *JpegSplitter) handleSegment(markerId byte, markerName string, headerSize int, payload []byte) (err error) { 394 | defer func() { 395 | if state := recover(); state != nil { 396 | err = log.Wrap(state.(error)) 397 | } 398 | }() 399 | 400 | cloned := make([]byte, len(payload)) 401 | copy(cloned, payload) 402 | 403 | s := &Segment{ 404 | MarkerId: markerId, 405 | MarkerName: markerName, 406 | Offset: js.currentOffset, 407 | Data: cloned, 408 | } 409 | 410 | jpegLogger.Debugf(nil, "Encountered marker (0x%02x) [%s] at offset (%d)", markerId, markerName, js.currentOffset) 411 | 412 | js.currentOffset += headerSize + len(payload) 413 | 414 | js.segments.Add(s) 415 | 416 | sv, ok := js.visitor.(SegmentVisitor) 417 | if ok == true { 418 | err = sv.HandleSegment(js.lastMarkerId, js.lastMarkerName, js.counter, js.lastIsScanData) 419 | log.PanicIf(err) 420 | } 421 | 422 | if markerId >= MARKER_SOF0 && markerId <= MARKER_SOF15 { 423 | ssv, ok := js.visitor.(SofSegmentVisitor) 424 | if ok == true { 425 | sof, err := js.parseSof(payload) 426 | log.PanicIf(err) 427 | 428 | err = ssv.HandleSof(sof) 429 | log.PanicIf(err) 430 | } 431 | } else if markerId >= MARKER_APP0 && markerId <= MARKER_APP15 { 432 | err := js.parseAppData(markerId, payload) 433 | log.PanicIf(err) 434 | } 435 | 436 | return nil 437 | } 438 | -------------------------------------------------------------------------------- /v2/splitter_test.go: -------------------------------------------------------------------------------- 1 | package jpegstructure 2 | 3 | import ( 4 | "bufio" 5 | "bytes" 6 | "os" 7 | "reflect" 8 | "testing" 9 | 10 | "github.com/dsoprea/go-logging" 11 | ) 12 | 13 | type collectorVisitor struct { 14 | markerList []byte 15 | sofList []SofSegment 16 | } 17 | 18 | func (v *collectorVisitor) HandleSegment(lastMarkerId byte, lastMarkerName string, counter int, lastIsScanData bool) (err error) { 19 | defer func() { 20 | if state := recover(); state != nil { 21 | err = log.Wrap(state.(error)) 22 | } 23 | }() 24 | 25 | v.markerList = append(v.markerList, lastMarkerId) 26 | 27 | return nil 28 | } 29 | 30 | func (v *collectorVisitor) HandleSof(sof *SofSegment) (err error) { 31 | defer func() { 32 | if state := recover(); state != nil { 33 | err = log.Wrap(state.(error)) 34 | } 35 | }() 36 | 37 | v.sofList = append(v.sofList, *sof) 38 | 39 | return nil 40 | } 41 | 42 | func Test_JpegSplitter_Split(t *testing.T) { 43 | defer func() { 44 | if state := recover(); state != nil { 45 | err := log.Wrap(state.(error)) 46 | log.PrintErrorf(err, "Test failure.") 47 | t.Fatalf("Test failure.") 48 | } 49 | }() 50 | 51 | filepath := GetTestImageFilepath() 52 | 53 | f, err := os.Open(filepath) 54 | log.PanicIf(err) 55 | 56 | defer f.Close() 57 | 58 | stat, err := f.Stat() 59 | log.PanicIf(err) 60 | 61 | size := stat.Size() 62 | 63 | v := new(collectorVisitor) 64 | js := NewJpegSplitter(v) 65 | 66 | s := bufio.NewScanner(f) 67 | 68 | // Since each segment can be any size, our buffer must allowed to grow as 69 | // large as the file. 70 | buffer := []byte{} 71 | s.Buffer(buffer, int(size)) 72 | 73 | s.Split(js.Split) 74 | 75 | for s.Scan() != false { 76 | } 77 | 78 | if s.Err() != nil { 79 | log.PrintError(s.Err()) 80 | t.Fatalf("error while scanning: %v", s.Err()) 81 | } 82 | 83 | expectedMarkers := []byte{0xd8, 0xe1, 0xe1, 0xdb, 0xc0, 0xc4, 0xda, 0x00, 0xd9} 84 | 85 | if bytes.Compare(v.markerList, expectedMarkers) != 0 { 86 | t.Fatalf("Markers found are not correct: %v\n", DumpBytesToString(v.markerList)) 87 | } 88 | 89 | expectedSofList := []SofSegment{ 90 | { 91 | BitsPerSample: 8, 92 | Width: 3840, 93 | Height: 2560, 94 | ComponentCount: 3, 95 | }, 96 | { 97 | BitsPerSample: 0, 98 | Width: 1281, 99 | Height: 1, 100 | ComponentCount: 1, 101 | }, 102 | } 103 | 104 | if reflect.DeepEqual(v.sofList, expectedSofList) == false { 105 | t.Fatalf("SOF segments not equal: %v\n", v.sofList) 106 | } 107 | } 108 | -------------------------------------------------------------------------------- /v2/testing_common.go: -------------------------------------------------------------------------------- 1 | package jpegstructure 2 | 3 | import ( 4 | "os" 5 | "path" 6 | 7 | "github.com/dsoprea/go-logging" 8 | ) 9 | 10 | var ( 11 | testImageRelFilepath = "NDM_8901.jpg" 12 | ) 13 | 14 | var ( 15 | moduleRootPath = "" 16 | assetsPath = "" 17 | ) 18 | 19 | // GetModuleRootPath returns the root-path of the module. 20 | func GetModuleRootPath() string { 21 | if moduleRootPath == "" { 22 | moduleRootPath = os.Getenv("JPEG_MODULE_ROOT_PATH") 23 | if moduleRootPath != "" { 24 | return moduleRootPath 25 | } 26 | 27 | currentWd, err := os.Getwd() 28 | log.PanicIf(err) 29 | 30 | currentPath := currentWd 31 | visited := make([]string, 0) 32 | 33 | for { 34 | tryStampFilepath := path.Join(currentPath, ".MODULE_ROOT") 35 | 36 | _, err := os.Stat(tryStampFilepath) 37 | if err != nil && os.IsNotExist(err) != true { 38 | log.Panic(err) 39 | } else if err == nil { 40 | break 41 | } 42 | 43 | visited = append(visited, tryStampFilepath) 44 | 45 | currentPath = path.Dir(currentPath) 46 | if currentPath == "/" { 47 | log.Panicf("could not find module-root: %v", visited) 48 | } 49 | } 50 | 51 | moduleRootPath = currentPath 52 | } 53 | 54 | return moduleRootPath 55 | } 56 | 57 | // GetTestAssetsPath returns the path of the test-assets. 58 | func GetTestAssetsPath() string { 59 | if assetsPath == "" { 60 | moduleRootPath := GetModuleRootPath() 61 | assetsPath = path.Join(moduleRootPath, "assets") 62 | } 63 | 64 | return assetsPath 65 | } 66 | 67 | // GetTestImageFilepath returns the file-path of the common test-image. 68 | func GetTestImageFilepath() string { 69 | assetsPath := GetTestAssetsPath() 70 | filepath := path.Join(assetsPath, testImageRelFilepath) 71 | 72 | return filepath 73 | } 74 | -------------------------------------------------------------------------------- /v2/utility.go: -------------------------------------------------------------------------------- 1 | package jpegstructure 2 | 3 | import ( 4 | "bytes" 5 | "fmt" 6 | "sort" 7 | "strings" 8 | 9 | "github.com/dsoprea/go-logging" 10 | "github.com/go-xmlfmt/xmlfmt" 11 | ) 12 | 13 | // DumpBytes prints the hex for a given byte-slice. 14 | func DumpBytes(data []byte) { 15 | fmt.Printf("DUMP: ") 16 | for _, x := range data { 17 | fmt.Printf("%02x ", x) 18 | } 19 | 20 | fmt.Printf("\n") 21 | } 22 | 23 | // DumpBytesClause prints a Go-formatted byte-slice expression. 24 | func DumpBytesClause(data []byte) { 25 | fmt.Printf("DUMP: ") 26 | 27 | fmt.Printf("[]byte { ") 28 | 29 | for i, x := range data { 30 | fmt.Printf("0x%02x", x) 31 | 32 | if i < len(data)-1 { 33 | fmt.Printf(", ") 34 | } 35 | } 36 | 37 | fmt.Printf(" }\n") 38 | } 39 | 40 | // DumpBytesToString returns a string of hex-encoded bytes. 41 | func DumpBytesToString(data []byte) string { 42 | b := new(bytes.Buffer) 43 | 44 | for i, x := range data { 45 | _, err := b.WriteString(fmt.Sprintf("%02x", x)) 46 | log.PanicIf(err) 47 | 48 | if i < len(data)-1 { 49 | _, err := b.WriteRune(' ') 50 | log.PanicIf(err) 51 | } 52 | } 53 | 54 | return b.String() 55 | } 56 | 57 | // DumpBytesClauseToString returns a string of Go-formatted byte values. 58 | func DumpBytesClauseToString(data []byte) string { 59 | b := new(bytes.Buffer) 60 | 61 | for i, x := range data { 62 | _, err := b.WriteString(fmt.Sprintf("0x%02x", x)) 63 | log.PanicIf(err) 64 | 65 | if i < len(data)-1 { 66 | _, err := b.WriteString(", ") 67 | log.PanicIf(err) 68 | } 69 | } 70 | 71 | return b.String() 72 | } 73 | 74 | // FormatXml prettifies XML data. 75 | func FormatXml(raw string) (formatted string, err error) { 76 | defer func() { 77 | if state := recover(); state != nil { 78 | err = log.Wrap(state.(error)) 79 | } 80 | }() 81 | 82 | formatted = xmlfmt.FormatXML(raw, " ", " ") 83 | formatted = strings.TrimSpace(formatted) 84 | 85 | return formatted, nil 86 | } 87 | 88 | // SortStringStringMap sorts a string-string dictionary and returns it as a list 89 | // of 2-tuples. 90 | func SortStringStringMap(data map[string]string) (sorted [][2]string) { 91 | // Sort keys. 92 | 93 | sortedKeys := make([]string, len(data)) 94 | i := 0 95 | for key := range data { 96 | sortedKeys[i] = key 97 | i++ 98 | } 99 | 100 | sort.Strings(sortedKeys) 101 | 102 | // Build result. 103 | 104 | sorted = make([][2]string, len(sortedKeys)) 105 | for i, key := range sortedKeys { 106 | sorted[i] = [2]string{key, data[key]} 107 | } 108 | 109 | return sorted 110 | } 111 | --------------------------------------------------------------------------------