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: Alternativelly, the minimum possible dimensions may be selected automatically if a dimensions constraint is set instead:
- `-pots` – a power-of-two square - `-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 - `-square` – any square dimensions
- `-square2` – square with even side length - `-square2` – square with even side length
- `-square4` (default) – square with side length divisible by four - `-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 ### Outputs
Any non-empty subset of the following may be specified: 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 ### Glyph configuration
- `-size <EM size>` &ndash; sets the size of the glyphs in the atlas in pixels per EM - `-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 - `-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 - `-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 - `-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 - `-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) { void GlyphGeometry::placeBox(int x, int y) {
box.rect.x = x, box.rect.y = y; box.rect.x = x, box.rect.y = y;
} }
@ -122,10 +153,18 @@ int GlyphGeometry::getIdentifier(GlyphIdentifierType type) const {
return 0; return 0;
} }
double GlyphGeometry::getGeometryScale() const {
return geometryScale;
}
const msdfgen::Shape & GlyphGeometry::getShape() const { const msdfgen::Shape & GlyphGeometry::getShape() const {
return shape; return shape;
} }
const msdfgen::Shape::Bounds & GlyphGeometry::getShapeBounds() const {
return bounds;
}
double GlyphGeometry::getAdvance() const { double GlyphGeometry::getAdvance() const {
return advance; 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 /// 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 alignOrigin = false);
void wrapBox(double scale, double range, double miterLimit, bool alignOriginX, bool alignOriginY); 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 /// Sets the glyph's box's position in the atlas
void placeBox(int x, int y); void placeBox(int x, int y);
/// Sets the glyph's box's rectangle in the atlas /// Sets the glyph's box's rectangle in the atlas
@ -34,8 +36,12 @@ public:
unicode_t getCodepoint() const; unicode_t getCodepoint() const;
/// Returns the glyph's identifier specified by the supplied identifier type /// Returns the glyph's identifier specified by the supplied identifier type
int getIdentifier(GlyphIdentifierType type) const; int getIdentifier(GlyphIdentifierType type) const;
/// Returns the glyph's geometry scale
double getGeometryScale() const;
/// Returns the glyph's shape /// Returns the glyph's shape
const msdfgen::Shape & getShape() const; const msdfgen::Shape & getShape() const;
/// Returns the glyph's shape's raw bounds
const msdfgen::Shape::Bounds & getShapeBounds() const;
/// Returns the glyph's advance /// Returns the glyph's advance
double getAdvance() const; double getAdvance() const;
/// Returns the glyph's box in the atlas /// 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); dimensions = packRectangles<SquareSizeSelector<2> >(rectangles.data(), rectangles.size(), padding);
break; break;
case DimensionsConstraint::SQUARE: case DimensionsConstraint::SQUARE:
default:
dimensions = packRectangles<SquareSizeSelector<> >(rectangles.data(), rectangles.size(), padding); dimensions = packRectangles<SquareSizeSelector<> >(rectangles.data(), rectangles.size(), padding);
break; break;
} }

View File

@ -1,32 +1,24 @@
#pragma once #pragma once
#include "types.h"
#include "GlyphGeometry.h" #include "GlyphGeometry.h"
namespace msdf_atlas { 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 * also find the minimum required dimensions and/or the maximum glyph scale
*/ */
class TightAtlasPacker { class TightAtlasPacker {
public: 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(); TightAtlasPacker();
/// Computes the layout for the array of glyphs. Returns 0 on success /// Computes the layout for the array of glyphs. Returns 0 on success
int pack(GlyphGeometry *glyphs, int count); 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); void setDimensions(int width, int height);
/// Sets the atlas's dimensions to be determined during pack /// Sets the atlas's dimensions to be determined during pack
void unsetDimensions(); void unsetDimensions();

View File

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

View File

@ -8,7 +8,21 @@
namespace msdf_atlas { 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 /// 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 -pots / -potr / -square / -square2 / -square4
Picks the minimum atlas dimensions that fit all glyphs and satisfy the selected constraint: 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 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> -yorigin <bottom / top>
Determines whether the Y-axis is oriented upwards (bottom origin, default) or downwards (top origin). 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. Generates a Shadron script that uses the generated atlas to draw a sample text as a preview.
GLYPH CONFIGURATION GLYPH CONFIGURATION
-size <EM size> -size <em size>
Specifies the size of the glyphs in the atlas bitmap in pixels per EM. Specifies the size of the glyphs in the atlas bitmap in pixels per em.
-minsize <EM size> -minsize <em size>
Specifies the minimum size. The largest possible size that fits the same atlas dimensions will be used. Specifies the minimum size. The largest possible size that fits the same atlas dimensions will be used.
-emrange <EM range> -emrange <em range>
Specifies the SDF distance range in EM's. Specifies the SDF distance range in em's.
-pxrange <pixel range> -pxrange <pixel range>
Specifies the SDF distance range in output pixels. The default value is 2. Specifies the SDF distance range in output pixels. The default value is 2.
-pxalign <on / off / horizontal / vertical> -pxalign <on / off / horizontal / vertical>
@ -253,6 +263,11 @@ struct Configuration {
double angleThreshold; double angleThreshold;
double miterLimit; double miterLimit;
bool alignOriginX, alignOriginY; bool alignOriginX, alignOriginY;
struct {
int cellWidth, cellHeight;
int cols, rows;
bool hFixed, vFixed;
} grid;
void (*edgeColoring)(msdfgen::Shape &, double, unsigned long long); void (*edgeColoring)(msdfgen::Shape &, double, unsigned long long);
bool expensiveColoring; bool expensiveColoring;
unsigned long long coloringSeed; unsigned long long coloringSeed;
@ -319,10 +334,12 @@ int main(int argc, const char * const *argv) {
config.imageType = ImageType::MSDF; config.imageType = ImageType::MSDF;
config.imageFormat = ImageFormat::UNSPECIFIED; config.imageFormat = ImageFormat::UNSPECIFIED;
config.yDirection = YDirection::BOTTOM_UP; config.yDirection = YDirection::BOTTOM_UP;
config.grid.vFixed = true;
config.edgeColoring = msdfgen::edgeColoringInkTrap; config.edgeColoring = msdfgen::edgeColoringInkTrap;
config.kerning = true; config.kerning = true;
const char *imageFormatName = nullptr; const char *imageFormatName = nullptr;
int fixedWidth = -1, fixedHeight = -1; int fixedWidth = -1, fixedHeight = -1;
int fixedCellWidth = -1, fixedCellHeight = -1;
config.preprocessGeometry = ( config.preprocessGeometry = (
#ifdef MSDFGEN_USE_SKIA #ifdef MSDFGEN_USE_SKIA
true true
@ -334,13 +351,15 @@ int main(int argc, const char * const *argv) {
config.generatorAttributes.scanlinePass = !config.preprocessGeometry; config.generatorAttributes.scanlinePass = !config.preprocessGeometry;
double minEmSize = 0; double minEmSize = 0;
enum { enum {
/// Range specified in EMs /// Range specified in ems
RANGE_EM, RANGE_EM,
/// Range specified in output pixels /// Range specified in output pixels
RANGE_PIXEL, RANGE_PIXEL,
} rangeMode = RANGE_PIXEL; } rangeMode = RANGE_PIXEL;
double rangeValue = 0; 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.angleThreshold = DEFAULT_ANGLE_THRESHOLD;
config.miterLimit = DEFAULT_MITER_LIMIT; config.miterLimit = DEFAULT_MITER_LIMIT;
config.alignOriginX = false, config.alignOriginY = true; config.alignOriginX = false, config.alignOriginY = true;
@ -491,31 +510,31 @@ int main(int argc, const char * const *argv) {
continue; continue;
} }
ARG_CASE("-pots", 0) { ARG_CASE("-pots", 0) {
atlasSizeConstraint = TightAtlasPacker::DimensionsConstraint::POWER_OF_TWO_SQUARE; atlasSizeConstraint = DimensionsConstraint::POWER_OF_TWO_SQUARE;
fixedWidth = -1, fixedHeight = -1; fixedWidth = -1, fixedHeight = -1;
++argPos; ++argPos;
continue; continue;
} }
ARG_CASE("-potr", 0) { ARG_CASE("-potr", 0) {
atlasSizeConstraint = TightAtlasPacker::DimensionsConstraint::POWER_OF_TWO_RECTANGLE; atlasSizeConstraint = DimensionsConstraint::POWER_OF_TWO_RECTANGLE;
fixedWidth = -1, fixedHeight = -1; fixedWidth = -1, fixedHeight = -1;
++argPos; ++argPos;
continue; continue;
} }
ARG_CASE("-square", 0) { ARG_CASE("-square", 0) {
atlasSizeConstraint = TightAtlasPacker::DimensionsConstraint::SQUARE; atlasSizeConstraint = DimensionsConstraint::SQUARE;
fixedWidth = -1, fixedHeight = -1; fixedWidth = -1, fixedHeight = -1;
++argPos; ++argPos;
continue; continue;
} }
ARG_CASE("-square2", 0) { ARG_CASE("-square2", 0) {
atlasSizeConstraint = TightAtlasPacker::DimensionsConstraint::EVEN_SQUARE; atlasSizeConstraint = DimensionsConstraint::EVEN_SQUARE;
fixedWidth = -1, fixedHeight = -1; fixedWidth = -1, fixedHeight = -1;
++argPos; ++argPos;
continue; continue;
} }
ARG_CASE("-square4", 0) { ARG_CASE("-square4", 0) {
atlasSizeConstraint = TightAtlasPacker::DimensionsConstraint::MULTIPLE_OF_FOUR_SQUARE; atlasSizeConstraint = DimensionsConstraint::MULTIPLE_OF_FOUR_SQUARE;
fixedWidth = -1, fixedHeight = -1; fixedWidth = -1, fixedHeight = -1;
++argPos; ++argPos;
continue; continue;
@ -534,7 +553,7 @@ int main(int argc, const char * const *argv) {
ARG_CASE("-size", 1) { ARG_CASE("-size", 1) {
double s; double s;
if (!(parseDouble(s, argv[++argPos]) && s > 0)) 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; config.emSize = s;
++argPos; ++argPos;
continue; continue;
@ -542,7 +561,7 @@ int main(int argc, const char * const *argv) {
ARG_CASE("-minsize", 1) { ARG_CASE("-minsize", 1) {
double s; double s;
if (!(parseDouble(s, argv[++argPos]) && s > 0)) 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; minEmSize = s;
++argPos; ++argPos;
continue; continue;
@ -550,7 +569,7 @@ int main(int argc, const char * const *argv) {
ARG_CASE("-emrange", 1) { ARG_CASE("-emrange", 1) {
double r; double r;
if (!(parseDouble(r, argv[++argPos]) && r >= 0)) 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; rangeMode = RANGE_EM;
rangeValue = r; rangeValue = r;
++argPos; ++argPos;
@ -587,6 +606,63 @@ int main(int argc, const char * const *argv) {
argPos += 2; argPos += 2;
continue; 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) { ARG_CASE("-errorcorrection", 1) {
msdfgen::ErrorCorrectionConfig &ec = config.generatorAttributes.config.errorCorrection; msdfgen::ErrorCorrectionConfig &ec = config.generatorAttributes.config.errorCorrection;
if (!memcmp(argv[argPos+1], "disable", 7) || !strcmp(argv[argPos+1], "0") || !strcmp(argv[argPos+1], "none")) { 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); fontInputs.push_back(fontInput);
// Fix up configuration based on related values // 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)) if (!(config.imageType == ImageType::PSDF || config.imageType == ImageType::MSDF || config.imageType == ImageType::MTSDF))
config.miterLimit = 0; config.miterLimit = 0;
if (config.emSize > minEmSize) if (config.emSize > minEmSize)
minEmSize = config.emSize; 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); fputs("Neither atlas size nor glyph size selected, using default...\n", stderr);
minEmSize = MSDF_ATLAS_DEFAULT_EM_SIZE; 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 ||
config.imageFormat == ImageFormat::BINARY_FLOAT_BE 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 // Load fonts
std::vector<GlyphGeometry> glyphs; std::vector<GlyphGeometry> glyphs;
@ -975,13 +1056,15 @@ int main(int argc, const char * const *argv) {
} }
bool fixedDimensions = fixedWidth >= 0 && fixedHeight >= 0; bool fixedDimensions = fixedWidth >= 0 && fixedHeight >= 0;
bool fixedScale = config.emSize > 0; bool fixedScale = config.emSize > 0;
switch (packingStyle) {
case PackingStyle::TIGHT: {
TightAtlasPacker atlasPacker; TightAtlasPacker atlasPacker;
if (fixedDimensions) if (fixedDimensions)
atlasPacker.setDimensions(fixedWidth, fixedHeight); atlasPacker.setDimensions(fixedWidth, fixedHeight);
else else
atlasPacker.setDimensionsConstraint(atlasSizeConstraint); atlasPacker.setDimensionsConstraint(atlasSizeConstraint);
atlasPacker.setPadding(config.imageType == ImageType::MSDF || config.imageType == ImageType::MTSDF ? 0 : -1); atlasPacker.setPadding(padding);
// 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.
if (fixedScale) if (fixedScale)
atlasPacker.setScale(config.emSize); atlasPacker.setScale(config.emSize);
else else
@ -1004,9 +1087,77 @@ int main(int argc, const char * const *argv) {
config.emSize = atlasPacker.getScale(); config.emSize = atlasPacker.getScale();
config.pxRange = atlasPacker.getPixelRange(); config.pxRange = atlasPacker.getPixelRange();
if (!fixedScale) if (!fixedScale)
printf("Glyph size: %.9g pixels/EM\n", config.emSize); printf("Glyph size: %.9g pixels/em\n", config.emSize);
if (!fixedDimensions) if (!fixedDimensions)
printf("Atlas dimensions: %d x %d\n", config.width, config.height); 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 // Generate atlas bitmap
@ -1075,8 +1226,25 @@ int main(int argc, const char * const *argv) {
fputs("Failed to write CSV output file.\n", stderr); fputs("Failed to write CSV output file.\n", stderr);
} }
} }
if (config.jsonFilename) { 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); fputs("Glyph layout and metadata written into JSON file.\n", stderr);
else { else {
result = 1; result = 1;

View File

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

View File

@ -49,4 +49,20 @@ enum class YDirection {
TOP_DOWN 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
};
} }