Initial version of grid atlas packer, CLI integration
This commit is contained in:
parent
ae441c9989
commit
37ffbd85ee
19
README.md
19
README.md
|
|
@ -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>` – sets the number of columns
|
||||
- `-uniformcell <width> <height>` – sets the dimensions of the grid's cells
|
||||
- `-uniformcellconstraint <none / pots / potr / square / square2 / square4>` – sets constraint for cell dimensions (see explanation of options above)
|
||||
- `-uniformorigin <yes / no / horizontal / vertical>` – 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>` – sets the size of the glyphs in the atlas in pixels per EM
|
||||
- `-minsize <EM size>` – sets the minimum size. The largest possible size that fits the same atlas dimensions will be used
|
||||
- `-emrange <EM range>` – sets the distance field range in EM's
|
||||
- `-size <em size>` – sets the size of the glyphs in the atlas in pixels per em
|
||||
- `-minsize <em size>` – sets the minimum size. The largest possible size that fits the same atlas dimensions will be used
|
||||
- `-emrange <em range>` – sets the distance field range in em's
|
||||
- `-pxrange <pixel range>` (default = 2) – sets the distance field range in output pixels
|
||||
- `-pxalign <on / off / horizontal / vertical>` (default = vertical) – enables or disables alignment of glyph's origin point with the pixel grid
|
||||
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -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;
|
||||
|
||||
};
|
||||
|
||||
}
|
||||
|
|
@ -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;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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,38 +1056,108 @@ int main(int argc, const char * const *argv) {
|
|||
}
|
||||
bool fixedDimensions = fixedWidth >= 0 && fixedHeight >= 0;
|
||||
bool fixedScale = config.emSize > 0;
|
||||
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.
|
||||
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;
|
||||
switch (packingStyle) {
|
||||
|
||||
case PackingStyle::TIGHT: {
|
||||
TightAtlasPacker atlasPacker;
|
||||
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();
|
||||
if (!fixedScale)
|
||||
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;
|
||||
}
|
||||
|
||||
}
|
||||
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();
|
||||
if (!fixedScale)
|
||||
printf("Glyph size: %.9g pixels/EM\n", config.emSize);
|
||||
if (!fixedDimensions)
|
||||
printf("Atlas dimensions: %d x %d\n", config.width, config.height);
|
||||
}
|
||||
|
||||
// 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;
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
|
|
|
|||
|
|
@ -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
|
||||
};
|
||||
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in New Issue