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
| Method | Purpose |
|---|---|
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:
| Option | Purpose |
|---|---|
layerStack | Token layer stack to re-seed (a prior result.layerStack) |
avatarLayerStack | Avatar layer stack to re-seed |
isDynamicRingMode | Force dynamic-ring mode (else inferred from the layer stack) |
isOversized | Force oversized state (else inferred from layer transforms) |
avatarAspect / avatarCanvasSize | Re-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 modeprototypeToken.ring.*) to apply to the new actoravatarUpdate-{ [avatarKey]: path }patch for the actor's portrait fieldlayerStack- 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) unlesstargetFolderis 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.prototypeTokencarries the exact dotted-key patch that the writes would have used, including the cache-bustedprototypeToken.texture.src.result.layersis the serialised layer stack, ready forsetFlagor merging intoflags["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.kindis"frame"or"mask".registry.clearCache(id)- drop a cached loader result so the next browser open re-invokesload. Useful when your module's settings change.
Constraints:
- Registration must happen inside the
tokenizer-2.registerFrames/tokenizer-2.registerMaskslisteners (same constraint astokenizer-2.registerPlugins). Modules registering later miss the hook. - Section
idmust be unique within its kind; duplicates are rejected and logged. - Loader failures (
throwornullreturn) 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.