Skip to content

Instantly share code, notes, and snippets.

@reknih
Created August 23, 2025 16:24
Show Gist options
  • Select an option

  • Save reknih/9fb2e4fefd4af9c2019853c164a72218 to your computer and use it in GitHub Desktop.

Select an option

Save reknih/9fb2e4fefd4af9c2019853c164a72218 to your computer and use it in GitHub Desktop.
Possible API for adding custom XMP info to PDF files
// Expressing the Factur-X schema in Typst
#let factur-x-ns = pdf.xmp.namespace(
prefix: "fx",
urn: "urn:factur-x:pdfa:CrossIndustryDocument:invoice:1p0#",
)
#let factur-x = factur-x-ns.schema(
DocumentType: (
// The type can be str (Text), bool (Boolean), datetime (Date), int
// (Integer), float (Real), pdf.xmp.bag, pdf.xmp.seq, pdf.xmp.alt,
// pdf.xmp.agent-name, pdf.xmp.choice-open, pdf.xmp.choice-closed,
// pdf.xmp.guid, pdf.xmp.language-alternatives, pdf.xmp.locale,
// pdf.xmp.mime, pdf.xmp.proper-name, pdf.xmp.rendition-class,
// pdf.xmp.resource-ref, pdf.xmp.uri, pdf.xmp.url, pdf.xmp.colorant,
// pdf.xmp.dimensions, pdf.xmp.font, pdf.xmp.thumbnail,
// pdf.xmp.rational.
type: str,
// We could allow omitting the description if the target is not PDF/A.
description: "The type of the hybrid document.",
// Whether the value of the field can be inferred from the document. The
// authority to set an external property generally belongs to a human,
// whereas it falls to the software to set an internal property.
// Can be omitted, Default is true.
external: true,
),
DocumentFileName: (
type: str,
description: "The file name of the embedded XML file.",
external: true,
),
Version: (
type: str,
description: "The version of the embedded XML file.",
external: true,
),
)
#set pdf.metadata(
factur-x.document-type("INVOICE"),
factur-x.document-file-name("factur-x.xml"),
factur-x.version("1.0"),
)
---
// Expressing the W3C TDM Reservation Protocol in Typst
#let tdm-ns = pdf.xmp.namespace(
prefix: "tdm",
urn: "http://www.w3.org/ns/tdmrep/",
)
#let tdm = tdm-ns.schema(
reservation: (
type: pdf.xmp.choice-closed(1, 0),
description: "Whether the rights for text and data mining techniques (TDM) are reserved (1) or not (0).",
),
policy: (
type: pdf.xmp.uri,
description: "A machine-readable policy for TDM uses of this document.",
)
)
#set pdf.metadata(
tdm.reservation(0),
tdm.policy("https://publisher.com/policies/policy.json"),
)
---
// Expressing the XMP Dynamic Media schema in Typst
#let xmp-dm-ns = pdf.xmp.namespace(
prefix: "xmpDM",
urn: "http://ns.adobe.com/xap/1.0/dynamicMedia/",
)
#let timecode = xmp-dm-ns.type(
name: "Timecode",
description: "A timecode value in video.",
schema: (
timeFormat: (
// Open and closed choices infer their base type from the values.
// They may only have one base type.
type: pdf.xmp.choice-closed(
"24Timecode",
"25Timecode",
"2997DropTimecode",
"2997NonDropTimecode",
"30Timecode",
"50Timecode",
"5994DropTimecode",
"5994NonDropTimecode",
"60Timecode",
"23976Timecode",
),
description: "The format used in the timeValue.",
),
timeValue: (
type: str,
description: "A time value in the specified format. Time values use a colon delimiter in all formats except 2997drop and 5994drop, which uses a semicolon. The four fields indicate hours, minutes, seconds, and frames: hh:mm:ss:ff The actual duration in seconds depends on the format.",
),
)
)
#let time = xmp-dm-ns.type(
name: "Time",
description: "A representation of a time value in seconds. ",
schema: (
scale: (
type: pdf.xmp.rational,
description: "The scale for the time value.",
),
value: (
type: int,
description: "The time value in the specified scale.",
),
)
)
#let beat-splice-stretch = xmp-dm-ns.type(
name: "BeatSpliceStretch",
description: "A set of parameters used when stretching audio using the Beat Splice stretch mode.",
schema: (
riseInDecibel: (
type: float,
description: "The amount sound must increase in decibels before it is considered a beat.",
),
riseInTimeDuration: (
type: time,
description: "The duration of the sampling window used to measure the audio increase for locating beats.",
)
useFileBeatsMarker: (
type: bool,
description: "If true, the file’s beats markers are used for stretching.",
),
)
)
#let media = xmp-dm-ns.type(
name: "Media",
description: "A reference to a media asset.",
schema: (
duration: (
type: time,
description: "The duration of the media asset in the timeline.",
),
managed: (
type: bool,
description: "If true, the media asset is rights-managed.",
),
path: (
type: pdf.xmp.uri,
description: "The path to the media asset.",
),
startTime: (
type: time,
description: "The start time of the media asset in the timeline.",
),
track: (
type: str,
description: "The track in the timeline where the media asset is located.",
),
webStatement: (
type: pdf.xmp.uri,
description: "The location of a web page describing the owner and/or rights statement for this resource.",
)
)
)
#let xmp-dm = xmp-dm-ns.schema(
absPeakAudioFilePath: (
type: pdf.xmp.uri,
description: "The absolute path to the file’s peak audio file. If empty, no peak file exists.",
external: false,
),
album: (
type: str,
description: "The name of the album.",
),
altTapeName: (
type: str,
description: "An alternative tape name, set via the project window or timecode dialog in Premiere. If an alternative name has been set and has not been reverted, that name is displayed.",
),
altTimecode: (
type: timecode,
description: "A timecode set by the user. When specified, it is used instead of the startTimecode.",
),
/* ... */
beatSpliceParams: (
type: beat-splice-stretch,
description: "Parameters used when stretching audio using the Beat Splice stretch mode.",
external: false,
),
/* ... */
cameraAngle: (
type: pdf.xmp.choice-open(
"Low Angle",
"Eye Level",
"High Angle",
"Overhead Shot",
"Birds Eye Shot",
"Dutch Angle",
"POV",
"Over the Shoulder",
"Reaction Shot",
)
),
/* ... */
contributedMedia: (
type: pdf.xmp.bag(media),
description: "An unordered list of all media used to create this media item.",
external: false,
)
)
#set pdf.metadata(
// String are automatically converted to pdf.xmp.uri
xmp-dm.abs-peak-audio-file-path("file:///path/to/peak/file.peaks"),
xmp-dm.album("Greatest Hits"),
xmp-dm.alt-tape-name("Alternative Tape Name"),
// Types can be constructed using the keys in their schema as named arguments.
xmp-dm.alt-timecode(timecode(time-format: "24Timecode", time-value: "01:02:03:04")),
xmp-dm.beat-splice-params(
beat-splice-stretch(
rise-in-decibel: 3.0,
rise-in-time-duration: time(
// Rational numbers expressed as a fraction. They are serialized
// as 1/25 in the XMP.
scale: pdf.xmp.rational(1, 25),
value: 500,
),
use-file-beats-marker: true,
)
),
// Autocomplete is provided for open choices.
xmp-dm.camera-angle("Eye Level"),
// The array types bag, seq, and alt are constructed using multiple
// positional arguments.
xmp-dm.contributed-media(
media(
duration: time(scale: pdf.xmp.rational(1, 25), value: 1200),
managed: true,
path: pdf.xmp.uri("file:///path/to/media/file.mp4"),
startTime: time(scale: pdf.xmp.rational(1, 25), value: 0),
track: "Video",
webStatement: pdf.xmp.uri("https://publisher.com/rights/statement.html")
),
media(
duration: time(scale: pdf.xmp.rational(1, 25), value: 600),
managed: false,
path: pdf.xmp.uri("file:///path/to/media/file2.mp4"),
startTime: time(scale: pdf.xmp.rational(1, 25), value: 0),
track: "Video",
webStatement: pdf.xmp.uri("https://publisher.com/rights/statement.html")
),
),
)
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment