C++ Library

The Lotio C++ library provides headers and static libraries for programmatic use in C++ applications.

Installation

Homebrew (macOS)

brew tap matrunchyk/lotio

brew install lotio

This installs:
- Binary: lotio
- Headers: /opt/homebrew/include/lotio/ (or /usr/local/include/lotio/)
- Libraries: /opt/homebrew/lib/ (Skia static libraries)
- pkg-config: lotio.pc

From Source

See the README for build instructions. The developer package includes:
- Headers in include/lotio/
- Static libraries in lib/
- pkg-config file in lib/pkgconfig/

Headers

Headers are organized by module:

#include <lotio/core/animation_setup.h> // Animation initialization

#include <lotio/core/renderer.h> // Frame rendering

#include <lotio/core/frame_encoder.h> // Frame encoding (PNG)

#include <lotio/text/text_processor.h> // Text processing

#include <lotio/text/layer_overrides.h> // Layer overrides

#include <lotio/text/font_utils.h> // Text measurement modes

#include <lotio/utils/logging.h> // Logging utilities

Text Measurement Mode

enum class TextMeasurementMode {

FAST, // Fastest, basic accuracy

ACCURATE, // Good balance, accounts for kerning and glyph metrics (default)

PIXEL_PERFECT // Most accurate, accounts for anti-aliasing and subpixel rendering

};

The TextMeasurementMode enum controls the accuracy vs performance trade-off for measuring text width:

Basic Usage

Setting Up an Animation

#include <lotio/core/animation_setup.h>

#include <lotio/core/renderer.h>

#include <lotio/core/frame_encoder.h>

#include <string>

int main() {

std::string inputJson = "animation.json";

std::string layerOverridesJson = ""; // Optional

// Setup and create animation with default text padding and measurement mode

AnimationSetupResult result = setupAndCreateAnimation(

inputJson,

layerOverridesJson,

0.97f, // textPadding: 97% of width (3% padding)

TextMeasurementMode::ACCURATE // textMeasurementMode: good balance

);

if (!result.success) {

std::cerr << "Failed to setup animation: " << result.errorMessage << std::endl;

return 1;

}

// Use the animation...

return 0;

}

Rendering Frames

#include <lotio/core/renderer.h>

#include <lotio/core/frame_encoder.h>

// Render a single frame

double time = 0.5; // Time in seconds

int width = 800;

int height = 600;

// Render frame

std::vector<uint8_t> rgbaData = renderFrame(result.animation, time, width, height);

// Encode to PNG

std::string outputPath = "frame_0001.png";

bool success = encodeFrameToPNG(rgbaData, width, height, outputPath);

Text Processing

#include <lotio/text/text_processor.h>

#include <lotio/text/layer_overrides.h>

// Load layer overrides

auto layerOverrides = parseLayerOverrides("layer-overrides.json");

auto imageLayers = parseImageLayers("layer-overrides.json");

// Process layer overrides in animation JSON

std::string modifiedJson = processLayerOverrides(

originalJson,

config

);

Linking

Using pkg-config (Recommended)

g++ $(pkg-config --cflags --libs lotio) your_app.cpp -o your_app

Manual Linking

g++ -I/opt/homebrew/include \

-L/opt/homebrew/lib \

-llotio \

-lskottie -lskia -lskparagraph -lsksg -lskshaper \

-lskunicode_icu -lskunicode_core -lskresources -ljsonreader \

your_app.cpp -o your_app

API Reference

Animation Setup

struct AnimationSetupResult {

bool success;

std::string errorMessage;

skottie::Animation* animation; // Skottie animation pointer

};

AnimationSetupResult setupAndCreateAnimation(

const std::string& inputJsonPath,

const std::string& layerOverridesPath,

float textPadding = 0.97f,

TextMeasurementMode textMeasurementMode = TextMeasurementMode::ACCURATE,

const std::string& fontsDir = ""

);

Parameters:
- inputJsonPath: Path to Lottie animation JSON file
- Absolute paths: Used as-is (e.g., /path/to/animation.json)
- Relative paths: Resolved relative to the current working directory (cwd) where your program is executed
- Example: If your program runs from /home/user/project/ and you pass animation.json, it resolves to /home/user/project/animation.json
- The parent directory of this file is used as the base directory for resolving relative image paths in the Lottie JSON
- layerOverridesPath: Path to layer overrides JSON file (empty string if not used)
- Absolute paths: Used as-is (e.g., /path/to/layer-overrides.json)
- Relative paths: Resolved relative to the current working directory (cwd) where your program is executed
- Example: If your program runs from /home/user/project/ and you pass config/overrides.json, it resolves to /home/user/project/config/overrides.json
- The parent directory of this file is used as the base directory for resolving relative image paths in imageLayers.filePath
- textPadding: Text padding factor (0.0-1.0, default: 0.97 = 3% padding). Controls how much of the target text box width is used for text sizing.
- textMeasurementMode: Text measurement accuracy mode (default: ACCURATE). See TextMeasurementMode enum above.
- fontsDir: Optional directory for font files (e.g. .ttf). When non-empty, fonts are looked up here first, then in the JSON file's directory and its fonts/ subdirectory. Empty string (default) uses only the JSON directory and fonts/.


### Frame Rendering

```cpp
std::vector<uint8_t> renderFrame(
    skottie::Animation* animation,
    double time,
    int width,
    int height
);

Returns RGBA pixel data as a std::vector<uint8_t> (4 bytes per pixel).

Frame Encoding

bool encodeFrameToPNG(

const std::vector<uint8_t>& rgbaData,

int width,

int height,

const std::string& outputPath

);

Encodes RGBA pixel data to PNG format. The C++ library outputs PNG frames by default.

Text Processing

struct LayerOverride {

float minSize; // Optional: minimum font size (0 = not specified, no auto-fit)

float maxSize; // Optional: maximum font size (0 = not specified, no auto-fit)

std::string fallbackText; // Optional: fallback text if text doesn't fit (defaults to empty)

float textBoxWidth; // Optional: text box width (0 = use from JSON or animation width)

std::string value; // Optional: text value to set (defaults to original text from data.json)

int direction; // Optional: text direction (-1 = not specified/preserve, 0 = LTR, 1 = RTL)

};

struct ImageLayerOverride {

std::string filePath; // Optional: directory path (defaults to assets[].u, empty string = use full path from fileName)

std::string fileName; // Optional: filename (defaults to assets[].p)

};

std::map<std::string, LayerOverride> parseLayerOverrides(const std::string& configPath);

std::map<std::string, ImageLayerOverride> parseImageLayers(const std::string& configPath);

std::string processLayerOverrides(

std::string& animationJson,

const std::string& layerOverridesPath,

float textPadding = 0.97f,

TextMeasurementMode textMeasurementMode = TextMeasurementMode::ACCURATE

);

Text Direction:
- The direction field in LayerOverride controls text direction:
- -1: Not specified - preserves existing direction from Lottie JSON
- 0: Left-to-Right (LTR) - for languages like English, Spanish, etc.
- 1: Right-to-Left (RTL) - for languages like Hebrew, Arabic, etc.
- If direction is not specified in layer-overrides.json, the existing direction from the Lottie JSON file is preserved, maintaining backward compatibility.

Text layer visibility: Overridden text layers must be visible in the Lottie composition. Lotio does not fix invalid or mis-exported comps at runtime. When tp is omitted in the Lottie JSON, lotio's Skottie build uses a track-matte patch: the matte source is the nearest prior layer with td !== 0 (Bodymovin behavior), so comps like sample5 typically render correctly without editing data.json. If a text layer is under a 0-opacity null parent, has track matte (tt !== 0) with wrong or missing matte source, or is masked by a layer with td !== 0 and wrong tp, it may render invisible. Lotio logs [WARNING] to stderr with fix suggestions. Fix the Lottie JSON or re-export from AE/Bodymovin when the built-in track-matte patch does not apply. See CLI docs “Text layer visibility requirements” for details.

Image Layer Path Resolution:

When using imageLayers in layer-overrides.json:
- Absolute paths: Used as-is (e.g., /workspace/images/logo.png)
- Relative paths: Resolved relative to the layer-overrides.json file's directory (NOT the current working directory)
- Example: If layer-overrides.json is at /workspace/config/layer-overrides.json and imageLayers contains "image_0": { "filePath": "images/", "fileName": "logo.png" }, it resolves to /workspace/config/images/logo.png
- Important: This is different from inputJsonPath and layerOverridesPath parameters, which are resolved relative to the current working directory
- URLs are NOT supported: HTTP (http://) and HTTPS (https://) URLs are not supported
- Empty filePath: If filePath is an empty string, fileName must contain the full path

Notes:
- Image paths in the original Lottie JSON are resolved relative to the input JSON file's parent directory
- If an asset ID is not in imageLayers, the original u and p from the Lottie JSON are used
- Both filePath and fileName are optional - if not specified, defaults from assets[].u and assets[].p are used


## Complete Example

```cpp
#include <lotio/core/animation_setup.h>
#include <lotio/core/renderer.h>
#include <lotio/core/frame_encoder.h>
#include <lotio/text/text_processor.h>
#include <iostream>
#include <iomanip>
#include <sstream>

int main(int argc, char* argv[]) {
    if (argc < 3) {
        std::cerr << "Usage: " << argv[0] << " --data <input.json> [--fps <fps>] <output_dir>" << std::endl;
        return 1;
    }

    std::string inputJson;
    std::string outputDir;
    float fps = 30.0f;

    // Parse arguments
    for (int i = 1; i < argc; i++) {
        std::string arg = argv[i];
        if (arg == "--data" && i + 1 < argc) {
            inputJson = argv[++i];
        } else if (arg == "--fps" && i + 1 < argc) {
            fps = std::stof(argv[++i]);
        } else if (arg[0] != '-') {
            outputDir = arg;
        }
    }

    if (inputJson.empty() || outputDir.empty()) {
        std::cerr << "Error: --data and output_dir are required" << std::endl;
        return 1;
    }

    // Setup animation with custom text padding and measurement mode
    AnimationSetupResult result = setupAndCreateAnimation(
        inputJson, 
        "",
        0.95f,  // textPadding: 95% of width (5% padding)
        TextMeasurementMode::PIXEL_PERFECT  // Most accurate measurement
    );

    if (!result.success) {
        std::cerr << "Error: " << result.errorMessage << std::endl;
        return 1;
    }

    // Get animation info
    double duration = result.animation->duration();
    int width = static_cast<int>(result.animation->size().width());
    int height = static_cast<int>(result.animation->size().height());

    // Use provided fps, or animation fps, or fallback to 30
    float animationFps = result.animation->fps();
    if (fps == 30.0f && animationFps > 0.0f) {
        fps = animationFps;
    }

    // Render frames
    int totalFrames = static_cast<int>(duration * fps);

    for (int frame = 0; frame < totalFrames; frame++) {
        double time = static_cast<double>(frame) / fps;

        // Render frame
        std::vector<uint8_t> rgbaData = renderFrame(result.animation, time, width, height);

        // Save as PNG
        std::ostringstream filename;
        filename << outputDir << "/frame_" 
                 << std::setfill('0') << std::setw(4) << frame << ".png";

        if (!encodeFrameToPNG(rgbaData, width, height, filename.str())) {
            std::cerr << "Failed to encode frame " << frame << std::endl;
            continue;
        }

        std::cout << "Rendered frame " << frame << "/" << totalFrames << std::endl;
    }

    std::cout << "Rendering complete!" << std::endl;
    return 0;
}

Using Skia Directly

The lotio package includes Skia headers and libraries, so you can use Skia features directly:

// Use Skia directly

#include <skia/core/SkCanvas.h>

#include <skia/core/SkSurface.h>

#include <skia/modules/skottie/include/Skottie.h>

// Use lotio

#include <lotio/core/animation_setup.h>

int main() {

// Use Skia API directly

SkImageInfo info = SkImageInfo::MakeN32(800, 600, kOpaque_SkAlphaType);

auto surface = SkSurfaces::Raster(info);

SkCanvas* canvas = surface->getCanvas();

// Use lotio functions

AnimationSetupResult result = setupAndCreateAnimation("input.json", "");

// Render using Skia

result.animation->render(canvas);

return 0;

}

Include Paths

The pkg-config file includes all necessary include paths:
- -I${includedir} - Lotio headers
- -I${includedir}/skia - Skia core headers
- -I${includedir}/skia/gen - Skia generated headers

Libraries

The following Skia libraries are included:
- libskottie.a - Skottie animation library
- libskia.a - Skia core library
- libskparagraph.a - Text paragraph library
- libsksg.a - Scene graph library
- libskshaper.a - Text shaping library
- libskunicode_icu.a - Unicode support (ICU)
- libskunicode_core.a - Unicode core
- libskresources.a - Resource management
- libjsonreader.a - JSON parsing

Multi-threaded Rendering

Lotio supports multi-threaded rendering for improved performance:

#include <lotio/core/renderer.h>

#include <thread>

#include <vector>

void renderFrameRange(

skottie::Animation* animation,

int startFrame,

int endFrame,

int fps,

int width,

int height,

const std::string& outputDir

) {

for (int frame = startFrame; frame < endFrame; frame++) {

double time = static_cast<double>(frame) / fps;

std::vector<uint8_t> rgbaData = renderFrame(animation, time, width, height);

// Save frame...

}

}

// Render with multiple threads

int numThreads = std::thread::hardware_concurrency();

int framesPerThread = totalFrames / numThreads;

std::vector<std::thread> threads;

for (int i = 0; i < numThreads; i++) {

int start = i * framesPerThread;

int end = (i == numThreads - 1) ? totalFrames : (i + 1) * framesPerThread;

threads.emplace_back(renderFrameRange,

result.animation, start, end, fps, width, height, outputDir);

}

for (auto& thread : threads) {

thread.join();

}

Troubleshooting

Include Errors

Linker Errors

Runtime Errors

See Also