Skip to content

Commit

Permalink
Rework implementation to use exiftool as the facit
Browse files Browse the repository at this point in the history
  • Loading branch information
bep committed Jul 10, 2024
1 parent e2e8168 commit 294e125
Show file tree
Hide file tree
Showing 180 changed files with 11,159 additions and 444 deletions.
Binary file added .DS_Store
Binary file not shown.
26 changes: 24 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,29 @@
[![Go Report Card](https://goreportcard.com/badge/github.com/bep/imagemeta)](https://goreportcard.com/report/github.com/bep/imagemeta)
[![GoDoc](https://godoc.org/github.com/bep/imagemeta?status.svg)](https://godoc.org/github.com/bep/imagemeta)

# Work in progress

This is not ready, yet, but the goal is to use this as the library to read image metadata in Hugo. The existing Go libraries I have found is not maintained anymore and too complex to build on top of. The goal of this library is to be relatively small and simple, but I may prove myself wrong.
## When in doubt, Exiftools is right

The output of this library is tested against `exiftool -n -json`. This means, for example, that:

* We use f-numbers and not APEX for aperture values.
* We use seconds and not APEX for shutter speed values.
* EXIF field definitions are fetched from this table: https://exiftool.org/TagNames/EXIF.html
* IPTC field definitions are fetched from this table: https://exiftool.org/TagNames/IPTC.html
* The XMP handling is currently very simple, you can supply your own XMP handler (see the `HandleXMP` option) if you need more.


## Development

Many of the tests depends on generated golden files. To update these, run:

```bash
go generate ./gen
```

Note that you need a working `exiftool` and updated binary in your `PATH` for this to work. This was tested OK with:

```
exiftool -ver
12.76
```
1 change: 0 additions & 1 deletion exif_fields.go

This file was deleted.

35 changes: 35 additions & 0 deletions exiftype_string.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

53 changes: 53 additions & 0 deletions gen/main.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
//go:generate go run main.go
package main

import (
"bytes"
"log"
"os"
"os/exec"
"path/filepath"
"strings"
)

func main() {
outDir := "testdata_exiftool"
os.RemoveAll(outDir)
if err := os.MkdirAll(outDir, 0o755); err != nil {
log.Fatal(err)
}
base := "../testdata"

if err := filepath.Walk(base, func(path string, info os.FileInfo, err error) error {
if err != nil {
return err
}
if info.IsDir() || strings.HasPrefix(info.Name(), ".") {
return nil
}

basePath := strings.TrimPrefix(path, base)

var buf bytes.Buffer
cmd := exec.Command("exiftool", path, "-json", "-n", "-g", "-e")
cmd.Stdout = &buf
cmd.Stderr = os.Stderr

if err := cmd.Run(); err != nil {
return err
}

outFilename := filepath.Join(outDir, basePath+".json")
if err := os.MkdirAll(filepath.Dir(outFilename), 0o755); err != nil {
return err
}

if err := os.WriteFile(outFilename, buf.Bytes(), 0o644); err != nil {
return err
}

return nil
}); err != nil {
log.Fatal(err)
}
}
232 changes: 232 additions & 0 deletions gen/testdata_exiftool/IPTC-PhotometadataRef-Std2021.1.jpg.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,232 @@
[{
"SourceFile": "../testdata/IPTC-PhotometadataRef-Std2021.1.jpg",
"ExifTool": {
"ExifToolVersion": 12.76
},
"File": {
"FileName": "IPTC-PhotometadataRef-Std2021.1.jpg",
"Directory": "../testdata",
"FileSize": 134078,
"FileModifyDate": "2023:08:14 17:57:27+02:00",
"FileAccessDate": "2024:07:09 18:54:23+02:00",
"FileInodeChangeDate": "2024:07:08 15:16:12+02:00",
"FilePermissions": 100644,
"FileType": "JPEG",
"FileTypeExtension": "JPG",
"MIMEType": "image/jpeg",
"ExifByteOrder": "MM",
"CurrentIPTCDigest": "ed3d9bf1276b54654a9169c8c1e2c081",
"Comment": "IPTC Photo Metadata Standard 2021.1 reference image file - release 2b, as of 2022-07-12",
"ImageWidth": 2000,
"ImageHeight": 1000,
"EncodingProcess": 2,
"BitsPerSample": 8,
"ColorComponents": 3,
"YCbCrSubSampling": "2 2"
},
"EXIF": {
"ImageDescription": "The description aka caption (ref2021.1)",
"XResolution": 72,
"YResolution": 72,
"ResolutionUnit": 2,
"Artist": "Creator1 (ref2021.1)",
"YCbCrPositioning": 1,
"Copyright": "Copyright (Notice) 2021.1 IPTC - www.iptc.org (ref2021.1)",
"ExifVersion": "0232",
"DateTimeOriginal": "2021:10:20 21:01:01",
"OffsetTimeOriginal": "+00:00",
"ComponentsConfiguration": "1 2 3 0",
"FlashpixVersion": "0100",
"ColorSpace": 65535
},
"IPTC": {
"ObjectAttributeReference": "000:Actuality",
"ObjectName": "The Title (ref2021.1)",
"SubjectReference": ["IPTC:10020211","IPTC:20020211","IPTC:30020211"],
"Keywords": ["Keyword1ref2021.1","Keyword2ref2021.1","Keyword3ref2021.1"],
"SpecialInstructions": "An Instruction (ref2021.1)",
"DateCreated": "2021:10:20",
"TimeCreated": "21:01:01+00:00",
"By-line": "Creator1 (ref2021.1)",
"By-lineTitle": "Creator's Job Title (ref2021.1)",
"City": "City (Core) (ref2021.1)",
"Sub-location": "Sublocation (Core) (ref2021.1)",
"Province-State": "Province/State(Core)(ref2021.1)",
"Country-PrimaryLocationCode": "R21",
"Country-PrimaryLocationName": "Country (Core) (ref2021.1)",
"OriginalTransmissionReference": "Job Id (ref2021.1)",
"Headline": "The Headline (ref2021.1)",
"Credit": "Credit Line (ref2021.1)",
"Source": "Source (ref2021.1)",
"CopyrightNotice": "Copyright (Notice) 2021.1 IPTC - www.iptc.org (ref2021.1)",
"Caption-Abstract": "The description aka caption (ref2021.1)",
"Writer-Editor": "Description Writer (ref2021.1)",
"ApplicationRecordVersion": 4
},
"XMP": {
"XMPToolkit": "Image::ExifTool 12.41",
"AltTextAccessibility": "This is the Alt Text description to support accessibility in 2021.1",
"AltTextAccessibility-en": "This is the Alt Text description to support accessibility in 2021.1",
"CountryCode": "R21",
"CreatorCity": "Creator's CI: City (ref2021.1)",
"CreatorCountry": "Creator's CI: Country (ref2021.1)",
"CreatorAddress": "Creator's CI: Address, line 1 (ref2021.1)",
"CreatorPostalCode": "Creator's CI: Postcode (ref2021.1)",
"CreatorRegion": "Creator's CI: State/Province (ref2021.1)",
"CreatorWorkEmail": "Creator's CI: Email@1, Email@2 (ref2021.1)",
"CreatorWorkTelephone": "Creator's CI: Phone # 1, Phone # 2 (ref2021.1)",
"CreatorWorkURL": "http://www.Creators.CI/WebAddress/ref2021.1",
"ExtDescrAccessibility": "This is the Extended Description to support accessibility in 2021.1",
"ExtDescrAccessibility-en": "This is the Extended Description to support accessibility in 2021.1",
"IntellectualGenre": "Actuality",
"Location": "Sublocation (Core) (ref2021.1)",
"Scene": ["010100","010400"],
"SubjectCode": [10020211,20020211,30020211],
"AboutCvTermCvId": "http://example.com/cv/about/ref2021.1",
"AboutCvTermId": "http://example.com/cv/about/ref2021.1/code987",
"AboutCvTermName": "CV-Term Name 1 (ref2021.1)",
"AboutCvTermRefinedAbout": "http://example.com/cv/refinements2/ref2021.1/codeX145",
"AdditionalModelInformation": "Additional Model Info (ref2021.1)",
"ArtworkCircaDateCreated": "AO Circa Date: between 1550 and 1600 (ref2021.1)",
"ArtworkContentDescription": "AO Content Description 1 (ref2021.1)",
"ArtworkContributionDescription": "AO Contribution Description 1 (ref2021.1)",
"ArtworkCopyrightNotice": "AO Copyright Notice 1 (ref2021.1)",
"ArtworkCreator": ["AO Creator Name 1a (ref2021.1)","AO Creator Name 1b (ref2021.1)"],
"ArtworkCreatorID": ["AO Creator Id 1a (ref2021.1)","AO Creator Id 1b (ref2021.1)"],
"ArtworkCopyrightOwnerID": "AO Current Copyright Owner ID 1 (ref2021.1)",
"ArtworkCopyrightOwnerName": "AO Current Copyright Owner Name 1 (ref2021.1)",
"ArtworkLicensorID": "AO Current Licensor ID 1 (ref2021.1)",
"ArtworkLicensorName": "AO Current Licensor Name 1 (ref2021.1)",
"ArtworkDateCreated": "1921:10:20 21:01:00+00:00",
"ArtworkPhysicalDescription": "AO Physical Description 1 (ref2021.1)",
"ArtworkSource": "AO Source 1 (ref2021.1)",
"ArtworkSourceInventoryNo": "AO Source Inventory No 1 (ref2021.1)",
"ArtworkSourceInvURL": "AO Source Inventory URL (ref2021.1)",
"ArtworkStylePeriod": ["AO Style Baroque (ref2021.1)","AO Style Italian Baroque (ref2021.1)"],
"ArtworkTitle": "AO Title 1 (ref2021.1)",
"DigitalImageGUID": "http://example.com/imageGUIDs/TestGUID12345/ref2021.1",
"DigitalSourceType": "http://cv.iptc.org/newscodes/digitalsourcetype/softwareImage",
"EmbeddedEncodedRightsExpr": "The Encoded Rights Expression (ref2021.1)",
"EmbeddedEncodedRightsExprType": "IANA Media Type of ERE (ref2021.1)",
"EmbeddedEncodedRightsExprLangID": "http://example.org/RELids/id4711/ref2021.1",
"Event": "Event Name (ref2021.1)",
"EventID": ["https://example.com/events/id2021.1a","https://example.com/events/id2021.1b"],
"GenreCvId": "http://example.com/cv/genre/ref2021.1",
"GenreCvTermId": "http://example.com/cv/genre/ref2021.1/code1369",
"GenreCvTermName": "Genre CV-Term Name 1 (ref2021.1)",
"GenreCvTermRefinedAbout": "http://example.com/cv/genrerefinements2/ref2021.1/codeY864",
"ImageRegionName": ["Listener 1","Listener 2","Speaker 1"],
"ImageRegionOrganisationInImageName": ["Organisation name no 1 in region persltr2 (ref2021.1)","Organisation name no 1 in region persltr2 (ref2021.1)","Organisation name no 1 in region persltr3 (ref2021.1)"],
"ImageRegionPersonInImage": ["Person name no 1 in region persltr2 (ref2021.1)","Person name no 1 in region persltr3 (ref2021.1)","Person name no 1 in region persltr1 (ref2021.1)"],
"ImageRegionBoundaryH": 0.385,
"ImageRegionBoundaryShape": ["rectangle","circle","polygon"],
"ImageRegionBoundaryUnit": ["relative","relative","relative"],
"ImageRegionBoundaryW": 0.127,
"ImageRegionBoundaryX": [0.31,0.59],
"ImageRegionBoundaryY": [0.18,0.426],
"ImageRegionCtypeName": ["Region Boundary Content Type Name (ref2021.1)","Region Boundary Content Type Name (ref2021.1)","Region Boundary Content Type Name (ref2021.1)"],
"ImageRegionCtypeIdentifier": ["https://example.org/rctype/type2021.1a","https://example.org/rctype/type2021.1b","https://example.org/rctype/type2021.1a","https://example.org/rctype/type2021.1b","https://example.org/rctype/type2021.1a","https://example.org/rctype/type2021.1b"],
"ImageRegionID": ["persltr2","persltr3","persltr1"],
"ImageRegionRoleName": ["Region Boundary Content Role Name (ref2021.1)","Region Boundary Content Role Name (ref2021.1)","Region Boundary Content Role Name (ref2021.1)"],
"ImageRegionRoleIdentifier": ["https://example.org/rrole/role2021.1a","https://example.org/rrole/role2021.1b","https://example.org/rrole/role2021.1a","https://example.org/rrole/role2021.1b","https://example.org/rrole/role2021.1a","https://example.org/rrole/role2021.1b"],
"ImageRegionBoundaryRx": 0.068,
"ImageRegionBoundaryVerticesX": [0.05,0.148,0.375],
"ImageRegionBoundaryVerticesY": [0.713,0.041,0.863],
"LinkedEncodedRightsExpr": "http://example.org/linkedrightsexpression/id986/ref2021.1",
"LinkedEncodedRightsExprType": "IANA Media Type of ERE (ref2021.1)",
"LinkedEncodedRightsExprLangID": "http://example.org/RELids/id4712/ref2021.1",
"LocationCreatedCity": "City (Location created1) (ref2021.1)",
"LocationCreatedCountryCode": "R17",
"LocationCreatedCountryName": "CountryName (Location created1) (ref2021.1)",
"LocationCreatedLocationId": "Location Id (Location created1) (ref2021.1)",
"LocationCreatedLocationName": "Location Name (Location created1) (ref2021.1)",
"LocationCreatedProvinceState": "Province/State (Location created1) (ref2021.1)",
"LocationCreatedSublocation": "Sublocation (Location created1) (ref2021.1)",
"LocationCreatedWorldRegion": "Worldregion (Location created1) (ref2021.1)",
"LocationCreatedGPSAltitude": 480,
"LocationCreatedGPSLatitude": 48.275,
"LocationCreatedGPSLongitude": 16.338,
"LocationShownCity": ["City (Location shown1) (ref2021.1)","City (Location shown2) (ref2021.1)"],
"LocationShownCountryCode": ["R17","R17"],
"LocationShownCountryName": ["CountryName (Location shown1) (ref2021.1)","CountryName (Location shown2) (ref2021.1)"],
"LocationShownLocationId": ["Location Id 1a(Location shown1) (ref2021.1)","Location Id 1b(Location shown1) (ref2021.1)","Location Id 2a(Location shown2) (ref2021.1)","Location Id 2b(Location shown2) (ref2021.1)"],
"LocationShownLocationName": ["Location Name (Location shown1) (ref2021.1)","Location Name (Location shown2) (ref2021.1)"],
"LocationShownProvinceState": ["Province/State (Location shown1) (ref2021.1)","Province/State (Location shown2) (ref2021.1)"],
"LocationShownSublocation": ["Sublocation (Location shown1) (ref2021.1)","Sublocation (Location shown2) (ref2021.1)"],
"LocationShownWorldRegion": ["Worldregion (Location shown1) (ref2021.1)","Worldregion (Location shown2) (ref2021.1)"],
"LocationShownGPSAltitude": [140,120],
"LocationShownGPSLatitude": [48.147,47.952],
"LocationShownGPSLongitude": [17.098,16.83],
"MaxAvailHeight": 20,
"MaxAvailWidth": 21,
"ModelAge": [25,27,30],
"OrganisationInImageCode": ["Organisation Code 1 (ref2021.1)","Organisation Code 2 (ref2021.1)","Organisation Code 3 (ref2021.1)"],
"OrganisationInImageName": ["Organisation Name 1 (ref2021.1)","Organisation Name 2 (ref2021.1)","Organisation Name 3 (ref2021.1)"],
"PersonInImage": ["Person Shown 1 (ref2021.1)","Person Shown 2 (ref2021.1)"],
"PersonInImageCvTermCvId": "http://example.com/cv/test99/ref2021.1",
"PersonInImageCvTermId": "http://example.com/cv/test99/code987/ref2021.1",
"PersonInImageCvTermName": "Person Characteristic Name 1 (ref2021.1)",
"PersonInImageCvTermRefinedAbout": "http://example.com/cv/refinements987/codeY765/ref2021.1",
"PersonInImageDescription": "Person Description 1 (ref2021.1)",
"PersonInImageId": ["http://wikidata.org/item/Q123456789/ref2021.1","http://freebase.com/m/987654321/ref2021.1"],
"PersonInImageName": "Person Name 1 (ref2021.1)",
"ProductInImageDescription": "Product Description 1 (ref2021.1)",
"ProductInImageGTIN": 123456782021.1,
"ProductInImageName": "Product Name 1 (ref2021.1)",
"RegistryEntryRole": ["Registry Entry Role ID 1 (ref2021.1)","Registry Entry Role ID 2 (ref2021.1)"],
"RegistryItemID": ["Registry Image ID 1 (ref2021.1)","Registry Image ID 2 (ref2021.1)"],
"RegistryOrganisationID": ["Registry Organisation ID 1 (ref2021.1)","Registry Organisation ID 2 (ref2021.1)"],
"Creator": "Creator1 (ref2021.1)",
"Description": "The description aka caption (ref2021.1)",
"Rights": "Copyright (Notice) 2021.1 IPTC - www.iptc.org (ref2021.1)",
"Subject": ["Keyword1ref2021.1","Keyword2ref2021.1","Keyword3ref2021.1"],
"Title": "The Title (ref2021.1)",
"AuthorsPosition": "Creator's Job Title (ref2021.1)",
"CaptionWriter": "Description Writer (ref2021.1)",
"City": "City (Core) (ref2021.1)",
"Country": "Country (Core) (ref2021.1)",
"Credit": "Credit Line (ref2021.1)",
"DateCreated": "2021:10:20 21:01:01+00:00",
"Headline": "The Headline (ref2021.1)",
"Instructions": "An Instruction (ref2021.1)",
"Source": "Source (ref2021.1)",
"State": "Province/State(Core)(ref2021.1)",
"TransmissionReference": "Job Id (ref2021.1)",
"CopyrightOwnerID": ["Copyright Owner Id 1 (ref2021.1)","Copyright Owner Id 2 (ref2021.1)"],
"CopyrightOwnerName": ["Copyright Owner Name 1 (ref2021.1)","Copyright Owner Name 2 (ref2021.1)"],
"ImageCreatorID": "Image Creator Id 1 (ref2021.1)",
"ImageCreatorName": "Image Creator Name 1 (ref2021.1)",
"ImageCreatorImageID": "Image Creator Image ID (ref2021.1)",
"ImageSupplierID": "Image Supplier Id (ref2021.1)",
"ImageSupplierName": "Image Supplier Name (ref2021.1)",
"ImageSupplierImageID": "Image Supplier Image ID (ref2021.1)",
"LicensorCity": ["Licensor City 1 (ref2021.1)","Licensor City 2 (ref2021.1)"],
"LicensorCountry": ["Licensor Country 1 (ref2021.1)","Licensor Country 2 (ref2021.1)"],
"LicensorEmail": ["Licensor Email 1 (ref2021.1)","Licensor Email 2 (ref2021.1)"],
"LicensorExtendedAddress": ["Licensor Ext Addr 1 (ref2021.1)","Licensor Ext Addr 2 (ref2021.1)"],
"LicensorID": ["Licensor ID 1 (ref2021.1)","Licensor ID 2 (ref2021.1)"],
"LicensorName": ["Licensor Name 1 (ref2021.1)","Licensor Name 2 (ref2021.1)"],
"LicensorPostalCode": ["Licensor Postcode 1 (ref2021.1)","Licensor Postcode 2 (ref2021.1)"],
"LicensorRegion": ["Licensor Region 1 (ref2021.1)","Licensor Region 2 (ref2021.1)"],
"LicensorStreetAddress": ["Licensor Street Addr 1 (ref2021.1)","Licensor Street Addr 2 (ref2021.1)"],
"LicensorTelephone1": ["Licensor Phone1 1 (ref2021.1)","Licensor Phone1 2 (ref2021.1)"],
"LicensorTelephone2": ["Licensor Phone2 1 (ref2021.1)","Licensor Phone2 2 (ref2021.1)"],
"LicensorTelephoneType1": ["work","work"],
"LicensorTelephoneType2": ["cell","cell"],
"LicensorURL": ["https://example.com/LicensorURL_1_ref2021.1","https://example.com/LicensorURL_2_ref2021.1"],
"MinorModelAgeDisclosure": "AG-A25",
"ModelReleaseID": ["Model Release ID 1 (ref2021.1)","Model Release ID 2 (ref2021.1)"],
"ModelReleaseStatus": "MR-NAP",
"PropertyReleaseID": ["Property Release ID 1 (ref2021.1)","Property Release ID 2 (ref2021.1)"],
"PropertyReleaseStatus": "PR-NAP",
"Rating": 1.0,
"UsageTerms": "Rights Usage Terms (ref2021.1)",
"WebStatement": "https://example.com/WebStatementOfRights/2021.1"
},
"APP14": {
"DCTEncodeVersion": 100,
"APP14Flags0": 32768,
"APP14Flags1": 0,
"ColorTransform": 1
}
}]
25 changes: 25 additions & 0 deletions gen/testdata_exiftool/corrupt/huge_tag_exif.jpg.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
[{
"SourceFile": "../testdata/corrupt/huge_tag_exif.jpg",
"ExifTool": {
"ExifToolVersion": 12.76,
"Warning": "JPEG format error"
},
"File": {
"FileName": "huge_tag_exif.jpg",
"Directory": "../testdata/corrupt",
"FileSize": 65536,
"FileModifyDate": "2023:08:14 17:57:27+02:00",
"FileAccessDate": "2024:07:09 18:54:23+02:00",
"FileInodeChangeDate": "2023:08:14 17:57:27+02:00",
"FilePermissions": 100644,
"FileType": "JPEG",
"FileTypeExtension": "JPG",
"MIMEType": "image/jpeg"
},
"JFIF": {
"JFIFVersion": "1 1",
"ResolutionUnit": 1,
"XResolution": 72,
"YResolution": 72
}
}]
Loading

0 comments on commit 294e125

Please sign in to comment.