Custom Preview Generation with the Lambda Synchronizer

Besides the preview types OMN generates out of the box, you can configure your own custom preview types — for example a square thumbnail for a shop feed, or a watermarked rendition for a partner portal. Which custom previews an asset gets is decided by a script that OMN evaluates for every checked-in file.

With the Lambda synchronizer that script is JavaScript stored in an S3 bucket, and it no longer renders anything itself: it only declares which previews to produce and with which tool parameters. The rendering is done by the preview Lambdas.

The on-prem synchronizer still uses the mechanism described in Custom Preview Configuration: a local shell script that renders the preview file and prints the resulting OMN file type on stdout. That mechanism is unchanged. The Lambda synchronizer uses the JavaScript contract described here instead. A single OMN installation can hold both kinds of configuration side by side — see Migrating from a shell custom preview for how to convert an existing shell script.

Prerequisites

Custom preview scripts require the Lambda synchronizer to be active and a bucket for the script objects to be configured. Both are part of the global OMN configuration:

synchronizer.mode=lambda
preview.lambda.custom.script.bucket=config-<account>
Property Purpose

synchronizer.mode

Selects the synchronizer implementation. lambda activates the Lambda synchronizer and, with it, everything described on this page. Unset, the value is rmi, which keeps the on-prem synchronizer, where custom previews are shell scripts.

preview.lambda.custom.script.bucket

The bucket the custom preview scripts are loaded from. A standard OMN deployment points it at the configuration bucket provisioned for the respective AWS account, whose name is derived from the account.

So in a standard deployment you only have to upload your scripts into that bucket and reference them as described under Registering a custom preview type.

Unlike the queue URLs and the source and preview bucket names, preview.lambda.custom.script.bucket is not published to SSM Parameter Store by the synchronizer’s CDK stack. On a deployment that deviates from the standard setup — or if the property is empty — it has to be set in the omn config properties by hand.

Registering a custom preview type

A custom preview type is a configuration record consisting of a name, a pointer to the script and the script type. Maintain the records in the configuration client under Custom Preview configuration, where each row has three fields:

Field Required Description

Preview name

yes

The custom preview type name, for example RotatedImage. It is the retrieval name of the generated preview, the base name of the stored file, and the value the script sees as context.get('identifier').

Command line / Path

yes

The pointer to the script. For is lambda the path of the script object relative to the root of the configuration bucket, for example <account-prefix>/omn-web/scripts/rotated-image.js. For is shell the local command line on the sync host.

Script type

yes

is lambda — the script is fetched from the configuration bucket and run by the Lambda synchronizer. is shell — the script is a local script run by the on-prem synchronizer. Existing rows were migrated to is shell.

For is lambda, Command line / Path is always a path relative to the root of the bucket named by preview.lambda.custom.script.bucket — not a file system path and not a URL. Enter it without a leading slash, without the bucket name and without an s3:// prefix.

Inside the configuration bucket the objects are organised under an account-specific path, so the value normally starts with that prefix:

Bucket (preview.lambda.custom.script.bucket)  config-<account>
Command line / Path                           <account-prefix>/omn-web/scripts/rotated-image.js
Resulting object                              s3://config-<account>/<account-prefix>/omn-web/scripts/rotated-image.js

Ask your project’s AWS administrator for the prefix that applies to your account.

The same records can be read and written through the configuration API, where the three fields are called identifier, command and scriptType (LAMBDA / SHELL):

GET  /api/config/v1/system/preview-configuration/custom
PUT  /api/config/v1/system/preview-configuration/custom

PUT replaces the complete list, so always send every custom preview type you want to keep — types missing from the payload are deleted. A record sent without scriptType defaults to SHELL.

[
  {
    "identifier" : "RotatedImage",
    "command" : "<account-prefix>/omn-web/scripts/rotated-image.js",
    "scriptType" : "LAMBDA"
  }
]

The records are stored in the table SUI_EXTENDED_PREVIEW_TYPE, whose SCRIPT_TYPE column was added in OMN 7.0 with the default value SHELL.

The table is shared with the on-prem synchronizer, and Script type decides which of the two runs a row: the on-prem synchronizer processes the is shell rows and skips the is lambda ones, the Lambda synchronizer does the opposite. The two execution paths are complements, so every configured row runs exactly once — on whichever synchronizer its type names. A system being migrated can therefore hold both kinds of row side by side.

What does have to stay unique is the Preview name: two rows sharing it would write to the same retrieval name and overwrite each other’s preview.

The script contract

The script is evaluated in the OMN WebApp once per configured custom preview type and per checked-in file. It receives all inputs in a single binding and returns its decision by assigning a global variable.

The script must assign its result to a global variable named result. A return statement at script level has no effect — the value would be discarded and OMN would treat the run as "no custom preview".

Script input

All inputs are exposed as one binding named context. It is a Java map, so read it with context.get('<key>') — dot access such as context.fileType is not available.

Key Type Description

assetId

number

Database identity of the asset being checked in.

dataBucket

string

S3 bucket the source file is stored in.

previewBucket

string

S3 bucket the generated previews are written to.

sourcePath

string

Bucket-relative S3 object key of the source file.

guid

string

Storage GUID pre-generated for this custom preview type.

targetPath

string

Bucket-relative S3 object key the preview will be written to, without file extension — the extension is appended from the descriptor’s fileType. Shape: preview folder, GUID sub-path, identifier, e.g. omn/previews/custom/ab/cd/ef/gh/ij/kl/RotatedImage. How the GUID is split into folders depends on the storage path configuration.

identifier

string

The custom preview type being evaluated, i.e. the identifier of its configuration record.

fileType

string

OMN file type name of the source file, trimmed, e.g. JPEG, TIFF, PDF, 8BPS.

pageCount

number

Page count of the source document.

fileInfo

object

The source file descriptor, for branching on more than the file type.

On the check-in path fileInfo is a minimal object that only reports the file type; its other properties are empty. For the same reason pageCount is always 1 there. Branch on fileType rather than on fileInfo or pageCount.

Two further bindings are in scope: logger, an SLF4J logger writing to the per-script category described under Applying changes, and entryFacade / api, the accessors OMN provides to all its JavaScript scripts.

Script output

Assign to result either one descriptor object or an array of descriptor objects — one per custom preview to generate. Each descriptor is dispatched as its own Lambda invocation.

Field Required Description

name

yes

Retrieval name of the generated preview and base name of the stored file. An entry without a non-blank name is skipped with a warning. Use context.get('identifier') unless one script produces several previews.

fileType

yes

OMN file type of the generated preview, e.g. JPEG, PNG, WEBM, MP3. A descriptor with a blank fileType is skipped with a warning.

cliArgs

no

The tool arguments, passed verbatim to the tool of the Lambda the source is routed to — see Standard preview Lambdas and their tools. Omitting the field lets OMN compute the standard arguments for that tool; an empty array means "explicitly no arguments"; a non-empty array is used verbatim and then toolParams and basePreviewType are ignored. The tuning fields of Optional tuning fields are not affected — they are separate job settings and keep applying either way.

basePreviewType

no

Which standard preview type’s arguments to inherit when cliArgs is omitted. One of THUMBNAIL, COARSE, ORIGINAL, CUSTOM, ORIGINAL_PIXEL, ORIGINAL_VECTOR, ORIGINAL_MULTIMEDIA (case-insensitive; an unknown value is ignored with a warning). Defaults to ORIGINAL_MULTIMEDIA for video and audio sources and to COARSE otherwise.

lambda

no

standard (default) routes the preview through the standard preview Lambda matching the source type, see Standard preview Lambdas and their tools. custom routes it to a project-specific Lambda — see Project-specific Lambdas. The value is compared case-insensitively, so custom and CUSTOM are equivalent; only that word selects the custom route, and every other value — including an unrecognised one — falls back to standard.

When name equals context.get('identifier'), the pre-generated guid and targetPath from the input are reused, so the stored preview ends up exactly where the script was told. A descriptor with a different name gets a freshly generated GUID and its own target path.

The following script generates one 80×80 JPEG thumbnail for every checked-in file:

result = [
  {
    name: context.get('identifier'),
    fileType: "JPEG",
    cliArgs: ["-resize", "80x80>", "-quality", "80"]
  }
];

Returning an empty array is how a script opts out for a given file. The following script produces the preview for raster images only:

var RASTER = ["JPEG", "TIFF", "PNGf", "8BPS"];
var fileType = context.get('fileType');

if (RASTER.indexOf(fileType) < 0) {
    logger.info("skipping custom preview for fileType {}", fileType);
    result = [];
} else {
    result = {
        name: context.get('identifier'),
        fileType: "JPEG",
        cliArgs: ["-resize", "1200x1200>", "-quality", "85"]
    };
}

Standard preview Lambdas and their tools

A standard descriptor is routed to one of the standard preview Lambdas. The script does not choose: OMN derives it from the source file type. That also decides which tool the cliArgs are passed to, so the arguments have to match it.

Lambda Source cliArgs are arguments for

image

Raster images, camera raw, Photoshop

ImageMagick convert, applied to the source file.

pdf

PDF

ImageMagick convert, applied to the page raster pdfium produced from the source.

office

Office documents, text, HTML, SVG, EPS

ImageMagick convert, applied to the page raster LibreOffice and pdfium produced from the source.

video

Video

ffmpeg, producing WebM.

audio

Audio

ffmpeg, producing MP3.

prepress

InDesign, QuarkXPress

Nothing — this Lambda renders with PDFBox and ignores cliArgs.

Which file types belong to which Lambda is not hard-coded: it follows the preview plugin registry, where the first entry whose supported file types contain the source type wins. A file type no entry claims is routed to image, which rarely yields a usable preview — check the log in that case.

The route also decides which tuning fields of Optional tuning fields have an effect: iccProfileMode, sourceFileModifier and sourcePreArgs only for image, renderDpi for pdf, office and prepress. Setting one of them on a descriptor routed elsewhere is not an error, it simply has no effect.

Optional tuning fields

Each descriptor may additionally override the job settings OMN derived from the source file. The convention is uniform: omit a field, or set it to null, to inherit the derived value; set it to override that value for this custom preview only.

These are separate job fields rather than tool arguments, so — unlike toolParams and basePreviewType — they keep applying even when the descriptor supplies cliArgs verbatim.

Field Type Description

renderDpi

number

Render resolution in DPI for PDF and Office sources.

iccProfileMode

string

ICC profile chain to apply: none, raster_srgb or print_isocoated_srgb.

sourceFileModifier

string

ImageMagick read modifier appended to the source path, e.g. [0] to read only the first layer or page.

sourcePreArgs

array of string

Tool arguments placed before the source path. An empty array means "explicitly none", omitting the field inherits.

maxPages

number

Upper bound on the pages expanded from a multi-page source. -1 disables the cap; 0 is a literal cap of zero pages.

dimensionLimit

number

Maximum source width and height in pixels. A larger source is not processed.

pdfFirstPagePreviewOnly

boolean

Render only the first page of a page-based source — pdf, office and prepress.

toolParams

object

Overrides merged into the parameter map OMN uses to compute cliArgs. Only evaluated when cliArgs is omitted; the script’s keys win over every resolved default, whether it came from the preview plugin registry, the volume configuration or the omn config properties.

The toolParams keys are the standard preview tool parameters of the matching processor type — the same names previewPluginRegistry.xml uses, such as IMCoarseQuality or Html5VideoCodec. The following script produces a WebM rendition, inheriting the standard multimedia arguments but overriding two transcoding parameters:

result = {
    name: context.get('identifier'),
    fileType: "WEBM",
    basePreviewType: "ORIGINAL_MULTIMEDIA",
    toolParams: {
        Html5VideoCodec: "libvpx",
        Html5Crf: 30
    }
};

Project-specific Lambdas

When a preview cannot be expressed as arguments to the standard tools, set lambda to custom and name the Lambda that should generate it:

result = {
    name: context.get('identifier'),
    fileType: "JPEG",
    lambda: "custom",
    handler: "demo",
    params: "text=OMN 7.0 demo;width=600;height=600"
};
Field Required Description

handler

yes

The logical handler key of the target Lambda. A custom descriptor without a handler key cannot be routed and is skipped with a warning.

params

no

An arbitrary parameter string passed through to that Lambda verbatim. OMN never parses it; its format is defined by the Lambda itself.

The key=value;key=value form above is not a convention OMN imposes — it is simply what the demo handler happens to parse. Your own handler may expect JSON, a single value, or nothing at all; whatever it defines, the script has to match it.

The script never names a deployed function directly. The orchestrator resolves the handler key through the SSM parameter /omn/preview/custom-handlers, which holds a JSON object mapping keys to function names:

{
  "demo" : "omn-preview-custom-demo",
  "watermark" : "omn-preview-custom-watermark"
}

The registry is cached by the orchestrator and refreshed every 60 seconds, so a new key takes effect within a minute without redeploying anything.

Using lambda: "custom" is not a configuration-only step. The Lambda has to be implemented and deployed for the target account, and its key has to be registered in /omn/preview/custom-handlers, before a script may reference it.

How to build, package, deploy and register such a function — including the job and result contracts it has to implement, and why the shared execution role usually needs no change — is described in Project-specific Custom Preview Lambda.

Applying changes

  • Upload the changed script into the configuration bucket under the object key entered as the record’s Command line / Path. No restart is needed.

  • OMN caches the script bodies and verifies them against the S3 object’s ETag once per check-in batch, so a new version is picked up at the start of the next batch. A single check-in outside a batch — a REST upload, for example — still uses the previously loaded version.

  • If the bucket is temporarily unreachable, the last successfully loaded script body is used and an error is logged.

  • Changing a record through the configuration API takes effect the same way; only the script body is cached, not the record itself.

Every custom preview type logs to its own category, named after the type:

com.meylemueller.isy.sync.lambda.CustomPreviewScript.<identifier>

Each run writes the received context tagged [input] and the resulting descriptors tagged [output]; [skip] and [error] mark a script that produced nothing. Anything the script logs itself through logger appears in the same category. Set the parent category com.meylemueller.isy.sync.lambda.CustomPreviewScript to DEBUG to trace all types at once.

Custom previews are supplementary: they are added to the previews an asset already has, and a failure never breaks or delays the check-in. Every failure is logged and swallowed — an empty script, an unusable result, a malformed descriptor, an exception, or a run exceeding the time limit set by the JVM system property omn.customPreview.script.timeoutMs (30000 ms by default). A single bad descriptor in an array does not discard the others.

When a custom preview does not appear, start at the [output] line of its own category. If it lists the descriptor, the problem is downstream — look at LambdaPreviewDispatcher, which reports descriptors it cannot dispatch. If it lists none, look at CustomPreviewScriptRunner and RhinoValues, which report an unusable result and values that could not be converted.

The time limit releases the check-in thread, but it cannot abort the script itself: a script caught in an endless loop keeps occupying a thread until the process is restarted.

The script also runs unsandboxed inside the WebApp JVM and can reach Java classes and OMN beans. Treat it as trusted server code: keep it short, free of side effects, and limited to deciding which previews to generate.

Migrating from a shell custom preview

A shell custom preview received its inputs as command-line placeholders, rendered the file into a temporary directory and printed the resulting OMN file type on stdout. The JavaScript equivalent receives the same inputs from context and returns a descriptor instead of a file. The inputs map as follows:

Shell placeholder JavaScript equivalent

{sourceFilePath}

context.get('sourcePath'), the S3 object key inside context.get('dataBucket')

{fileType}

context.get('fileType')

{tempDirPath}

context.get('targetPath'), the S3 object key inside context.get('previewBucket'). The preview is written there by the Lambda; the script does not create files.

{pageCount}

context.get('pageCount')

{previewType}

context.get('identifier')

The file type printed on stdout

The descriptor’s fileType

The sample shell script that rotates a file by 90°:

#!/bin/bash
convert "$1" -rotate 90 "$3/${5}.${2,,}"
echo $2

The same custom preview as a Lambda script — the rotation moves from an executed command into the declared cliArgs, and the file type moves from stdout into fileType:

result = {
    name: context.get('identifier'),
    fileType: context.get('fileType'),
    cliArgs: ["-rotate", "90"]
};

To migrate:

  1. Rewrite each shell script as a JavaScript script that assigns result, using the mapping above.

  2. Upload each script to the bucket configured as preview.lambda.custom.script.bucket.

  3. For each custom preview type, replace Command line / Path with the script’s path relative to the root of the configuration bucket and set Script type to is lambda.

  4. Leave or remove the old is shell row as you like — the Lambda synchronizer skips it either way. Keep it only while the on-prem synchronizer still runs against the same configuration, and give it a Preview name different from the Lambda row so the two do not overwrite each other’s preview.

  5. Check in a test asset and verify the [input] and [output] lines in the category com.meylemueller.isy.sync.lambda.CustomPreviewScript.<identifier>.

A Lambda custom preview script must not render anything. Everything the shell script did with ImageMagick, Ghostscript or ffmpeg has to be expressed as cliArgs or toolParams of a standard preview Lambda, or moved into a project-specific Lambda selected through lambda: "custom" (see Project-specific Lambdas).

Welcome to the AI Chat!

Write a prompt to get started...