Bulk Personalized Image & GIF Generator from Lottie Template
Pricing
from $5.00 / 1,000 image generateds
Bulk Personalized Image & GIF Generator from Lottie Template
Returns a personalized PNG and/or GIF for every row of your data, rendered from one Lottie template (Bodymovin JSON or .lottie): dynamic text, colors and images per row, from JSON, a CSV URL or an Apify dataset. Template-based, not AI. No browser, no MP4. $0.005 per image; failed rows are free.
Pricing
from $5.00 / 1,000 image generateds
Rating
0.0
(0)
Developer
Djam
Maintained by CommunityActor stats
0
Bookmarked
2
Total users
1
Monthly active users
19 days ago
Last modified
Categories
Share
Returns a personalized PNG and/or GIF for every row of your data, rendered from one Lottie animation template, in bulk, with no browser and no After Effects. Each row replaces the dynamic text, the colors and the images of the template. $0.005 per image, and nothing for a row that fails. This is template rendering, not AI image generation: you get your own design with your data in it, the same way every time.
Give it a Lottie template (Bodymovin JSON exported from After Effects or Lottie Creator, or a .lottie file) and your rows: a JSON list, the URL of a CSV (a Google Sheet export for example), or an Apify dataset, such as the output of another Actor. For every row you get a PNG (one still frame) and/or a GIF (the whole animation), stored in the key-value store, with one dataset row of links.
Typical uses
- Certificates and name badges: one PNG per attendee from a CSV (name, course, date). PNG only, there is no PDF output.
- Greeting cards and invitations: a card per contact, with the name, a message and a color.
- Social media visuals: one 1080 x 1080 image per product, city or offer of a catalog, for example from the dataset of a product or Google Maps scraper.
- A personalized GIF for an e-mailing: the animation with the name of each recipient, 480 px and light by default.
It is an alternative to template-based image APIs (Bannerbear, Placid, Creatomate) for people who already design in Lottie.
Not included: no MP4 or WebM (PNG and GIF only), no PDF, right-to-left and complex scripts (Arabic, Hebrew, Indic, Thai) are refused for free, an embedded SVG image is refused, no font given by URL, at most 1000 rows per run. See Limits.
Quick start
The input form is prefilled with a small greeting card (a public domain Lottie that is embedded in the form) and three rows. Run it as is: you get three PNG images in under 30 seconds, for $0.015.
{"templateJson": { "...": "the Lottie animation, see the form" },"rows": [{"Title": "Hello Zoé", "Subtitle": "Welcome to the team", "Badge": "#E4572E"},{"Title": "Hello Sam", "Subtitle": "Happy birthday", "Badge": "#17BEBB"},{"Title": "Hello Bartholomew", "Subtitle": "Thanks for your order", "Badge": "#FFC914"}]}
By default, a key of a row that has the name of a text layer replaces its text, the id of an image asset (or the name of an image layer) replaces the image with the picture at that URL, and the name of a shape layer replaces its fill color (#RRGGBB or #RGB). Replace the example rows with yours, or empty the list to inspect your template (see Inspection mode). Use templateUrl instead of templateJson for a template that is online.
One real output row (same template, with a GIF as well; the links are shortened):
{"index": 0,"id": "#FFC914","status": "ok","error": null,"values": {"Title": "Hello Bartholomew", "Subtitle": "Thanks for your order", "Badge": "#FFC914"},"png": {"url": "https://api.apify.com/v2/key-value-stores/<id>/records/row-0.png", "width": 1080, "height": 1080, "frame": 59.0, "timeSeconds": 1.967},"gif": {"url": "https://api.apify.com/v2/key-value-stores/<id>/records/row-0.gif", "width": 480, "height": 480, "frames": 30, "fps": 15.0, "delayMs": 70},"textFit": [{"layer": "Title", "fontScale": 0.604, "overflow": false}, {"layer": "Subtitle", "fontScale": 1.0, "overflow": false}],"warnings": [],"imagesCharged": 2}
id comes from the idColumn input (here Badge, only to show the field). textFit says the title was reduced to 60.4 % of its size to fit the width of the template text. The files are row-<index>.png and row-<index>.gif in the default key-value store, and the output tab shows the PNG in the overview table.
From a CSV with your own column names
Your spreadsheet columns do not have to be named like the layers of the template. Say a CSV (csvUrl) has the header Name;Offer;Color: add mappings to tell the Actor where each column goes (this run was tested on the greeting card of the form):
{"csvUrl": "https://docs.google.com/spreadsheets/d/<sheet id>/export?format=csv","mappings": [{"column": "Name", "target": "text", "layer": "Title"},{"column": "Offer", "target": "text", "layer": "Subtitle"},{"column": "Color", "target": "color", "layer": "Badge"}],"idColumn": "Name"}
(Together with templateJson or templateUrl.) Without mappings and with columns that match nothing, the run stops at once, free, and says which columns it read and which layers the template has. A cell with a wrong color (teal instead of #17BEBB) fails only its row, free, and the message names the column. For a Google Sheet, see the note in Limits.
Where do I get a template?
- Design it: in After Effects, export with the Bodymovin plugin (tick Glyphs for your font); or in Lottie Creator. Name the text layers
Title,Subtitleand so on, and a shape layerBadge. - Start from the example: the prefilled card of the input form is a public domain (CC0) Lottie with two texts and two colors. Copy it from the form and change it.
- A Lottie downloaded from a library usually has no text layers to replace. Run the Actor once with no rows (see Inspection mode):
TEMPLATE_INFOlists exactly which texts, colors and images can be replaced in it (empty lists mean nothing can). Check the license of the animation before you publish images made from it.
Use it from an AI agent (MCP)
Through the Apify MCP server, call the Actor with call-actor, then read the rows with get-dataset-items. The agent has to send the template itself (templateJson, up to 5 MB, or templateUrl). Keep to 100 rows for PNG or 20 rows for GIF so that the run ends within the 45 seconds of call-actor; in our local test 50 PNG rows took 4 seconds. Set maxTotalChargeUsd to cap the cost ($0.005 per image, so 50 images are $0.25). Each dataset row is small (status, values, links, no image data), and status tells which rows to fix. For a first call with no rows, TEMPLATE_INFO tells the agent which keys the rows may use.
{"templateUrl": "https://example.com/card.json","rows": [{"Title": "Hello Zoé", "Subtitle": "Welcome", "Badge": "#E4572E"}],"png": true,"pngSize": 1080}
From code (HTTP API): one call that waits for the run and returns the dataset rows (the 300 s limit of this endpoint applies):
curl -X POST "https://api.apify.com/v2/acts/lakadev~lottie-template-bulk-png-gif/run-sync-get-dataset-items?token=$APIFY_TOKEN" \-H "Content-Type: application/json" \-d '{"templateUrl": "https://example.com/card.json", "rows": [{"Title": "Hello Zoé"}]}'
Each returned row has png.url (and gif.url): download it with a plain GET.
Prepare your template
The Actor changes the JSON of the template, so the template must give it handles:
- Name your layers in After Effects (or in the Lottie editor): a text layer named
Title, a shape layer namedBadge, an image layer namedPhoto. Names are case sensitive inmappings, and case-insensitive in the automatic matching. - Write the longest expected value as the placeholder text of a text layer. The Actor reduces a longer text to the width of the placeholder (see Text that is too long). Or give
maxWidthin a mapping. - Embed your font when you export (the Glyphs option of the Bodymovin export, or an embedded font). Otherwise the text is drawn with DejaVu Sans, a fallback font: the picture is correct but not in your font. The Actor never downloads fonts. Only the characters that the template font contains can be used (see Unsupported scripts). Fills and strokes drawn inside font glyphs (
chars) are ignored, because the renderer crashes on them: the text color comes from the text layer, andTEMPLATE_INFO.warningsthen hasGLYPH_STYLE_IGNORED. - Embed images in the file (base64, PNG, JPEG, WebP or GIF; an embedded SVG image is refused). An image that is not embedded is never downloaded and stays empty, unless you replace it with an
imagemapping. It is reported inTEMPLATE_INFO.warnings. - Keep replaceable images at 4 megapixels at most (and 4096 px on a side): the picture of each row is scaled to the size of the asset.
Not supported in a template: expressions and some effects (the engine is ThorVG, close to lottie-web but not identical to the pixel). To check a template before a bulk run, use the companion Actor Lottie Animation Preview & Validator (lottie-animation-preview-validator): it renders key frames and lists the features that other players ignore. This Actor also renders your template once before it starts, and refuses it, for free, if the engine cannot draw it.
Inspection mode
Run with a template and no rows (empty rows, no csvUrl, no datasetId). Nothing is rendered and nothing is billed except the start of the run. The key-value store gets a TEMPLATE_INFO record that lists what can be replaced:
{"width": 1080, "height": 1080, "frameRate": 30, "durationSeconds": 2.0,"texts": [{"layer": "Title", "text": "Hello Name", "font": "Greeting-Regular", "fontSource": "fallback", "box": false}],"colors": [{"layer": "Badge", "kind": "shape", "color": "#3366E6"}],"images": [{"asset": "photo", "width": 400, "height": 400, "layers": ["Photo"]}],"mapping": [],"warnings": []}
(Shortened.) fontSource is embedded, glyphs or fallback; box is true for a text in a box (paragraph text). colors lists the shape layers that have a fill, and the solids. The status message says, for example, "Template inspected: 2 text layers, 2 color layers, 0 image assets. See TEMPLATE_INFO; add rows to render." In a normal run, TEMPLATE_INFO.mapping shows which column was matched to which element.
Mappings
With the default (empty mappings), columns are matched to template elements by name, in this order, ignoring case and spaces at the ends: text layer names, image asset ids, image layer names, shape layer names that have a fill. The first kind found wins; other columns are ignored. For a CSV the columns are those of the header line; for JSON rows and datasets, the keys of the first 100 rows. If no column matches anything, the run is refused with the list of columns and of template elements.
When your column names differ from the layer names, give mappings, a list of objects:
[{"column": "name", "target": "text", "layer": "Title", "fit": "shrink", "maxWidth": 900},{"column": "brand_color", "target": "color", "layer": "Badge"},{"column": "location.city", "target": "text", "layer": "Subtitle"},{"column": "photo_url", "target": "image", "asset": "photo", "imageFit": "cover"}]
column: a key of the row. A path with dots follows nested objects (location.city) and array positions (images.0).target:text,colororimage.layer: the exact name (nm) of the layer, fortextandcolor. All layers with that name are changed, including those inside precompositions. A color applies to every fill of a shape layer (groups included; gradients and strokes are not changed), to the color of a text layer, or to a solid.assetorlayerforimage: the id of an image asset, or the name of an image layer. The value must be the public URL of a PNG, JPEG, WebP or GIF (first frame). It is scaled to the size of the asset:imageFitcover(default) fills it and crops,containfits it whole on a transparent background. An image smaller than the asset is enlarged, with the warningIMAGE_UPSCALED.fitandmaxWidth, fortextonly (see below).
A missing or null value leaves the element of the template as it is. An empty string empties a text, and leaves a color or an image as it is. Numbers are written as text (42), booleans as true or false, objects and lists as compact JSON. Text values are normalized (Unicode NFC, line breaks \n become a line break of the animation, tabs become spaces) and limited to 1000 characters. Values are never interpreted: no expression, no template syntax, no HTML.
Colors must be #RRGGBB or #RGB. red or rgb(0,0,0) give an invalid_row (free).
Text that is too long
By default (fit: shrink) the Actor reduces the font size, never the text, down to 50 % of the size of the template. The rule is measured with the rendering engine itself, in the units of the layer:
- Point text (the usual case): the reference width is the width of the placeholder text of the template, or
maxWidthif you give it. A longer value is scaled byreference / width. A shorter value keeps its size. - Text in a box (paragraph text with a fixed size): the engine wraps the words in the box, and the Actor takes the largest factor from 1 down to 0.5 for which the height of the wrapped text fits the height of the box.
- If the value does not fit even at 50 %, it is drawn at 50 %, the row stays
okand billed, and has the warningTEXT_OVERFLOWandoverflow: trueintextFit. fit: nonenever measures and never changes the size.
textFit in each row gives the fontScale used per text layer.
Unsupported scripts
A row is text_unsupported (free, no image) when its text contains:
- a character that the template font does not have (the
errorlists the first ten asU+XXXX, for example "Characters not available in the template font "Brand-Regular": U+6771 U+4EAC"). Without this check, the image would contain empty boxes and you would pay for it; - a character of a right-to-left or complex script: Arabic, Hebrew, Indic scripts, Thai, Khmer and so on. The engine does not shape them (letters would not be joined and the order would be wrong). Emoji in color are not supported either.
Latin, Cyrillic, Greek, accented letters and Vietnamese work with the fallback font. To use another script, embed a font that has it in your template.
After another Actor
Choose the dataset of another run in the Apify dataset field: its items become the rows, read page by page (100 at a time), and the Actor only reads that dataset. Through the API or MCP, pass the id of the dataset as the string datasetId. Item fields become columns, nested fields with dot paths, for example location.city for a Google Maps place or images.0 for the first image of a product. Only the columns you map are kept in memory.
Use the idColumn input to copy a key of each item (a place id, a SKU) into the id field of the output row. The output rows are in the same order as the source.
We have not verified that an integration which starts this Actor when another finishes can read the dataset on every account: if a run says that the dataset is not found or not readable, pass the id through the API with your own token.
Output
- Dataset: one row per data row, in order.
statusisok(all requested files written, billed),invalid_row,image_failed,text_unsupportedorrender_failed, all free.erroris one English sentence that names the column.valuesshows what was applied, also for failed rows, so you can fix the data. - Key-value store:
row-<index>.png,row-<index>.gif,TEMPLATE_INFO, andTEMPLATE_PROGRESS(the run resumes where it stopped after a migration, with no duplicate and no double billing). To download all the files as a ZIP, use the key-value store API:GET https://api.apify.com/v2/key-value-stores/<store id>/records(it answers with a ZIP; add?prefix=row-to leave out the other records) with your token. The store id is in the links of the rows. - The PNG is the frame given by
pngFrame(percent of the duration, 100 = the last frame, 0 = the first), atpngSizepixels on the longest side (default: the size of the template, at most 2048).backgroundistransparent(PNG with alpha) or#RRGGBB. - The GIF covers the whole animation at
gifFpsimages per second (5 to 25, default 15), at most 150 images and 30 seconds,gifSizepixels on the longest side (64 to 720, default 480), looping forever. A GIF has no alpha: a transparent background becomes white. A longer animation lowers its own frame rate to stay at 150 images. The 15 fps GIF of a 3 second template has 45 images and 70 ms between two images. - The links are share links: anyone who has a link can open the file. They stay as long as the run's storages exist (storages of runs are deleted after a time that depends on your Apify plan; save what you keep).
Limits
- 1000 rows per run (
maxRows); beyond that the status message says "1000 of 2400 rows processed (maxRows)". - Template: 10 MB by URL, 5 MB inline, 5000 layers, GIF at most 30 s. PNG at most 2048 px, GIF at most 720 px.
- A row may use 1000 characters of text per value. An image to download is 10 MB at most; PNG, WebP and GIF at most 16 megapixels, JPEG 40 megapixels. A download that does not answer within 15 s fails the row (free).
- CSV: UTF-8, 10 MB, 200 columns, comma, semicolon or tab separators, quotes as in RFC 4180. A CSV that is not UTF-8 or has an unclosed quote is refused. Google Sheets: the
export?format=csvlink redirects to a Google host whoserobots.txtthis Actor respects, and the answer varies from one host to another; if a run says that agoogleusercontent.comhost disallows the download, run again or use File > Share > Publish to web > CSV and give that link. We do not promise that every Google Sheets link works. - Memory: 1024 MB (default) to 4096 MB. More memory does not make a run faster.
- The rendering of a row has 30 s. After 3 time-outs or 5 crashes of the renderer in one run, the following rows are
render_failedwithout rendering. After 5 network failures on one image host, or 20 time-outs in the run, the images that remain areimage_failedwithout a request. - Not included: MP4 and WebM, a font given by URL, scripts that need shaping, colors of strokes and gradients, position and size of layers, Lottie slots and dotLottie themes, file names from a column, more than 1000 rows.
Pricing
Pay per event: $0.005 per image (image-generated): one event per file of a row with status ok, so a row that gets a PNG and a GIF is $0.01. Plus $0.00005 when the run starts. Failed rows, refused inputs and inspections cost nothing. 100 PNG images are $0.50. You can set a maximum cost per run: the Actor stops before a row that would exceed it, and exits normally with what it has written.
Networking and personal data
Every URL (template, CSV, images) is checked: http and https only, no credentials, public addresses only (also after each redirect), at most 5 redirects, robots.txt read once per site and respected (the user agent is LakadevLottieBot/1.0 (+https://apify.com/lakadev/lottie-template-bulk-png-gif)), no cookies, no proxy. A dataset is read with the token of the run, read only.
Rows often contain names. The Actor keeps nothing outside the storages of the run, and does not send your data anywhere except to fetch the URLs you give. The links of the files are share links (see Output); you remain responsible for the processing of the personal data in your rows and for the licenses of your template, fonts and images. No LottieFiles API is used.
Licenses
The rendering engine is ThorVG (MIT), used through the thorvg-python binding (LGPL-2.1, used unmodified, source at https://github.com/laggykiller/thorvg-python). Other components: Pillow, fonttools (MIT, reads the character tables of fonts), DejaVu Sans (fallback font), aiohttp, Protego. The full notices are in THIRD_PARTY_NOTICES.md. The example greeting card is dedicated to the public domain (CC0 1.0).