Skip to main content

Programmatic API

The module exposes an API on window.Tokenizer2 and game.modules.get("tokenizer-2").api once the tokenizer-2.ready hook fires.

Functions

MethodPurpose
openEditor(actor, token?)Open the editor for an existing actor (the same entry the portrait click uses)
openEditorStandalone(opts)Open the editor with no actor - for flows that compose art before an actor exists
tokenize(actor, opts?)Headless single-actor tokenize (no UI)
tokenizeBatch(actors, opts?)Headless batch tokenize
exportLayers(layers, opts?)Render a layer stack to a blob/dataURL
promptConfig(opts?)Show a config dialog; resolves to a config object suitable for tokenize() (or null if cancelled)

openEditorStandalone(options)

Opens the full editor without an Actor document. Saves still render and upload the image(s) to disk, but nothing is written to a document - instead the generated paths and the prototype-token patch are returned so the caller can apply them to an actor it creates afterward. Designed for character-creation wizards (e.g. Hero Mancer) that let the user pick portrait/token art before the actor is created.

const result = await game.modules.get("tokenizer-2").api.openEditorStandalone({
name: "Aria the Wizard", // drives the save filename
type: "character", // "character"/"pc" route to the PC save folder
sourceImage: "path/to/portrait.png",
// optional:
disposition: CONST.TOKEN_DISPOSITIONS.FRIENDLY, // default-ring selection
hasPlayerOwner: true, // overrides PC detection (defaults true for character/pc)
targetFolder: "[data] my/folder",// overrides the configured pc/npc save location
userId: game.user.id, // advisory - used only for a FILES_UPLOAD permission check
onComplete: (res) => console.log(res),
// optional re-edit: re-seed a prior design instead of starting fresh (see below)
layerStack: previousResult.layerStack,
});

Re-editing a prior design. To let a user reopen and tweak a token they already composed (e.g. a Hero Mancer submission), store the previous call's result.layerStack and pass it back in. The editor reopens showing that stack instead of seeding fresh from sourceImage. Optional companions for full fidelity:

OptionPurpose
layerStackToken layer stack to re-seed (a prior result.layerStack)
avatarLayerStackAvatar layer stack to re-seed
isDynamicRingModeForce dynamic-ring mode (else inferred from the layer stack)
isOversizedForce oversized state (else inferred from layer transforms)
avatarAspect / avatarCanvasSizeRe-seed avatar aspect ratio / canvas size
// First pass:
const r1 = await game.modules.get("tokenizer-2").api.openEditorStandalone({ name: "Aria", sourceImage: "portrait.png" });
// Later, re-open on the same design:
const r2 = await game.modules.get("tokenizer-2").api.openEditorStandalone({ name: "Aria", layerStack: r1.layerStack });

Returns a Promise that resolves when the editor closes (the onComplete callback fires with the same value)::

  • On save, an object: { tokenPath?, avatarPath?, prototypeToken?, avatarUpdate?, layerStack }
    • tokenPath / avatarPath - server paths of the uploaded image(s)
    • prototypeToken - a dotted patch (prototypeToken.texture.src, .texture.scaleX/Y, and in dynamic-ring mode prototypeToken.ring.*) to apply to the new actor
    • avatarUpdate - { [avatarKey]: path } patch for the actor's portrait field
    • layerStack - the serialised layer stack, to store as a flag for future non-destructive re-edits
  • On close without saving, null.

Recipe for applying the result to a freshly-created actor:

if (result) {
await actor.update({ ...result.prototypeToken, ...result.avatarUpdate });
await actor.setFlag("tokenizer-2", "layerStack", result.layerStack);
// Opening Tokenizer 2 on this actor later rehydrates the saved composition.
}

Notes

  • Only one editor can be open at a time; calling this closes any open editor first.
  • Files are written to the configured PC/NPC save locations (by type) unless targetFolder is supplied; the in-editor Save dialog still lets the user tweak folder/filename.
  • The default ring is applied only if the Apply Default Ring setting is enabled; a standalone actor has no real disposition, so it defaults to friendly (or the supplied disposition).

promptConfig(options)

Shows the same options form as the compendium auto-tokenizer (frame, mask, portrait fit, image source, wildcard handling, save folder) but returns the chosen settings instead of running a batch. Designed for external modules (e.g. DDB Importer) that prompt the user once and apply the same config to many imported actors.

const api = game.modules.get("tokenizer-2").api;
const config = await api.promptConfig({
defaults: { saveFolder: "[data] tokenizer/imports" }, // optional
// title: "...", description: "...", previewActor: someActor,
});
if (!config) return; // user cancelled
for (const actor of importedActors) {
await api.tokenize(actor, { ...config, filename: actor.name });
}

Returns Promise<object|null> - the object has the same shape as tokenize() options (forceDynamicRing, forceBakedRing, frameSrc, maskSrc, portraitFit, useActorImg, wildcardMode, saveFolder) and can be spread directly into tokenize(). The preview uses a synthetic standalone actor when no previewActor is supplied.

Pre-creation use: tokenize(actorData, { updateActor: false })

tokenize() can also be used before an actor document exists - for example, when DDB Importer assembles a monster's actor-data and wants to embed the rendered token into that data before calling Actor.create(). Pass updateActor: false to skip every actor mutation while still uploading the file:

const api = game.modules.get("tokenizer-2").api;
const cfg = await api.promptConfig({ defaults: { saveFolder: "[data] imports/tokens" } });

const { path, prototypeToken, layers } = await api.tokenize(actorData, {
...cfg,
filename: actorData.name,
updateActor: false,
});

// Merge the patch into the actor-data being assembled. The keys are dotted, so
// expand before merging.
foundry.utils.mergeObject(actorData, foundry.utils.expandObject(prototypeToken));
actorData.flags ??= {};
actorData.flags["tokenizer-2"] = { layerStack: layers };

await Actor.create(actorData);

With updateActor: false:

  • The blob is still uploaded to the configured (or supplied) save folder.
  • actor.update, actor.setFlag, and the placed-token patch are all skipped.
  • result.prototypeToken carries the exact dotted-key patch that the writes would have used, including the cache-busted prototypeToken.texture.src.
  • result.layers is the serialised layer stack, ready for setFlag or merging into flags["tokenizer-2"].layerStack.

Extending the Frame & Mask Browser

External modules can add their own tabs (sections) and categories (subsections) to either browser via two Hooks fired once during the Foundry ready phase:

Hooks.on("tokenizer-2.registerFrames", (registry) => {
// Static section - built up-front, appears immediately on next browser open
registry.registerSection({
id: "my-pack",
label: "My Pack", // literal or i18n key (auto-localized when label contains a ".")
subsections: [
{
label: "Heroes", // subsection divider, optional
frames: [
{ src: "modules/my-pack/heroes/aragorn.webp", label: "Aragorn" },
{ src: "modules/my-pack/heroes/legolas.webp", label: "Legolas" },
],
},
{ label: "Villains", frames: [/* ... */] },
],
});

// Async loader - runs once on first browser open, result cached
registry.registerLoader({
id: "my-scanned-pack",
label: "MY_PACK.TAB_LABEL", // i18n key
load: async () => {
const result = await foundry.applications.apps.FilePicker.implementation
.browse("data", "modules/my-pack/scanned-frames");
return {
subsections: [{
label: null,
frames: result.files.map((src) => ({
src,
label: src.split("/").pop().replace(/\.\w+$/, ""),
})),
}],
};
},
});
});

Hooks.on("tokenizer-2.registerMasks", (registry) => {
// Same API, but the registered sections appear in the Mask Browser instead
registry.registerSection({ id: "my-mask-pack", label: "My Masks", subsections: [/* ... */] });
});

Registered sections appear after the built-in tabs (Tokenizer, OMFG, TheGreatNacho, Token Frames) and before the Custom tab. Favourites and recents tracking work automatically for any registered frame.

Other registry methods:

  • registry.unregister(kind, id) - remove a previously-registered section. kind is "frame" or "mask".
  • registry.clearCache(id) - drop a cached loader result so the next browser open re-invokes load. Useful when your module's settings change.

Constraints:

  • Registration must happen inside the tokenizer-2.registerFrames / tokenizer-2.registerMasks listeners (same constraint as tokenizer-2.registerPlugins). Modules registering later miss the hook.
  • Section id must be unique within its kind; duplicates are rejected and logged.
  • Loader failures (throw or null return) are logged and the section is omitted that open; the next open retries.
  • Sections with zero subsections are hidden from the browser.

The registry singleton is exposed as game.modules.get("tokenizer-2").api.frameRegistry for inspection at runtime.