Skip to content

Image Token Calculator — C# source

Estimate the vision token cost of an image before sending it to an LLM - low/high/auto detail modes, the 512px tile math, the 2048/768 downscaling steps, and a full base + tiles + detail breakdown. Runs entirely in your browser.

This is the C# implementation — the same logic the interactive tool runs, in a shareable, citable form.

// Image Token Calculator — estimate the vision token cost of an image using
// OpenAI-style tile math.
//
// Language: C# (C# 12 / .NET 8, standard library only)
// Source:   CosmoDev polyglot showcase port of the Image Token Calculator
//           tool, ported from src/lib/imageTokenCalculator.ts (the canonical
//           TypeScript implementation).
// Live at:  https://dev.cosmolabs.org/tools/image-token-calculator
// License:  display source — part of CosmoDev's polyglot tool pages.
//
// Design goals:
//   - Pure + deterministic; invalid input throws ArgumentException (as the
//     TS reference throws).
//   - Functionally equivalent to the TS reference: same inputs -> same outputs.
//   - Self-contained: the BCL only (no NuGet packages).
//
// Rounding note: the TS reference uses Math.round (half up); Shrink spells it
// as Math.Floor(x + 0.5) for exact parity.

namespace CosmoDev.Polyglot;

/// <summary>Requested detail mode of an image (<see cref="DetailLevel.Auto"/>
/// mirrors the TS default).</summary>
public enum DetailLevel
{
    Low,
    High,
    Auto,
}

/// <summary>Mirrors the <c>TokenBreakdown</c> interface in the TS lib.</summary>
/// <param name="Detail">Detail level actually applied ("low" or "high").</param>
/// <param name="ScaledWidth">Dimensions after the high-detail downscaling
/// pipeline (identity for low).</param>
/// <param name="ScaledHeight">See <paramref name="ScaledWidth"/>.</param>
/// <param name="TilesX">512 px tiles along each axis (both 1 in low detail).</param>
/// <param name="TilesY">See <paramref name="TilesX"/>.</param>
/// <param name="Tiles">Total 512 px tiles used (<c>TilesX * TilesY</c>).</param>
/// <param name="Base">Fixed base cost of the low-resolution view, in tokens.</param>
/// <param name="DetailTokens">Extra tokens for the high-resolution tile views
/// (0 in low detail).</param>
/// <param name="Total">Total estimated tokens: <c>Base + DetailTokens</c>.</param>
public sealed record TokenBreakdown(
    string Detail,
    int ScaledWidth,
    int ScaledHeight,
    int TilesX,
    int TilesY,
    int Tiles,
    int Base,
    int DetailTokens,
    int Total);

/// <summary>Image Token Calculator — estimate the vision token cost of an
/// image using OpenAI-style tile math.</summary>
public static class ImageTokenCalculator
{
    /// <summary>Fixed token cost of the low-resolution image view.</summary>
    public const int LowDetailTokens = 85;

    /// <summary>Token cost of one high-resolution 512 px tile.</summary>
    public const int TileTokens = 170;

    /// <summary>Images are first scaled to fit inside this square.</summary>
    public const int MaxSide = 2048;

    /// <summary>Then the shortest side is capped at this length.</summary>
    public const int MaxShortSide = 768;

    /// <summary>Tile edge length in pixels.</summary>
    public const int TileSize = 512;

    /// <summary>Both dimensions at or under this -> <see cref="DetailLevel.Auto"/>
    /// stays low detail.</summary>
    public const int AutoLowMax = 512;

    /// <summary>JS <c>Math.round</c> parity, floored at 1 px: half up, never zero.</summary>
    private static int Shrink(int side, double scale) =>
        Math.Max(1, (int)Math.Floor(side * scale + 0.5));

    /// <summary>ceil(n / d) for positive integers, without floating point.</summary>
    private static int CeilDiv(int n, int d) => (n + d - 1) / d;

    /// <summary>Scale (width, height) per the vision preprocessing pipeline:
    /// 1. fit inside a <see cref="MaxSide"/> x <see cref="MaxSide"/> square
    /// (longest side capped), then 2. cap the shortest side at
    /// <see cref="MaxShortSide"/>. Aspect ratio is preserved; each step is
    /// skipped when already satisfied.</summary>
    public static (int Width, int Height) PreprocessImage(int width, int height)
    {
        int w = width;
        int h = height;
        int longest = Math.Max(w, h);
        if (longest > MaxSide)
        {
            double scale = (double)MaxSide / longest;
            w = Shrink(w, scale);
            h = Shrink(h, scale);
        }
        int shortest = Math.Min(w, h);
        if (shortest > MaxShortSide)
        {
            double scale = (double)MaxShortSide / shortest;
            w = Shrink(w, scale);
            h = Shrink(h, scale);
        }
        return (w, h);
    }

    /// <summary>Estimate the token cost of a width x height image at the given
    /// detail level.
    ///
    /// - <see cref="DetailLevel.Low"/>: fixed <see cref="LowDetailTokens"/>,
    ///   whatever the size.
    /// - <see cref="DetailLevel.High"/>: the image is downscaled by
    ///   <see cref="PreprocessImage"/>, tiled into <see cref="TileSize"/>
    ///   squares, and each tile costs <see cref="TileTokens"/> on top of the
    ///   base.
    /// - <see cref="DetailLevel.Auto"/>: low when both dimensions are
    ///   &lt;= <see cref="AutoLowMax"/>, otherwise high.
    ///
    /// Throws <see cref="ArgumentException"/> for non-positive dimensions or
    /// an unknown detail level.</summary>
    public static TokenBreakdown ImageTokens(int width, int height, DetailLevel detail)
    {
        if (width <= 0 || height <= 0)
        {
            throw new ArgumentException("Width and height must be greater than zero");
        }

        bool resolvedHigh = detail switch
        {
            DetailLevel.Low => false,
            DetailLevel.High => true,
            DetailLevel.Auto => width > AutoLowMax || height > AutoLowMax,
            _ => throw new ArgumentException($"Unknown detail level: {detail}"),
        };

        if (!resolvedHigh)
        {
            return new TokenBreakdown(
                Detail: "low",
                ScaledWidth: width,
                ScaledHeight: height,
                TilesX: 1,
                TilesY: 1,
                Tiles: 1,
                Base: LowDetailTokens,
                DetailTokens: 0,
                Total: LowDetailTokens);
        }

        (int scaledWidth, int scaledHeight) = PreprocessImage(width, height);
        int tilesX = CeilDiv(scaledWidth, TileSize);
        int tilesY = CeilDiv(scaledHeight, TileSize);
        int tiles = tilesX * tilesY;
        int detailTokens = tiles * TileTokens;
        return new TokenBreakdown(
            Detail: "high",
            ScaledWidth: scaledWidth,
            ScaledHeight: scaledHeight,
            TilesX: tilesX,
            TilesY: tilesY,
            Tiles: tiles,
            Base: LowDetailTokens,
            DetailTokens: detailTokens,
            Total: LowDetailTokens + detailTokens);
    }

    /// <summary>Parse a detail-level string ("low" | "high" | "auto") — the
    /// bridge from the TS string union to <see cref="DetailLevel"/>.</summary>
    public static DetailLevel ParseDetail(string detail) => detail switch
    {
        "low" => DetailLevel.Low,
        "high" => DetailLevel.High,
        "auto" => DetailLevel.Auto,
        _ => throw new ArgumentException($"Unknown detail level: {detail}"),
    };
}

Also available in 13 other languages

Every CosmoDev tool ships its pure logic in TypeScript (web) and Go (CLI), with authored implementations in a dozen-plus languages — the same contract, ported. Compare all languages side by side →