Skip to content

proposal: image: add generic metadata support for jpeg, gif, png #33457

@drswork

Description

@drswork

Proposal

Currently the standard image libraries provide no way to read or write most of the metadata associated with an image. I'd like to change that so that the standard image loading libraries surface the metadata in images they read and allow code to annotate images with metadata.

Each image format has its own metadata. Additionally there are two common image metadata formats (eXif and XMP) which are each used in multiple image formats. (XMP is used in GIF, JPEG, and PNG files, while eXif is used in PNG and JPEG files) The formats themselves also have some interesting quirks which make excessive cleverness unwise. (There are three(!) separate key/value stores attached to PNG files, for example, and the format doesn't disallow a key being used multiple times in the different, or even the same, kv store)

With this in mind, the three guiding principles behind this proposal are:

  1. Metadata should be minimally processed on read and write
  2. It should be possible to read an image and immediately write it out in the same format, with the output having semantically identical metadata to the original.
  3. It should be drop-in compatible with the existing image interfaces.

Point 1 means that PNG's three key/value stores will be exposed as three separate stores, and can't be a plain string/string map. It also means that code which wishes to pull information out of a PNG needs to know to look in both the XMP and eXif data.

Point 2 means we need to at least record any metadata that we don't currently know how to interpret so it can be written back out, and the images that are returned from decoding an image need to have its metadata silently attached.

Point 3 means we can't add methods to existing interfaces, since it's possible there are other packages that implement them. It also means we can't alter the calling conventions of RegisterFormat() in image.

This proposal should make it possible for third-party image libraries to participate in the new standard if they choose.

This proposal has some decisions which are either potentially controversial or potentially fragile. The ones I can think of are noted in the Potential Issues section

NOTE: "Metadata" means any data not directly related to the pixels in an image on disk. For example PPI and rotation, for example, are metadata, while alpha channel is not. (Rotation is arguably pixel data the way that alpha channel is, but since it massively affects the translation of disk pixels to image pixels we'll classify it as metadata for now)

Changes to current packages

Some of the standard packages will be changed. The changes are backwards-compatible.

image

  • New registration function RegisterFormatExtended
  type Format struct {
    Name           string
    MagicString    string
    Decode         func(io.Reader) (Image, error)
    DecodeConfig   func(io.Reader) (Image, error)
    DecodeMetadata func(io.Reader) (*metadata.Metadata, error)
    GetMetadata    func(Image) (applies bool, *metadata.Metadata, error)
  }

  RegisterFormatExtended(*Format)

The new registration format includes a function to decode the metatada, as well as a format-specific function to return the parsed metadata for an image.

  • RegisterFormat marked deprecated

It will still work, but internally will just construct a Format struct and call RegisterFormatExtended.

  • GetMetadata added as registered function

This function will be passed an Image. Because there's no way to tell what type the image is, the metadata getting code must iterate through each format's get function. The function sets applies to false if it doesn't apply. Iteration short-circuits at the first function that says it does apply.

image/gif

  • Metadata type, and supporting types, added
type Metadata struct {
  Comment     []string
  XMPMetadata *xmp.XMP
  AppBlocks   []AppBlock
}  

type AppBlock struct {
  AppID [8]byte
  AppAuthCode [3]byte
  BlockData []byte
}
  • Metadata added to Options
type Options struct {
    NumColors int
    Quantizer draw.Quantizer
    Drawer    draw.Drawer
    Metadata  *Metadata // Added
}

image/png

  • Metadata added. Note that for the initial proposal not all the chunks are proposed to be exported. Any chunk the library can't process is stored in the unexported field, which is only accessible to the library's reader.
type Metadata struct {
  KVText           []struct{string, string}
  CompressedKVText []struct{string, string}
  UnicodeKVText    []struct{string, string}
  ChangeTime       *time.Time
  XMPMetadata      *xmp.XMP
  ExifMetadata     *exif.Exif
  unexported       map[string][]byte // For currently uninterpreted chunks
}
  • Encode function gets optional Options parameter:
func Encode(w io.Writer, m image.Image, o *Options) error

type Options {
  Metadata *Metadata
}

image/jpeg

  • Metadata added
type Metadata struct {
  ExifMetadata *exif.Exif
  XMPMetadata  *xmp.XMP
  unexported []byte // For other uninterpreted data
}
  • Metadata added to Options struct
type Options struct {
    Quality  int
    Metadata *Metadata
}

New packages

We add a new general-purpose metadata package to hold the metadata get function. We also add packages for the two shared metadata formats, XMP and eXif.

images/metadata

  interface Metadata {
    // Make sure the Metadata struct is actually valid, as each format has
    // its own quirks and restrictions.
    Validate() error
    // Here mostly to disambiguate from all the other interfaces with just
    // Validate in them.
    IsImageLibraryMetadata()
  }

  // Returns the filetype-specific metadata. Type-switch to tell what kind.
  Get(image.Image) (*Metadata, error)

Get will internally iterate through the registered GetMetadata functions until it finds one that matches.

The Validate method allows code to make sure the metadata struct is valid for the format before writing it out. This is important since many of the fields have quirks and limitations -- for example the key in png's key/value store must be ISO 8859-1 and between 1 and 79 characters.

images/metadata/exif

The exif package decodes metadata in eXif format.

  type Exif {
    // Handwave on the internals until the proposal is generally deemed OK
  }

  // Decode a binary blob, without any leading or trailing markers, as exif
  func Decode([]byte) (*Exif, error)

images/metadata/xmp

The xmp package decodes metadata in XMP format. (Or, if you prefer, ISO 16684-1:2019 as it's an official ISO standard)

  type XMP {
    // Handwave on the internals until the proposal is generally deemed OK
  }

  // Decode a binary blob, without leading or trailing markers, as xmp
  func Decode([]byte) (*XMP, error)

Potential Issues

The proposal has some decisions which are either potentially controversial or potentially fragile. They are noted

Iterating through metadata get functions is fragile

I can't think of a better way, without having the libraries tag the returned Image with a type, which the API doesn't support that I can see.

Adding an optional *Options parameter to png.Encode

This has to be done by actually adding a ...*Options parameter to the signature and then complaining if multiple ones are passed. This requires runtime signature validation, which isn't great

Adding *Metadata to existing Options parameters may cause unexpected behaviour

jpeg.Options has an integer quality parameter, so it can't actually be left off. We'd have to interpret 0 == default. gif.Options has an integer color count parameter, and we'd have to interpret 0 == default.

No access to uninterpreted metadata fields and chunks

This is intentional. It means that writers can't add their own chunks to the output, and it means that readers can't implement their own interpretation code. This is definitely inconvenient, but it means that when the additional chunks are interpreted by the standard library there won't potentially be two ways to access the same data.

Once the implementation is deemed complete then read/write access to these additional chunks shoud be added.

Optional metadata entites are stored as pointers to things.

This allows us to interpret nil pointers as elided entities and not require a HasFoo functions for each element. Not everyone is fond of this style.

No translation between image format metadata

If you read a jpeg in, then write it as a gif, the metadata isn't translated. This is intentional, since there's currently no clear 1:1 translation between file format metadata information.

Metadata

Metadata

Assignees

No one assigned

    Type

    No type
    No fields configured for issues without a type.

    Projects

    No projects

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions