Repository: python-pillow/Pillow

Issue:
Enhance EXIF handling
The `getexif()` method of `PIL.Image` could use some enhancements, which most likely require an API bump. I'm just listing some ideas/shortcomings of the current implementation here, which I would love to see addressed (and could possibly contribute to, if the changes are agreed upon).

- [ ] The return type of `getexif()` is `Exif`. But without further processing, it's really only IFD0. However, already returning an IFD makes the `get_ifd()` method look out of place. Therefore, I recommend to implement the `Exif` class as a dict linking to all extracted IFDs.
- [x] Either providing specialized methods for common IFDs (e.g. `Exif.get_ifd0()`) or named constants for common IFDs (e.g. `Exif.get_ifd(ExifTags.IFD.IFD0)`) would improve the usage of the API a lot. (#6748)
- [x] Implement tag names for missing IFDs (especially IFD1 and Interop) (#6724)
- [x] Some tags are purely technical and should never be edited by hand (the IFD offsets come to mind here). It would be nice to have a flag not to return them to the user. (#6762)
- [x] Some integer tag values encode a human readable description, for example Flash and LightSource. It would be great to provide named constants for those as well as methods to return the description as a string. (#6749 for LightSource)

I can't say anything about the feasability regarding licencing, but maybe the tag names could be scraped from the Exiftool website (the quasi reference implementation of the Exif format): https://exiftool.org/TagNames/EXIF.html

There are already other python libraries for Exif handling, but they all suffer from fragmentation. As the Pillow library already does the heavy lifting of extracting the IFDs anyway, I think providing a convenient and nearly complete view of the data can be a significant improvement to the project.


Discussion:
> The return type of getexif() is Exif. But without further processing, it's really only IFD0. However, already returning an IFD makes the get_ifd() method look out of place. Therefore, I recommend to implement the Exif class as a dict linking to all extracted IFDs.

Isn't this just part of the unfortunate reality that https://exiftool.org/TagNames/EXIF.html lists EXIF tags, and one of the groups listed is an ExifIFD? I would have thought the names were inherently confusing.

> Either providing specialized methods for common IFDs (e.g. Exif.get_ifd0()) or named constants for common IFDs (e.g. Exif.get_ifd(ExifTags.IFD.IFD0)) would improve the usage of the API a lot.

I'm not opposed to the idea of named constants. I'd want to see what happens with #6630 first.

> Implement tag names for missing IFDs (especially IFD1 and Interop)

You mean adding more entries to ExifTags? Again, not opposed, just would want to see what happens with #6630.

> Some tags are purely technical and should never be edited by hand (the IFD offsets come to mind here). It would be nice to have a flag not to return them to the user.

I find this a bit vague. Some more detail on what other tags you're thinking of could be helpful.

> Some integer tag values encode a human readable description, for example Flash and LightSource. It would be great to provide named constants for those as well as methods to return the description as a string.

#6630 again. Rather than methods to return the description as a string, I wonder if returning IntEnums as the values would be a solution? That would allow the names to be inspected without having to add new arguments or methods.
> Isn't this just part of the unfortunate reality that https://exiftool.org/TagNames/EXIF.html lists EXIF tags, and one of the groups listed is an ExifIFD? I would have thought the names were inherently confusing.

I don't think so. I might be totally wrong here, though. As far as I understand, the Exif segment (which is returned by `Image.info.get('exif')` is basically a linked list of IFDs. First one is IFD0, which contains the information that `getexif()` returns. This IFD links to the next one (which is called ExifIFD by ExifTool) which contains most of the camera information. Then on the the next IFD (Interop) and so on. Yes, the names are a bit confusing. What I was trying to say is that `getexif()` returns an actual IFD, but is semantically closer to a handle to the whole EXIF information segment of the image.

> You mean adding more entries to ExifTags? Again, not opposed, just would want to see what happens with #6630.

Yes. I just rechecked. GPSTags, which can only occur in the GPSInfo IFD, can have "duplicate" tag numbers. For all other tags (maybe excluding MakerNotes, not so sure about them), including for example the Interop tags, this does not seem to be the case and Exiftool itself puts all the tag numbers in one big list. So it should be safe for Pillow to just extend the ExifTags dict (or enum in the future) as well.

> I find this a bit vague. Some more detail on what other tags you're thinking of could be helpful.

That should only be the offsets to the next IFD (tag 0xa005 InteropOffset, tag 0x8825 GPSInfo, and a few others). It's not very important though. When I have to write code for "Give me all the information about this image" then the GPS long/lat are important, but it's not really important (obviously depending on the use case) at which byte the GPS Information starts. We extract the information anyway to parse the whole Exif segment, but it would be a convenience function to filter out the offsets.

> #6630 again. Rather than methods to return the description as a string, I wonder if returning IntEnums as the values would be a solution? That would allow the names to be inspected without having to add new arguments or methods.

#6630 should cover at least 99%, yes. One could argue that "Light Source" is a nicer string representation than what we get from `Enum.name` "LightSource" (without the space). However, that definitely needn't be a priority.

But additionally, think for example of tag 0x9208 LightSource. Currently, we could only retrieve an integer as the value of this tag. But for example "1" is not very descriptive. "Daylight" (for which the 1 stands) is much nicer. This should also apply for setting this tag value. Currently, we would have to do something similar to `exif_tags[0x9208] = 1`. It would be great to be able to write `exif_tags[TAGS.LightSource] = Exif.LightSource.Daylight`
> What I was trying to say is that `getexif()` returns an actual IFD, but is semantically closer to a handle to the whole EXIF information segment of the image.

yep, many people get confused by this and use `_getexif` cause it give more info instead public method.
I've created #6724 to add Interop tags.
It's not clear to me what IFD1 should involve, since "[IFD1 tags are not listed separately](https://exiv2.org/tags.html)".
I don't know what other missing IFDs you were thinking of.

maybe it can help, I have the problem that the examples to obtain the gpsinfo with getexif() is not working in the last version of pillow, because gpsInfo is a number like is described in [this question](https://stackoverflow.com/questions/72530975/extract-gps-data-using-python-and-pil-is-failing) , the only way to obtain the gpsinfo was using _getexif
> It's not clear to me what IFD1 should involve, since "IFD1 tags are not listed separately".

Correct. I should check if tag names, that are allowed to appear in IFD0 and IFD1, are still missing. But that would be a minor addition.

> the only way to obtain the gpsinfo was using _getexif

@olmerg You can do it by using `getexif().get_ifd(0x8825)`.
That is inconvenient. And I haven't been able to find the IFD tag number for IFD1 and for the raw thumbnail data.

Resolving patch:
diff --git a/src/PIL/ExifTags.py b/src/PIL/ExifTags.py
--- a/src/PIL/ExifTags.py
+++ b/src/PIL/ExifTags.py
@@ -354,3 +354,27 @@ class IFD(IntEnum):
     Makernote = 37500
     Interop = 40965
     IFD1 = -1
+
+
+class LightSource(IntEnum):
+    Unknown = 0
+    Daylight = 1
+    Fluorescent = 2
+    Tungsten = 3
+    Flash = 4
+    Fine = 9
+    Cloudy = 10
+    Shade = 11
+    DaylightFluorescent = 12
+    DayWhiteFluorescent = 13
+    CoolWhiteFluorescent = 14
+    WhiteFluorescent = 15
+    StandardLightA = 17
+    StandardLightB = 18
+    StandardLightC = 19
+    D55 = 20
+    D65 = 21
+    D75 = 22
+    D50 = 23
+    ISO = 24
+    Other = 255


Test patch: