Repository navigation
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.
- 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.
Open PhotoPress › Settings and select the Metadata tab.

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.
| 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.
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:WebStatementorplus:Licensorthe file has is removed first, as an element or an attribute, wherever it sits. A second run adds no duplicates. -
xmpRights:WebStatementand aplus:Licensorsequence of one entry, withplus:LicensorNameandplus:LicensorURL, are added to the firstrdf: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.
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):
- 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.
- Writes a temporary file (
pp-xmp-*) in the file's own folder. - Flushes it, and syncs it to disk with
fsync()where PHP has it (PHP 8.1 and later). - 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. - 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.
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.
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:

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.
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.
| 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).
| 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. |
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."
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.
- 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 ordata-idattribute. An image block without either gets no structured data.
Galleries
- Gallery Layouts
- Full-Screen Slideshow
- Gallery Slideshow Block
- Taxonomy Gallery
- Child Pages Gallery
- Gallery Images Added and Removed
- Image Templates and Blocks
Metadata
Pages and search
Media
Operation