Transformation Language Specification
| Version | v0 (https://transformation-serializer.cloudinary.com/api/v0, https://transformation-serializer.cloudinary.com/schema/v0) |
| Status | Draft |
| Category | Standards Track for the transformation serializer |
Abstract#
This document specifies the Cloudinary transformation language as the transformation serializer understands it: the lexical and syntactic structure of a transformation string, the structured model that represents one, the JSON Schema that defines the model, the expressions and conditions the language allows, and the behaviour of the encoder and decoder that translate between the two forms. It defines a canonical representation so that one transformation has one model and one string.
It is not a replacement for the Cloudinary transformation reference. That reference describes every action and property delivery supports; this document describes the language those actions are written in, and leaves the catalogue of actions to the schema.
Status of This Document#
This is a working draft published with each version of the service. It specifies the language and the behaviour of a conforming serializer; where an implementation differs, the implementation is what changes.
Index#
- Introduction
- 1.1 Scope
- 1.2 Conventions and Requirement Keywords
- 1.3 Terminology
- Schema
- 2.1 JSON Schema
- 2.2 Organisation
- 2.3 Naming and Referencing
- 2.4 Generated Types
- 2.5 Validity and Delivery
- Language
- 3.1 Lexical Structure
- 3.2 Syntactic Structure
- 3.3 Values
- 3.4 Variables
- 3.5 Expressions
- 3.6 Conditions
- 3.7 Canonical Representation
- Serializer
- 4.1 Entrypoints
- 4.2 Encoder
- 4.3 Decoder
- 4.3.1 Lexer
- 4.3.2 Abstract Syntax Tree
- 4.3.3 Compiler
- Security Considerations
- References
1. Introduction#
A Cloudinary transformation is written into a delivery URL as a string such as
c_scale,w_200/e_blur:500. The same transformation can be represented as a structured model, a
JSON array of actions, which is what an editor, a validator or a program generating URLs works with:
[
{ "actionType": "scale", "dimensions": { "width": 200 } },
{ "actionType": "blur", "strength": 500 }
]The transformation serializer translates between the two: decoding a string into a model and encoding a model into a string. This document specifies the language in enough detail that two implementations would agree on both directions.
1.1 Scope#
This document specifies:
- the characters, tokens and structure of a transformation string (Section 3);
- the values, variables, expressions and conditions the string may contain, and how each is represented in the model (Section 3);
- the JSON Schema that defines the model and the conventions it follows (Section 2);
- the canonical representation and the round-trip guarantees that follow from it (Section 3.7);
- the behaviour of the encoder and the three-phase decoder (Section 4).
It does not specify which actions exist, what their parameters mean, or what delivery does with them. That is the schema's responsibility, one file per action, and the Cloudinary transformation reference is the authority on their meaning.
1.2 Conventions and Requirement Keywords#
The key words "MUST", "MUST NOT", "REQUIRED", "SHALL", "SHALL NOT", "SHOULD", "SHOULD NOT", "RECOMMENDED", "NOT RECOMMENDED", "MAY", and "OPTIONAL" in this document are to be interpreted as described in BCP 14 [RFC2119] [RFC8174] when, and only when, they appear in all capitals, as shown here.
The string is described in ABNF [RFC5234], with every rule named after the lexer
token or parser node that produces it (Section 4.3), so the grammar and the code use one vocabulary.
The model is described as TypeScript types named after the schema's titles, exactly as
pnpm types:generate emits them, and each type is given the URL form it is written as. Examples show
a transformation string and the model it corresponds to; every example in this document was produced
by the serializer at this version, or, for expressions, validated against the schema and checked
against delivery.
1.3 Terminology#
| Term | Meaning |
|---|---|
| Transformation string | The text between the upload type and the public id in a delivery URL: c_scale,w_200/e_blur:500. |
| Component | One /-separated part of a transformation string. Components are applied in order, each to the output of the previous one. Also called a segment. |
| Qualifier | One ,-separated part of a component: a name, an underscore, and a value (w_200), or a name and a prefix (c_scale, e_blur:500). |
| Action | One element of the model: an object with an actionType and the properties that action takes. One component usually becomes one action; some qualifiers merge into one action and some actions span several components. |
| Model | A JSON array of actions and variable assignments, in the order they are written. Validated by the schema in Section 2. |
| Expression | A qualifier value computed from asset properties, variables and literals, such as iw_div_2 (Section 3.5). |
| Condition | A boolean expression guarding a component, written as if_… (Section 3.6). |
| Canonical form | The one string the encoder produces for a model, and the one model the decoder produces for a string (Section 3.7). |
| Delivery | The Cloudinary delivery pipeline that evaluates a transformation URL. The authority on what a transformation means. |
2. Schema#
2.1 JSON Schema#
The model is defined by a tree of JSON Schema documents written to
JSON Schema draft 2020-12. The root is
transformation.json; a model MUST validate against it to be a model in the sense of this
document. The JSON Schema page is the practical guide to fetching and using it.
curl -s https://transformation-serializer.cloudinary.com/schema/v0/transformation.json| Schema | Covers |
|---|---|
/schema/v0/transformation.json |
any transformation: image or video |
/schema/v0/image_transformation.json |
image actions only |
/schema/v0/video_transformation.json |
video actions only |
/schema/v0/actions/<group>/<action>.json |
one file per action |
/schema/v0/qualifiers/<qualifier>.json |
shapes shared by several actions: dimensions, colour, gravity, … |
/schema/v0/grammar/<element>.json |
the values, references, assignment, expressions and conditions of Sections 3.3–3.6 |
2.2 Organisation#
schemas/
├── transformation.json anyOf: image_transformation | video_transformation
├── image_transformation.json array of image actions
├── video_transformation.json array of video actions
├── actions/
│ ├── resize/scale.json, crop.json, …
│ ├── effect/blur.json, …
│ ├── other/if.json, custom.json, …
│ └── <group>/…
├── qualifiers/
│ └── dimensions.json, width.json, height.json, gravity.json, color.json, …
└── grammar/
├── numeric_value.json, string_value.json, property_value.json, collection_property_value.json
├── variable_reference.json, context_reference.json, metadata_reference.json, asset_reference.json
├── numeric_expression.json, product_expression.json, sum_expression.json, arithmetic_expression.json
├── comparison_operand.json, comparison_expression.json, and_expression.json, or_expression.json, boolean_expression.json
└── variable_assignment.jsonEach transformation schema is an array whose items are a union (anyOf) of the variable assignment
and the action schemas it admits. Each action schema is an object with a constant actionType, the
qualifiers it takes as properties, and additionalProperties: false. Qualifier schemas are
referenced from the actions that share them, and grammar schemas from the qualifiers whose value is
grammar.
2.3 Naming and Referencing#
File names are
snake_caseand name the thing defined:round_corners.json,product_expression.json.titleis the PascalCase name of the generated type:RoundCorners,ProductExpression. Every schema that defines a type MUST carry atitle; a schema that is only a union MAY.$idis absolute and canonical. Every schema hardcodes the same origin:{ "$id": "https://transformation-serializer.cloudinary.com/schema/v0/transformation.json" }A schema is identified by its
$id, not by the URL it was fetched from, so two copies obtained from different hosts are the same schema to a validator. The$idcarries the API version, and the file is served at that path; a new contract gets a new version segment and new identities.$refis always relative (../qualifiers/dimensions.json,./numeric_value.json), so the tree resolves against the$idwherever it is served from. A validator that must stay off the network SHOULD be pointed at a local copy of the whole tree.Discriminators. An action is identified by
actionType; a grammar node bytype, including the variable assignment that sits among the actions; a union of qualifier shapes by a<name>Typeproperty (gravityType,backgroundType). Discriminator values are the file name of the schema that defines the variant, with two documented exceptions in the grammar (Section 3.5). ANumericValuehas no discriminator: it is a bare number or string (Section 3.3).Unions are
anyOffor action lists andoneOffor grammar alternatives, where exactly one branch MUST match.groupandassetTypesare custom keywords on action schemas, naming the action's category and whether it applies to images, videos or both. Validators ignore them.
2.4 Generated Types#
The serializer's Transformation type is generated from the schema with
json-schema-to-typescript, so anything
the schema allows, the type allows, and nothing else:
npx json2ts -i src/schemas/transformation.json --cwd src/schemas \
-o src/types/transformation.d.ts --maxItems=-1--maxItems=-1 matters: without it, fixed-length array schemas become tuple types that reject
valid input. Consumers who generate their own types SHOULD use the same options so that the types
agree.
2.5 Validity and Delivery#
A model that validates is well-formed. The schema is written so that a well-formed model also works, that is, encodes to a string delivery accepts, and a schema-valid model that delivery rejects is a schema defect. The guarantee holds for what the schema models; it cannot see every rule of delivery (Section 3.5.5 lists the known exceptions), and delivery remains the authority.
import Ajv2020 from 'ajv/dist/2020.js';
const ajv = new Ajv2020({ loadSchema: async (uri) => (await fetch(uri)).json() });
const schema = await (await fetch('https://transformation-serializer.cloudinary.com/schema/v0/transformation.json')).json();
const validate = await ajv.compileAsync(schema);
validate([{ actionType: 'scale', dimensions: { width: 200 } }]); // true3. Language#
3.1 Lexical Structure#
A transformation string is a sequence of Unicode characters. The lexer (Section 4.3.1) divides it
into tokens; the rules below are named after the TokenType each produces. Characters not covered
by any rule MUST be rejected.
IDENTIFIER = 1*( ALPHA / DIGIT / "%" / "@" / "#" / SP ) ; trailing spaces become WHITESPACE
NUMBER = [ "-" ] 1*DIGIT [ "." *DIGIT ] ; a bare trailing "." only before , / _ : or the end
STRING = "!" *( %x00-20 / %x22-7F ) "!" ; any character but "!", delimiters stripped
VARIABLE = "$" 1*( ALPHA / DIGIT / "%" / "@" / "#" ) ; sigil stripped
UNDERSCORE = "_" COLON = ":" SEMICOLON = ";" COMMA = "," SLASH = "/"
DOT = "." MINUS = "-" PLUS = "+" ASTERISK = "*" EQUALS = "="
QUOTE = "'" DQUOTE = %x22 LPAREN = "(" RPAREN = ")"
WHITESPACE = 1*SP ; only where not absorbed into IDENTIFIERThree rules decide ambiguous input:
- A
-directly followed by a digit begins aNUMBER; otherwise it isMINUS.a_-20isIDENTIFIER(a) UNDERSCORE NUMBER(-20);a-bisIDENTIFIER(a) MINUS IDENTIFIER(b). - A
.between digits is part of aNUMBER, and so is a.that ends one right before,,/,_,:or the end of the string; elsewhere it isDOT.a_2.5is one number,w_1.isNUMBER(1.),a_vflip.20isIDENTIFIER(vflip) DOT NUMBER(20), andw_1.xis an error. - Spaces inside a run of identifier characters do not end the
IDENTIFIER.a bis one token; this keeps text overlays intact. Spaces next to punctuation, as ina - b, areWHITESPACEtokens. Newlines produce no token.
% is an identifier character, so a percent-encoded sequence such as %20 is carried inside an
IDENTIFIER rather than decoded; the serializer neither decodes nor encodes URL escapes.
3.2 Syntactic Structure#
The parser (Section 4.3.2) builds a tree from the tokens; the rules below are named after the node each produces.
Transformation = Segment *( SLASH Segment )
Segment = SegmentBody *( COMMA SegmentBody )
SegmentBody = Qualifier / VariableAssignment / Layer / LayerApply
Qualifier = IDENTIFIER UNDERSCORE RawValue ; w_200, f_jpg, bo_5px_solid_red
/ IDENTIFIER UNDERSCORE IDENTIFIER [ COLON RawValue ] ; c_scale, e_blur:500, l_text:Arial_20:Hi
VariableAssignment = VARIABLE UNDERSCORE RawValue ; $w_400, $n_ctx:!k! (Section 3.4)
LayerApply = "fl_layer_apply"
Layer = Qualifier Segment *( SLASH Segment ) LayerApply ; see below
RawValue = 1*token ; every token up to the next COMMA or SLASH- A
Segmentis applied to the output of the previous one; the qualifiers within it describe one step and are unordered (Section 3.7 fixes their order in the canonical form). - Only the names
c,e,fl,landutake the secondQualifierform, with a prefix. For every other name, everything after the firstUNDERSCOREup to the nextCOMMAorSLASHis theRawValue:bo_5px_solid_redis oneQualifierwhose value is5px_solid_red, not three. - A
Layerbegins with anl_(overlay) oru_(underlay)Qualifierand extends to theSegmentcontaining the matchingLayerApply. The segments between are the layer's own transformation; the qualifiers besidel_/u_in its first segment describe the layer source, and those besideLayerApplydescribe its placement. Layers nest. - An unrecognised
Qualifieris not an error. It MUST be preserved: the decoder represents it as acustomaction holding its text, and the encoder emits that text unchanged (Section 3.7).
3.3 Values#
A value is known when the string is written: it always exists, and its type is fixed.
Numbers. An integer (200) or a decimal (0.5, 1.0). In dimension qualifiers an integer is
pixels and a decimal is a multiple of the original, so w_1 is one pixel, w_1.0 the full width
and w_0.5 half of it. The model holds a number as a NumericValue: an integer as a JSON
number, a decimal as a string holding its text verbatim. w_200 is 200, w_1.0 is "1.0", and
both encode back as written. A decimal MUST NOT be reformatted (3.00 is not 3.0 to delivery). An
number written with a trailing dot (w_1.) is a lexical NUMBER, so it no longer stops the
decoder, but it is not a NumericValue: the decoder keeps its text as written. Delivery reads 1.
as 1.0 in w, h, dpr, z and o, and rejects it in x, y, ar, q, an assignment and
arithmetic. An integer is canonical without leading zeros, since its value is all delivery reads: w_007 decodes
to 7 and encodes as w_7, the same image. An integer the model cannot hold exactly, beyond
±(2^53 − 1), MUST NOT be decoded as a number, since rounding it would change the value; the decoder
keeps its text. An encoder MUST write an integer in plain digits, never in exponent notation. A
NumericValue is not wrapped in an object, so the same type serves every numeric position: a
qualifier's whole value, an operand, an assigned value. A ratio (3:4) is not a NumericValue;
ar takes one as its own string.
Strings. Text delimited by !…!: !sale!, !sale:in_stock!. A string may contain any
character except !; it is used as a variable's value (Section 3.4) and as a comparison operand
(Section 3.6). The model holds it as a StringValue without the delimiters,
{ "type": "string_value", "value": "sale" }. It keeps an object where a number does not, because
a bare string is already a decimal NumericValue.
Properties. An asset property, written as its code (Appendix B):
iw, fc. The model holds it as a PropertyValue under its long name,
{ "type": "property_value", "name": "initial_width" }. It is read at delivery time but is still a
value: a property always exists and has a known type.
References. $w, ctx:!key!, md:!id! and ref:!id! point at something resolved only when
the string is evaluated: it may be missing, and its type is unknown until then. Each is a grammar
node of its own, never a string:
| Type | Written as | Model |
|---|---|---|
VariableReference |
$ + name |
{ "type": "variable_reference", "name": "w" } |
ContextReference |
ctx:!key! |
{ "type": "context_reference", "key": "productType" } |
MetadataReference |
md:!id! |
{ "type": "metadata_reference", "key": "stock-level" } |
AssetReference |
ref:!publicId! |
{ "type": "asset_reference", "publicId": "cloudinary_icon" } |
A variable read before it is assigned gets a 400 response from delivery, and so does an asset
reference to an asset that does not exist; a missing context key evaluates to nothing. An
AssetReference is only ever a variable's value (Section 3.4).
Identifiers name actions, modes, formats, colours and fonts: scale, hflip, jpg,
rgb:ff33cc, Arial. Their meaning is the action's, and the schema enumerates them where the set
is closed.
3.4 Variables#
A variable is declared with an assignment, $name_<value>, and read through a VariableReference,
$name (Section 3.3), wherever a value may be one.
The value of a VariableAssignment is any of six kinds, and the kind is the value's own type:
| Written as | Value | Example |
|---|---|---|
a number, property, $name or conversion |
NumericExpression (Section 3.5) |
$w_400 · $ratio_0.8 · $w_iw |
| arithmetic | ArithmeticExpression |
$w_iw_div_2 |
!text! |
StringValue |
$label_!sale! |
ctx:!key! |
ContextReference |
$name_ctx:!productType! |
md:!id! |
MetadataReference |
$stock_md:!stock-level! |
ref:!publicId! |
AssetReference |
$logo_ref:!cloudinary_icon! |
A context or metadata lookup converted with _to_i or _to_f is a ConversionExpression of the
reference, and so a NumericExpression: $n_ctx:!breadth!_to_i.
A name MUST begin with a letter and contain only letters and digits. Within a component,
assignments are evaluated before the qualifiers that use them, whatever their written order, so
c_scale,w_$w,$w_400 is valid and means the same as $w_400,c_scale,w_$w. An assignment inside
a conditional component is evaluated only if the condition holds.
Each assignment is one VariableAssignment in the model. It is a grammar node, not an action: it
sits among the actions in model order and acts on nothing by itself, and it is discriminated by
type, not actionType.
| String | Model |
|---|---|
$w_400 |
{ "type": "variable_assignment", "name": "w", "value": 400 } |
$label_!sale! |
{ …, "name": "label", "value": { "type": "string_value", "value": "sale" } } |
$stock_md:!stock-level!_to_i |
{ …, "name": "stock", "value": { "type": "conversion_expression", "to": "integer", "value": { "type": "metadata_reference", "key": "stock-level" } } } |
$logo_ref:!cloudinary_icon! |
{ …, "name": "logo", "value": { "type": "asset_reference", "publicId": "cloudinary_icon" } } |
A reference in a qualifier's value is a VariableReference, whether it is the whole value or an
operand:
$w_400/c_scale,w_$w ⇄ [{ "type": "variable_assignment", "name": "w", "value": 400 },
{ "actionType": "scale", "dimensions": { "width": { "type": "variable_reference", "name": "w" } } }]A decoder MUST produce one VariableAssignment per assignment, and an encoder MUST write each one
back as $name_<value>, so that a declaration survives a round trip.
The $(name) interpolation form used inside text layers is outside this specification.
3.5 Expressions#
An expression is a qualifier value computed from asset properties, variables and literals:
c_scale,w_iw_div_2 is half the original width. This section specifies the language of
expressions and the model that represents them; the model is the subject of the
expressions RFC,
which records the reasoning and the verification behind it.
3.5.1 Form#
An arithmetic expression is a sequence of operands joined by operators, written flat:
<operand> _ <operator> _ <operand> _ <operator> _ <operand> … 100_add_100_mul_10, iw_div_2, $w_add_50| Type | Written as |
|---|---|
NumericValue |
the number verbatim: 2, 0.5 |
VariableReference |
$ + name: $w |
PropertyValue |
the property's code (Appendix B): iw, w, ar |
ConversionExpression |
a reference + _to_i or _to_f: $s_to_i, ctx:!n!_to_f |
SumExpression, ProductExpression |
<left>_<operator>_<right>, operator one of add sub · mul div mod pow |
The form is flat: there is no grouping token. w_(10_add_10)_mul_2 and w_!10_add_10!_mul_2
both get a 400 response from delivery. The only way to group is a variable:
$t_10_add_10/c_scale,w_$t_mul_2.
3.5.2 Evaluation#
Delivery evaluates an arithmetic string with two precedence levels, both left to right:
| Level | Operators | Note |
|---|---|---|
| tighter | mul div mod pow |
pow is on this level, not above it: 2_mul_3_pow_4 is (2 mul 3) pow 4 = 1296 |
| looser | add sub |
So 100_add_100_mul_10 is 1100, 100_sub_10_sub_20 is 70 and 1000_div_10_div_2 is 50.
Arithmetic is floating point; when the result becomes a dimension it is rounded once to whole
pixels (10_div_3_mul_100 gives 333). A property is read when the component is evaluated: w and
h are the current size, after earlier components, while iw and ih are the original and
never change.
A string is not accepted in arithmetic. A string-valued variable MUST be converted with _to_i
or _to_f before use: $s_!100!/c_scale,w_$s_to_i_mul_2 is accepted, w_100_add_$s is not.
3.5.3 Model#
Because the string has no grouping and delivery gives it exactly one reading, the model of an
expression is the tree delivery evaluates. Each operator level is a node whose right side
holds only tighter levels and whose left side MAY also hold the level itself (left-to-right
evaluation):
type NumericExpression = NumericValue | PropertyValue | VariableReference | ConversionExpression;
type ProductExpression = {
type: 'arithmetic_expression';
operator: 'mul' | 'div' | 'mod' | 'pow';
left: ProductExpression | NumericExpression;
right: NumericExpression;
};
type SumExpression = {
type: 'arithmetic_expression';
operator: 'add' | 'sub';
left: ArithmeticExpression | NumericExpression;
right: ProductExpression | NumericExpression;
};
type ArithmeticExpression = SumExpression | ProductExpression;The leaves are the NumericExpressions: a NumericValue (2, or "0.5" for a decimal), a
VariableReference ({ "type": "variable_reference", "name": "w" }), a PropertyValue
({ "type": "property_value", "name": "initial_width" }, the long name;
Appendix B maps it to the code) and a
{ "type": "conversion_expression", "to": "integer" | "float", "value": … }, whose value is a
variable, context or metadata reference; a literal needs no conversion. A NumericExpression is
also, on its own, a qualifier's whole value (Section 3.5.4).
Both tiers carry "type": "arithmetic_expression" and are told apart by operator, so a
single-operator model reads the same whichever tier it belongs to.
A schema for this language MUST accept exactly one model per string and MUST NOT accept a model whose in-order printing re-parses to a different tree. A tree needs parentheses, and therefore has no string, exactly when a child binds looser than its parent or a right child binds as tight as its parent; the shapes above make both unconstructible.
c_scale,w_100_add_100_mul_10 — 1100 wide. The product nests on the right;
mul(add(100, 100), 10) = 2000 has no string and cannot be built.
[
{
"actionType": "scale",
"dimensions": {
"width": {
"type": "arithmetic_expression",
"operator": "add",
"left": 100,
"right": {
"type": "arithmetic_expression",
"operator": "mul",
"left": 100,
"right": 10
}
}
}
}
]c_scale,w_100_sub_10_sub_20 — 70 wide. Repeats nest on the left.
[
{
"actionType": "scale",
"dimensions": {
"width": {
"type": "arithmetic_expression",
"operator": "sub",
"left": {
"type": "arithmetic_expression",
"operator": "sub",
"left": 100,
"right": 10
},
"right": 20
}
}
}
]$s_!100!/c_scale,w_$s_to_i_mul_2 — 200 wide; a converted string variable as an operand.
[
{
"type": "variable_assignment",
"name": "s",
"value": {
"type": "string_value",
"value": "100"
}
},
{
"actionType": "scale",
"dimensions": {
"width": {
"type": "arithmetic_expression",
"operator": "mul",
"left": {
"type": "conversion_expression",
"to": "integer",
"value": {
"type": "variable_reference",
"name": "s"
}
},
"right": 2
}
}
}
]3.5.4 Where expressions are accepted#
A numeric qualifier's value is grammar. For most, it is a NumericExpression (w_300, w_1.0,
w_iw, w_$w, w_$s_to_i) or an ArithmeticExpression, with no plain-string branch beside them:
"$w" and a VariableReference would be two models of one string. q, a, o and r keep their
range-checked plain number, and take every other NumericExpression — a VariableReference, a
PropertyValue or a ConversionExpression — or an ArithmeticExpression beside it. Every
row was verified on delivery: each form produced the same output as the plain value it stands for.
| Qualifier | Value | Verified forms |
|---|---|---|
w, h |
NumericExpression · ArithmeticExpression |
w_iw, w_$w, w_$s_to_i, w_iw_div_2, h_150_mul_2 |
x, y |
NumericExpression · ArithmeticExpression |
x_iw_div_4, x_250_mul_2 |
ar |
ratio string, ignore_aspect_ratio · NumericExpression · ArithmeticExpression |
ar_iar, ar_$v, ar_$s_to_i, ar_iw_div_ih, ar_1_add_0.5, ar_$s_to_f |
z |
NumericExpression · ArithmeticExpression |
z_1.5, z_$v, z_fc, z_1_add_1, z_iw_div_1000, z_$v_add_1 |
dpr |
auto · NumericExpression · ArithmeticExpression |
dpr_2.0, dpr_$v, dpr_pc, dpr_1_add_1, dpr_iw_div_1000, dpr_$s_to_f |
dn |
NumericExpression · ArithmeticExpression |
dn_$v, dn_pc, dn_100_mul_3, dn_$v_mul_3 |
q |
1–100, auto and its modes · VariableReference · PropertyValue · ConversionExpression · ArithmeticExpression |
q_pc, q_$s_to_i, q_iw_div_40, q_200_mod_150, q_$s_to_i_mul_2, q_0.5_mul_100 |
a |
integer, flip modes · VariableReference · PropertyValue · ConversionExpression · ArithmeticExpression |
a_pc, a_$s_to_i, a_90_div_2, a_iw_div_40, a_0_sub_45, a_hflip.90_div_2 |
o |
0–100 · VariableReference · PropertyValue · ConversionExpression · ArithmeticExpression |
o_pc, o_$s_to_i, o_100_div_2, o_iw_div_40, o_$s_to_f_mul_100 |
r |
1–4 corners, each an integer · VariableReference · PropertyValue · ConversionExpression · ArithmeticExpression; max |
r_pc, r_$s_to_i:pc:30:40, r_iw_div_100, r_10_mul_2:20_add_20, r_5_mul_2:20:30:40 |
| a variable's value | Section 3.4 | $w_iw_div_2 |
| either side of a comparison | Section 3.6 | if_w_div_2_gt_900 |
Five limits of delivery that the schema expresses or the author must know:
- A computed quality carries no suffix.
q_50:qmax_10andq_50:444work;q_25_mul_2:qmax_10,q_$v:qmax_10,q_pc:qmax_10andq_25_mul_2:444get a400response. Aqualityaction whoselevelis anything but a plain number or mode — a variable, a property, a conversion, an expression — MUST NOT carryquantizationorchromaSubSampling; the schema enforces this. - An angle has no decimal point. In
a,.separates several rotations (a_hflip.90), soa_45_mul_1.5is read as45_mul_1followed by5, that is 50°, anda_67.5as 67 then 5. ANumericValueinside an angle MUST be an integer; an expression MAY still evaluate to a fraction (a_90_div_4rotates by 22.5°). - Effects and the border width take a variable, not an expression.
e_blur:$vandbo_$v_solid_redwork;e_blur:200_mul_2,e_blur:$v_mul_2,e_blur:$s_to_i,bo_2_add_3_solid_redandbo_$vpx_solid_redget a400response, ande_tint:25_mul_2is accepted but the expression is ignored. Those qualifiers keep their plain branch; variable references in them are variable support, not expression support, and are specified separately.pgaccepts neither. qandrtake no context or metadata reference. Both split their value at:—qfor its suffixes (q_auto:good,q_50:qmax_10),rbetween corners — andctx:!key!andmd:!key!contain one.q_ctx:!q!_to_i,q_md:!q!_to_iandr_ctx:!r!_to_iget a400response, alone or in arithmetic (q_ctx:!q!_to_i_mul_2). The schema does not express this: aConversionExpressionconverts any reference. A variable works (q_$s_to_i,r_$s_to_i), andoandatake context references (o_ctx:!o!_to_i).dpranddnread only the properties known before the asset is processed:w,h,iw,ih,ar,iar,pcandidn. The others (tar,cp,fc,ccc,rc,ils,px,py,du,idu) get a400response, alone or in arithmetic:c_scale,dpr_fc,w_100reportsdpr expressions must be known before execution, anddn_fcreportsRequested density is too large. The schema does not express this;PropertyValueis shared by every qualifier.
3.5.5 Limits of the schema#
Two rules of delivery depend on what a variable holds, which the schema cannot see:
- a string variable in arithmetic needs
_to_i/_to_f($s_!100!/c_scale,w_100_add_$sis rejected); - a string cannot be ordered against a number (
$a_!x!/if_$a_lt_1is rejected; equality is accepted and false).
Both are valid models. An implementation that follows assignments MAY report them; the schema does not.
The schema does not constrain the sign of a numeric value either, since one NumericValue serves
every qualifier. c_scale,w_0 is a valid model and gets a 400 response (Invalid width in transformation: 0); a negative width and an expression that evaluates to zero are accepted by
delivery.
A conforming decoder MUST produce the trees of Section 3.5.3 for an expression, and a conforming
encoder MUST print a tree in order, left _ operator _ right, with no further logic.
3.6 Conditions#
A condition guards a component: if_w_gt_1000/c_scale,w_1000/if_end, optionally with an
if_else branch. The guard is a boolean expression over comparisons.
| Type | Written as |
|---|---|
ComparisonExpression |
<left>_<operator>_<right>, operator one of eq ne lt gt lte gte in nin |
AndExpression |
<left>_and_<right> |
OrExpression |
<left>_or_<right> |
VariableCheckExpression |
isdef_$name or isndef_$name |
StringValue |
!text! |
CollectionPropertyValue |
tags · rn · pgnames · ctx · md |
ContextReference, MetadataReference |
ctx:!key!, md:!id! |
| anything numeric | as in Section 3.5.1 |
Delivery evaluates and before or, both left to right, and arithmetic before comparison, so
if_w_gt_1500_add_1000 means w > 2500. Comparisons do not chain: w_gt_1_gt_0 is rejected.
Strings compare as text (!10!_lt_!9! is true); in and nin read a string on the right as a
:-separated list; a string and a number never compare equal.
The model follows the same construction as Section 3.5.3, with three levels:
type BooleanExpression = OrExpression | AndExpression | ComparisonExpression | VariableCheckExpression;
type OrExpression = {
type: 'or_expression';
left: BooleanExpression;
right: AndExpression | ComparisonExpression | VariableCheckExpression;
};
type AndExpression = {
type: 'and_expression';
left: AndExpression | ComparisonExpression | VariableCheckExpression;
right: ComparisonExpression | VariableCheckExpression;
};
type ComparisonExpression = {
type: 'comparison_expression';
operator: 'eq' | 'ne' | 'lt' | 'gt' | 'lte' | 'gte' | 'in' | 'nin';
left: ComparisonOperand;
right: ComparisonOperand;
};
type VariableCheckExpression = { type: 'variable_defined_expression' | 'variable_not_defined_expression'; variable: VariableReference };
type ComparisonOperand = ArithmeticExpression | NumericExpression | StringValue | CollectionPropertyValue | ContextReference | MetadataReference;The if action holds the condition and the guarded transformations, in the shape of an ESTree
IfStatement:
if_w_gt_1000_and_isdef_$big/e_grayscale/if_end
[
{
"actionType": "if",
"condition": {
"type": "and_expression",
"left": {
"type": "comparison_expression",
"operator": "gt",
"left": {
"type": "property_value",
"name": "width"
},
"right": 1000
},
"right": {
"type": "variable_defined_expression",
"variable": {
"type": "variable_reference",
"name": "big"
}
}
},
"consequent": [
{
"actionType": "grayscale"
}
]
}
]if_iw_lte_400/c_fill,h_120,w_240/if_else/c_fill,h_240,w_400/if_end
[
{
"actionType": "if",
"condition": {
"type": "comparison_expression",
"operator": "lte",
"left": {
"type": "property_value",
"name": "initial_width"
},
"right": 400
},
"consequent": [
{
"actionType": "fill",
"dimensions": {
"width": 240,
"height": 120
}
}
],
"alternate": [
{
"actionType": "fill",
"dimensions": {
"width": 400,
"height": 240
}
}
]
}
]A decoder MUST produce one if action for the components from an if_ to its matching if_end,
with the components between as consequent and the components after an if_else as alternate.
An encoder MUST write an if action as if_<condition>/<consequent>/if_end, or as
if_<condition>/<consequent>/if_else/<alternate>/if_end when alternate is present.
3.7 Canonical Representation#
A transformation has many spellings (w_500,h_300 and h_300,w_500 are the same transformation)
and, without the rules of this section, could have several models. The canonical representation
makes both unique.
Canonical string. The encoder MUST produce, for a model, the string in which:
- actions appear in model order, each as one component, joined by
/; - within a component, qualifiers are sorted by their encoded text, ascending, so
{ "width": 500, "height": 300 }encodes toc_scale,h_300,w_500; - a number is written as the model holds it (
200,"1.0"), an integer in plain digits, and an expression in order,left _ operator _ right; - a
customaction contributes its text verbatim, with leading and trailing/removed; - an action that encodes to nothing is omitted, and an empty model encodes to the empty string.
Canonical model. The decoder MUST produce, for a string, the model in which:
- each component's qualifiers are compiled into actions by the rules of Section 4.3.3, with resize actions before the others;
- a property is present only when its qualifier is present; the decoder MUST NOT supply defaults;
- an expression is the one tree of Section 3.5.3, and an integer has no leading zeros (Section 3.3);
- an unrecognised qualifier is a
customaction holding its text.
Round trips. With these rules:
decode(encode(m))ismfor every canonical modelm;encode(decode(s))is the canonical spelling ofsfor every strings, which equalsswhensis already canonical and differs from it only in qualifier order, in an integer's leading zeros, and in text the serializer does not model, whichcustompreserves.
A round trip is therefore stable but not byte-identical for a non-canonical input, and the canonical spelling is what a consumer SHOULD store.
"c_scale,w_500,h_300" → decode → [{ "actionType": "scale", "dimensions": { "width": 500, "height": 300 } }]
→ encode → "c_scale,h_300,w_500"4. Serializer#
4.1 Entrypoints#
The serializer is one implementation reached three ways:
| Entrypoint | Use |
|---|---|
| HTTP API | POST /api/v0/encode, POST /api/v0/decode |
| ESM bundle | import { encode, decode } from 'https://transformation-serializer.cloudinary.com/esm/latest/serializer.mjs' |
| Playground | the bundle, in a page |
encode(model) returns the canonical string; decode(string) returns the canonical model.
Both are pure: the same input always gives the same output, and nothing is sent anywhere by the
bundle.
4.2 Encoder#
The encoder maps each action to its text through a registry keyed by actionType, then joins the
results with /:
model → encodeAction(each) → filter empty → join("/")Each action encoder returns the qualifiers of that action and sorts them; the sort is what makes the output canonical (Section 3.7).
| Model | String |
|---|---|
[{ "actionType": "scale", "dimensions": { "width": 200 } }, { "actionType": "blur", "strength": 500 }] |
c_scale,w_200/e_blur:500 |
[{ "actionType": "scale", "dimensions": { "width": 200, "height": 100 } }] |
c_scale,h_100,w_200 |
[{ "actionType": "crop", "dimensions": { "width": 300, "aspectRatio": "1:1" }, "gravity": { "gravityType": "auto", "autoFocus": [{ "object": "face" }] } }] |
ar_1:1,c_crop,g_auto:face,w_300 |
[{ "actionType": "blur", "strength": 500 }, { "actionType": "sepia" }, { "actionType": "grayscale" }] |
e_blur:500/e_sepia/e_grayscale |
[{ "actionType": "quality", "level": 80, "quantization": 10 }] |
q_80:qmax_10 |
[{ "actionType": "border", "width": 5, "style": "solid", "color": "red" }] |
bo_5px_solid_red |
[{ "actionType": "overlay", "source": { "sourceType": "image", "publicId": "logo", "transformation": [{ "actionType": "blur", "strength": 500 }] } }] |
l_logo/e_blur:500/fl_layer_apply |
[{ "actionType": "overlay", "source": { "sourceType": "image", "publicId": "logo" }, "position": { "gravity": { "gravityType": "direction", "compass": "north" }, "offsetX": 10, "offsetY": 20 } }] |
l_logo/fl_layer_apply,g_north,x_10,y_20 |
[{ "actionType": "custom", "transformationString": "/e_blur:50/e_grayscale/" }] |
e_blur:50/e_grayscale |
[{ "actionType": "scale", "dimensions": { "width": "1.0" } }] |
c_scale,w_1.0 |
[{ "type": "variable_assignment", "name": "w", "value": 400 }, { "actionType": "scale", "dimensions": { "width": { "type": "variable_reference", "name": "w" } } }] |
$w_400/c_scale,w_$w |
[] |
(empty string) |
A layer encodes as its source qualifier, its transformation's components, and a closing
fl_layer_apply carrying the placement qualifiers; nested layers nest the same way.
Every action the schema admits MUST have an encoding; an action the encoder does not know is a defect, not an empty string. The encoder assumes a schema-valid model and does not validate it.
4.3 Decoder#
The decoder is a three-phase pipeline. Each phase has one responsibility and can be run alone.
"c_scale,w_200/e_blur:500"
→ lexer → IDENTIFIER(c) UNDERSCORE IDENTIFIER(scale) COMMA IDENTIFIER(w) UNDERSCORE NUMBER(200) SLASH …
→ parser → Transformation { segments: [ Segment { body: [ Qualifier(c, scale), Qualifier(w, 200) ] }, … ] }
→ compiler → [{ "actionType": "scale", "dimensions": { "width": 200 } }, { "actionType": "blur", "strength": 500 }]4.3.1 Lexer#
The lexer turns the string into a flat token stream. Each token carries a type, its value and
its position: { start, end }. The stream MUST end with exactly one EOF, including for the empty
string. The table lists every token type with lexer output.
| Token | Emitted for | Example |
|---|---|---|
IDENTIFIER |
a run of letters, digits, %, @, # and embedded spaces |
w_200 → IDENTIFIER(w); l_text:Arial_20:a b → IDENTIFIER(a b) |
NUMBER |
an integer or decimal, optionally - prefixed, possibly ending in a bare . before a separator |
a_-20 → NUMBER(-20); a_2.5 → NUMBER(2.5); w_1. → NUMBER(1.) |
UNDERSCORE |
_ |
w_200 → UNDERSCORE(_) |
COLON |
: |
e_blur:500 → COLON(:) |
SEMICOLON |
; |
br_av:video_(value_2m);audio_(value_128k) |
COMMA |
, |
w_100,h_200 |
SLASH |
/ |
w_100/h_200 |
DOT |
. not inside a number |
a_vflip.20 → DOT(.) |
MINUS |
- not followed by a digit |
l_text:Arial_20:a-b → MINUS(-) |
PLUS |
+ |
l_text:Arial_20:a+b |
ASTERISK |
* |
l_text:Arial_20:a*b |
EQUALS |
= |
l_fetch:aHR0cHM6== → EQUALS(=) EQUALS(=) |
QUOTE |
' |
l_text:Arial_20:it's → IDENTIFIER(it) QUOTE(') IDENTIFIER(s) |
DQUOTE |
" |
l_text:Arial_20:say "hi" |
LPAREN, RPAREN |
(, ) |
br_av:video_(value_2m) |
STRING |
text between a pair of !, delimiters stripped |
$label_!sale! → STRING(sale); !! → STRING() |
VARIABLE |
$ and a name, sigil stripped |
$w_400 → VARIABLE(w) |
WHITESPACE |
spaces not absorbed into an identifier | l_text:Arial_20:a - b → IDENTIFIER(a) WHITESPACE( ) MINUS(-) WHITESPACE( ) IDENTIFIER(b) |
EOF |
the end of input, always once | "" → EOF() |
Declared but never emitted. Three token types exist in the lexer's type and are never produced, and an
implementation MUST NOT depend on them:
UNIT (w_2px is NUMBER(2) IDENTIFIER(px)), PERCENT_ENCODED (prompt_a%20b is one
IDENTIFIER(a%20b)) and EXCLAMATION (a ! always opens a string: a!b!c is
IDENTIFIER(a) STRING(b) IDENTIFIER(c)).
Newlines produce no token: "w_200\n/h_100" lexes as w_200/h_100.
Lexical errors carry a position and the message below:
| Input | Error |
|---|---|
!unterminated |
Unterminated string |
$ |
Invalid variable name |
w_1.x |
Invalid number format |
w_& |
Unexpected character '&' |
4.3.2 Abstract Syntax Tree#
The parser consumes the token stream and builds a tree of seven node types. Every node carries
type and position; the table lists its other fields.
| Node | type |
Fields | Produced by |
|---|---|---|---|
| Transformation | Transformation |
segments: Segment[] |
the root of every parse |
| Segment | Segment |
body: (Qualifier | VariableAssignment | Layer | LayerApply)[] |
each /-delimited component |
| Qualifier | Qualifier |
name, prefix?, value?: RawValue |
w_200, e_blur:500, c_crop |
| VariableAssignment | VariableAssignment |
name, prefix?, value: RawValue |
$w_400, $n_ctx:!k! |
| Layer | Layer |
qualifier: Qualifier, layerSegment: Segment, transformation: Transformation |
l_… / u_… up to its fl_layer_apply |
| LayerApply | LayerApply |
value: "fl_layer_apply" |
fl_layer_apply |
| RawValue | RawValue |
value: string |
the value of any qualifier or assignment |
The tree MUST preserve the order of the string: Transformation.segments holds the components in
the order they are written, and Segment.body holds a component's qualifiers in the order they are
written, with nothing dropped, merged or sorted. The compiler's rules depend on it — which qualifier
wins when one repeats, and which component an action belongs to
(Section 4.3.3) — and so does the position-sensitive reading of a layer's qualifiers.
e_blur,e_sepia/e_grayscale MUST therefore parse to two segments, the first holding e_blur then
e_sepia, before any rule about repeats is applied.
The parser does not interpret values: every value is a RawValue holding the text between the
first _ and the next , or /. Interpreting it is the compiler's job, which is what lets a new
action be supported without touching the grammar.
e_blur:500
{ "type": "Qualifier", "name": "e", "prefix": "blur", "value": { "type": "RawValue", "value": "500" } }c_crop — no value
{ "type": "Qualifier", "name": "c", "prefix": "crop" }bo_5px_solid_red — one qualifier; bo takes no prefix, so the whole tail is the value
{ "type": "Qualifier", "name": "bo", "value": { "type": "RawValue", "value": "5px_solid_red" } }$n_ctx:!k! — the parser restores the ! delimiters the lexer stripped
{ "type": "VariableAssignment", "name": "n", "prefix": "ctx", "value": { "type": "RawValue", "value": "!k!" } }co_rgb:fff,l_text:Arial_20:Hi/fl_layer_apply — a layer: layerSegment is the whole component
the l_ qualifier sits in, because position carries meaning (co_ beside l_text colours the
text; in its own component it would be a separate action)
{
"type": "Layer",
"qualifier": { "type": "Qualifier", "name": "l", "prefix": "text", "value": { "type": "RawValue", "value": "Arial_20:Hi" } },
"layerSegment": {
"type": "Segment",
"body": [
{ "type": "Qualifier", "name": "co", "value": { "type": "RawValue", "value": "rgb:fff" } },
{ "type": "Qualifier", "name": "l", "prefix": "text", "value": { "type": "RawValue", "value": "Arial_20:Hi" } }
]
},
"transformation": {
"type": "Transformation",
"segments": [
{ "type": "Segment", "body": [{ "type": "Qualifier", "name": "co", "value": { "type": "RawValue", "value": "rgb:fff" } }] },
{ "type": "Segment", "body": [{ "type": "LayerApply", "value": "fl_layer_apply" }] }
]
}
}4.3.3 Compiler#
The compiler turns each segment into actions. Its rules, each with the serializer's output:
Segments compile independently and their actions are concatenated in order.
e_blur,e_sepia/e_grayscale → [{ "actionType": "sepia" }, { "actionType": "grayscale" }]Within a segment, resize comes first. A c_ qualifier and the dimension, gravity and offset
qualifiers it consumes (w, h, ar, g, x, y, z, b) compile into one resize action,
placed before the segment's other actions whatever the written order.
e_sepia,c_scale,w_100 → [{ "actionType": "scale", "dimensions": { "width": 100 } }, { "actionType": "sepia" }]
c_pad,w_200,b_blue → [{ "actionType": "pad", "dimensions": { "width": 200 }, "background": { "backgroundType": "color", "color": "blue" } }]
c_crop,w_200,h_100,g_auto:face,x_10,y_20 → [{ "actionType": "crop", "dimensions": { "width": 200, "height": 100 }, "gravity": { "gravityType": "auto", "autoFocus": [{ "object": "face" }] }, "x": 10, "y": 20 }]
w_200 → [{ "actionType": "scale", "dimensions": { "width": 200 } }]The last qualifier of a kind wins.
e_blur:500,e_sepia:80 → [{ "actionType": "sepia", "level": 80 }]
w_100,w_200 → [{ "actionType": "scale", "dimensions": { "width": 200 } }]No defaults. A property is present only when its qualifier is. A value the action compiler cannot read is omitted rather than invented:
e_blur:invalid → [{ "actionType": "blur" }]Unrecognised text is kept. A qualifier no compiler claims becomes a custom action, after the
segment's recognised actions:
unknown_qualifier,w_200 → [{ "actionType": "scale", "dimensions": { "width": 200 } }, { "actionType": "custom", "transformationString": "unknown_qualifier" }]Layers compile into overlay / underlay actions. The layer's own components become the
source's transformation; qualifiers in the layer segment describe the source; qualifiers beside
fl_layer_apply describe the placement.
l_logo/e_blur:500/fl_layer_apply → [{ "actionType": "overlay", "source": { "sourceType": "image", "publicId": "logo", "transformation": [{ "actionType": "blur", "strength": 500 }] } }]
l_logo/fl_layer_apply,g_north,x_10,y_20 → [{ "actionType": "overlay", "source": { "sourceType": "image", "publicId": "logo" }, "position": { "gravity": { "gravityType": "direction", "compass": "north" }, "offsetX": 10, "offsetY": 20 } }]
b_blue,co_white,l_text:Arial_100:Hello/fl_layer_apply
→ [{ "actionType": "overlay", "source": { "sourceType": "text", "text": "Hello", "textStyle": { "fontFamily": "Arial", "fontSize": 100 }, "backgroundColor": "blue", "textColor": "white" } }]Position decides meaning for a text layer: c_fit in the layer segment is textFit (wrap the
text to a width), while c_fit in its own component is a resize of the rendered text.
c_fit,w_400,l_text:Arial_20:Hello/fl_layer_apply → source: { …, "textFit": { "width": 400 } }
l_text:Arial_20:Hello/c_fit,w_400/fl_layer_apply → source: { …, "transformation": [{ "actionType": "fit", "dimensions": { "width": 400 } }] }5. Security Considerations#
A transformation string is data, and the serializer treats it as such: decoding and encoding are pure functions of their input, evaluate nothing, and reach nothing. The language itself has no escape from that: an expression (Section 3.5) refers only to asset properties, variables and literals, and a condition (Section 3.6) only decides which components apply.
Two consequences for anyone handling models:
- Unrecognised text passes through unchanged as a
customaction (Section 3.7). A model produced from an untrusted string therefore carries that text verbatim, and MUST be treated as untrusted when it is encoded back into a URL. - Validation against the schema (Section 2) establishes that a model is well-formed, not that it is safe or cheap to deliver. Delivery applies its own limits, and a well-formed model can still describe a transformation delivery refuses.
6. References#
Normative
- [RFC2119] Bradner, S., "Key words for use in RFCs to Indicate Requirement Levels", BCP 14, RFC 2119, March 1997. https://www.rfc-editor.org/rfc/rfc2119
- [RFC8174] Leiba, B., "Ambiguity of Uppercase vs Lowercase in RFC 2119 Key Words", BCP 14, RFC 8174, May 2017. https://www.rfc-editor.org/rfc/rfc8174
- [RFC5234] Crocker, D. and P. Overell, "Augmented BNF for Syntax Specifications: ABNF", STD 68, RFC 5234, January 2008. https://www.rfc-editor.org/rfc/rfc5234
- JSON Schema: A Media Type for Describing JSON Documents, draft 2020-12. https://json-schema.org/draft/2020-12
Informative
- Cloudinary, "Transformation URL API reference". https://cloudinary.com/documentation/transformation_reference
- Cloudinary, "Conditional transformations". https://cloudinary.com/documentation/conditional_transformations
- Cloudinary, "User-defined variables and arithmetic expressions". https://cloudinary.com/documentation/user_defined_variables
- Transformation serializer, "RFC: Expressions and variables in the transformation schema". https://github.com/CloudinaryLtd/transformation-serializer/blob/master/docs/features/expressions-and-conditionals/rfc-expressions.md
Appendix A. Collected Grammar#
The string, in ABNF, with rules named after the tokens (Section 3.1) and nodes (Section 3.2) that produce them:
Transformation = Segment *( SLASH Segment )
Segment = SegmentBody *( COMMA SegmentBody )
SegmentBody = Qualifier / VariableAssignment / Layer / LayerApply
Qualifier = IDENTIFIER UNDERSCORE RawValue
/ IDENTIFIER UNDERSCORE IDENTIFIER [ COLON RawValue ]
VariableAssignment = VARIABLE UNDERSCORE RawValue
LayerApply = "fl_layer_apply"
Layer = Qualifier Segment *( SLASH Segment ) LayerApply
RawValue = 1*token
IDENTIFIER = 1*( ALPHA / DIGIT / "%" / "@" / "#" / SP )
NUMBER = [ "-" ] 1*DIGIT [ "." *DIGIT ] ; a bare trailing "." only before , / _ : or the end
STRING = "!" *( %x00-20 / %x22-7F ) "!"
VARIABLE = "$" 1*( ALPHA / DIGIT / "%" / "@" / "#" )The expression types and the URL form each is written as, from Sections 3.5 and 3.6:
| Type | Written as | Children |
|---|---|---|
OrExpression |
<left>_or_<right> |
left: BooleanExpression · right: AndExpression | ComparisonExpression | VariableCheckExpression |
AndExpression |
<left>_and_<right> |
left: AndExpression | ComparisonExpression | VariableCheckExpression · right: ComparisonExpression | VariableCheckExpression |
ComparisonExpression |
<left>_<eq|ne|lt|gt|lte|gte|in|nin>_<right> |
left, right: ComparisonOperand |
VariableCheckExpression |
isdef_$name · isndef_$name |
variable: VariableReference |
SumExpression |
<left>_<add|sub>_<right> |
left: ArithmeticExpression | NumericExpression · right: ProductExpression | NumericExpression |
ProductExpression |
<left>_<mul|div|mod|pow>_<right> |
left: ProductExpression | NumericExpression · right: NumericExpression |
NumericExpression |
any of its members | NumericValue | PropertyValue | VariableReference | ConversionExpression |
ConversionExpression |
<value>_to_i · <value>_to_f |
value: VariableReference | ContextReference | MetadataReference |
NumericValue · PropertyValue · StringValue · CollectionPropertyValue |
2 · iw · !text! · tags |
— |
VariableReference · ContextReference · MetadataReference · AssetReference |
$w · ctx:!key! · md:!id! · ref:!publicId! |
— |
VariableAssignment |
$name_<value> |
value: NumericExpression | ArithmeticExpression | StringValue | ContextReference | MetadataReference | AssetReference |
Precedence, tightest first: mul div mod pow → add sub → comparison → and → or.
Every level associates left to right; comparison does not associate. A right child is always a
tighter level than its parent, and only a left child may repeat its parent's level — that is how
the types above encode the precedence and the associativity.
Appendix B. Property Codes#
| Model name | URL | Model name | URL |
|---|---|---|---|
width · height |
w · h |
page_count · current_page |
pc · cp |
initial_width · initial_height |
iw · ih |
face_count |
fc |
aspect_ratio · initial_aspect_ratio |
ar · iar |
custom_coordinates_count · region_count |
ccc · rc |
trimmed_aspect_ratio |
tar |
illustration_score · initial_density |
ils · idn |
page_x · page_y |
px · py |
duration · initial_duration |
du · idu |
Collections, for in and nin: tags → tags, region_names → rn, layer_names →
pgnames, context → ctx, structured_metadata → md.