feae7a22b855ea17109a9c99eb95c3369626ff19
braney
  Mon Jun 29 14:13:49 2026 -0700
pngToLolly: new utility to render a PNG as a bigLolly dot-mosaic track

Each opaque pixel of the downscaled image becomes one stemless lolly: column
-> genomic position, row -> lolly value, pixel color -> itemRgb, and local
image detail varies the dot size. Emits a ready-to-build hub plus makeHub.sh.
By default writes a self-contained synthetic assembly hub; with -db/-pos it
lays the mosaic across a window of an existing assembly as a track hub. Dot
sizes use the browser's standard percent-of-height lolly sizing. Adds a README
covering usage, options, and the canvas-size/dot-size trade-off.

diff --git src/utils/pngToLolly/pngToLolly.c src/utils/pngToLolly/pngToLolly.c
new file mode 100644
index 00000000000..65e6e621c42
--- /dev/null
+++ src/utils/pngToLolly/pngToLolly.c
@@ -0,0 +1,545 @@
+/* pngToLolly - turn a PNG image into a "stemless lolly" bigLolly track, so the
+ * Genome Browser renders the picture as a mosaic of colored dots.  Each opaque
+ * pixel becomes one lolly: its column maps to a genomic position, its row maps
+ * to the lolly value (vertical position), and its color becomes itemRgb.  The
+ * lolly's size can be varied by the amount of local detail (variation) in that
+ * part of the image.
+ *
+ * The program writes everything needed for a self-contained assembly hub
+ * (BED, autoSql, chrom.sizes, a blank synthetic genome, trackDb, hub.txt,
+ * genomes.txt) plus a tiny makeHub.sh that runs faToTwoBit and bedToBigBed. */
+
+/* png.h must come before common.h because of setjmp checking in pngconf.h */
+#include "png.h"
+#include <sys/stat.h>
+#include "common.h"
+#include "linefile.h"
+#include "portable.h"
+#include "hash.h"
+#include "options.h"
+#include "dystring.h"
+#include "obscure.h"
+
+/* The synthetic single-chromosome assembly the dots live on. */
+#define CHROM_NAME "chrImg"
+#define GENOME_DB  "lollyImg"
+
+void usage()
+/* Explain usage and exit. */
+{
+errAbort(
+  "pngToLolly - render a PNG as a stemless-lolly bigLolly track (a dot mosaic).\n"
+  "usage:\n"
+  "   pngToLolly in.png outDir\n"
+  "Writes a ready-to-build assembly hub into outDir.  Then:\n"
+  "   cd outDir && ./makeHub.sh\n"
+  "   # move outDir under ~/public_html and load the printed hubUrl\n"
+  "options:\n"
+  "   -maxDim=N     longest side of the image after downscaling (default 64).\n"
+  "                 Smaller values give smoother lolly-size variation; larger\n"
+  "                 values give more spatial detail.\n"
+  "   -dotSize=N    on-screen dot diameter in pixels (default 8).\n"
+  "   -sizeVar=N    neighborhood radius used to measure local variation\n"
+  "                 (default 1, i.e. a 3x3 window).  0 turns off size variation.\n"
+  "   -sizeLo=F     smallest dot radius, as a fraction of the tiling radius\n"
+  "                 (default 0.65); used for ordinarily-busy areas.\n"
+  "   -sizeBusy=F   extra-small dot radius fraction for the very busiest areas\n"
+  "                 (default 0.4).  The dots shrink from sizeLo toward this only\n"
+  "                 at the high end of busyness, so fine detail stays sharp.\n"
+  "                 Set equal to sizeLo to turn the extra shrink off.\n"
+  "   -sizeHi=F     largest dot radius fraction (default 2.3); by default used\n"
+  "                 for the flattest areas.  Raise sizeLo toward ~1.45 if you want\n"
+  "                 the busy areas to stay overlapped (no white).\n"
+  "   -invertSize   flip it: make busy areas big and flat areas small (default is\n"
+  "                 busier -> smaller dot).\n"
+  "   -jitter=F     random horizontal nudge per dot, as a fraction of a column\n"
+  "                 (default 0.3).  Together with row stagger this breaks up the\n"
+  "                 obvious vertical columns.  0 turns it off.\n"
+  "   -alpha=N      skip pixels whose alpha is below N (0-255, default 128).\n"
+  "   -name=string  short label for the track (default derived from outDir).\n"
+  "   -db=database  instead of a synthetic assembly, emit a track hub for this\n"
+  "                 existing assembly (e.g. hg38).  Requires -pos.\n"
+  "   -pos=chr:s-e  genomic window the mosaic is spread across, when -db is set\n"
+  "                 (e.g. chr19:11089417-11133830).  The image fills the window.\n"
+  );
+}
+
+static struct optionSpec options[] = {
+   {"maxDim", OPTION_INT},
+   {"dotSize", OPTION_INT},
+   {"sizeVar", OPTION_INT},
+   {"sizeLo", OPTION_FLOAT},
+   {"sizeBusy", OPTION_FLOAT},
+   {"sizeHi", OPTION_FLOAT},
+   {"invertSize", OPTION_BOOLEAN},
+   {"jitter", OPTION_FLOAT},
+   {"alpha", OPTION_INT},
+   {"name", OPTION_STRING},
+   {"db", OPTION_STRING},
+   {"pos", OPTION_STRING},
+   {NULL, 0},
+};
+
+/* horizontal sub-column resolution: each dot column spans this many bp, so a
+ * dot can be placed at a fraction of a column to break up the grid. */
+#define SUB 64
+
+/* command line options, with defaults */
+int clMaxDim = 64;
+int clDotSize = 8;
+int clSizeVar = 1;
+double clSizeLo = 0.65;
+double clSizeBusy = 0.4;
+double clSizeHi = 2.3;
+boolean clInvertSize = FALSE;
+double clJitter = 0.3;
+int clAlpha = 128;
+char *clName = NULL;
+char *clDb = NULL;
+char *clPos = NULL;
+
+static double hashJitter(int x, int y)
+/* Deterministic pseudo-random value in [-1,1) from the pixel coordinates, so
+ * the dot scatter is stable across runs without needing a RNG. */
+{
+unsigned h = ((unsigned)x * 73856093u) ^ ((unsigned)y * 19349663u);
+h = h * 2654435761u + 0x9e3779b9u;
+return ((double)((h >> 8) & 0xffff) / 32768.0) - 1.0;
+}
+
+static unsigned char *readPngRgba(char *fileName, int *retWidth, int *retHeight)
+/* Read a PNG into a freshly allocated row-major RGBA buffer (4 bytes/pixel). */
+{
+png_image image;
+zeroBytes(&image, sizeof(image));
+image.version = PNG_IMAGE_VERSION;
+
+if (!png_image_begin_read_from_file(&image, fileName))
+    errAbort("Can't read PNG %s: %s", fileName, image.message);
+image.format = PNG_FORMAT_RGBA;
+
+unsigned char *buffer = needLargeMem(PNG_IMAGE_SIZE(image));
+if (!png_image_finish_read(&image, NULL, buffer, 0, NULL))
+    {
+    png_image_free(&image);
+    errAbort("Error decoding PNG %s: %s", fileName, image.message);
+    }
+*retWidth = image.width;
+*retHeight = image.height;
+return buffer;
+}
+
+static unsigned char *downscale(unsigned char *src, int sw, int sh, int maxDim,
+                                int *retW, int *retH)
+/* Box-average src down so its longest side is at most maxDim.  Averaging is
+ * alpha-weighted so transparent pixels don't darken their neighbors.  Returns a
+ * new RGBA buffer (caller frees); the source dimensions are returned when no
+ * scaling is needed. */
+{
+int big = (sw > sh) ? sw : sh;
+if (maxDim <= 0 || big <= maxDim)
+    {
+    *retW = sw;
+    *retH = sh;
+    return cloneMem(src, (size_t)sw * sh * 4);
+    }
+double scale = (double)maxDim / big;
+int dw = round(sw * scale);
+int dh = round(sh * scale);
+if (dw < 1) dw = 1;
+if (dh < 1) dh = 1;
+
+unsigned char *dst = needLargeMem((size_t)dw * dh * 4);
+int dx, dy;
+for (dy = 0; dy < dh; ++dy)
+    {
+    int sy0 = (int)((double)dy * sh / dh);
+    int sy1 = (int)((double)(dy + 1) * sh / dh);
+    if (sy1 <= sy0) sy1 = sy0 + 1;
+    for (dx = 0; dx < dw; ++dx)
+        {
+        int sx0 = (int)((double)dx * sw / dw);
+        int sx1 = (int)((double)(dx + 1) * sw / dw);
+        if (sx1 <= sx0) sx1 = sx0 + 1;
+        double sumR = 0, sumG = 0, sumB = 0, sumA = 0;
+        int n = 0;
+        int x, y;
+        for (y = sy0; y < sy1; ++y)
+            for (x = sx0; x < sx1; ++x)
+                {
+                unsigned char *p = src + ((size_t)y * sw + x) * 4;
+                double a = p[3];
+                sumR += p[0] * a;
+                sumG += p[1] * a;
+                sumB += p[2] * a;
+                sumA += a;
+                ++n;
+                }
+        unsigned char *o = dst + ((size_t)dy * dw + dx) * 4;
+        if (sumA > 0)
+            {
+            o[0] = (unsigned char)(sumR / sumA + 0.5);
+            o[1] = (unsigned char)(sumG / sumA + 0.5);
+            o[2] = (unsigned char)(sumB / sumA + 0.5);
+            o[3] = (unsigned char)(sumA / n + 0.5);
+            }
+        else
+            o[0] = o[1] = o[2] = o[3] = 0;
+        }
+    }
+*retW = dw;
+*retH = dh;
+return dst;
+}
+
+static double *luminanceMap(unsigned char *img, int w, int h)
+/* Return a w*h array of luminance values (0-255), -1 for transparent pixels. */
+{
+double *lum = needLargeMem((size_t)w * h * sizeof(double));
+int i;
+for (i = 0; i < w * h; ++i)
+    {
+    unsigned char *p = img + (size_t)i * 4;
+    if (p[3] < clAlpha)
+        lum[i] = -1;
+    else
+        lum[i] = 0.299 * p[0] + 0.587 * p[1] + 0.114 * p[2];
+    }
+return lum;
+}
+
+static double localVariation(double *lum, int w, int h, int x, int y, int radius)
+/* Standard deviation of luminance over the window around (x,y), considering
+ * only opaque pixels.  Returns 0 when there isn't enough to compare. */
+{
+double sum = 0, sumSq = 0;
+int n = 0;
+int dx, dy;
+for (dy = -radius; dy <= radius; ++dy)
+    for (dx = -radius; dx <= radius; ++dx)
+        {
+        int nx = x + dx, ny = y + dy;
+        if (nx < 0 || nx >= w || ny < 0 || ny >= h)
+            continue;
+        double v = lum[(size_t)ny * w + nx];
+        if (v < 0)
+            continue;
+        sum += v;
+        sumSq += v * v;
+        ++n;
+        }
+if (n < 2)
+    return 0;
+double mean = sum / n;
+double var = sumSq / n - mean * mean;
+if (var < 0)
+    var = 0;
+return sqrt(var);
+}
+
+void pngToLolly(char *inPng, char *outDir)
+/* Read the PNG, downscale, and write the hub files into outDir. */
+{
+int sw, sh;
+unsigned char *raw = readPngRgba(inPng, &sw, &sh);
+verbose(1, "read %s: %dx%d\n", inPng, sw, sh);
+
+int w = 0, h = 0;
+unsigned char *img = downscale(raw, sw, sh, clMaxDim, &w, &h);
+freez(&raw);
+verbose(1, "rendering as %dx%d dots\n", w, h);
+
+double *lum = (clSizeVar > 0) ? luminanceMap(img, w, h) : NULL;
+
+/* Geometry.  Adjacent dot columns are dotSize pixels apart, and rows are
+ * dotSize pixels apart vertically.  Each column spans SUB bp so a dot can be
+ * placed at a fraction of a column; the genome is therefore w*SUB bp wide and
+ * 1 bp == dotSize/SUB pixels.  See lollyTrack.c for how the value maps onto
+ * usableHeight = trackHeight - LOLLY_DIAMETER - fontHeight. */
+double tileRadius = clDotSize / 2.0;             /* radius that exactly tiles */
+/* The largest dot fraction sets the pixel radius used for the top/bottom
+ * margin and the maxHeightPixels we emit. */
+double maxFrac = (clSizeHi > 1.0) ? clSizeHi : 1.0;
+int maxRadius = (int)ceil(tileRadius * (lum != NULL ? maxFrac : 1.0));
+int lollyDiameter = 2 * (maxRadius + 2);         /* matches LOLLY_DIAMETER */
+int fontHeight = 9;                              /* tl.fontHeight + 1, default font */
+int trackHeight = (h - 1) * clDotSize + lollyDiameter + fontHeight;
+if (trackHeight < lollyDiameter + fontHeight + clDotSize)
+    trackHeight = lollyDiameter + fontHeight + clDotSize;
+int pix = w * clDotSize;
+
+/* Where the dots live.  By default a synthetic single-chromosome assembly with
+ * each column spanning SUB bp.  With -db/-pos the dots instead spread across a
+ * real genomic window on an existing assembly, so the image fills that window. */
+char *chromName = CHROM_NAME;
+long winStart = 0;
+int chromSize = w * SUB;                          /* synthetic chrom length */
+double colSpacing = SUB;                          /* bp between dot columns */
+if (clDb != NULL)
+    {
+    if (clPos == NULL)
+        errAbort("-db requires -pos (e.g. -pos=chr19:11089417-11133830)");
+    /* parse chr:start-end, tolerating commas in the numbers */
+    char *spec = cloneString(clPos);
+    stripChar(spec, ',');
+    char *colon = strrchr(spec, ':');
+    char *dash = colon ? strchr(colon, '-') : NULL;
+    if (colon == NULL || dash == NULL)
+        errAbort("can't parse -pos=%s (expected chr:start-end)", clPos);
+    *colon = *dash = '\0';
+    long e = atol(dash + 1);
+    winStart = atol(colon + 1) - 1;               /* 1-based display -> 0-based */
+    if (winStart < 0)
+        winStart = 0;
+    if (e <= winStart)
+        errAbort("-pos end must be greater than start: %s", clPos);
+    chromName = cloneString(spec);
+    colSpacing = (double)(e - winStart) / w;
+    chromSize = e;                                /* only used for clamping */
+    }
+
+makeDirsOnPath(outDir);
+char path[PATH_LEN];
+
+/* The BED: bed9 + a lolly-size field.  Color comes from itemRgb (field 9);
+ * the score field carries the row value that becomes the vertical position. */
+safef(path, sizeof(path), "%s/img.bed", outDir);
+FILE *bed = mustOpen(path, "w");
+int x, y;
+long dots = 0;
+int minSize = BIGNUM, maxSize = 0;
+for (y = 0; y < h; ++y)
+    {
+    for (x = 0; x < w; ++x)
+        {
+        unsigned char *p = img + ((size_t)y * w + x) * 4;
+        if (p[3] < clAlpha)
+            continue;
+        int val = (h - 1) - y;                   /* top row -> highest value */
+
+        /* lolly radius in pixels, from local variation.  By default busier
+         * areas (more variation) get smaller dots, flat areas get bigger ones;
+         * -invertSize flips that. */
+        double radius = tileRadius;
+        double busy = 0.5;
+        if (lum != NULL)
+            {
+            busy = localVariation(lum, w, h, x, y, clSizeVar) / 80.0;
+            if (busy > 1.0) busy = 1.0;
+            busy = pow(busy, 0.6);               /* push more areas toward small */
+            if (clInvertSize) busy = 1.0 - busy;
+            double frac = clSizeHi + (clSizeLo - clSizeHi) * busy;
+            /* The very busiest areas shrink further, from sizeLo toward sizeBusy.
+             * The cubic concentrates this at the high end so ordinarily-busy
+             * areas keep their sizeLo dots. */
+            if (clSizeBusy < clSizeLo)
+                frac -= (clSizeLo - clSizeBusy) * pow(busy, 3.0);
+            radius = tileRadius * frac;
+            }
+        /* The browser renders the lolly radius as sizeField * trackHeight / 100
+         * (see lollyTrack.c), so convert our desired pixel radius into that
+         * percent-of-height unit.  trackHeight is large, so this quantizes to a
+         * few integer levels - that is an inherent limit of the percent scheme. */
+        int sizeVal = round(radius * 100.0 / trackHeight);
+        if (sizeVal < 1)
+            sizeVal = 1;
+        if (sizeVal < minSize) minSize = sizeVal;
+        if (sizeVal > maxSize) maxSize = sizeVal;
+
+        /* Horizontal placement, in fractions of a column.  Stagger alternate
+         * rows by half a column and add a size-weighted random nudge so the
+         * dots don't line up into obvious vertical stripes. */
+        double offset = (y & 1) ? 0.5 : 0.0;
+        offset += clJitter * (1.0 + busy) * hashJitter(x, y);
+        double centerBp = winStart + (x + 0.5 + offset) * colSpacing;
+        long start = (long)floor(centerBp);      /* lolly center uses start+0.5 */
+        if (start < winStart)
+            start = winStart;
+        if (start > chromSize - 1)
+            start = chromSize - 1;
+
+        fprintf(bed, "%s\t%ld\t%ld\tp%d_%d\t%d\t.\t%ld\t%ld\t%d,%d,%d\t%d\n",
+            chromName, start, start + 1, x, y, val, start, start + 1,
+            p[0], p[1], p[2], sizeVal);
+        ++dots;
+        }
+    }
+carefulClose(&bed);
+verbose(1, "wrote %ld dots; %d size level(s), rendered radius %g-%g px\n",
+    dots, maxSize - minSize + 1, minSize * trackHeight / 100.0,
+    maxSize * trackHeight / 100.0);
+
+/* autoSql for the bed9+1 */
+safef(path, sizeof(path), "%s/lolly.as", outDir);
+FILE *as = mustOpen(path, "w");
+fprintf(as,
+    "table lolly\n"
+    "\"stemless lolly mosaic\"\n"
+    "    (\n"
+    "    string chrom;      \"Reference sequence chromosome\"\n"
+    "    uint   chromStart; \"Start position in chromosome\"\n"
+    "    uint   chromEnd;   \"End position in chromosome\"\n"
+    "    string name;       \"Pixel id\"\n"
+    "    uint   score;      \"Row value (vertical position)\"\n"
+    "    char[1] strand;    \"Unused\"\n"
+    "    uint   thickStart; \"Unused\"\n"
+    "    uint   thickEnd;   \"Unused\"\n"
+    "    uint   itemRgb;    \"Pixel color\"\n"
+    "    uint   lollySize;  \"Dot size (percent of track height *100)\"\n"
+    "    )\n");
+carefulClose(&as);
+
+/* chrom.sizes and a blank synthetic genome.  For a track hub on an existing
+ * assembly we skip these; makeHub.sh fetches the real chrom.sizes instead. */
+if (clDb == NULL)
+    {
+    safef(path, sizeof(path), "%s/chrom.sizes", outDir);
+    FILE *cs = mustOpen(path, "w");
+    fprintf(cs, "%s\t%d\n", CHROM_NAME, chromSize);
+    carefulClose(&cs);
+
+    safef(path, sizeof(path), "%s/img.fa", outDir);
+    FILE *fa = mustOpen(path, "w");
+    fprintf(fa, ">%s\n", CHROM_NAME);
+    int col;
+    for (col = 0; col < chromSize; ++col)
+        {
+        fputc('N', fa);
+        if ((col % 60) == 59)
+            fputc('\n', fa);
+        }
+    if ((chromSize % 60) != 0)
+        fputc('\n', fa);
+    carefulClose(&fa);
+    }
+
+char *name = clName;
+if (name == NULL)
+    {
+    char dir[PATH_LEN], base[FILENAME_LEN], ext[FILEEXT_LEN];
+    splitPath(outDir, dir, base, ext);
+    name = cloneString(base[0] ? base : "lollyImg");
+    }
+
+/* trackDb.txt.  Squish visibility gives clean filled dots (no black outline).
+ * Squish halves the configured height, so maxHeightPixels is set to 2*height.
+ * lollyMaxSize is the biggest rendered radius (maxSize * trackHeight / 100), so
+ * the top/bottom margin (LOLLY_DIAMETER) leaves room for the largest dot. */
+int lollyMaxSizePx = (int)ceil(maxSize * trackHeight / 100.0);
+safef(path, sizeof(path), "%s/trackDb.txt", outDir);
+FILE *tdb = mustOpen(path, "w");
+fprintf(tdb,
+    "track %s\n"
+    "type bigLolly\n"
+    "shortLabel %s\n"
+    "longLabel %s lolly mosaic\n"
+    "bigDataUrl img.bb\n"
+    "visibility squish\n"
+    "lollyNoStems on\n"
+    "lollySizeField lollySize\n"
+    "lollyMaxSize %d\n"
+    "yAxisNumLabels off\n"
+    "autoScale off\n"
+    "viewLimits 0:%d\n"
+    "maxHeightPixels %d:%d:1\n"
+    "itemRgb on\n",
+    GENOME_DB, name, name, lollyMaxSizePx, h - 1, 2 * trackHeight, 2 * trackHeight);
+carefulClose(&tdb);
+
+/* hub.txt and genomes.txt for a single-genome assembly hub */
+safef(path, sizeof(path), "%s/hub.txt", outDir);
+FILE *hub = mustOpen(path, "w");
+fprintf(hub,
+    "hub %sLolly\n"
+    "shortLabel %s lolly\n"
+    "longLabel %s rendered as a stemless lolly mosaic\n"
+    "genomesFile genomes.txt\n"
+    "email %s\n",
+    name, name, name, "genome-www@soe.ucsc.edu");
+carefulClose(&hub);
+
+safef(path, sizeof(path), "%s/genomes.txt", outDir);
+FILE *gen = mustOpen(path, "w");
+if (clDb != NULL)
+    fprintf(gen,
+        "genome %s\n"
+        "trackDb trackDb.txt\n",
+        clDb);
+else
+    fprintf(gen,
+        "genome %s\n"
+        "trackDb trackDb.txt\n"
+        "twoBitPath img.2bit\n"
+        "organism Lolly art\n"
+        "scientificName %s\n"
+        "description %s lolly mosaic\n"
+        "defaultPos %s:1-%d\n",
+        GENOME_DB, name, name, CHROM_NAME, chromSize);
+carefulClose(&gen);
+
+/* makeHub.sh - turns the text/data files into the 2bit and bigBed */
+safef(path, sizeof(path), "%s/makeHub.sh", outDir);
+FILE *shf = mustOpen(path, "w");
+if (clDb != NULL)
+    fprintf(shf,
+        "#!/bin/sh\n"
+        "# Build the bigLolly for this track hub.  Run from this directory.\n"
+        "set -e\n"
+        "fetchChromSizes %s > chrom.sizes\n"
+        "bedToBigBed -sort -as=lolly.as -type=bed9+1 -tab img.bed chrom.sizes img.bb\n"
+        "echo 'done - move this directory under ~/public_html and load hub.txt'\n",
+        clDb);
+else
+    fprintf(shf,
+        "#!/bin/sh\n"
+        "# Build the 2bit and bigLolly for this hub.  Run from this directory.\n"
+        "set -e\n"
+        "faToTwoBit img.fa img.2bit\n"
+        "bedToBigBed -sort -as=lolly.as -type=bed9+1 -tab img.bed chrom.sizes img.bb\n"
+        "echo 'done - move this directory under ~/public_html and load hub.txt'\n");
+carefulClose(&shf);
+chmod(path, 0755);
+
+verbose(1, "wrote hub to %s\n", outDir);
+printf("\n");
+printf("Next steps:\n");
+printf("  cd %s && ./makeHub.sh\n", outDir);
+printf("  # then move %s under ~/public_html and load:\n", outDir);
+if (clDb != NULL)
+    printf("  https://hgwdev-braney.gi.ucsc.edu/cgi-bin/hgTracks?"
+           "db=%s&hubUrl=<your-public-url>/%s/hub.txt"
+           "&position=%s:%ld-%d&pix=%d&%s=squish\n",
+           clDb, name, chromName, winStart + 1, chromSize, pix, GENOME_DB);
+else
+    printf("  https://hgwdev-braney.gi.ucsc.edu/cgi-bin/hgTracks?"
+           "hubUrl=<your-public-url>/%s/hub.txt"
+           "&genome=%s&position=%s:1-%d&pix=%d&%s=squish\n",
+           name, GENOME_DB, CHROM_NAME, chromSize, pix, GENOME_DB);
+
+freez(&img);
+freez(&lum);
+}
+
+int main(int argc, char *argv[])
+/* Process command line. */
+{
+optionInit(&argc, argv, options);
+if (argc != 3)
+    usage();
+clMaxDim = optionInt("maxDim", clMaxDim);
+clDotSize = optionInt("dotSize", clDotSize);
+clSizeVar = optionInt("sizeVar", clSizeVar);
+clSizeLo = optionFloat("sizeLo", clSizeLo);
+clSizeBusy = optionFloat("sizeBusy", clSizeBusy);
+clSizeHi = optionFloat("sizeHi", clSizeHi);
+clInvertSize = optionExists("invertSize");
+clJitter = optionFloat("jitter", clJitter);
+clAlpha = optionInt("alpha", clAlpha);
+clName = optionVal("name", clName);
+clDb = optionVal("db", clDb);
+clPos = optionVal("pos", clPos);
+if (clDotSize < 2)
+    errAbort("-dotSize must be at least 2");
+pngToLolly(argv[1], argv[2]);
+return 0;
+}