Grid atlas origin pixel alignment

This commit is contained in:
Chlumsky 2024-03-08 02:07:35 +01:00
parent 37ffbd85ee
commit 0f48aaa727
12 changed files with 165 additions and 103 deletions

View File

@ -1,6 +1,6 @@
MIT License MIT License
Copyright (c) 2020 - 2023 Viktor Chlumsky Copyright (c) 2020 - 2024 Viktor Chlumsky
Permission is hereby granted, free of charge, to any person obtaining a copy Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal of this software and associated documentation files (the "Software"), to deal

View File

@ -102,7 +102,7 @@ In that case, these additional options are available to customize the layout:
- `-uniformcols <N>` &ndash; sets the number of columns - `-uniformcols <N>` &ndash; sets the number of columns
- `-uniformcell <width> <height>` &ndash; sets the dimensions of the grid's cells - `-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) - `-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 - `-uniformorigin <off / on / horizontal / vertical>` &ndash; sets whether the glyph's origin point should be fixed at the same position in each cell
### Outputs ### Outputs
@ -120,7 +120,7 @@ Any non-empty subset of the following may be specified:
- `-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 <off / on / horizontal / vertical>` (default = vertical) &ndash; enables or disables alignment of glyph's origin point with the pixel grid
### Distance field generator settings ### Distance field generator settings

View File

@ -5,6 +5,10 @@
#include <set> #include <set>
#include "types.h" #include "types.h"
#ifndef MSDF_ATLAS_PUBLIC
#define MSDF_ATLAS_PUBLIC
#endif
namespace msdf_atlas { namespace msdf_atlas {
/// Represents a set of Unicode codepoints (characters) /// Represents a set of Unicode codepoints (characters)

View File

@ -51,11 +51,11 @@ void GlyphGeometry::edgeColoring(void (*fn)(msdfgen::Shape &, double, unsigned l
fn(shape, angleThreshold, seed); fn(shape, angleThreshold, seed);
} }
void GlyphGeometry::wrapBox(double scale, double range, double miterLimit, bool alignOrigin) { void GlyphGeometry::wrapBox(double scale, double range, double miterLimit, bool pxAlignOrigin) {
wrapBox(scale, range, miterLimit, alignOrigin, alignOrigin); wrapBox(scale, range, miterLimit, pxAlignOrigin, pxAlignOrigin);
} }
void GlyphGeometry::wrapBox(double scale, double range, double miterLimit, bool alignOriginX, bool alignOriginY) { void GlyphGeometry::wrapBox(double scale, double range, double miterLimit, bool pxAlignOriginX, bool pxAlignOriginY) {
scale *= geometryScale; scale *= geometryScale;
range /= geometryScale; range /= geometryScale;
box.range = range; box.range = range;
@ -66,7 +66,7 @@ void GlyphGeometry::wrapBox(double scale, double range, double miterLimit, bool
r += .5*range, t += .5*range; r += .5*range, t += .5*range;
if (miterLimit > 0) if (miterLimit > 0)
shape.boundMiters(l, b, r, t, .5*range, miterLimit, 1); shape.boundMiters(l, b, r, t, .5*range, miterLimit, 1);
if (alignOriginX) { if (pxAlignOriginX) {
int sl = (int) floor(scale*l-.5); int sl = (int) floor(scale*l-.5);
int sr = (int) ceil(scale*r+.5); int sr = (int) ceil(scale*r+.5);
box.rect.w = sr-sl; box.rect.w = sr-sl;
@ -76,7 +76,7 @@ void GlyphGeometry::wrapBox(double scale, double range, double miterLimit, bool
box.rect.w = (int) ceil(w)+1; box.rect.w = (int) ceil(w)+1;
box.translate.x = -l+.5*(box.rect.w-w)/scale; box.translate.x = -l+.5*(box.rect.w-w)/scale;
} }
if (alignOriginY) { if (pxAlignOriginY) {
int sb = (int) floor(scale*b-.5); int sb = (int) floor(scale*b-.5);
int st = (int) ceil(scale*t+.5); int st = (int) ceil(scale*t+.5);
box.rect.h = st-sb; box.rect.h = st-sb;
@ -92,7 +92,11 @@ 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) { void GlyphGeometry::frameBox(double scale, double range, double miterLimit, int width, int height, const double *fixedX, const double *fixedY, bool pxAlignOrigin) {
frameBox(scale, range, miterLimit, width, height, fixedX, fixedY, pxAlignOrigin, pxAlignOrigin);
}
void GlyphGeometry::frameBox(double scale, double range, double miterLimit, int width, int height, const double *fixedX, const double *fixedY, bool pxAlignOriginX, bool pxAlignOriginY) {
scale *= geometryScale; scale *= geometryScale;
range /= geometryScale; range /= geometryScale;
box.range = range; box.range = range;
@ -110,13 +114,21 @@ void GlyphGeometry::frameBox(double scale, double range, double miterLimit, int
shape.boundMiters(l, b, r, t, .5*range, miterLimit, 1); shape.boundMiters(l, b, r, t, .5*range, miterLimit, 1);
if (fixedX) if (fixedX)
box.translate.x = *fixedX/geometryScale; box.translate.x = *fixedX/geometryScale;
else { else if (pxAlignOriginX) {
int sl = (int) floor(scale*l-.5);
int sr = (int) ceil(scale*r+.5);
box.translate.x = (-sl+(box.rect.w-(sr-sl))/2)/scale;
} else {
double w = scale*(r-l); double w = scale*(r-l);
box.translate.x = -l+.5*(box.rect.w-w)/scale; box.translate.x = -l+.5*(box.rect.w-w)/scale;
} }
if (fixedY) if (fixedY)
box.translate.y = *fixedY/geometryScale; box.translate.y = *fixedY/geometryScale;
else { else if (pxAlignOriginY) {
int sb = (int) floor(scale*b-.5);
int st = (int) ceil(scale*t+.5);
box.translate.y = (-sb+(box.rect.h-(st-sb))/2)/scale;
} else {
double h = scale*(t-b); double h = scale*(t-b);
box.translate.y = -b+.5*(box.rect.h-h)/scale; box.translate.y = -b+.5*(box.rect.h-h)/scale;
} }

View File

@ -20,10 +20,11 @@ public:
/// Applies edge coloring to glyph shape /// Applies edge coloring to glyph shape
void edgeColoring(void (*fn)(msdfgen::Shape &, double, unsigned long long), double angleThreshold, unsigned long long seed); void edgeColoring(void (*fn)(msdfgen::Shape &, double, unsigned long long), double angleThreshold, unsigned long long seed);
/// 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 pxAlignOrigin = false);
void wrapBox(double scale, double range, double miterLimit, bool alignOriginX, bool alignOriginY); void wrapBox(double scale, double range, double miterLimit, bool pxAlignOriginX, bool pxAlignOriginY);
/// Computes the glyph's transformation and alignment (unless specified) for given dimensions /// 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); void frameBox(double scale, double range, double miterLimit, int width, int height, const double *fixedX, const double *fixedY, bool pxAlignOrigin = false);
void frameBox(double scale, double range, double miterLimit, int width, int height, const double *fixedX, const double *fixedY, bool pxAlignOriginX, bool pxAlignOriginY);
/// 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

View File

@ -101,6 +101,7 @@ GridAtlasPacker::GridAtlasPacker() :
unitRange(0), unitRange(0),
pxRange(0), pxRange(0),
miterLimit(0), miterLimit(0),
pxAlignOriginX(false), pxAlignOriginY(false),
scaleMaximizationTolerance(.001), scaleMaximizationTolerance(.001),
alignedColumnsBias(.125) alignedColumnsBias(.125)
{ } { }
@ -131,10 +132,15 @@ msdfgen::Shape::Bounds GridAtlasPacker::getMaxBounds(double &maxWidth, double &m
} }
if (maxBounds.l >= maxBounds.r || maxBounds.b >= maxBounds.t) if (maxBounds.l >= maxBounds.r || maxBounds.b >= maxBounds.t)
maxBounds = msdfgen::Shape::Bounds(); maxBounds = msdfgen::Shape::Bounds();
// If origin is pixel-aligned but not fixed, one pixel has to be added to max dimensions to allow for aligning the origin by shifting by < 1 pixel
if (hFixed) if (hFixed)
maxWidth = maxBounds.r-maxBounds.l; maxWidth = maxBounds.r-maxBounds.l;
else if (pxAlignOriginX)
maxWidth += 1;
if (vFixed) if (vFixed)
maxHeight = maxBounds.t-maxBounds.b; maxHeight = maxBounds.t-maxBounds.b;
else if (pxAlignOriginY)
maxHeight += 1;
return maxBounds; return maxBounds;
} }
@ -144,6 +150,7 @@ double GridAtlasPacker::scaleToFit(GlyphGeometry *glyphs, int count, int cellWid
cellWidth = BIG_VALUE; cellWidth = BIG_VALUE;
if (cellHeight <= 0) if (cellHeight <= 0)
cellHeight = BIG_VALUE; cellHeight = BIG_VALUE;
--cellWidth, --cellHeight; // Implicit half-pixel padding from each side to make sure that no representable values are beyond outermost pixel centers
cellWidth -= padding, cellHeight -= padding; cellWidth -= padding, cellHeight -= padding;
bool lastResult = false; 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) #define TRY_FIT(scale) (maxWidth = 0, maxHeight = 0, maxBounds = getMaxBounds(maxWidth, maxHeight, glyphs, count, (scale), unitRange+pxRange/(scale)), lastResult = maxWidth <= cellWidth && maxHeight <= cellHeight)
@ -169,23 +176,29 @@ double GridAtlasPacker::scaleToFit(GlyphGeometry *glyphs, int count, int cellWid
return minScale; return minScale;
} }
// Ultra spaghetti // TODO tame spaghetti code
int GridAtlasPacker::pack(GlyphGeometry *glyphs, int count) { int GridAtlasPacker::pack(GlyphGeometry *glyphs, int count) {
if (!count)
return 0;
bool cellHeightFinal = cellHeight > 0; bool cellHeightFinal = cellHeight > 0;
bool explicitRows = rows > 0; bool explicitRows = rows > 0;
int cellCount = count; int cellCount = 0;
if (!cellCount)
return 0;
if (columns > 0 && rows > 0) if (columns > 0 && rows > 0)
cellCount = columns*rows; cellCount = columns*rows;
else if (columns > 0) else {
rows = (cellCount+columns-1)/columns; // Count non-whitespace glyphs only
else if (rows > 0) for (const GlyphGeometry *glyph = glyphs, *end = glyphs+count; glyph < end; ++glyph) {
columns = (cellCount+rows-1)/rows; if (!glyph->isWhitespace())
else if (width > 0 && cellWidth > 0) { ++cellCount;
columns = (width+padding)/cellWidth; }
rows = (cellCount+columns-1)/columns; 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; bool dimensionsChanged = false;
@ -212,14 +225,16 @@ int GridAtlasPacker::pack(GlyphGeometry *glyphs, int count) {
if (dimensionsChanged) if (dimensionsChanged)
lowerToConstraint(cellWidth, cellHeight, cellDimensionsConstraint); lowerToConstraint(cellWidth, cellHeight, cellDimensionsConstraint);
if ((cellWidth > 0 && cellWidth-padding <= pxRange) || (cellHeight > 0 && cellHeight-padding <= pxRange)) // cells definitely too small if ((cellWidth > 0 && cellWidth-padding-1 <= pxRange) || (cellHeight > 0 && cellHeight-padding-1 <= pxRange)) // cells definitely too small
return -1; return -1;
msdfgen::Shape::Bounds maxBounds = { };
double maxWidth = 0, maxHeight = 0;
if (scale <= 0) { if (scale <= 0) {
// If both pxRange and miterLimit is non-zero, miter bounds have to be computed for all potential scales // If both pxRange and miterLimit is non-zero, miter bounds have to be computed for all potential scales
if (pxRange && miterLimit > 0) { if (pxRange && miterLimit > 0) {
double maxWidth = 0, maxHeight = 0;
msdfgen::Shape::Bounds maxBounds;
if (cellWidth > 0 || cellHeight > 0) { if (cellWidth > 0 || cellHeight > 0) {
scale = scaleToFit(glyphs, count, cellWidth, cellHeight, maxBounds, maxWidth, maxHeight); scale = scaleToFit(glyphs, count, cellWidth, cellHeight, maxBounds, maxWidth, maxHeight);
@ -283,8 +298,8 @@ int GridAtlasPacker::pack(GlyphGeometry *glyphs, int count) {
else { else {
maxBounds = getMaxBounds(maxWidth, maxHeight, glyphs, count, minScale, unitRange+pxRange/minScale); maxBounds = getMaxBounds(maxWidth, maxHeight, glyphs, count, minScale, unitRange+pxRange/minScale);
cellWidth = (int) ceil(maxWidth)+padding; cellWidth = (int) ceil(maxWidth)+padding+1;
cellHeight = (int) ceil(maxHeight)+padding; cellHeight = (int) ceil(maxHeight)+padding+1;
raiseToConstraint(cellWidth, cellHeight, cellDimensionsConstraint); raiseToConstraint(cellWidth, cellHeight, cellDimensionsConstraint);
scale = scaleToFit(glyphs, count, cellWidth, cellHeight, maxBounds, maxWidth, maxHeight); scale = scaleToFit(glyphs, count, cellWidth, cellHeight, maxBounds, maxWidth, maxHeight);
if (scale < minScale) if (scale < minScale)
@ -292,20 +307,26 @@ int GridAtlasPacker::pack(GlyphGeometry *glyphs, int count) {
} }
if (!explicitRows && !cellHeightFinal) if (!explicitRows && !cellHeightFinal)
cellHeight = (int) ceil(maxHeight)+padding; cellHeight = (int) ceil(maxHeight)+padding+1;
fixedX = (-maxBounds.l+.5*(cellWidth-padding-maxWidth))/scale;
fixedY = (-maxBounds.b+.5*(cellHeight-padding-maxHeight))/scale;
} else { } else {
double maxWidth = 0, maxHeight = 0; maxBounds = getMaxBounds(maxWidth, maxHeight, glyphs, count, 1, unitRange);
msdfgen::Shape::Bounds maxBounds = getMaxBounds(maxWidth, maxHeight, glyphs, count, 1, unitRange); int hSlack = 0, vSlack = 0;
if (pxAlignOriginX && !hFixed) {
maxWidth -= 1; // Added by getMaxBounds
hSlack = 1;
}
if (pxAlignOriginY && !vFixed) {
maxHeight -= 1; // Added by getMaxBounds
vSlack = 1;
}
double hScale = 0, vScale = 0; double hScale = 0, vScale = 0;
if (cellWidth > 0) if (cellWidth > 0)
hScale = (cellWidth-padding-pxRange)/maxWidth; hScale = (cellWidth-hSlack-padding-1-pxRange)/maxWidth;
if (cellHeight > 0) if (cellHeight > 0)
vScale = (cellHeight-padding-pxRange)/maxHeight; vScale = (cellHeight-vSlack-padding-1-pxRange)/maxHeight;
if (hScale || vScale) { if (hScale || vScale) {
if (hScale && vScale) if (hScale && vScale)
scale = std::min(hScale, vScale); scale = std::min(hScale, vScale);
@ -318,7 +339,7 @@ int GridAtlasPacker::pack(GlyphGeometry *glyphs, int count) {
else if (width > 0 && height > 0) { else if (width > 0 && height > 0) {
double bestAlignedScale = 0; double bestAlignedScale = 0;
int bestCols = 0, bestAlignedCols = 0; int bestCols = 0, bestAlignedCols = 0;
// TODO sqrtize // TODO optimize to only test up to sqrt(cellCount) cols and rows like in the above branch (for (int q = (int) sqrt(cellCount)+1; ...)
for (int cols = 1; cols < width; ++cols) { for (int cols = 1; cols < width; ++cols) {
int rows = (cellCount+cols-1)/cols; int rows = (cellCount+cols-1)/cols;
int tWidth = (width+padding)/cols; int tWidth = (width+padding)/cols;
@ -326,8 +347,8 @@ int GridAtlasPacker::pack(GlyphGeometry *glyphs, int count) {
if (!(tWidth > 0 && tHeight > 0)) if (!(tWidth > 0 && tHeight > 0))
continue; continue;
lowerToConstraint(tWidth, tHeight, cellDimensionsConstraint); lowerToConstraint(tWidth, tHeight, cellDimensionsConstraint);
hScale = (tWidth-padding-pxRange)/maxWidth; hScale = (tWidth-hSlack-padding-pxRange)/maxWidth;
vScale = (tHeight-padding-pxRange)/maxHeight; vScale = (tHeight-vSlack-padding-pxRange)/maxHeight;
double curScale = std::min(hScale, vScale); double curScale = std::min(hScale, vScale);
if (curScale > scale) { if (curScale > scale) {
scale = curScale; scale = curScale;
@ -354,32 +375,47 @@ int GridAtlasPacker::pack(GlyphGeometry *glyphs, int count) {
} }
else { else {
cellWidth = (int) ceil(minScale*maxWidth+pxRange)+padding; cellWidth = (int) ceil(minScale*maxWidth+pxRange)+hSlack+padding+1;
cellHeight = (int) ceil(minScale*maxHeight+pxRange)+padding; cellHeight = (int) ceil(minScale*maxHeight+pxRange)+vSlack+padding+1;
raiseToConstraint(cellWidth, cellHeight, cellDimensionsConstraint); raiseToConstraint(cellWidth, cellHeight, cellDimensionsConstraint);
hScale = (cellWidth-padding-pxRange)/maxWidth; hScale = (cellWidth-hSlack-padding-1-pxRange)/maxWidth;
vScale = (cellHeight-padding-pxRange)/maxHeight; vScale = (cellHeight-vSlack-padding-1-pxRange)/maxHeight;
scale = std::min(hScale, vScale); scale = std::min(hScale, vScale);
} }
if (!explicitRows && !cellHeightFinal) if (!explicitRows && !cellHeightFinal)
cellHeight = (int) ceil(scale*maxHeight+pxRange)+padding; cellHeight = (int) ceil(scale*maxHeight+pxRange)+vSlack+padding+1;
fixedX = -maxBounds.l+.5*((cellWidth-padding)/scale-maxWidth); maxBounds.l *= scale, maxBounds.b *= scale;
fixedY = -maxBounds.b+.5*((cellHeight-padding)/scale-maxHeight); maxBounds.r *= scale, maxBounds.t *= scale;
maxWidth *= scale, maxHeight *= scale;
} }
} else {
double maxWidth = 0, maxHeight = 0;
msdfgen::Shape::Bounds maxBounds = getMaxBounds(maxWidth, maxHeight, glyphs, count, scale, unitRange+pxRange/scale);
} else {
maxBounds = getMaxBounds(maxWidth, maxHeight, glyphs, count, scale, unitRange+pxRange/scale);
if (cellWidth < 0 || cellHeight < 0) { if (cellWidth < 0 || cellHeight < 0) {
cellWidth = (int) ceil(maxWidth)+padding; cellWidth = (int) ceil(maxWidth)+padding+1;
cellHeight = (int) ceil(maxHeight)+padding; cellHeight = (int) ceil(maxHeight)+padding+1;
raiseToConstraint(cellWidth, cellHeight, cellDimensionsConstraint); raiseToConstraint(cellWidth, cellHeight, cellDimensionsConstraint);
} }
}
fixedX = (-maxBounds.l+.5*(cellWidth-padding-maxWidth))/scale; // Compute fixed origin
fixedY = (-maxBounds.b+.5*(cellHeight-padding-maxHeight))/scale; if (hFixed) {
if (pxAlignOriginX) {
int sl = (int) floor(maxBounds.l-.5);
int sr = (int) ceil(maxBounds.r+.5);
fixedX = (-sl+(cellWidth-padding-(sr-sl))/2)/scale;
} else
fixedX = (-maxBounds.l+.5*(cellWidth-padding-maxWidth))/scale;
}
if (vFixed) {
if (pxAlignOriginY) {
int sb = (int) floor(maxBounds.b-.5);
int st = (int) ceil(maxBounds.t+.5);
fixedY = (-sb+(cellHeight-padding-(st-sb))/2)/scale;
} else
fixedY = (-maxBounds.b+.5*(cellHeight-padding-maxHeight))/scale;
} }
if (width < 0 || height < 0) { if (width < 0 || height < 0) {
@ -420,7 +456,7 @@ int GridAtlasPacker::pack(GlyphGeometry *glyphs, int count) {
for (GlyphGeometry *glyph = glyphs, *end = glyphs+count; glyph < end; ++glyph) { for (GlyphGeometry *glyph = glyphs, *end = glyphs+count; glyph < end; ++glyph) {
if (!glyph->isWhitespace()) { if (!glyph->isWhitespace()) {
Rectangle rect = { }; Rectangle rect = { };
glyph->frameBox(scale, unitRange+pxRange/scale, miterLimit, cellWidth-padding, cellHeight-padding, hFixed ? &fixedX : nullptr, vFixed ? &fixedY : nullptr); glyph->frameBox(scale, unitRange+pxRange/scale, miterLimit, cellWidth-padding, cellHeight-padding, hFixed ? &fixedX : nullptr, vFixed ? &fixedY : nullptr, pxAlignOriginX, pxAlignOriginY);
glyph->placeBox(col*cellWidth, height-(row+1)*cellHeight); glyph->placeBox(col*cellWidth, height-(row+1)*cellHeight);
if (++col >= columns) { if (++col >= columns) {
if (++row >= rows) { if (++row >= rows) {
@ -431,9 +467,6 @@ int GridAtlasPacker::pack(GlyphGeometry *glyphs, int count) {
} }
} }
if (columns*rows < cellCount) {
// TODO return lower number
}
return 0; return 0;
} }
@ -505,6 +538,14 @@ void GridAtlasPacker::setMiterLimit(double miterLimit) {
this->miterLimit = miterLimit; this->miterLimit = miterLimit;
} }
void GridAtlasPacker::setOriginPixelAlignment(bool align) {
pxAlignOriginX = align, pxAlignOriginY = align;
}
void GridAtlasPacker::setOriginPixelAlignment(bool alignX, bool alignY) {
pxAlignOriginX = alignX, pxAlignOriginY = alignY;
}
void GridAtlasPacker::getDimensions(int &width, int &height) const { void GridAtlasPacker::getDimensions(int &width, int &height) const {
width = this->width, height = this->height; width = this->width, height = this->height;
} }

View File

@ -45,6 +45,9 @@ public:
void setPixelRange(double pxRange); void setPixelRange(double pxRange);
/// Sets the miter limit for bounds computation /// Sets the miter limit for bounds computation
void setMiterLimit(double miterLimit); void setMiterLimit(double miterLimit);
/// Sets whether each glyph's origin point should stay aligned with the pixel grid
void setOriginPixelAlignment(bool align);
void setOriginPixelAlignment(bool alignX, bool alignY);
/// Outputs the atlas's final dimensions /// Outputs the atlas's final dimensions
void getDimensions(int &width, int &height) const; void getDimensions(int &width, int &height) const;
@ -71,6 +74,7 @@ private:
double unitRange; double unitRange;
double pxRange; double pxRange;
double miterLimit; double miterLimit;
bool pxAlignOriginX, pxAlignOriginY;
double scaleMaximizationTolerance; double scaleMaximizationTolerance;
double alignedColumnsBias; double alignedColumnsBias;

View File

@ -17,7 +17,7 @@ TightAtlasPacker::TightAtlasPacker() :
unitRange(0), unitRange(0),
pxRange(0), pxRange(0),
miterLimit(0), miterLimit(0),
alignOriginX(false), alignOriginY(false), pxAlignOriginX(false), pxAlignOriginY(false),
scaleMaximizationTolerance(.001) scaleMaximizationTolerance(.001)
{ } { }
@ -31,7 +31,7 @@ int TightAtlasPacker::tryPack(GlyphGeometry *glyphs, int count, DimensionsConstr
for (GlyphGeometry *glyph = glyphs, *end = glyphs+count; glyph < end; ++glyph) { for (GlyphGeometry *glyph = glyphs, *end = glyphs+count; glyph < end; ++glyph) {
if (!glyph->isWhitespace()) { if (!glyph->isWhitespace()) {
Rectangle rect = { }; Rectangle rect = { };
glyph->wrapBox(scale, range, miterLimit, alignOriginX, alignOriginY); glyph->wrapBox(scale, range, miterLimit, pxAlignOriginX, pxAlignOriginY);
glyph->getBoxSize(rect.w, rect.h); glyph->getBoxSize(rect.w, rect.h);
if (rect.w > 0 && rect.h > 0) { if (rect.w > 0 && rect.h > 0) {
rectangles.push_back(rect); rectangles.push_back(rect);
@ -157,12 +157,12 @@ void TightAtlasPacker::setMiterLimit(double miterLimit) {
this->miterLimit = miterLimit; this->miterLimit = miterLimit;
} }
void TightAtlasPacker::setOriginAlignment(bool align) { void TightAtlasPacker::setOriginPixelAlignment(bool align) {
alignOriginX = align, alignOriginY = align; pxAlignOriginX = align, pxAlignOriginY = align;
} }
void TightAtlasPacker::setOriginAlignment(bool alignX, bool alignY) { void TightAtlasPacker::setOriginPixelAlignment(bool alignX, bool alignY) {
alignOriginX = alignX, alignOriginY = alignY; pxAlignOriginX = alignX, pxAlignOriginY = alignY;
} }
void TightAtlasPacker::getDimensions(int &width, int &height) const { void TightAtlasPacker::getDimensions(int &width, int &height) const {

View File

@ -37,8 +37,8 @@ public:
/// Sets the miter limit for bounds computation /// Sets the miter limit for bounds computation
void setMiterLimit(double miterLimit); void setMiterLimit(double miterLimit);
/// Sets whether each glyph's origin point should stay aligned with the pixel grid /// Sets whether each glyph's origin point should stay aligned with the pixel grid
void setOriginAlignment(bool align); void setOriginPixelAlignment(bool align);
void setOriginAlignment(bool alignX, bool alignY); void setOriginPixelAlignment(bool alignX, bool alignY);
/// Outputs the atlas's final dimensions /// Outputs the atlas's final dimensions
void getDimensions(int &width, int &height) const; void getDimensions(int &width, int &height) const;
@ -56,7 +56,7 @@ private:
double unitRange; double unitRange;
double pxRange; double pxRange;
double miterLimit; double miterLimit;
bool alignOriginX, alignOriginY; bool pxAlignOriginX, pxAlignOriginY;
double scaleMaximizationTolerance; double scaleMaximizationTolerance;
int tryPack(GlyphGeometry *glyphs, int count, DimensionsConstraint dimensionsConstraint, int &width, int &height, double scale) const; int tryPack(GlyphGeometry *glyphs, int count, DimensionsConstraint dimensionsConstraint, int &width, int &height, double scale) const;

View File

@ -2,7 +2,7 @@
/* /*
* MULTI-CHANNEL SIGNED DISTANCE FIELD ATLAS GENERATOR - standalone console program * MULTI-CHANNEL SIGNED DISTANCE FIELD ATLAS GENERATOR - standalone console program
* -------------------------------------------------------------------------------- * --------------------------------------------------------------------------------
* A utility by Viktor Chlumsky, (c) 2020 - 2023 * A utility by Viktor Chlumsky, (c) 2020 - 2024
*/ */
#ifdef MSDF_ATLAS_STANDALONE #ifdef MSDF_ATLAS_STANDALONE
@ -91,7 +91,7 @@ ATLAS CONFIGURATION
Sets fixed dimensions of the grid's cells. Sets fixed dimensions of the grid's cells.
-uniformcellconstraint <none / pots / potr / square / square2 / square4> -uniformcellconstraint <none / pots / potr / square / square2 / square4>
Constrains cell dimensions to the given rule (see -pots / ... above) Constrains cell dimensions to the given rule (see -pots / ... above)
-uniformorigin <yes / no / horizontal / vertical> -uniformorigin <off / on / horizontal / vertical>
Sets whether the glyph's origin point should be fixed at the same position in each cell 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).
@ -121,7 +121,7 @@ GLYPH CONFIGURATION
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 <off / on / horizontal / vertical>
Specifies whether each glyph's origin point should be aligned with the pixel grid. Specifies whether each glyph's origin point should be aligned with the pixel grid.
-nokerning -nokerning
Disables inclusion of kerning pair table in output files. Disables inclusion of kerning pair table in output files.
@ -262,11 +262,11 @@ struct Configuration {
double pxRange; double pxRange;
double angleThreshold; double angleThreshold;
double miterLimit; double miterLimit;
bool alignOriginX, alignOriginY; bool pxAlignOriginX, pxAlignOriginY;
struct { struct {
int cellWidth, cellHeight; int cellWidth, cellHeight;
int cols, rows; int cols, rows;
bool hFixed, vFixed; bool fixedOriginX, fixedOriginY;
} grid; } grid;
void (*edgeColoring)(msdfgen::Shape &, double, unsigned long long); void (*edgeColoring)(msdfgen::Shape &, double, unsigned long long);
bool expensiveColoring; bool expensiveColoring;
@ -334,7 +334,7 @@ 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.grid.fixedOriginX = false, config.grid.fixedOriginY = true;
config.edgeColoring = msdfgen::edgeColoringInkTrap; config.edgeColoring = msdfgen::edgeColoringInkTrap;
config.kerning = true; config.kerning = true;
const char *imageFormatName = nullptr; const char *imageFormatName = nullptr;
@ -362,7 +362,7 @@ int main(int argc, const char * const *argv) {
DimensionsConstraint cellSizeConstraint = 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.pxAlignOriginX = false, config.pxAlignOriginY = true;
config.threadCount = 0; config.threadCount = 0;
// Parse command line // Parse command line
@ -585,16 +585,16 @@ int main(int argc, const char * const *argv) {
continue; continue;
} }
ARG_CASE("-pxalign", 1) { ARG_CASE("-pxalign", 1) {
if (!strcmp(argv[argPos+1], "off") || !memcmp(argv[argPos+1], "disable", 7) || !strcmp(argv[argPos+1], "0") || argv[argPos+1][0] == 'n') if (!strcmp(argv[argPos+1], "off") || !memcmp(argv[argPos+1], "disable", 7) || !strcmp(argv[argPos+1], "0") || !strcmp(argv[argPos+1], "false") || argv[argPos+1][0] == 'n')
config.alignOriginX = false, config.alignOriginY = false; config.pxAlignOriginX = false, config.pxAlignOriginY = false;
else if (!strcmp(argv[argPos+1], "on") || !memcmp(argv[argPos+1], "enable", 6) || !strcmp(argv[argPos+1], "1") || argv[argPos+1][0] == 'y') else if (!strcmp(argv[argPos+1], "on") || !memcmp(argv[argPos+1], "enable", 6) || !strcmp(argv[argPos+1], "1") || !strcmp(argv[argPos+1], "true") || !strcmp(argv[argPos+1], "hv") || argv[argPos+1][0] == 'y')
config.alignOriginX = true, config.alignOriginY = true; config.pxAlignOriginX = true, config.pxAlignOriginY = true;
else if (argv[argPos+1][0] == 'h') else if (argv[argPos+1][0] == 'h')
config.alignOriginX = true, config.alignOriginY = false; config.pxAlignOriginX = true, config.pxAlignOriginY = false;
else if (argv[argPos+1][0] == 'v' || !strcmp(argv[argPos+1], "baseline") || !strcmp(argv[argPos+1], "default")) else if (argv[argPos+1][0] == 'v' || !strcmp(argv[argPos+1], "baseline") || !strcmp(argv[argPos+1], "default"))
config.alignOriginX = false, config.alignOriginY = true; config.pxAlignOriginX = false, config.pxAlignOriginY = true;
else else
ABORT("Unknown -pxalign setting. Use on, off, horizontal, or vertical."); ABORT("Unknown -pxalign setting. Use one of: off, on, horizontal, vertical.");
argPos += 2; argPos += 2;
continue; continue;
} }
@ -650,16 +650,16 @@ int main(int argc, const char * const *argv) {
} }
ARG_CASE("-uniformorigin", 1) { ARG_CASE("-uniformorigin", 1) {
packingStyle = PackingStyle::GRID; 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")) if (!strcmp(argv[argPos+1], "off") || !memcmp(argv[argPos+1], "disable", 7) || !strcmp(argv[argPos+1], "0") || !strcmp(argv[argPos+1], "false") || argv[argPos+1][0] == 'n')
config.grid.hFixed = false, config.grid.vFixed = false; config.grid.fixedOriginX = false, config.grid.fixedOriginY = 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")) else if (!strcmp(argv[argPos+1], "on") || !memcmp(argv[argPos+1], "enable", 6) || !strcmp(argv[argPos+1], "1") || !strcmp(argv[argPos+1], "true") || !strcmp(argv[argPos+1], "hv") || argv[argPos+1][0] == 'y')
config.grid.hFixed = true, config.grid.vFixed = true; config.grid.fixedOriginX = true, config.grid.fixedOriginY = true;
else if (!strcmp(argv[argPos+1], "horizontal") || !strcmp(argv[argPos+1], "h")) else if (argv[argPos+1][0] == 'h')
config.grid.hFixed = true, config.grid.vFixed = false; config.grid.fixedOriginX = true, config.grid.fixedOriginY = false;
else if (!strcmp(argv[argPos+1], "vertical") || !strcmp(argv[argPos+1], "v") || !strcmp(argv[argPos+1], "default")) else if (argv[argPos+1][0] == 'v' || !strcmp(argv[argPos+1], "baseline") || !strcmp(argv[argPos+1], "default"))
config.grid.hFixed = false, config.grid.vFixed = true; config.grid.fixedOriginX = false, config.grid.fixedOriginY = true;
else else
ABORT("Unknown option for -uniformorigin. Use one of: yes, no, horizontal, vertical."); ABORT("Unknown -uniformorigin setting. Use one of: off, on, horizontal, vertical.");
argPos += 2; argPos += 2;
continue; continue;
} }
@ -889,7 +889,7 @@ int main(int argc, const char * const *argv) {
if (!config.arteryFontFilename) { if (!config.arteryFontFilename) {
if (imageExtension != ImageFormat::UNSPECIFIED) if (imageExtension != ImageFormat::UNSPECIFIED)
config.imageFormat = imageExtension; config.imageFormat = imageExtension;
else else if (config.imageFilename)
fputs("Warning: Could not infer image format from file extension, PNG will be used.\n", stderr); fputs("Warning: Could not infer image format from file extension, PNG will be used.\n", stderr);
} }
} }
@ -1072,7 +1072,7 @@ int main(int argc, const char * const *argv) {
atlasPacker.setPixelRange(pxRange); atlasPacker.setPixelRange(pxRange);
atlasPacker.setUnitRange(unitRange); atlasPacker.setUnitRange(unitRange);
atlasPacker.setMiterLimit(config.miterLimit); atlasPacker.setMiterLimit(config.miterLimit);
atlasPacker.setOriginAlignment(config.alignOriginX, config.alignOriginY); atlasPacker.setOriginPixelAlignment(config.pxAlignOriginX, config.pxAlignOriginY);
if (int remaining = atlasPacker.pack(glyphs.data(), glyphs.size())) { if (int remaining = atlasPacker.pack(glyphs.data(), glyphs.size())) {
if (remaining < 0) { if (remaining < 0) {
ABORT("Failed to pack glyphs into atlas."); ABORT("Failed to pack glyphs into atlas.");
@ -1095,7 +1095,7 @@ int main(int argc, const char * const *argv) {
case PackingStyle::GRID: { case PackingStyle::GRID: {
GridAtlasPacker atlasPacker; GridAtlasPacker atlasPacker;
atlasPacker.setFixedOrigin(config.grid.hFixed, config.grid.vFixed); atlasPacker.setFixedOrigin(config.grid.fixedOriginX, config.grid.fixedOriginY);
if (fixedCellWidth >= 0 && fixedCellHeight >= 0) if (fixedCellWidth >= 0 && fixedCellHeight >= 0)
atlasPacker.setCellDimensions(fixedCellWidth, fixedCellHeight); atlasPacker.setCellDimensions(fixedCellWidth, fixedCellHeight);
else else
@ -1114,7 +1114,7 @@ int main(int argc, const char * const *argv) {
atlasPacker.setPixelRange(pxRange); atlasPacker.setPixelRange(pxRange);
atlasPacker.setUnitRange(unitRange); atlasPacker.setUnitRange(unitRange);
atlasPacker.setMiterLimit(config.miterLimit); atlasPacker.setMiterLimit(config.miterLimit);
//atlasPacker.setOriginAlignment(config.alignOriginX, config.alignOriginY); atlasPacker.setOriginPixelAlignment(config.pxAlignOriginX, config.pxAlignOriginY);
if (int remaining = atlasPacker.pack(glyphs.data(), glyphs.size())) { if (int remaining = atlasPacker.pack(glyphs.data(), glyphs.size())) {
if (remaining < 0) { if (remaining < 0) {
ABORT("Failed to pack glyphs into atlas."); ABORT("Failed to pack glyphs into atlas.");
@ -1133,14 +1133,14 @@ int main(int argc, const char * const *argv) {
config.grid.rows = atlasPacker.getRows(); config.grid.rows = atlasPacker.getRows();
if (!fixedScale) if (!fixedScale)
printf("Glyph size: %.9g pixels/em\n", config.emSize); printf("Glyph size: %.9g pixels/em\n", config.emSize);
if (config.grid.hFixed || config.grid.vFixed) { if (config.grid.fixedOriginX || config.grid.fixedOriginY) {
atlasPacker.getFixedOrigin(uniformOriginX, uniformOriginY); atlasPacker.getFixedOrigin(uniformOriginX, uniformOriginY);
printf("Grid cell origin: "); printf("Grid cell origin: ");
if (config.grid.hFixed) if (config.grid.fixedOriginX)
printf("X = %.9g", uniformOriginX); printf("X = %.9g", uniformOriginX);
if (config.grid.hFixed && config.grid.vFixed) if (config.grid.fixedOriginX && config.grid.fixedOriginY)
printf(", "); printf(", ");
if (config.grid.vFixed) { if (config.grid.fixedOriginY) {
switch (config.yDirection) { switch (config.yDirection) {
case YDirection::BOTTOM_UP: case YDirection::BOTTOM_UP:
printf("Y = %.9g", uniformOriginY); printf("Y = %.9g", uniformOriginY);
@ -1237,9 +1237,9 @@ int main(int argc, const char * const *argv) {
if (packingStyle == PackingStyle::GRID) { if (packingStyle == PackingStyle::GRID) {
gridMetrics.cellWidth = config.grid.cellWidth, gridMetrics.cellHeight = config.grid.cellHeight; gridMetrics.cellWidth = config.grid.cellWidth, gridMetrics.cellHeight = config.grid.cellHeight;
gridMetrics.columns = config.grid.cols, gridMetrics.rows = config.grid.rows; gridMetrics.columns = config.grid.cols, gridMetrics.rows = config.grid.rows;
if (config.grid.hFixed) if (config.grid.fixedOriginX)
gridMetrics.originX = &uniformOriginX; gridMetrics.originX = &uniformOriginX;
if (config.grid.vFixed) if (config.grid.fixedOriginY)
gridMetrics.originY = &uniformOriginY; gridMetrics.originY = &uniformOriginY;
gridMetrics.padding = padding; gridMetrics.padding = padding;
jsonMetrics.grid = &gridMetrics; jsonMetrics.grid = &gridMetrics;

View File

@ -4,7 +4,7 @@
/* /*
* MULTI-CHANNEL SIGNED DISTANCE FIELD ATLAS GENERATOR * MULTI-CHANNEL SIGNED DISTANCE FIELD ATLAS GENERATOR
* --------------------------------------------------- * ---------------------------------------------------
* A utility by Viktor Chlumsky, (c) 2020 - 2023 * A utility by Viktor Chlumsky, (c) 2020 - 2024
* Generates compact bitmap font atlases using MSDFgen * Generates compact bitmap font atlases using MSDFgen
*/ */

View File

@ -15,7 +15,7 @@ SquareSizeSelector<MULTIPLE>::SquareSizeSelector(int minArea) : lowerBound(0), u
template <int MULTIPLE> template <int MULTIPLE>
void SquareSizeSelector<MULTIPLE>::updateCurrent() { void SquareSizeSelector<MULTIPLE>::updateCurrent() {
if (upperBound < 0) if (upperBound < 0)
current = 5*lowerBound/4+16/MULTIPLE; current = 5*lowerBound/4+16/MULTIPLE+1;
else else
current = lowerBound+(upperBound-lowerBound)/2; current = lowerBound+(upperBound-lowerBound)/2;
} }