Initial version of grid atlas packer, CLI integration

This commit is contained in:
Chlumsky 2023-01-29 00:58:28 +01:00
parent ae441c9989
commit 37ffbd85ee
12 changed files with 971 additions and 80 deletions

View File

@ -88,11 +88,22 @@ Use the following command line arguments for the standalone version of the atlas
Alternativelly, the minimum possible dimensions may be selected automatically if a dimensions constraint is set instead:
- `-pots` – a power-of-two square
- `-potr` – a power-of-two square or rectangle (2:1)
- `-potr` – a power-of-two square or rectangle (typically 2:1 aspect ratio)
- `-square` – any square dimensions
- `-square2` – square with even side length
- `-square4` (default) – square with side length divisible by four
### Uniform grid atlas
By default, glyphs in the atlas have different dimensions and are bin-packed in an irregular fashion to maximize use of space.
With the `-uniformgrid` switch, you can instead force all glyphs to have identical dimensions and be laid out in a grid.
In that case, these additional options are available to customize the layout:
- `-uniformcols <N>` &ndash; sets the number of columns
- `-uniformcell <width> <height>` &ndash; sets the dimensions of the grid's cells
- `-uniformcellconstraint <none / pots / potr / square / square2 / square4>` &ndash; sets constraint for cell dimensions (see explanation of options above)
- `-uniformorigin <yes / no / horizontal / vertical>` &ndash; sets whether the glyph's origin point should be fixed at the same position in each cell
### Outputs
Any non-empty subset of the following may be specified:
@ -105,9 +116,9 @@ Any non-empty subset of the following may be specified:
### Glyph configuration
- `-size <EM size>` &ndash; sets the size of the glyphs in the atlas in pixels per EM
- `-minsize <EM size>` &ndash; sets the minimum size. The largest possible size that fits the same atlas dimensions will be used
- `-emrange <EM range>` &ndash; sets the distance field range in EM's
- `-size <em size>` &ndash; sets the size of the glyphs in the atlas in pixels per em
- `-minsize <em size>` &ndash; sets the minimum size. The largest possible size that fits the same atlas dimensions will be used
- `-emrange <em range>` &ndash; sets the distance field range in em's
- `-pxrange <pixel range>` (default = 2) &ndash; sets the distance field range in output pixels
- `-pxalign <on / off / horizontal / vertical>` (default = vertical) &ndash; enables or disables alignment of glyph's origin point with the pixel grid

View File

@ -92,6 +92,37 @@ void GlyphGeometry::wrapBox(double scale, double range, double miterLimit, bool
}
}
void GlyphGeometry::frameBox(double scale, double range, double miterLimit, int width, int height, const double *fixedX, const double *fixedY) {
scale *= geometryScale;
range /= geometryScale;
box.range = range;
box.scale = scale;
box.rect.w = width;
box.rect.h = height;
if (fixedX && fixedY) {
box.translate.x = *fixedX/geometryScale;
box.translate.y = *fixedY/geometryScale;
} else {
double l = bounds.l, b = bounds.b, r = bounds.r, t = bounds.t;
l -= .5*range, b -= .5*range;
r += .5*range, t += .5*range;
if (miterLimit > 0)
shape.boundMiters(l, b, r, t, .5*range, miterLimit, 1);
if (fixedX)
box.translate.x = *fixedX/geometryScale;
else {
double w = scale*(r-l);
box.translate.x = -l+.5*(box.rect.w-w)/scale;
}
if (fixedY)
box.translate.y = *fixedY/geometryScale;
else {
double h = scale*(t-b);
box.translate.y = -b+.5*(box.rect.h-h)/scale;
}
}
}
void GlyphGeometry::placeBox(int x, int y) {
box.rect.x = x, box.rect.y = y;
}
@ -122,10 +153,18 @@ int GlyphGeometry::getIdentifier(GlyphIdentifierType type) const {
return 0;
}
double GlyphGeometry::getGeometryScale() const {
return geometryScale;
}
const msdfgen::Shape & GlyphGeometry::getShape() const {
return shape;
}
const msdfgen::Shape::Bounds & GlyphGeometry::getShapeBounds() const {
return bounds;
}
double GlyphGeometry::getAdvance() const {
return advance;
}

View File

@ -22,6 +22,8 @@ public:
/// Computes the dimensions of the glyph's box as well as the transformation for the generator function
void wrapBox(double scale, double range, double miterLimit, bool alignOrigin = false);
void wrapBox(double scale, double range, double miterLimit, bool alignOriginX, bool alignOriginY);
/// Computes the glyph's transformation and alignment (unless specified) for given dimensions
void frameBox(double scale, double range, double miterLimit, int width, int height, const double *fixedX, const double *fixedY);
/// Sets the glyph's box's position in the atlas
void placeBox(int x, int y);
/// Sets the glyph's box's rectangle in the atlas
@ -34,8 +36,12 @@ public:
unicode_t getCodepoint() const;
/// Returns the glyph's identifier specified by the supplied identifier type
int getIdentifier(GlyphIdentifierType type) const;
/// Returns the glyph's geometry scale
double getGeometryScale() const;
/// Returns the glyph's shape
const msdfgen::Shape & getShape() const;
/// Returns the glyph's shape's raw bounds
const msdfgen::Shape::Bounds & getShapeBounds() const;
/// Returns the glyph's advance
double getAdvance() const;
/// Returns the glyph's box in the atlas

View File

@ -0,0 +1,537 @@
#include "GridAtlasPacker.h"
#include <algorithm>
namespace msdf_atlas {
static int floorPOT(int x) {
int y = 1;
while (x >= y)
y <<= 1;
return y>>1;
}
static int ceilPOT(int x) {
int y = 1;
while (x > y)
y <<= 1;
return y;
}
static bool squareConstraint(DimensionsConstraint constraint) {
switch (constraint) {
case DimensionsConstraint::SQUARE:
case DimensionsConstraint::EVEN_SQUARE:
case DimensionsConstraint::MULTIPLE_OF_FOUR_SQUARE:
case DimensionsConstraint::POWER_OF_TWO_SQUARE:
return true;
case DimensionsConstraint::NONE:
case DimensionsConstraint::POWER_OF_TWO_RECTANGLE:
return false;
}
return true;
}
void GridAtlasPacker::lowerToConstraint(int &width, int &height, DimensionsConstraint constraint) {
if (squareConstraint(constraint))
width = height = std::min(width, height);
switch (constraint) {
case DimensionsConstraint::NONE:
case DimensionsConstraint::SQUARE:
break;
case DimensionsConstraint::EVEN_SQUARE:
width &= ~1;
height &= ~1;
break;
case DimensionsConstraint::MULTIPLE_OF_FOUR_SQUARE:
width &= ~3;
height &= ~3;
break;
case DimensionsConstraint::POWER_OF_TWO_RECTANGLE:
case DimensionsConstraint::POWER_OF_TWO_SQUARE:
if (width > 0)
width = floorPOT(width);
if (height > 0)
height = floorPOT(height);
break;
}
}
void GridAtlasPacker::raiseToConstraint(int &width, int &height, DimensionsConstraint constraint) {
if (squareConstraint(constraint))
width = height = std::max(width, height);
switch (constraint) {
case DimensionsConstraint::NONE:
case DimensionsConstraint::SQUARE:
break;
case DimensionsConstraint::EVEN_SQUARE:
width += width&1;
height += height&1;
break;
case DimensionsConstraint::MULTIPLE_OF_FOUR_SQUARE:
width += -width&3;
height += -height&3;
break;
case DimensionsConstraint::POWER_OF_TWO_RECTANGLE:
case DimensionsConstraint::POWER_OF_TWO_SQUARE:
if (width > 0)
width = ceilPOT(width);
if (height > 0)
height = ceilPOT(height);
break;
}
}
double GridAtlasPacker::dimensionsRating(int width, int height, bool aligned) const {
return ((double) width*width+(double) height*height)*(aligned ? 1-alignedColumnsBias : 1);
}
GridAtlasPacker::GridAtlasPacker() :
columns(-1), rows(-1),
width(-1), height(-1),
cellWidth(-1), cellHeight(-1),
padding(0),
dimensionsConstraint(DimensionsConstraint::NONE),
cellDimensionsConstraint(DimensionsConstraint::NONE),
hFixed(false), vFixed(false),
scale(-1),
minScale(1),
fixedX(0), fixedY(0),
unitRange(0),
pxRange(0),
miterLimit(0),
scaleMaximizationTolerance(.001),
alignedColumnsBias(.125)
{ }
msdfgen::Shape::Bounds GridAtlasPacker::getMaxBounds(double &maxWidth, double &maxHeight, GlyphGeometry *glyphs, int count, double scale, double range) const {
static const double LARGE_VALUE = 1e240;
msdfgen::Shape::Bounds maxBounds = { +LARGE_VALUE, +LARGE_VALUE, -LARGE_VALUE, -LARGE_VALUE };
for (GlyphGeometry *glyph = glyphs, *end = glyphs+count; glyph < end; ++glyph) {
if (!glyph->isWhitespace()) {
double geometryScale = glyph->getGeometryScale();
double shapeRange = range/geometryScale;
geometryScale *= scale;
const msdfgen::Shape::Bounds &shapeBounds = glyph->getShapeBounds();
double l = shapeBounds.l, b = shapeBounds.b, r = shapeBounds.r, t = shapeBounds.t;
l -= .5*shapeRange, b -= .5*shapeRange;
r += .5*shapeRange, t += .5*shapeRange;
if (miterLimit > 0)
glyph->getShape().boundMiters(l, b, r, t, .5*shapeRange, miterLimit, 1);
l *= geometryScale, b *= geometryScale;
r *= geometryScale, t *= geometryScale;
maxBounds.l = std::min(maxBounds.l, l);
maxBounds.b = std::min(maxBounds.b, b);
maxBounds.r = std::max(maxBounds.r, r);
maxBounds.t = std::max(maxBounds.t, t);
maxWidth = std::max(maxWidth, r-l);
maxHeight = std::max(maxHeight, t-b);
}
}
if (maxBounds.l >= maxBounds.r || maxBounds.b >= maxBounds.t)
maxBounds = msdfgen::Shape::Bounds();
if (hFixed)
maxWidth = maxBounds.r-maxBounds.l;
if (vFixed)
maxHeight = maxBounds.t-maxBounds.b;
return maxBounds;
}
double GridAtlasPacker::scaleToFit(GlyphGeometry *glyphs, int count, int cellWidth, int cellHeight, msdfgen::Shape::Bounds &maxBounds, double &maxWidth, double &maxHeight) const {
static const int BIG_VALUE = 1<<28;
if (cellWidth <= 0)
cellWidth = BIG_VALUE;
if (cellHeight <= 0)
cellHeight = BIG_VALUE;
cellWidth -= padding, cellHeight -= padding;
bool lastResult = false;
#define TRY_FIT(scale) (maxWidth = 0, maxHeight = 0, maxBounds = getMaxBounds(maxWidth, maxHeight, glyphs, count, (scale), unitRange+pxRange/(scale)), lastResult = maxWidth <= cellWidth && maxHeight <= cellHeight)
double minScale = 1, maxScale = 1;
if (TRY_FIT(1)) {
while (maxScale < 1e+32 && ((maxScale = 2*minScale), TRY_FIT(maxScale)))
minScale = maxScale;
} else {
while (minScale > 1e-32 && ((minScale = .5*maxScale), !TRY_FIT(minScale)))
maxScale = minScale;
}
if (minScale == maxScale)
return 0;
while (minScale/maxScale < 1-scaleMaximizationTolerance) {
double midScale = .5*(minScale+maxScale);
if (TRY_FIT(midScale))
minScale = midScale;
else
maxScale = midScale;
}
if (!lastResult)
TRY_FIT(minScale);
return minScale;
}
// Ultra spaghetti
int GridAtlasPacker::pack(GlyphGeometry *glyphs, int count) {
bool cellHeightFinal = cellHeight > 0;
bool explicitRows = rows > 0;
int cellCount = count;
if (!cellCount)
return 0;
if (columns > 0 && rows > 0)
cellCount = columns*rows;
else if (columns > 0)
rows = (cellCount+columns-1)/columns;
else if (rows > 0)
columns = (cellCount+rows-1)/rows;
else if (width > 0 && cellWidth > 0) {
columns = (width+padding)/cellWidth;
rows = (cellCount+columns-1)/columns;
}
bool dimensionsChanged = false;
if (width < 0 && cellWidth > 0 && columns > 0) {
width = columns*cellWidth-padding;
dimensionsChanged = true;
}
if (height < 0 && cellHeight > 0 && rows > 0) {
height = rows*cellHeight-padding;
dimensionsChanged = true;
}
if (dimensionsChanged)
raiseToConstraint(width, height, dimensionsConstraint);
dimensionsChanged = false;
if (cellWidth < 0 && width > 0 && columns > 0) {
cellWidth = (width+padding)/columns;
dimensionsChanged = true;
}
if (cellHeight < 0 && height > 0 && rows > 0) {
cellHeight = (height+padding)/rows;
dimensionsChanged = true;
}
if (dimensionsChanged)
lowerToConstraint(cellWidth, cellHeight, cellDimensionsConstraint);
if ((cellWidth > 0 && cellWidth-padding <= pxRange) || (cellHeight > 0 && cellHeight-padding <= pxRange)) // cells definitely too small
return -1;
if (scale <= 0) {
// If both pxRange and miterLimit is non-zero, miter bounds have to be computed for all potential scales
if (pxRange && miterLimit > 0) {
double maxWidth = 0, maxHeight = 0;
msdfgen::Shape::Bounds maxBounds;
if (cellWidth > 0 || cellHeight > 0) {
scale = scaleToFit(glyphs, count, cellWidth, cellHeight, maxBounds, maxWidth, maxHeight);
if (scale < minScale)
return -1;
}
else if (width > 0 && height > 0) {
double bestAlignedScale = 0;
int bestCols = 0, bestAlignedCols = 0;
for (int q = (int) sqrt(cellCount)+1; q > 0; --q) {
int cols = q;
int rows = (cellCount+cols-1)/cols;
int tWidth = (width+padding)/cols;
int tHeight = (height+padding)/rows;
if (!(tWidth > 0 && tHeight > 0))
continue;
lowerToConstraint(tWidth, tHeight, cellDimensionsConstraint);
double curScale = scaleToFit(glyphs, count, tWidth, tHeight, maxBounds, maxWidth, maxHeight);
if (curScale > scale) {
scale = curScale;
bestCols = cols;
}
if (cols*tWidth == width && curScale > bestAlignedScale) {
bestAlignedScale = curScale;
bestAlignedCols = cols;
}
cols = (cellCount+q-1)/q;
rows = (cellCount+cols-1)/cols;
tWidth = (width+padding)/cols;
tHeight = (height+padding)/rows;
if (!(tWidth > 0 && tHeight > 0))
continue;
lowerToConstraint(tWidth, tHeight, cellDimensionsConstraint);
curScale = scaleToFit(glyphs, count, tWidth, tHeight, maxBounds, maxWidth, maxHeight);
if (curScale > scale) {
scale = curScale;
bestCols = cols;
}
if (cols*tWidth == width && curScale > bestAlignedScale) {
bestAlignedScale = curScale;
bestAlignedCols = cols;
}
}
if (!bestCols)
return -1;
// If columns can be aligned with total width at a slight cost to glyph scale, use that number of columns instead
if (bestAlignedScale >= minScale && (alignedColumnsBias+1)*bestAlignedScale >= scale) {
scale = bestAlignedScale;
bestCols = bestAlignedCols;
}
columns = bestCols;
rows = (cellCount+columns-1)/columns;
cellWidth = (width+padding)/columns;
cellHeight = (height+padding)/rows;
lowerToConstraint(cellWidth, cellHeight, cellDimensionsConstraint);
scale = scaleToFit(glyphs, count, cellWidth, cellHeight, maxBounds, maxWidth, maxHeight);
}
else {
maxBounds = getMaxBounds(maxWidth, maxHeight, glyphs, count, minScale, unitRange+pxRange/minScale);
cellWidth = (int) ceil(maxWidth)+padding;
cellHeight = (int) ceil(maxHeight)+padding;
raiseToConstraint(cellWidth, cellHeight, cellDimensionsConstraint);
scale = scaleToFit(glyphs, count, cellWidth, cellHeight, maxBounds, maxWidth, maxHeight);
if (scale < minScale)
maxBounds = getMaxBounds(maxWidth, maxHeight, glyphs, count, scale = minScale, unitRange+pxRange/minScale);
}
if (!explicitRows && !cellHeightFinal)
cellHeight = (int) ceil(maxHeight)+padding;
fixedX = (-maxBounds.l+.5*(cellWidth-padding-maxWidth))/scale;
fixedY = (-maxBounds.b+.5*(cellHeight-padding-maxHeight))/scale;
} else {
double maxWidth = 0, maxHeight = 0;
msdfgen::Shape::Bounds maxBounds = getMaxBounds(maxWidth, maxHeight, glyphs, count, 1, unitRange);
double hScale = 0, vScale = 0;
if (cellWidth > 0)
hScale = (cellWidth-padding-pxRange)/maxWidth;
if (cellHeight > 0)
vScale = (cellHeight-padding-pxRange)/maxHeight;
if (hScale || vScale) {
if (hScale && vScale)
scale = std::min(hScale, vScale);
else
scale = hScale+vScale;
if (scale < minScale)
return -1;
}
else if (width > 0 && height > 0) {
double bestAlignedScale = 0;
int bestCols = 0, bestAlignedCols = 0;
// TODO sqrtize
for (int cols = 1; cols < width; ++cols) {
int rows = (cellCount+cols-1)/cols;
int tWidth = (width+padding)/cols;
int tHeight = (height+padding)/rows;
if (!(tWidth > 0 && tHeight > 0))
continue;
lowerToConstraint(tWidth, tHeight, cellDimensionsConstraint);
hScale = (tWidth-padding-pxRange)/maxWidth;
vScale = (tHeight-padding-pxRange)/maxHeight;
double curScale = std::min(hScale, vScale);
if (curScale > scale) {
scale = curScale;
bestCols = cols;
}
if (cols*tWidth == width && curScale > bestAlignedScale) {
bestAlignedScale = curScale;
bestAlignedCols = cols;
}
}
if (!bestCols)
return -1;
// If columns can be aligned with total width at a slight cost to glyph scale, use that number of columns instead
if (bestAlignedScale >= minScale && (alignedColumnsBias+1)*bestAlignedScale >= scale) {
scale = bestAlignedScale;
bestCols = bestAlignedCols;
}
columns = bestCols;
rows = (cellCount+columns-1)/columns;
cellWidth = (width+padding)/columns;
cellHeight = (height+padding)/rows;
lowerToConstraint(cellWidth, cellHeight, cellDimensionsConstraint);
}
else {
cellWidth = (int) ceil(minScale*maxWidth+pxRange)+padding;
cellHeight = (int) ceil(minScale*maxHeight+pxRange)+padding;
raiseToConstraint(cellWidth, cellHeight, cellDimensionsConstraint);
hScale = (cellWidth-padding-pxRange)/maxWidth;
vScale = (cellHeight-padding-pxRange)/maxHeight;
scale = std::min(hScale, vScale);
}
if (!explicitRows && !cellHeightFinal)
cellHeight = (int) ceil(scale*maxHeight+pxRange)+padding;
fixedX = -maxBounds.l+.5*((cellWidth-padding)/scale-maxWidth);
fixedY = -maxBounds.b+.5*((cellHeight-padding)/scale-maxHeight);
}
} else {
double maxWidth = 0, maxHeight = 0;
msdfgen::Shape::Bounds maxBounds = getMaxBounds(maxWidth, maxHeight, glyphs, count, scale, unitRange+pxRange/scale);
if (cellWidth < 0 || cellHeight < 0) {
cellWidth = (int) ceil(maxWidth)+padding;
cellHeight = (int) ceil(maxHeight)+padding;
raiseToConstraint(cellWidth, cellHeight, cellDimensionsConstraint);
}
fixedX = (-maxBounds.l+.5*(cellWidth-padding-maxWidth))/scale;
fixedY = (-maxBounds.b+.5*(cellHeight-padding-maxHeight))/scale;
}
if (width < 0 || height < 0) {
if (columns <= 0) {
double bestRating = -1;
for (int q = (int) sqrt(cellCount)+1; q > 0; --q) {
int cols = q;
int rows = (cellCount+cols-1)/cols;
int curWidth = cols*cellWidth, curHeight = rows*cellHeight;
raiseToConstraint(curWidth, curHeight, dimensionsConstraint);
double rating = dimensionsRating(curWidth, curHeight, cols*cellWidth == curWidth);
if (rating < bestRating || bestRating < 0) {
bestRating = rating;
columns = cols;
}
rows = q;
cols = (cellCount+rows-1)/rows;
curWidth = cols*cellWidth, curHeight = rows*cellHeight;
raiseToConstraint(curWidth, curHeight, dimensionsConstraint);
rating = dimensionsRating(curWidth, curHeight, cols*cellWidth == curWidth);
if (rating < bestRating || bestRating < 0) {
bestRating = rating;
columns = cols;
}
}
rows = (cellCount+columns-1)/columns;
}
width = columns*cellWidth, height = rows*cellHeight;
raiseToConstraint(width, height, dimensionsConstraint);
}
if (columns < 0) {
columns = (width+padding)/cellWidth;
rows = (cellCount+columns-1)/columns;
}
int col = 0, row = 0;
for (GlyphGeometry *glyph = glyphs, *end = glyphs+count; glyph < end; ++glyph) {
if (!glyph->isWhitespace()) {
Rectangle rect = { };
glyph->frameBox(scale, unitRange+pxRange/scale, miterLimit, cellWidth-padding, cellHeight-padding, hFixed ? &fixedX : nullptr, vFixed ? &fixedY : nullptr);
glyph->placeBox(col*cellWidth, height-(row+1)*cellHeight);
if (++col >= columns) {
if (++row >= rows) {
return end-glyph-1;
}
col = 0;
}
}
}
if (columns*rows < cellCount) {
// TODO return lower number
}
return 0;
}
void GridAtlasPacker::setFixedOrigin(bool horizontal, bool vertical) {
hFixed = horizontal, vFixed = vertical;
}
void GridAtlasPacker::setCellDimensions(int width, int height) {
cellWidth = width, cellHeight = height;
}
void GridAtlasPacker::unsetCellDimensions() {
cellWidth = -1, cellHeight = -1;
}
void GridAtlasPacker::setCellDimensionsConstraint(DimensionsConstraint dimensionsConstraint) {
cellDimensionsConstraint = dimensionsConstraint;
}
void GridAtlasPacker::setColumns(int columns) {
this->columns = columns;
}
void GridAtlasPacker::setRows(int rows) {
this->rows = rows;
}
void GridAtlasPacker::unsetColumns() {
columns = -1;
}
void GridAtlasPacker::unsetRows() {
rows = -1;
}
void GridAtlasPacker::setDimensions(int width, int height) {
this->width = width, this->height = height;
}
void GridAtlasPacker::unsetDimensions() {
width = -1, height = -1;
}
void GridAtlasPacker::setDimensionsConstraint(DimensionsConstraint dimensionsConstraint) {
this->dimensionsConstraint = dimensionsConstraint;
}
void GridAtlasPacker::setPadding(int padding) {
this->padding = padding;
}
void GridAtlasPacker::setScale(double scale) {
this->scale = scale;
}
void GridAtlasPacker::setMinimumScale(double minScale) {
this->minScale = minScale;
}
void GridAtlasPacker::setUnitRange(double unitRange) {
this->unitRange = unitRange;
}
void GridAtlasPacker::setPixelRange(double pxRange) {
this->pxRange = pxRange;
}
void GridAtlasPacker::setMiterLimit(double miterLimit) {
this->miterLimit = miterLimit;
}
void GridAtlasPacker::getDimensions(int &width, int &height) const {
width = this->width, height = this->height;
}
void GridAtlasPacker::getCellDimensions(int &width, int &height) const {
width = cellWidth, height = cellHeight;
}
int GridAtlasPacker::getColumns() const {
return columns;
}
int GridAtlasPacker::getRows() const {
return rows;
}
double GridAtlasPacker::getScale() const {
return scale;
}
double GridAtlasPacker::getPixelRange() const {
return pxRange;
}
void GridAtlasPacker::getFixedOrigin(double &x, double &y) {
x = fixedX-.5/scale;
y = fixedY-.5/scale;
}
}

View File

@ -0,0 +1,86 @@
#pragma once
#include "GlyphGeometry.h"
namespace msdf_atlas {
/**
* This class computes the layout of a static uniform grid atlas and may optionally
* also find the minimum required dimensions and/or the maximum glyph scale
*/
class GridAtlasPacker {
public:
GridAtlasPacker();
/// Computes the layout for the array of glyphs. Returns 0 on success
int pack(GlyphGeometry *glyphs, int count);
/// Sets whether the origin point should be at the same position in each glyph, separately for horizontal and vertical dimension
void setFixedOrigin(bool horizontal, bool vertical);
void setCellDimensions(int width, int height);
void unsetCellDimensions();
void setCellDimensionsConstraint(DimensionsConstraint dimensionsConstraint);
void setColumns(int columns);
void setRows(int rows);
void unsetColumns();
void unsetRows();
/// Sets the atlas's fixed dimensions
void setDimensions(int width, int height);
/// Sets the atlas's dimensions to be determined during pack
void unsetDimensions();
/// Sets the constraint to be used when determining dimensions
void setDimensionsConstraint(DimensionsConstraint dimensionsConstraint);
/// Sets the padding between glyph boxes
void setPadding(int padding);
/// Sets fixed glyph scale
void setScale(double scale);
/// Sets the minimum glyph scale
void setMinimumScale(double minScale);
/// Sets the unit component of the total distance range
void setUnitRange(double unitRange);
/// Sets the pixel component of the total distance range
void setPixelRange(double pxRange);
/// Sets the miter limit for bounds computation
void setMiterLimit(double miterLimit);
/// Outputs the atlas's final dimensions
void getDimensions(int &width, int &height) const;
void getCellDimensions(int &width, int &height) const;
int getColumns() const;
int getRows() const;
/// Returns the final glyph scale
double getScale() const;
/// Returns the final combined pixel range (including converted unit range)
double getPixelRange() const;
void getFixedOrigin(double &x, double &y);
private:
int columns, rows;
int width, height;
int cellWidth, cellHeight;
int padding;
DimensionsConstraint dimensionsConstraint;
DimensionsConstraint cellDimensionsConstraint;
bool hFixed, vFixed;
double scale;
double minScale;
double fixedX, fixedY;
double unitRange;
double pxRange;
double miterLimit;
double scaleMaximizationTolerance;
double alignedColumnsBias;
static void lowerToConstraint(int &width, int &height, DimensionsConstraint constraint);
static void raiseToConstraint(int &width, int &height, DimensionsConstraint constraint);
double dimensionsRating(int width, int height, bool aligned) const;
msdfgen::Shape::Bounds getMaxBounds(double &maxWidth, double &maxHeight, GlyphGeometry *glyphs, int count, double scale, double range) const;
double scaleToFit(GlyphGeometry *glyphs, int count, int cellWidth, int cellHeight, msdfgen::Shape::Bounds &maxBounds, double &maxWidth, double &maxHeight) const;
};
}

View File

@ -62,6 +62,7 @@ int TightAtlasPacker::tryPack(GlyphGeometry *glyphs, int count, DimensionsConstr
dimensions = packRectangles<SquareSizeSelector<2> >(rectangles.data(), rectangles.size(), padding);
break;
case DimensionsConstraint::SQUARE:
default:
dimensions = packRectangles<SquareSizeSelector<> >(rectangles.data(), rectangles.size(), padding);
break;
}

View File

@ -1,32 +1,24 @@
#pragma once
#include "types.h"
#include "GlyphGeometry.h"
namespace msdf_atlas {
/**
* This class computes the layout of a static atlas and may optionally
* This class computes the layout of a static tightly packed atlas and may optionally
* also find the minimum required dimensions and/or the maximum glyph scale
*/
class TightAtlasPacker {
public:
/// Constraints for the atlas's dimensions - see size selectors for more info
enum class DimensionsConstraint {
POWER_OF_TWO_SQUARE,
POWER_OF_TWO_RECTANGLE,
MULTIPLE_OF_FOUR_SQUARE,
EVEN_SQUARE,
SQUARE
};
TightAtlasPacker();
/// Computes the layout for the array of glyphs. Returns 0 on success
int pack(GlyphGeometry *glyphs, int count);
/// Sets the atlas's dimensions to be fixed
/// Sets the atlas's fixed dimensions
void setDimensions(int width, int height);
/// Sets the atlas's dimensions to be determined during pack
void unsetDimensions();

View File

@ -58,7 +58,7 @@ static const char * imageTypeString(ImageType type) {
return nullptr;
}
bool exportJSON(const FontGeometry *fonts, int fontCount, double fontSize, double pxRange, int atlasWidth, int atlasHeight, ImageType imageType, YDirection yDirection, const char *filename, bool kerning) {
bool exportJSON(const FontGeometry *fonts, int fontCount, ImageType imageType, const JsonAtlasMetrics &metrics, const char *filename, bool kerning) {
FILE *f = fopen(filename, "w");
if (!f)
return false;
@ -68,11 +68,31 @@ bool exportJSON(const FontGeometry *fonts, int fontCount, double fontSize, doubl
fputs("\"atlas\":{", f); {
fprintf(f, "\"type\":\"%s\",", imageTypeString(imageType));
if (imageType == ImageType::SDF || imageType == ImageType::PSDF || imageType == ImageType::MSDF || imageType == ImageType::MTSDF)
fprintf(f, "\"distanceRange\":%.17g,", pxRange);
fprintf(f, "\"size\":%.17g,", fontSize);
fprintf(f, "\"width\":%d,", atlasWidth);
fprintf(f, "\"height\":%d,", atlasHeight);
fprintf(f, "\"yOrigin\":\"%s\"", yDirection == YDirection::TOP_DOWN ? "top" : "bottom");
fprintf(f, "\"distanceRange\":%.17g,", metrics.distanceRange);
fprintf(f, "\"size\":%.17g,", metrics.size);
fprintf(f, "\"width\":%d,", metrics.width);
fprintf(f, "\"height\":%d,", metrics.height);
fprintf(f, "\"yOrigin\":\"%s\"", metrics.yDirection == YDirection::TOP_DOWN ? "top" : "bottom");
if (metrics.grid) {
fputs(",\"grid\":{", f);
fprintf(f, "\"cellWidth\":%d,", metrics.grid->cellWidth);
fprintf(f, "\"cellHeight\":%d,", metrics.grid->cellHeight);
fprintf(f, "\"columns\":%d,", metrics.grid->columns);
fprintf(f, "\"rows\":%d", metrics.grid->rows);
if (metrics.grid->originX)
fprintf(f, ",\"originX\":%.17g", *metrics.grid->originX);
if (metrics.grid->originY) {
switch (metrics.yDirection) {
case YDirection::BOTTOM_UP:
fprintf(f, ",\"originY\":%.17g", *metrics.grid->originY);
break;
case YDirection::TOP_DOWN:
fprintf(f, ",\"originY\":%.17g", (metrics.grid->cellHeight-metrics.grid->padding-1)/metrics.size-*metrics.grid->originY);
break;
}
}
fputs("}", f);
}
} fputs("},", f);
if (fontCount > 1)
@ -89,14 +109,14 @@ bool exportJSON(const FontGeometry *fonts, int fontCount, double fontSize, doubl
// Font metrics
fputs("\"metrics\":{", f); {
double yFactor = yDirection == YDirection::TOP_DOWN ? -1 : 1;
const msdfgen::FontMetrics &metrics = font.getMetrics();
fprintf(f, "\"emSize\":%.17g,", metrics.emSize);
fprintf(f, "\"lineHeight\":%.17g,", metrics.lineHeight);
fprintf(f, "\"ascender\":%.17g,", yFactor*metrics.ascenderY);
fprintf(f, "\"descender\":%.17g,", yFactor*metrics.descenderY);
fprintf(f, "\"underlineY\":%.17g,", yFactor*metrics.underlineY);
fprintf(f, "\"underlineThickness\":%.17g", metrics.underlineThickness);
double yFactor = metrics.yDirection == YDirection::TOP_DOWN ? -1 : 1;
const msdfgen::FontMetrics &fontMetrics = font.getMetrics();
fprintf(f, "\"emSize\":%.17g,", fontMetrics.emSize);
fprintf(f, "\"lineHeight\":%.17g,", fontMetrics.lineHeight);
fprintf(f, "\"ascender\":%.17g,", yFactor*fontMetrics.ascenderY);
fprintf(f, "\"descender\":%.17g,", yFactor*fontMetrics.descenderY);
fprintf(f, "\"underlineY\":%.17g,", yFactor*fontMetrics.underlineY);
fprintf(f, "\"underlineThickness\":%.17g", fontMetrics.underlineThickness);
} fputs("},", f);
// Glyph mapping
@ -116,7 +136,7 @@ bool exportJSON(const FontGeometry *fonts, int fontCount, double fontSize, doubl
double l, b, r, t;
glyph.getQuadPlaneBounds(l, b, r, t);
if (l || b || r || t) {
switch (yDirection) {
switch (metrics.yDirection) {
case YDirection::BOTTOM_UP:
fprintf(f, ",\"planeBounds\":{\"left\":%.17g,\"bottom\":%.17g,\"right\":%.17g,\"top\":%.17g}", l, b, r, t);
break;
@ -127,12 +147,12 @@ bool exportJSON(const FontGeometry *fonts, int fontCount, double fontSize, doubl
}
glyph.getQuadAtlasBounds(l, b, r, t);
if (l || b || r || t) {
switch (yDirection) {
switch (metrics.yDirection) {
case YDirection::BOTTOM_UP:
fprintf(f, ",\"atlasBounds\":{\"left\":%.17g,\"bottom\":%.17g,\"right\":%.17g,\"top\":%.17g}", l, b, r, t);
break;
case YDirection::TOP_DOWN:
fprintf(f, ",\"atlasBounds\":{\"left\":%.17g,\"top\":%.17g,\"right\":%.17g,\"bottom\":%.17g}", l, atlasHeight-t, r, atlasHeight-b);
fprintf(f, ",\"atlasBounds\":{\"left\":%.17g,\"top\":%.17g,\"right\":%.17g,\"bottom\":%.17g}", l, metrics.height-t, r, metrics.height-b);
break;
}
}

View File

@ -8,7 +8,21 @@
namespace msdf_atlas {
struct JsonAtlasMetrics {
struct GridMetrics {
int cellWidth, cellHeight;
int columns, rows;
const double *originX, *originY;
int padding;
};
double distanceRange;
double size;
int width, height;
YDirection yDirection;
const GridMetrics *grid;
};
/// Writes the font and glyph metrics and atlas layout data into a comprehensive JSON file
bool exportJSON(const FontGeometry *fonts, int fontCount, double fontSize, double pxRange, int atlasWidth, int atlasHeight, ImageType imageType, YDirection yDirection, const char *filename, bool kerning);
bool exportJSON(const FontGeometry *fonts, int fontCount, ImageType imageType, const JsonAtlasMetrics &metrics, const char *filename, bool kerning);
}

View File

@ -83,6 +83,16 @@ ATLAS CONFIGURATION
-pots / -potr / -square / -square2 / -square4
Picks the minimum atlas dimensions that fit all glyphs and satisfy the selected constraint:
power of two square / ... rectangle / any square / square with side divisible by 2 / ... 4
-uniformgrid
Lays out the atlas into a uniform grid. Enables following options starting with -uniform:
-uniformcols <N>
Sets the number of grid columns.
-uniformcell <width> <height>
Sets fixed dimensions of the grid's cells.
-uniformcellconstraint <none / pots / potr / square / square2 / square4>
Constrains cell dimensions to the given rule (see -pots / ... above)
-uniformorigin <yes / no / horizontal / vertical>
Sets whether the glyph's origin point should be fixed at the same position in each cell
-yorigin <bottom / top>
Determines whether the Y-axis is oriented upwards (bottom origin, default) or downwards (top origin).
@ -103,12 +113,12 @@ R"(
Generates a Shadron script that uses the generated atlas to draw a sample text as a preview.
GLYPH CONFIGURATION
-size <EM size>
Specifies the size of the glyphs in the atlas bitmap in pixels per EM.
-minsize <EM size>
-size <em size>
Specifies the size of the glyphs in the atlas bitmap in pixels per em.
-minsize <em size>
Specifies the minimum size. The largest possible size that fits the same atlas dimensions will be used.
-emrange <EM range>
Specifies the SDF distance range in EM's.
-emrange <em range>
Specifies the SDF distance range in em's.
-pxrange <pixel range>
Specifies the SDF distance range in output pixels. The default value is 2.
-pxalign <on / off / horizontal / vertical>
@ -253,6 +263,11 @@ struct Configuration {
double angleThreshold;
double miterLimit;
bool alignOriginX, alignOriginY;
struct {
int cellWidth, cellHeight;
int cols, rows;
bool hFixed, vFixed;
} grid;
void (*edgeColoring)(msdfgen::Shape &, double, unsigned long long);
bool expensiveColoring;
unsigned long long coloringSeed;
@ -319,10 +334,12 @@ int main(int argc, const char * const *argv) {
config.imageType = ImageType::MSDF;
config.imageFormat = ImageFormat::UNSPECIFIED;
config.yDirection = YDirection::BOTTOM_UP;
config.grid.vFixed = true;
config.edgeColoring = msdfgen::edgeColoringInkTrap;
config.kerning = true;
const char *imageFormatName = nullptr;
int fixedWidth = -1, fixedHeight = -1;
int fixedCellWidth = -1, fixedCellHeight = -1;
config.preprocessGeometry = (
#ifdef MSDFGEN_USE_SKIA
true
@ -334,13 +351,15 @@ int main(int argc, const char * const *argv) {
config.generatorAttributes.scanlinePass = !config.preprocessGeometry;
double minEmSize = 0;
enum {
/// Range specified in EMs
/// Range specified in ems
RANGE_EM,
/// Range specified in output pixels
RANGE_PIXEL,
} rangeMode = RANGE_PIXEL;
double rangeValue = 0;
TightAtlasPacker::DimensionsConstraint atlasSizeConstraint = TightAtlasPacker::DimensionsConstraint::MULTIPLE_OF_FOUR_SQUARE;
PackingStyle packingStyle = PackingStyle::TIGHT;
DimensionsConstraint atlasSizeConstraint = DimensionsConstraint::NONE;
DimensionsConstraint cellSizeConstraint = DimensionsConstraint::NONE;
config.angleThreshold = DEFAULT_ANGLE_THRESHOLD;
config.miterLimit = DEFAULT_MITER_LIMIT;
config.alignOriginX = false, config.alignOriginY = true;
@ -491,31 +510,31 @@ int main(int argc, const char * const *argv) {
continue;
}
ARG_CASE("-pots", 0) {
atlasSizeConstraint = TightAtlasPacker::DimensionsConstraint::POWER_OF_TWO_SQUARE;
atlasSizeConstraint = DimensionsConstraint::POWER_OF_TWO_SQUARE;
fixedWidth = -1, fixedHeight = -1;
++argPos;
continue;
}
ARG_CASE("-potr", 0) {
atlasSizeConstraint = TightAtlasPacker::DimensionsConstraint::POWER_OF_TWO_RECTANGLE;
atlasSizeConstraint = DimensionsConstraint::POWER_OF_TWO_RECTANGLE;
fixedWidth = -1, fixedHeight = -1;
++argPos;
continue;
}
ARG_CASE("-square", 0) {
atlasSizeConstraint = TightAtlasPacker::DimensionsConstraint::SQUARE;
atlasSizeConstraint = DimensionsConstraint::SQUARE;
fixedWidth = -1, fixedHeight = -1;
++argPos;
continue;
}
ARG_CASE("-square2", 0) {
atlasSizeConstraint = TightAtlasPacker::DimensionsConstraint::EVEN_SQUARE;
atlasSizeConstraint = DimensionsConstraint::EVEN_SQUARE;
fixedWidth = -1, fixedHeight = -1;
++argPos;
continue;
}
ARG_CASE("-square4", 0) {
atlasSizeConstraint = TightAtlasPacker::DimensionsConstraint::MULTIPLE_OF_FOUR_SQUARE;
atlasSizeConstraint = DimensionsConstraint::MULTIPLE_OF_FOUR_SQUARE;
fixedWidth = -1, fixedHeight = -1;
++argPos;
continue;
@ -534,7 +553,7 @@ int main(int argc, const char * const *argv) {
ARG_CASE("-size", 1) {
double s;
if (!(parseDouble(s, argv[++argPos]) && s > 0))
ABORT("Invalid EM size argument. Use -size <EM size> with a positive real number.");
ABORT("Invalid em size argument. Use -size <em size> with a positive real number.");
config.emSize = s;
++argPos;
continue;
@ -542,7 +561,7 @@ int main(int argc, const char * const *argv) {
ARG_CASE("-minsize", 1) {
double s;
if (!(parseDouble(s, argv[++argPos]) && s > 0))
ABORT("Invalid minimum EM size argument. Use -minsize <EM size> with a positive real number.");
ABORT("Invalid minimum em size argument. Use -minsize <em size> with a positive real number.");
minEmSize = s;
++argPos;
continue;
@ -550,7 +569,7 @@ int main(int argc, const char * const *argv) {
ARG_CASE("-emrange", 1) {
double r;
if (!(parseDouble(r, argv[++argPos]) && r >= 0))
ABORT("Invalid range argument. Use -emrange <EM range> with a positive real number.");
ABORT("Invalid range argument. Use -emrange <em range> with a positive real number.");
rangeMode = RANGE_EM;
rangeValue = r;
++argPos;
@ -587,6 +606,63 @@ int main(int argc, const char * const *argv) {
argPos += 2;
continue;
}
ARG_CASE("-uniformgrid", 0) {
packingStyle = PackingStyle::GRID;
++argPos;
continue;
}
ARG_CASE("-uniformcols", 1) {
packingStyle = PackingStyle::GRID;
unsigned c;
if (!(parseUnsigned(c, argv[argPos+1]) && c))
ABORT("Invalid number of grid columns. Use -uniformcols <N> with a positive integer.");
config.grid.cols = c;
argPos += 2;
continue;
}
ARG_CASE("-uniformcell", 2) {
packingStyle = PackingStyle::GRID;
unsigned w, h;
if (!(parseUnsigned(w, argv[argPos+1]) && parseUnsigned(h, argv[argPos+2]) && w && h))
ABORT("Invalid cell dimensions. Use -uniformcell <width> <height> with two positive integers.");
fixedCellWidth = w, fixedCellHeight = h;
argPos += 3;
continue;
}
ARG_CASE("-uniformcellconstraint", 1) {
packingStyle = PackingStyle::GRID;
if (!strcmp(argv[argPos+1], "none") || !strcmp(argv[argPos+1], "rect"))
cellSizeConstraint = DimensionsConstraint::NONE;
else if (!strcmp(argv[argPos+1], "pots"))
cellSizeConstraint = DimensionsConstraint::POWER_OF_TWO_SQUARE;
else if (!strcmp(argv[argPos+1], "potr"))
cellSizeConstraint = DimensionsConstraint::POWER_OF_TWO_RECTANGLE;
else if (!strcmp(argv[argPos+1], "square"))
cellSizeConstraint = DimensionsConstraint::SQUARE;
else if (!strcmp(argv[argPos+1], "square2"))
cellSizeConstraint = DimensionsConstraint::EVEN_SQUARE;
else if (!strcmp(argv[argPos+1], "square4"))
cellSizeConstraint = DimensionsConstraint::MULTIPLE_OF_FOUR_SQUARE;
else
ABORT("Unknown dimensions constaint. Use -uniformcellconstraint with one of: none, pots, potr, square, square2, or square4.");
argPos += 2;
continue;
}
ARG_CASE("-uniformorigin", 1) {
packingStyle = PackingStyle::GRID;
if (!strcmp(argv[argPos+1], "no") || !strcmp(argv[argPos+1], "0") || !strcmp(argv[argPos+1], "none") || !strcmp(argv[argPos+1], "false") || !strcmp(argv[argPos+1], "disabled"))
config.grid.hFixed = false, config.grid.vFixed = false;
else if (!strcmp(argv[argPos+1], "yes") || !strcmp(argv[argPos+1], "1") || !strcmp(argv[argPos+1], "both") || !strcmp(argv[argPos+1], "true") || !strcmp(argv[argPos+1], "enabled"))
config.grid.hFixed = true, config.grid.vFixed = true;
else if (!strcmp(argv[argPos+1], "horizontal") || !strcmp(argv[argPos+1], "h"))
config.grid.hFixed = true, config.grid.vFixed = false;
else if (!strcmp(argv[argPos+1], "vertical") || !strcmp(argv[argPos+1], "v") || !strcmp(argv[argPos+1], "default"))
config.grid.hFixed = false, config.grid.vFixed = true;
else
ABORT("Unknown option for -uniformorigin. Use one of: yes, no, horizontal, vertical.");
argPos += 2;
continue;
}
ARG_CASE("-errorcorrection", 1) {
msdfgen::ErrorCorrectionConfig &ec = config.generatorAttributes.config.errorCorrection;
if (!memcmp(argv[argPos+1], "disable", 7) || !strcmp(argv[argPos+1], "0") || !strcmp(argv[argPos+1], "none")) {
@ -762,11 +838,13 @@ int main(int argc, const char * const *argv) {
fontInputs.push_back(fontInput);
// Fix up configuration based on related values
if (packingStyle == PackingStyle::TIGHT && atlasSizeConstraint == DimensionsConstraint::NONE)
atlasSizeConstraint = DimensionsConstraint::MULTIPLE_OF_FOUR_SQUARE;
if (!(config.imageType == ImageType::PSDF || config.imageType == ImageType::MSDF || config.imageType == ImageType::MTSDF))
config.miterLimit = 0;
if (config.emSize > minEmSize)
minEmSize = config.emSize;
if (!(fixedWidth > 0 && fixedHeight > 0) && !(minEmSize > 0)) {
if (!(fixedWidth > 0 && fixedHeight > 0) && !(fixedCellWidth > 0 && fixedCellHeight > 0) && !(minEmSize > 0)) {
fputs("Neither atlas size nor glyph size selected, using default...\n", stderr);
minEmSize = MSDF_ATLAS_DEFAULT_EM_SIZE;
}
@ -851,6 +929,9 @@ int main(int argc, const char * const *argv) {
config.imageFormat == ImageFormat::BINARY_FLOAT ||
config.imageFormat == ImageFormat::BINARY_FLOAT_BE
);
// TODO: In this case (if padding is -1), the border pixels of each glyph are black, but still computed. For floating-point output, this may play a role.
int padding = config.imageType == ImageType::MSDF || config.imageType == ImageType::MTSDF ? 0 : -1;
double uniformOriginX, uniformOriginY;
// Load fonts
std::vector<GlyphGeometry> glyphs;
@ -975,13 +1056,15 @@ int main(int argc, const char * const *argv) {
}
bool fixedDimensions = fixedWidth >= 0 && fixedHeight >= 0;
bool fixedScale = config.emSize > 0;
switch (packingStyle) {
case PackingStyle::TIGHT: {
TightAtlasPacker atlasPacker;
if (fixedDimensions)
atlasPacker.setDimensions(fixedWidth, fixedHeight);
else
atlasPacker.setDimensionsConstraint(atlasSizeConstraint);
atlasPacker.setPadding(config.imageType == ImageType::MSDF || config.imageType == ImageType::MTSDF ? 0 : -1);
// TODO: In this case (if padding is -1), the border pixels of each glyph are black, but still computed. For floating-point output, this may play a role.
atlasPacker.setPadding(padding);
if (fixedScale)
atlasPacker.setScale(config.emSize);
else
@ -1004,9 +1087,77 @@ int main(int argc, const char * const *argv) {
config.emSize = atlasPacker.getScale();
config.pxRange = atlasPacker.getPixelRange();
if (!fixedScale)
printf("Glyph size: %.9g pixels/EM\n", config.emSize);
printf("Glyph size: %.9g pixels/em\n", config.emSize);
if (!fixedDimensions)
printf("Atlas dimensions: %d x %d\n", config.width, config.height);
break;
}
case PackingStyle::GRID: {
GridAtlasPacker atlasPacker;
atlasPacker.setFixedOrigin(config.grid.hFixed, config.grid.vFixed);
if (fixedCellWidth >= 0 && fixedCellHeight >= 0)
atlasPacker.setCellDimensions(fixedCellWidth, fixedCellHeight);
else
atlasPacker.setCellDimensionsConstraint(cellSizeConstraint);
if (config.grid.cols > 0)
atlasPacker.setColumns(config.grid.cols);
if (fixedDimensions)
atlasPacker.setDimensions(fixedWidth, fixedHeight);
else
atlasPacker.setDimensionsConstraint(atlasSizeConstraint);
atlasPacker.setPadding(padding);
if (fixedScale)
atlasPacker.setScale(config.emSize);
else
atlasPacker.setMinimumScale(minEmSize);
atlasPacker.setPixelRange(pxRange);
atlasPacker.setUnitRange(unitRange);
atlasPacker.setMiterLimit(config.miterLimit);
//atlasPacker.setOriginAlignment(config.alignOriginX, config.alignOriginY);
if (int remaining = atlasPacker.pack(glyphs.data(), glyphs.size())) {
if (remaining < 0) {
ABORT("Failed to pack glyphs into atlas.");
} else {
fprintf(stderr, "Error: Could not fit %d out of %d glyphs into the atlas.\n", remaining, (int) glyphs.size());
return 1;
}
}
atlasPacker.getDimensions(config.width, config.height);
if (!(config.width > 0 && config.height > 0))
ABORT("Unable to determine atlas size.");
config.emSize = atlasPacker.getScale();
config.pxRange = atlasPacker.getPixelRange();
atlasPacker.getCellDimensions(config.grid.cellWidth, config.grid.cellHeight);
config.grid.cols = atlasPacker.getColumns();
config.grid.rows = atlasPacker.getRows();
if (!fixedScale)
printf("Glyph size: %.9g pixels/em\n", config.emSize);
if (config.grid.hFixed || config.grid.vFixed) {
atlasPacker.getFixedOrigin(uniformOriginX, uniformOriginY);
printf("Grid cell origin: ");
if (config.grid.hFixed)
printf("X = %.9g", uniformOriginX);
if (config.grid.hFixed && config.grid.vFixed)
printf(", ");
if (config.grid.vFixed) {
switch (config.yDirection) {
case YDirection::BOTTOM_UP:
printf("Y = %.9g", uniformOriginY);
break;
case YDirection::TOP_DOWN:
printf("Y = %.9g", (config.grid.cellHeight-padding-1)/config.emSize-uniformOriginY);
break;
}
}
printf("\n");
}
printf("Grid cell dimensions: %d x %d\n", config.grid.cellWidth, config.grid.cellHeight);
printf("Atlas dimensions: %d x %d (%d columns x %d rows)\n", config.width, config.height, config.grid.cols, config.grid.rows);
break;
}
}
}
// Generate atlas bitmap
@ -1075,8 +1226,25 @@ int main(int argc, const char * const *argv) {
fputs("Failed to write CSV output file.\n", stderr);
}
}
if (config.jsonFilename) {
if (exportJSON(fonts.data(), fonts.size(), config.emSize, config.pxRange, config.width, config.height, config.imageType, config.yDirection, config.jsonFilename, config.kerning))
JsonAtlasMetrics jsonMetrics = { };
JsonAtlasMetrics::GridMetrics gridMetrics = { };
jsonMetrics.distanceRange = config.pxRange;
jsonMetrics.size = config.emSize;
jsonMetrics.width = config.width, jsonMetrics.height = config.height;
jsonMetrics.yDirection = config.yDirection;
if (packingStyle == PackingStyle::GRID) {
gridMetrics.cellWidth = config.grid.cellWidth, gridMetrics.cellHeight = config.grid.cellHeight;
gridMetrics.columns = config.grid.cols, gridMetrics.rows = config.grid.rows;
if (config.grid.hFixed)
gridMetrics.originX = &uniformOriginX;
if (config.grid.vFixed)
gridMetrics.originY = &uniformOriginY;
gridMetrics.padding = padding;
jsonMetrics.grid = &gridMetrics;
}
if (exportJSON(fonts.data(), fonts.size(), config.imageType, jsonMetrics, config.jsonFilename, config.kerning))
fputs("Glyph layout and metadata written into JSON file.\n", stderr);
else {
result = 1;

View File

@ -26,6 +26,7 @@
#include "AtlasStorage.h"
#include "BitmapAtlasStorage.h"
#include "TightAtlasPacker.h"
#include "GridAtlasPacker.h"
#include "AtlasGenerator.h"
#include "ImmediateAtlasGenerator.h"
#include "DynamicAtlas.h"

View File

@ -49,4 +49,20 @@ enum class YDirection {
TOP_DOWN
};
/// The method of computing the layout of the atlas
enum class PackingStyle {
TIGHT,
GRID
};
/// Constraints for the atlas's dimensions - see size selectors for more info
enum class DimensionsConstraint {
NONE,
SQUARE,
EVEN_SQUARE,
MULTIPLE_OF_FOUR_SQUARE,
POWER_OF_TWO_RECTANGLE,
POWER_OF_TWO_SQUARE
};
}