Skip to content

Licensing

padams edited this page Oct 14, 2026 · 4 revisions

Licensing

PhotoPress writes your licensing information into each image file's XMP metadata and adds it as structured data (JSON-LD) to the pages that show the image. The settings are the Licensing card of the Metadata tab under PhotoPress › Settings.

Requirements

  • WordPress 6.6 or later.
  • PHP's DOM extension, to merge the license into the file's XMP.
  • Imagick (optional). On upload, PhotoPress uses it for a file it cannot write without re-encoding. Without it, such a file is uploaded without the license and the failure is recorded.

Settings

Open PhotoPress › Settings and select the Metadata tab.

The Licensing card with Licensor Name, Web Statement of Rights URL and Licensing URL filled in
The Licensing card.

Control What it does Default
Licensing switch ("Add your licensing information to your images and the pages that show them.") Turns licensing on or off. Saves at once. Off
Licensor Name The name of the person or organization that licenses your images. Written into files as plus:LicensorName. Empty
Web Statement of Rights URL The URL of your license statement. Written into files as xmpRights:WebStatement, and given as license in the structured data. Empty
Licensing URL The URL where people can obtain a license. Written into files as plus:LicensorURL, and given as acquireLicensePage in the structured data. Empty
Save Saves the three fields. Refuses to save unless all three are filled in and both URLs are valid. —
Save and reprocess all images Saves, then starts the Reprocess licensing metadata job over every image, after a confirmation. —

Licensing applies only while the switch is on and all three fields are filled in and saved (metadata.php#L335). Until then the card shows a warning, nothing is written into files, and no structured data is added to pages.

The job's progress shows under the card, with Cancel while it runs.

How it works

When the license is embedded

Event Files written
Upload The uploaded file, before WordPress moves it into the uploads folder. WordPress makes the sizes from it afterward.
File replaced through PhotoPress's replace route The new file, as for an upload.
Reprocess licensing metadata (job metadata.license) Every file of each image on this server: the attached file, the original it was scaled from, and each size.

On upload, only raster images are written: MIME types image/* other than image/svg+xml (metadata.php#L1719). An upload is never stopped by a licensing failure.

The job works five images per batch, for up to ten seconds, rests between batches, and waits while the server is busy.

What is written

PhotoPress merges the license into the file's existing XMP packet (metadata.php#L1934):

  • Every other XMP property stays: title, keywords, rating, creator, Lightroom settings.
  • Any xmpRights:WebStatement or plus:Licensor the file has is removed first, as an element or an attribute, wherever it sits. A second run adds no duplicates.
  • xmpRights:WebStatement and a plus:Licensor sequence of one entry, with plus:LicensorName and plus:LicensorURL, are added to the first rdf:Description.
  • A file with no XMP gets a new packet with the license.
  • Written by XmpFile (below), EXIF, IPTC-IIM and ICC data stay as they are.

Writing without re-encoding

XmpFile replaces the XMP packet inside the file and copies every other byte, so the image keeps the pixels and encoding it was exported with (XmpFile.php).

Format Where the packet goes
JPEG An APP1 segment before the image data
PNG An uncompressed iTXt chunk XML:com.adobe.xmp, after IHDR, or in place of the existing one
WebP An XMP chunk, with the VP8X flag set. A simple WebP is given a VP8X chunk.
GIF An XMP Data application extension before the first image. A GIF87a file becomes GIF89a.
TIFF Tag 700 in a copy of the first directory, appended at the end of the file. Nothing in the file moves.
HEIC, AVIF A new mime item in a new mdat box at the end, linked to the primary image

Each write (XmpFile.php#L86):

  1. Merges the license into the packet the file has. When the result equals that packet, it stops: the file, its date, and copies of it elsewhere are left alone.
  2. Writes a temporary file (pp-xmp-*) in the file's own folder.
  3. Flushes it, and syncs it to disk with fsync() where PHP has it (PHP 8.1 and later).
  4. Checks it: it must parse, hold the new packet, have every other part of the original unchanged, give the same type, width and height to getimagesize(), and read back with the new packet.
  5. Gives it the original's permissions and renames it over the original.

Any failed step leaves the original untouched and removes the temporary file.

These are not written by XmpFile:

  • A JPEG whose packet would need more than one segment (Extended XMP), a JPEG with more than one XMP segment, and a multi-picture JPEG (MPF) whose XMP segment comes after its MPF segment.
  • BigTIFF, and a TIFF that would grow past 4 GB.
  • HEIF image sequences, items stored outside the file, compressed packets, and other HEIF structures it does not recognize.
  • A GIF packet containing a zero byte.
  • A file with more than one XMP chunk, block or item.

The Imagick fallback

On upload only, a file XmpFile cannot write is written with Imagick, which re-encodes the image (metadata.php#L1761):

  • Not when the failure was a write failure (a full disk, a read-only folder): the file is left as it is.
  • Imagick reads the file's XMP profile, sets the merged packet, and writes the image to a temporary file (pp-license-*) in the same folder.
  • The temporary file replaces the upload only when it is non-empty and getimagesize() reads it (metadata.php#L1882). It is given the original's permissions.
  • If PHP's temporary file lands outside that folder, the write is refused, so the rename stays on one file system.

The Reprocess licensing metadata job does not use Imagick. A file it cannot write is left as it is.

Recorded failures

When the license could not be written, PhotoPress records why on the image, in the _photopress_license_error post meta: the message and the time (metadata.php#L1835). The job's message names each file that failed. A later successful write deletes the record.

While a record exists, the image's attachment details in the Media Library and its edit screen show a License field in red:

The License notice on an image's edit screen
An image whose license could not be written: the date, the reason, and what to run.

The job's errors also show with its progress under the card.

Structured data

PhotoPress prints one <script type="application/ld+json"> element holding a list with a schema.org ImageObject per image (metadata.php#L792):

[
    {
        "@context": "https://schema.org/",
        "@type": "ImageObject",
        "contentUrl": "https://example.com/wp-content/uploads/2026/10/photo.jpg",
        "license": "https://www.example.com/license-statement",
        "acquireLicensePage": "https://www.example.com/purchase-license"
    }
]
Where Images
After each Image block (core/image), including those inside a Gallery block, and each legacy PhotoPress Gallery (photopress/gallery) The images in its HTML, found by their wp-image-<ID> class or data-id attribute
In the footer of an attachment page, a search results page or an archive The images in the main query

Each image is given once per page. The footer adds only images no block gave. The JSON is encoded with JSON_HEX_TAG, so < and > in a value cannot close the script element. The Licensor Name is not in the structured data.

WordPress integration

Hook Type Code Purpose
pre_move_uploaded_file filter, priority 1 metadata.php#L112 Writes the license into the uploaded file. Returns the value it was given, so WordPress, or another plugin's decision, still moves the file.
add_attachment action metadata.php#L114 Records the upload's result on the image made from it.
photopress_attachment_file_replaced action (PhotoPress) metadata.php#L115 Records a replaced file's result on the image.
render_block filter, priority 11 metadata.php#L117 Adds the JSON-LD after Image blocks and legacy PhotoPress Galleries.
wp_footer action metadata.php#L124 Adds the JSON-LD of an attachment page's, search's or archive's images.
attachment_fields_to_edit filter metadata.php#L121 Shows a recorded failure as the License field.
image_strip_meta filter metadata.php#L127 Returns the strip_metadata_from_resized_image setting, false by default, so the Imagick editor keeps the file's metadata, license included, in the sizes it makes. The setting has no control on the tab.

The first four are added only while licensing applies. The check runs on init, so a change takes effect on the next request.

Core functions relied on: get_attached_file(), wp_get_attachment_metadata() (original_image and sizes), wp_update_attachment_metadata(), wp_get_attachment_url(), wp_json_encode(), is_attachment(), is_search(), is_archive().

PhotoPress's replace route passes the new file through wp_handle_sideload(), so pre_move_uploaded_file runs as for an upload (MediaRest.php#L311).

Data stored

Name Kind Holds
photopress_core_metadata option embed_licensor_enable, licensor_name, web_statement_of_rights, licensor_url. Read and written through /wp/v2/settings. The URLs are cleaned with esc_url_raw().
_photopress_license_error post meta message and time of the last failure. Deleted on success.

REST

The job starts through POST /photopress/v1/jobs with type metadata.license. The route needs manage_options. While licensing does not apply, each image fails with "Licensing is off, or not all of its settings are filled in."

Third-party integrations

WP Offload Media

PhotoPress does not call WP Offload Media. After the job writes any file of an image, it calls the core function wp_update_attachment_metadata() with the image's unchanged metadata (metadata.php#L1670). WP Offload Media acts on that update and uploads the image's files again. The image is then announced with photopress_image_files_rewritten, and its files are cleared from CloudFront when the job ends (Offload Media and CDN). When no file changed, as on a second run, nothing is updated, uploaded or cleared.

The job writes only files present on this server. Files WP Offload Media removed from the server are skipped. An image with none of its files here fails with "The file of image N is missing."

Without WP Offload Media, the metadata update has no further effect.

Limits, quirks and known issues

  • On upload, the license goes into the uploaded file only. Sizes made by an image editor that drops metadata lack it until Reprocess licensing metadata runs.
  • A file whose XMP does not parse as XML gets a new packet holding only the license. Its previous XMP is lost.
  • The Imagick fallback re-encodes the image.
  • 1.9.0 printed the structured data with the switch off. From 2.0.0 it requires the switch on and all three fields filled in.
  • Images in blocks are found only by a wp-image-<ID> class or data-id attribute. An image block without either gets no structured data.

Related

Clone this wiki locally