diff --git a/README.md b/README.md index 97534c8..892e60f 100644 --- a/README.md +++ b/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 ` – sets the number of columns +- `-uniformcell ` – sets the dimensions of the grid's cells +- `-uniformcellconstraint ` – sets constraint for cell dimensions (see explanation of options above) +- `-uniformorigin ` – 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 ` – sets the size of the glyphs in the atlas in pixels per EM -- `-minsize ` – sets the minimum size. The largest possible size that fits the same atlas dimensions will be used -- `-emrange ` – sets the distance field range in EM's +- `-size ` – sets the size of the glyphs in the atlas in pixels per em +- `-minsize ` – sets the minimum size. The largest possible size that fits the same atlas dimensions will be used +- `-emrange ` – sets the distance field range in em's - `-pxrange ` (default = 2) – sets the distance field range in output pixels - `-pxalign ` (default = vertical) – enables or disables alignment of glyph's origin point with the pixel grid diff --git a/msdf-atlas-gen/GlyphGeometry.cpp b/msdf-atlas-gen/GlyphGeometry.cpp index dfb4a17..6ab1f34 100644 --- a/msdf-atlas-gen/GlyphGeometry.cpp +++ b/msdf-atlas-gen/GlyphGeometry.cpp @@ -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; } diff --git a/msdf-atlas-gen/GlyphGeometry.h b/msdf-atlas-gen/GlyphGeometry.h index e3ffe20..dc3f0da 100644 --- a/msdf-atlas-gen/GlyphGeometry.h +++ b/msdf-atlas-gen/GlyphGeometry.h @@ -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 diff --git a/msdf-atlas-gen/GridAtlasPacker.cpp b/msdf-atlas-gen/GridAtlasPacker.cpp new file mode 100644 index 0000000..29dd5b2 --- /dev/null +++ b/msdf-atlas-gen/GridAtlasPacker.cpp @@ -0,0 +1,537 @@ + +#include "GridAtlasPacker.h" + +#include + +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; +} + +} diff --git a/msdf-atlas-gen/GridAtlasPacker.h b/msdf-atlas-gen/GridAtlasPacker.h new file mode 100644 index 0000000..177464d --- /dev/null +++ b/msdf-atlas-gen/GridAtlasPacker.h @@ -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; + +}; + +} diff --git a/msdf-atlas-gen/TightAtlasPacker.cpp b/msdf-atlas-gen/TightAtlasPacker.cpp index 6c66678..87fe4ad 100644 --- a/msdf-atlas-gen/TightAtlasPacker.cpp +++ b/msdf-atlas-gen/TightAtlasPacker.cpp @@ -62,6 +62,7 @@ int TightAtlasPacker::tryPack(GlyphGeometry *glyphs, int count, DimensionsConstr dimensions = packRectangles >(rectangles.data(), rectangles.size(), padding); break; case DimensionsConstraint::SQUARE: + default: dimensions = packRectangles >(rectangles.data(), rectangles.size(), padding); break; } diff --git a/msdf-atlas-gen/TightAtlasPacker.h b/msdf-atlas-gen/TightAtlasPacker.h index 70849e6..b89732a 100644 --- a/msdf-atlas-gen/TightAtlasPacker.h +++ b/msdf-atlas-gen/TightAtlasPacker.h @@ -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(); diff --git a/msdf-atlas-gen/json-export.cpp b/msdf-atlas-gen/json-export.cpp index 2bf24dc..b04f004 100644 --- a/msdf-atlas-gen/json-export.cpp +++ b/msdf-atlas-gen/json-export.cpp @@ -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; } } diff --git a/msdf-atlas-gen/json-export.h b/msdf-atlas-gen/json-export.h index fe7bc04..0d353ba 100644 --- a/msdf-atlas-gen/json-export.h +++ b/msdf-atlas-gen/json-export.h @@ -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); } diff --git a/msdf-atlas-gen/main.cpp b/msdf-atlas-gen/main.cpp index 7453824..ec237c8 100644 --- a/msdf-atlas-gen/main.cpp +++ b/msdf-atlas-gen/main.cpp @@ -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 + Sets the number of grid columns. + -uniformcell + Sets fixed dimensions of the grid's cells. + -uniformcellconstraint + Constrains cell dimensions to the given rule (see -pots / ... above) + -uniformorigin + Sets whether the glyph's origin point should be fixed at the same position in each cell -yorigin 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 - Specifies the size of the glyphs in the atlas bitmap in pixels per EM. - -minsize + -size + Specifies the size of the glyphs in the atlas bitmap in pixels per em. + -minsize Specifies the minimum size. The largest possible size that fits the same atlas dimensions will be used. - -emrange - Specifies the SDF distance range in EM's. + -emrange + Specifies the SDF distance range in em's. -pxrange Specifies the SDF distance range in output pixels. The default value is 2. -pxalign @@ -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 with a positive real number."); + ABORT("Invalid em size argument. Use -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 with a positive real number."); + ABORT("Invalid minimum em size argument. Use -minsize 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 with a positive real number."); + ABORT("Invalid range argument. Use -emrange 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 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 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 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; diff --git a/msdf-atlas-gen/msdf-atlas-gen.h b/msdf-atlas-gen/msdf-atlas-gen.h index e833a43..bf5f957 100644 --- a/msdf-atlas-gen/msdf-atlas-gen.h +++ b/msdf-atlas-gen/msdf-atlas-gen.h @@ -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" diff --git a/msdf-atlas-gen/types.h b/msdf-atlas-gen/types.h index 27311e7..78cc30c 100644 --- a/msdf-atlas-gen/types.h +++ b/msdf-atlas-gen/types.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 +}; + }