solvespace/src/resource.cpp
whitequark e2e74762f4 Rework path and file operations to be more robust.
This commit updates a *lot* of rather questionable path handling
logic to be robust. Specifically:
  * All path operations go through Platform::Path.
  * All ad-hoc path handling functions are removed, together with
    PATH_SEP. This removes code that was in platform-independent
    parts, but had platform-dependent behavior.
  * Group::linkFileRel is removed; only an absolute path is stored
    in Group::linkFile. However, only Group::linkFileRel is saved,
    with the relative path calculated on the fly, from the filename
    passed into SaveToFile. This eliminates dependence on global
    state, and makes it unnecessary to have separare code paths
    for saved and not yet saved files.
  * In a departure from previous practice, functions with
    platform-independent code but platform-dependent behavior
    are all grouped under platform/. This makes it easy to grep
    for functions with platform-dependent behavior.
  * Similarly, new (GUI-independent) code for all platforms is added
    in the same platform.cpp file, guarded with #ifs. It turns out
    that implementations for different platforms had a lot of shared
    code that tended to go out of sync.
2017-03-11 18:58:53 +00:00

1573 lines
48 KiB
C++

//-----------------------------------------------------------------------------
// Discovery and loading of our resources (icons, fonts, templates, etc).
//
// Copyright 2016 whitequark
//-----------------------------------------------------------------------------
#include <zlib.h>
#include <png.h>
#include <regex>
#include "solvespace.h"
namespace SolveSpace {
//-----------------------------------------------------------------------------
// Resource loading functions
//-----------------------------------------------------------------------------
std::string LoadString(const std::string &name) {
size_t size;
const void *data = Platform::LoadResource(name, &size);
std::string result(static_cast<const char *>(data), size);
// When editing resources under Windows, Windows newlines may sneak in.
// Any files with them won't be merged, but ignoring them during development
// helps external contributors.
result.erase(std::remove(result.begin(), result.end(), '\r'),
result.end());
return result;
}
std::string LoadStringFromGzip(const std::string &name) {
size_t deflatedSize;
const void *data = Platform::LoadResource(name, &deflatedSize);
z_stream stream;
stream.zalloc = Z_NULL;
stream.zfree = Z_NULL;
stream.opaque = Z_NULL;
ssassert(inflateInit2(&stream, /*decode gzip header*/16) == Z_OK,
"Cannot start inflation");
// Extract length mod 2**32 from the gzip trailer.
std::string result;
ssassert(deflatedSize >= 4, "Resource too small to have gzip trailer");
// *(uint32_t *) may perform an unaligned access, so do a memcpy.
uint32_t inflatedSize;
memcpy(&inflatedSize, (uint32_t *)((uintptr_t)data + deflatedSize - 4), sizeof(uint32_t));
result.resize(inflatedSize);
stream.next_in = (Bytef *)data;
stream.avail_in = (uInt)deflatedSize;
stream.next_out = (Bytef *)&result[0];
stream.avail_out = (uInt)result.length();
ssassert(inflate(&stream, Z_NO_FLUSH) == Z_STREAM_END, "Cannot inflate resource");
ssassert(stream.avail_out == 0, "Inflated resource larger than what trailer indicates");
inflateEnd(&stream);
return result;
}
std::shared_ptr<Pixmap> LoadPng(const std::string &name) {
size_t size;
const void *data = Platform::LoadResource(name, &size);
std::shared_ptr<Pixmap> pixmap = Pixmap::FromPng(static_cast<const uint8_t *>(data), size);
ssassert(pixmap != nullptr, "Cannot load pixmap");
return pixmap;
}
//-----------------------------------------------------------------------------
// Pixmap manipulation
//-----------------------------------------------------------------------------
size_t Pixmap::GetBytesPerPixel() const {
switch(format) {
case Format::RGBA: return 4;
case Format::BGRA: return 4;
case Format::RGB: return 3;
case Format::BGR: return 3;
case Format::A: return 1;
}
ssassert(false, "Unexpected pixmap format");
}
RgbaColor Pixmap::GetPixel(size_t x, size_t y) const {
const uint8_t *pixel = &data[y * stride + x * GetBytesPerPixel()];
switch(format) {
case Format::RGBA:
return RgbaColor::From(pixel[0], pixel[1], pixel[2], pixel[3]);
case Format::RGB:
return RgbaColor::From(pixel[0], pixel[1], pixel[2], 255);
case Format::BGRA:
return RgbaColor::From(pixel[2], pixel[1], pixel[0], pixel[3]);
case Format::BGR:
return RgbaColor::From(pixel[2], pixel[1], pixel[0], 255);
case Format::A:
return RgbaColor::From( 255, 255, 255, pixel[0]);
}
ssassert(false, "Unexpected resource format");
}
void Pixmap::SetPixel(size_t x, size_t y, RgbaColor color) {
uint8_t *pixel = &data[y * stride + x * GetBytesPerPixel()];
switch(format) {
case Format::RGBA:
pixel[0] = color.red;
pixel[1] = color.green;
pixel[2] = color.blue;
pixel[3] = color.alpha;
break;
case Format::RGB:
pixel[0] = color.red;
pixel[1] = color.green;
pixel[2] = color.blue;
break;
case Format::BGRA:
pixel[0] = color.blue;
pixel[1] = color.green;
pixel[2] = color.red;
pixel[3] = color.alpha;
break;
case Format::BGR:
pixel[0] = color.blue;
pixel[1] = color.green;
pixel[2] = color.red;
break;
case Format::A:
pixel[0] = color.alpha;
break;
}
}
void Pixmap::ConvertTo(Format newFormat) {
switch(format) {
case Format::RGBA:
ssassert(newFormat == Format::BGRA, "Unexpected target format");
break;
case Format::BGRA:
ssassert(newFormat == Format::RGBA, "Unexpected target format");
break;
case Format::RGB:
ssassert(newFormat == Format::BGR, "Unexpected target format");
break;
case Format::BGR:
ssassert(newFormat == Format::RGB, "Unexpected target format");
break;
case Format::A:
ssassert(false, "Unexpected target format");
}
size_t bpp = GetBytesPerPixel();
for(size_t j = 0; j != height; j++) {
uint8_t *row = &data[j * stride];
for(size_t i = 0; i != width * bpp; i += bpp) {
// This handles both RGB<>BGR and RGBA<>BGRA.
std::swap(row[i], row[i + 2]);
}
}
format = newFormat;
}
static std::shared_ptr<Pixmap> ReadPngIntoPixmap(png_struct *png_ptr, png_info *info_ptr,
bool flip) {
png_read_png(png_ptr, info_ptr, PNG_TRANSFORM_EXPAND | PNG_TRANSFORM_GRAY_TO_RGB, NULL);
std::shared_ptr<Pixmap> pixmap = std::make_shared<Pixmap>();
pixmap->width = png_get_image_width(png_ptr, info_ptr);
pixmap->height = png_get_image_height(png_ptr, info_ptr);
if((png_get_color_type(png_ptr, info_ptr) & PNG_COLOR_MASK_ALPHA) != 0) {
pixmap->format = Pixmap::Format::RGBA;
} else {
pixmap->format = Pixmap::Format::RGB;
}
size_t stride = pixmap->width * pixmap->GetBytesPerPixel();
if(stride % 4 != 0) stride += 4 - stride % 4;
pixmap->stride = stride;
pixmap->data = std::vector<uint8_t>(pixmap->stride * pixmap->height);
uint8_t **rows = png_get_rows(png_ptr, info_ptr);
for(size_t y = 0; y < pixmap->height; y++) {
uint8_t *srcRow = flip ? rows[pixmap->height - y - 1] : rows[y];
memcpy(&pixmap->data[pixmap->stride * y], srcRow,
pixmap->width * pixmap->GetBytesPerPixel());
}
png_destroy_read_struct(&png_ptr, &info_ptr, NULL);
return pixmap;
}
std::shared_ptr<Pixmap> Pixmap::FromPng(const uint8_t *data, size_t size, bool flip) {
struct Slice { const uint8_t *data; size_t size; };
Slice dataSlice = { data, size };
png_struct *png_ptr = NULL;
png_info *info_ptr = NULL;
png_ptr = png_create_read_struct(PNG_LIBPNG_VER_STRING, NULL, NULL, NULL);
if(!png_ptr) goto exit;
info_ptr = png_create_info_struct(png_ptr);
if(!info_ptr) goto exit;
if(setjmp(png_jmpbuf(png_ptr))) goto exit;
png_set_read_fn(png_ptr, &dataSlice,
[](png_struct *png_ptr, uint8_t *data, size_t size) {
Slice *dataSlice = (Slice *)png_get_io_ptr(png_ptr);
if(size <= dataSlice->size) {
memcpy(data, dataSlice->data, size);
dataSlice->data += size;
dataSlice->size -= size;
} else {
png_error(png_ptr, "EOF");
}
});
return ReadPngIntoPixmap(png_ptr, info_ptr, flip);
exit:
png_destroy_read_struct(&png_ptr, &info_ptr, NULL);
return nullptr;
}
std::shared_ptr<Pixmap> Pixmap::ReadPng(FILE *f, bool flip) {
png_struct *png_ptr = NULL;
png_info *info_ptr = NULL;
uint8_t header[8];
if(fread(header, 1, sizeof(header), f) != sizeof(header)) goto exit;
if(png_sig_cmp(header, 0, sizeof(header))) goto exit;
png_ptr = png_create_read_struct(PNG_LIBPNG_VER_STRING, NULL, NULL, NULL);
if(!png_ptr) goto exit;
info_ptr = png_create_info_struct(png_ptr);
if(!info_ptr) goto exit;
if(setjmp(png_jmpbuf(png_ptr))) goto exit;
png_init_io(png_ptr, f);
png_set_sig_bytes(png_ptr, sizeof(header));
return ReadPngIntoPixmap(png_ptr, info_ptr, flip);
exit:
png_destroy_read_struct(&png_ptr, &info_ptr, NULL);
return nullptr;
}
std::shared_ptr<Pixmap> Pixmap::ReadPng(const Platform::Path &filename, bool flip) {
FILE *f = OpenFile(filename, "rb");
if(!f) return NULL;
std::shared_ptr<Pixmap> pixmap = ReadPng(f, flip);
fclose(f);
return pixmap;
}
bool Pixmap::WritePng(FILE *f, bool flip) {
int colorType = 0;
bool bgr = false;
switch(format) {
case Format::RGBA: colorType = PNG_COLOR_TYPE_RGBA; bgr = false; break;
case Format::BGRA: colorType = PNG_COLOR_TYPE_RGBA; bgr = true; break;
case Format::RGB: colorType = PNG_COLOR_TYPE_RGB; bgr = false; break;
case Format::BGR: colorType = PNG_COLOR_TYPE_RGB; bgr = true; break;
case Format::A: colorType = PNG_COLOR_TYPE_GRAY; bgr = false; break;
}
std::vector<uint8_t *> rows;
for(size_t y = 0; y < height; y++) {
if(flip) {
rows.push_back(&data[stride * (height - y - 1)]);
} else {
rows.push_back(&data[stride * y]);
}
}
png_struct *png_ptr = NULL;
png_info *info_ptr = NULL;
png_ptr = png_create_write_struct(PNG_LIBPNG_VER_STRING, NULL, NULL, NULL);
if(!png_ptr) goto exit;
info_ptr = png_create_info_struct(png_ptr);
if(!info_ptr) goto exit;
if(setjmp(png_jmpbuf(png_ptr))) goto exit;
png_init_io(png_ptr, f);
png_set_IHDR(png_ptr, info_ptr,
(png_uint_32)width, (png_uint_32)height, 8, colorType,
PNG_INTERLACE_NONE, PNG_COMPRESSION_TYPE_DEFAULT, PNG_FILTER_TYPE_DEFAULT);
if(bgr) png_set_bgr(png_ptr);
png_write_info(png_ptr, info_ptr);
png_write_image(png_ptr, &rows[0]);
png_write_end(png_ptr, info_ptr);
png_destroy_write_struct(&png_ptr, &info_ptr);
return true;
exit:
png_destroy_write_struct(&png_ptr, &info_ptr);
return false;
}
bool Pixmap::WritePng(const Platform::Path &filename, bool flip) {
FILE *f = OpenFile(filename, "wb");
if(!f) return false;
bool success = WritePng(f, flip);
fclose(f);
return success;
}
bool Pixmap::Equals(const Pixmap &other) const {
if(format != other.format || width != other.width || height != other.height) {
return false;
}
size_t rowLength = width * GetBytesPerPixel();
for(size_t y = 0; y < height; y++) {
if(memcmp(&data[y * stride], &other.data[y * other.stride], rowLength)) {
return false;
}
}
return true;
}
std::shared_ptr<Pixmap> Pixmap::Create(Format format, size_t width, size_t height) {
std::shared_ptr<Pixmap> pixmap = std::make_shared<Pixmap>();
pixmap->format = format;
pixmap->width = width;
pixmap->height = height;
// Align to fulfill OpenGL texture requirements.
size_t stride = pixmap->width * pixmap->GetBytesPerPixel();
if(stride % 4 != 0) stride += 4 - stride % 4;
pixmap->stride = stride;
pixmap->data = std::vector<uint8_t>(pixmap->stride * pixmap->height);
return pixmap;
}
//-----------------------------------------------------------------------------
// ASCII sequence parsing
//-----------------------------------------------------------------------------
class ASCIIReader {
public:
std::string::const_iterator pos, end;
static ASCIIReader From(const std::string &str) {
return ASCIIReader({ str.cbegin(), str.cend() });
}
bool AtEnd() const {
return pos == end;
}
bool SkipSpace() {
bool skipped = false;
while(!AtEnd()) {
char c = *pos;
if(!(c == ' ' || c == '\t' || c == '\n')) break;
skipped = true;
pos++;
}
return skipped;
}
char PeekChar() {
ssassert(!AtEnd(), "Unexpected EOF");
return *pos;
}
char ReadChar() {
ssassert(!AtEnd(), "Unexpected EOF");
return *pos++;
}
bool TryChar(char c) {
if(AtEnd()) {
return false;
} else if(*pos == c) {
pos++;
return true;
} else {
return false;
}
}
void ExpectChar(char c) {
if(!TryChar(c)) {
dbp("Expecting character '%c'", c);
ssassert(false, "Unexpected character");
}
}
bool TryString(const std::string &s) {
if((size_t)(end - pos) >= s.size() && std::string(pos, pos + s.size()) == s) {
pos += s.size();
return true;
} else {
return false;
}
}
void ExpectString(const std::string &s) {
if(!TryString(s)) {
dbp("Expecting string '%s'", s.c_str());
ssassert(false, "Unexpected string");
}
}
size_t CountUntilEol() const {
return std::find(pos, end, '\n') - pos;
}
void SkipUntilEol() {
pos = std::find(pos, end, '\n');
}
std::string ReadUntilEol() {
auto eol = std::find(pos, end, '\n');
std::string result(pos, eol);
if(eol != end) {
pos = eol + 1;
} else {
pos = end;
}
return result;
}
uint8_t Read4HexBits() {
char c = ReadChar();
if(c >= '0' && c <= '9') {
return c - '0';
} else if(c >= 'a' && c <= 'f') {
return 10 + (c - 'a');
} else if(c >= 'A' && c <= 'F') {
return 10 + (c - 'A');
} else ssassert(false, "Unexpected hex digit");
}
uint8_t Read8HexBits() {
uint8_t h = Read4HexBits(),
l = Read4HexBits();
return (h << 4) + l;
}
uint16_t Read16HexBits() {
uint16_t h = Read8HexBits(),
l = Read8HexBits();
return (h << 8) + l;
}
long ReadIntegerDecimal(int base = 10) {
char *endptr;
long l = strtol(&*pos, &endptr, base);
ssassert(&*pos != endptr, "Cannot read an integer number");
pos += endptr - &*pos;
return l;
}
double ReadFloatDecimal() {
char *endptr;
double d = strtod(&*pos, &endptr);
ssassert(&*pos != endptr, "Cannot read a floating-point number");
pos += endptr - &*pos;
return d;
}
bool TryRegex(const std::regex &re, std::smatch *m) {
if(std::regex_search(pos, end, *m, re, std::regex_constants::match_continuous)) {
pos += m->length();
return true;
} else {
return false;
}
}
void ExpectRegex(const std::regex &re, std::smatch *m) {
ssassert(TryRegex(re, m), "Unmatched regex");
}
};
//-----------------------------------------------------------------------------
// Bitmap font manipulation
//-----------------------------------------------------------------------------
static uint8_t *BitmapFontTextureRow(std::shared_ptr<Pixmap> texture,
uint16_t position, size_t y) {
// position = 0;
size_t col = position % (texture->width / 16),
row = position / (texture->width / 16);
return &texture->data[texture->stride * (16 * row + y) + 16 * col];
}
BitmapFont BitmapFont::From(std::string &&unifontData) {
BitmapFont font = {};
font.unifontData = std::move(unifontData);
font.texture = Pixmap::Create(Pixmap::Format::A, 1024, 1024);
return font;
}
void BitmapFont::AddGlyph(char32_t codepoint, std::shared_ptr<const Pixmap> pixmap) {
ssassert((pixmap->width == 8 || pixmap->width == 16) && pixmap->height == 16,
"Unexpected pixmap dimensions");
ssassert(pixmap->format == Pixmap::Format::RGB,
"Unexpected pixmap format");
ssassert(glyphs.find(codepoint) == glyphs.end(),
"Glyph with this codepoint already exists");
ssassert(nextPosition != 0xffff,
"Too many glyphs for current texture size");
BitmapFont::Glyph glyph = {};
glyph.advanceCells = (uint8_t)(pixmap->width / 8);
glyph.position = nextPosition++;
glyphs.emplace(codepoint, std::move(glyph));
for(size_t y = 0; y < pixmap->height; y++) {
uint8_t *row = BitmapFontTextureRow(texture, glyph.position, y);
for(size_t x = 0; x < pixmap->width; x++) {
if((pixmap->GetPixel(x, y).ToPackedInt() & 0xffffff) != 0) {
row[x] = 255;
}
}
}
}
const BitmapFont::Glyph &BitmapFont::GetGlyph(char32_t codepoint) {
auto it = glyphs.find(codepoint);
if(it != glyphs.end()) {
return (*it).second;
}
ssassert(nextPosition != 0xffff,
"Too many glyphs for current texture size");
// Find the hex representation in the (sorted) Unifont file.
auto first = unifontData.cbegin(),
last = unifontData.cend();
while(first <= last) {
auto mid = first + (last - first) / 2;
while(mid != unifontData.cbegin()) {
if(*mid == '\n') {
mid++;
break;
}
mid--;
}
ASCIIReader reader = { mid, unifontData.cend() };
if(reader.AtEnd()) break;
// Read the codepoint.
char32_t foundCodepoint = reader.Read16HexBits();
reader.ExpectChar(':');
if(foundCodepoint > codepoint) {
last = mid - 1;
continue; // and first stays the same
}
if(foundCodepoint < codepoint) {
first = mid + 1;
while(first != unifontData.cend()) {
if(*first == '\n') break;
first++;
}
continue; // and last stays the same
}
// Found the codepoint.
Glyph glyph = {};
glyph.position = nextPosition++;
// Read glyph bits.
unsigned short glyphBits[16];
size_t glyphLength = reader.CountUntilEol();
if(glyphLength == 4 * 16) {
glyph.advanceCells = 2;
for(size_t i = 0; i < 16; i++) {
glyphBits[i] = reader.Read16HexBits();
}
} else if(glyphLength == 2 * 16) {
glyph.advanceCells = 1;
for(size_t i = 0; i < 16; i++) {
glyphBits[i] = (uint16_t)reader.Read8HexBits() << 8;
}
} else ssassert(false, "Unexpected glyph bitmap length");
// Fill in the texture (one texture byte per glyph bit).
for(size_t y = 0; y < 16; y++) {
uint8_t *row = BitmapFontTextureRow(texture, glyph.position, y);
for(size_t x = 0; x < 16; x++) {
if(glyphBits[y] & (1 << (15 - x))) {
row[x] = 255;
}
}
}
it = glyphs.emplace(codepoint, std::move(glyph)).first;
textureUpdated = true;
return (*it).second;
}
// Glyph doesn't exist; return replacement glyph instead.
ssassert(codepoint != 0xfffd, "Cannot parse replacement glyph");
return GetGlyph(0xfffd);
}
void BitmapFont::LocateGlyph(char32_t codepoint,
double *s0, double *t0, double *s1, double *t1,
size_t *w, size_t *h) {
const Glyph &glyph = GetGlyph(codepoint);
*w = glyph.advanceCells * 8;
*h = 16;
*s0 = (16.0 * (glyph.position % (texture->width / 16))) / texture->width;
*s1 = *s0 + (double)(*w) / texture->width;
*t0 = (16.0 * (glyph.position / (texture->width / 16))) / texture->height;
*t1 = *t0 + (double)(*h) / texture->height;
}
size_t BitmapFont::GetWidth(char32_t codepoint) {
if(codepoint >= 0xe000 && codepoint <= 0xefff) {
// These are special-cased because checkboxes predate support for 2 cell wide
// characters; and so all Printf() calls pad them with spaces.
return 1;
}
return GetGlyph(codepoint).advanceCells;
}
size_t BitmapFont::GetWidth(const std::string &str) {
size_t width = 0;
for(char32_t codepoint : ReadUTF8(str)) {
width += GetWidth(codepoint);
}
return width;
}
BitmapFont BitmapFont::Create() {
BitmapFont Font = BitmapFont::From(LoadStringFromGzip("fonts/unifont.hex.gz"));
// Unifont doesn't have a glyph for U+0020.
Font.AddGlyph(0x0020, Pixmap::Create(Pixmap::Format::RGB, 8, 16));
Font.AddGlyph(0xE000, LoadPng("fonts/private/0-check-false.png"));
Font.AddGlyph(0xE001, LoadPng("fonts/private/1-check-true.png"));
Font.AddGlyph(0xE002, LoadPng("fonts/private/2-radio-false.png"));
Font.AddGlyph(0xE003, LoadPng("fonts/private/3-radio-true.png"));
Font.AddGlyph(0xE004, LoadPng("fonts/private/4-stipple-dot.png"));
Font.AddGlyph(0xE005, LoadPng("fonts/private/5-stipple-dash-long.png"));
Font.AddGlyph(0xE006, LoadPng("fonts/private/6-stipple-dash.png"));
Font.AddGlyph(0xE007, LoadPng("fonts/private/7-stipple-zigzag.png"));
return Font;
}
//-----------------------------------------------------------------------------
// Vector font manipulation
//-----------------------------------------------------------------------------
const static int ARC_POINTS = 8;
static void MakePwlArc(VectorFont::Contour *contour, bool isReversed,
const Point2d &cp, double radius, double a1, double a2) {
if(radius < LENGTH_EPS) return;
double aSign = 1.0;
if(isReversed) {
if(a1 <= a2 + LENGTH_EPS) a1 += 2.0 * M_PI;
aSign = -1.0;
} else {
if(a2 <= a1 + LENGTH_EPS) a2 += 2.0 * M_PI;
}
double aStep = aSign * fabs(a2 - a1) / (double)ARC_POINTS;
for(int i = 0; i <= ARC_POINTS; i++) {
contour->points.emplace_back(cp.Plus(Point2d::FromPolar(radius, a1 + aStep * i)));
}
}
static void MakePwlBulge(VectorFont::Contour *contour, const Point2d &v, double bulge) {
bool reversed = bulge < 0.0;
double alpha = atan(bulge) * 4.0;
const Point2d &point = contour->points.back();
Point2d middle = point.Plus(v).ScaledBy(0.5);
double dist = point.DistanceTo(v) / 2.0;
double angle = point.AngleTo(v);
// alpha can't be 0.0 at this point
double radius = fabs(dist / sin(alpha / 2.0));
double wu = fabs(radius*radius - dist*dist);
double h = sqrt(wu);
if(bulge > 0.0) {
angle += M_PI_2;
} else {
angle -= M_PI_2;
}
if(fabs(alpha) > M_PI) {
h = -h;
}
Point2d center = Point2d::FromPolar(h, angle).Plus(middle);
double a1 = center.AngleTo(point);
double a2 = center.AngleTo(v);
MakePwlArc(contour, reversed, center, radius, a1, a2);
}
static void GetGlyphBBox(const VectorFont::Glyph &glyph,
double *rminx, double *rmaxx, double *rminy, double *rmaxy) {
double minx = 0.0, maxx = 0.0, miny = 0.0, maxy = 0.0;
if(!glyph.contours.empty()) {
const Point2d &start = glyph.contours[0].points[0];
minx = maxx = start.x;
miny = maxy = start.y;
for(const VectorFont::Contour &c : glyph.contours) {
for(const Point2d &p : c.points) {
maxx = std::max(maxx, p.x);
minx = std::min(minx, p.x);
maxy = std::max(maxy, p.y);
miny = std::min(miny, p.y);
}
}
}
if(rminx) *rminx = minx;
if(rmaxx) *rmaxx = maxx;
if(rminy) *rminy = miny;
if(rmaxy) *rmaxy = maxy;
}
VectorFont VectorFont::From(std::string &&lffData) {
VectorFont font = {};
font.lffData = std::move(lffData);
ASCIIReader reader = ASCIIReader::From(font.lffData);
std::smatch m;
while(reader.TryRegex(std::regex("#\\s*(\\w+)\\s*:\\s*(.+?)\n"), &m)) {
std::string name = m.str(1),
value = m.str(2);
std::transform(name.begin(), name.end(), name.begin(), ::tolower);
if(name == "letterspacing") {
font.rightSideBearing = std::stod(value);
} else if(name == "wordspacing") {
Glyph space = {};
space.advanceWidth = std::stod(value);
font.glyphs.emplace(' ', std::move(space));
}
}
GetGlyphBBox(font.GetGlyph('A'), nullptr, nullptr, nullptr, &font.capHeight);
GetGlyphBBox(font.GetGlyph('h'), nullptr, nullptr, nullptr, &font.ascender);
GetGlyphBBox(font.GetGlyph('p'), nullptr, nullptr, &font.descender, nullptr);
ssassert(!font.IsEmpty(), "Expected to load a font");
return font;
}
const VectorFont::Glyph &VectorFont::GetGlyph(char32_t codepoint) {
auto it = glyphs.find(codepoint);
if(it != glyphs.end()) {
return (*it).second;
}
auto firstGlyph = std::find(lffData.cbegin(), lffData.cend(), '[');
ssassert(firstGlyph != lffData.cend(), "Vector font contains no glyphs");
// Find the serialized representation in the (sorted) lff file.
auto first = firstGlyph,
last = lffData.cend();
while(first <= last) {
auto mid = first + (last - first) / 2;
while(mid > first) {
if(*mid == '[' && *(mid - 1) == '\n') break;
mid--;
}
ASCIIReader reader = { mid, lffData.cend() };
if(reader.AtEnd()) break;
// Read the codepoint.
reader.ExpectChar('[');
char32_t foundCodepoint = reader.Read16HexBits();
reader.ExpectChar(']');
reader.SkipUntilEol();
if(foundCodepoint > codepoint) {
last = mid - 1;
continue; // and first stays the same
}
if(foundCodepoint < codepoint) {
first = mid + 1;
while(first != lffData.cend()) {
if(*first == '[' && *(first - 1) == '\n') break;
first++;
}
continue; // and last stays the same
}
// Found the codepoint.
VectorFont::Glyph glyph = {};
// Read glyph contours.
while(!reader.AtEnd()) {
if(reader.TryChar('\n')) {
// Skip.
} else if(reader.TryChar('[')) {
// End of glyph.
if(glyph.contours.back().points.empty()) {
// Remove an useless empty contour, if any.
glyph.contours.pop_back();
}
break;
} else if(reader.TryChar('C')) {
// Another character is referenced in this one.
char32_t baseCodepoint = reader.Read16HexBits();
const VectorFont::Glyph &baseGlyph = GetGlyph(baseCodepoint);
std::copy(baseGlyph.contours.begin(), baseGlyph.contours.end(),
std::back_inserter(glyph.contours));
} else {
Contour contour;
do {
Point2d p;
p.x = reader.ReadFloatDecimal();
reader.ExpectChar(',');
p.y = reader.ReadFloatDecimal();
if(reader.TryChar(',')) {
// Point with a bulge.
reader.ExpectChar('A');
double bulge = reader.ReadFloatDecimal();
MakePwlBulge(&contour, p, bulge);
} else {
// Just a point.
contour.points.emplace_back(std::move(p));
}
} while(reader.TryChar(';'));
reader.ExpectChar('\n');
glyph.contours.emplace_back(std::move(contour));
}
}
// Calculate metrics.
GetGlyphBBox(glyph, &glyph.leftSideBearing, &glyph.boundingWidth, nullptr, nullptr);
glyph.advanceWidth = glyph.leftSideBearing + glyph.boundingWidth + rightSideBearing;
it = glyphs.emplace(codepoint, std::move(glyph)).first;
return (*it).second;
}
// Glyph doesn't exist; return replacement glyph instead.
ssassert(codepoint != 0xfffd, "Cannot parse replacement glyph");
return GetGlyph(0xfffd);
}
VectorFont *VectorFont::Builtin() {
static VectorFont Font;
if(Font.IsEmpty()) {
Font = VectorFont::From(LoadStringFromGzip("fonts/unicode.lff.gz"));
}
return &Font;
}
double VectorFont::GetCapHeight(double forCapHeight) const {
ssassert(!IsEmpty(), "Expected a loaded font");
return forCapHeight;
}
double VectorFont::GetHeight(double forCapHeight) const {
ssassert(!IsEmpty(), "Expected a loaded font");
return (ascender - descender) * (forCapHeight / capHeight);
}
double VectorFont::GetWidth(double forCapHeight, const std::string &str) {
ssassert(!IsEmpty(), "Expected a loaded font");
double width = 0;
for(char32_t codepoint : ReadUTF8(str)) {
width += GetGlyph(codepoint).advanceWidth;
}
width -= rightSideBearing;
return width * (forCapHeight / capHeight);
}
Vector VectorFont::GetExtents(double forCapHeight, const std::string &str) {
Vector ex = {};
ex.x = GetWidth(forCapHeight, str);
ex.y = GetHeight(forCapHeight);
return ex;
}
void VectorFont::Trace(double forCapHeight, Vector o, Vector u, Vector v, const std::string &str,
std::function<void(Vector, Vector)> traceEdge) {
ssassert(!IsEmpty(), "Expected a loaded font");
double scale = (forCapHeight / capHeight);
u = u.ScaledBy(scale);
v = v.ScaledBy(scale);
for(char32_t codepoint : ReadUTF8(str)) {
const Glyph &glyph = GetGlyph(codepoint);
for(const VectorFont::Contour &contour : glyph.contours) {
Vector prevp;
bool penUp = true;
for(const Point2d &pt : contour.points) {
Vector p = o.Plus(u.ScaledBy(pt.x))
.Plus(v.ScaledBy(pt.y));
if(!penUp) traceEdge(prevp, p);
prevp = p;
penUp = false;
}
}
o = o.Plus(u.ScaledBy(glyph.advanceWidth));
}
}
void VectorFont::Trace(double forCapHeight, Vector o, Vector u, Vector v, const std::string &str,
std::function<void(Vector, Vector)> traceEdge, const Camera &camera) {
ssassert(!IsEmpty(), "Expected a loaded font");
// Perform grid-fitting only when the text is parallel to the view plane.
if(camera.hasPixels && !(u.WithMagnitude(1).Equals(camera.projRight) &&
v.WithMagnitude(1).Equals(camera.projUp))) {
return Trace(forCapHeight, o, u, v, str, traceEdge);
}
double scale = forCapHeight / capHeight;
u = u.ScaledBy(scale);
v = v.ScaledBy(scale);
for(char32_t codepoint : ReadUTF8(str)) {
const Glyph &glyph = GetGlyph(codepoint);
double actualWidth = std::max(1.0, glyph.boundingWidth);
// Align (o+lsb), (o+lsb+u) and (o+lsb+v) to pixel grid.
Vector ao = o.Plus(u.ScaledBy(glyph.leftSideBearing));
Vector au = ao.Plus(u.ScaledBy(actualWidth));
Vector av = ao.Plus(v.ScaledBy(capHeight));
ao = camera.AlignToPixelGrid(ao);
au = camera.AlignToPixelGrid(au);
av = camera.AlignToPixelGrid(av);
au = au.Minus(ao).ScaledBy(1.0 / actualWidth);
av = av.Minus(ao).ScaledBy(1.0 / capHeight);
for(const VectorFont::Contour &contour : glyph.contours) {
Vector prevp;
bool penUp = true;
for(const Point2d &pt : contour.points) {
Vector p = ao.Plus(au.ScaledBy(pt.x - glyph.leftSideBearing))
.Plus(av.ScaledBy(pt.y));
if(!penUp) traceEdge(prevp, p);
prevp = p;
penUp = false;
}
}
o = o.Plus(u.ScaledBy(glyph.advanceWidth));
}
}
//-----------------------------------------------------------------------------
// Gettext plural expression evaluation
//-----------------------------------------------------------------------------
class PluralExpr {
public:
class Token {
public:
enum class Type {
END,
VALUE,
BINARY_OP,
QUERY,
COLON,
PAREN_LEFT,
PAREN_RIGHT,
};
// Only valid for type == BINARY_OP.
enum class Op {
NONE,
// comparison
EQ, // ==
NEQ, // !=
LT, // <
GT, // >
LE, // <=
GE, // >=
// logical
AND, // &&
OR, // ||
// arithmetic
MOD, // %
};
Type type;
Op op;
unsigned value;
int Precedence();
};
ASCIIReader reader;
std::vector<Token> stack;
unsigned value;
Token Lex();
Token PopToken();
void Reduce();
void Eval();
static unsigned Eval(const std::string &s, unsigned n);
};
int PluralExpr::Token::Precedence() {
switch(type) {
case Type::BINARY_OP:
switch(op) {
case Op::MOD:
return 7;
case Op::LT:
case Op::GT:
case Op::LE:
case Op::GE:
return 6;
case Op::EQ:
case Op::NEQ:
return 5;
case Op::AND:
return 4;
case Op::OR:
return 3;
case Op::NONE:
ssassert(false, "Unexpected operator");
}
case Type::QUERY:
case Type::COLON:
return 1;
case Type::VALUE:
return 0;
default:
ssassert(false, "Unexpected token op");
}
}
PluralExpr::Token PluralExpr::Lex() {
Token t = {};
reader.SkipSpace();
char c = reader.PeekChar();
if(c >= '0' && c <= '9') {
t.type = Token::Type::VALUE;
t.value = reader.ReadIntegerDecimal();
} else if(reader.TryChar('n')) {
t.type = Token::Type::VALUE;
t.value = value;
} else if(reader.TryChar('%')) {
t.type = Token::Type::BINARY_OP;
t.op = Token::Op::MOD;
} else if(reader.TryChar('<')) {
t.type = Token::Type::BINARY_OP;
if(reader.TryChar('=')) {
t.op = Token::Op::LE;
} else {
t.op = Token::Op::LT;
}
} else if(reader.TryChar('>')) {
t.type = Token::Type::BINARY_OP;
if(reader.TryChar('=')) {
t.op = Token::Op::GE;
} else {
t.op = Token::Op::GT;
}
} else if(reader.TryChar('!')) {
reader.ExpectChar('=');
t.type = Token::Type::BINARY_OP;
t.op = Token::Op::NEQ;
} else if(reader.TryChar('=')) {
reader.ExpectChar('=');
t.type = Token::Type::BINARY_OP;
t.op = Token::Op::EQ;
} else if(reader.TryChar('&')) {
reader.ExpectChar('&');
t.type = Token::Type::BINARY_OP;
t.op = Token::Op::AND;
} else if(reader.TryChar('|')) {
reader.ExpectChar('|');
t.type = Token::Type::BINARY_OP;
t.op = Token::Op::OR;
} else if(reader.TryChar('?')) {
t.type = Token::Type::QUERY;
} else if(reader.TryChar(':')) {
t.type = Token::Type::COLON;
} else if(reader.TryChar('(')) {
t.type = Token::Type::PAREN_LEFT;
} else if(reader.TryChar(')')) {
t.type = Token::Type::PAREN_RIGHT;
} else if(reader.AtEnd()) {
t.type = Token::Type::END;
} else {
ssassert(false, "Unexpected character");
}
return t;
}
PluralExpr::Token PluralExpr::PopToken() {
ssassert(stack.size() > 0, "Expected a non-empty stack");
Token t = stack.back();
stack.pop_back();
return t;
}
void PluralExpr::Reduce() {
Token r;
r.type = Token::Type::VALUE;
Token a = PopToken();
ssassert(a.type == Token::Type::VALUE, "Expected 1st operand to be a value");
Token op = PopToken();
switch(op.type) {
case Token::Type::BINARY_OP: {
Token b = PopToken();
ssassert(b.type == Token::Type::VALUE, "Expected 2nd operand to be a value");
switch(op.op) {
case Token::Op::EQ:
r.value = (a.value == b.value ? 1 : 0);
break;
case Token::Op::NEQ:
r.value = (a.value != b.value ? 1 : 0);
break;
case Token::Op::LT:
r.value = (b.value < a.value ? 1 : 0);
break;
case Token::Op::GT:
r.value = (b.value > a.value ? 1 : 0);
break;
case Token::Op::LE:
r.value = (b.value <= a.value ? 1 : 0);
break;
case Token::Op::GE:
r.value = (b.value >= a.value ? 1 : 0);
break;
case Token::Op::AND:
r.value = a.value && b.value;
break;
case Token::Op::OR:
r.value = a.value || b.value;
break;
case Token::Op::MOD:
r.value = b.value % a.value;
break;
case Token::Op::NONE:
ssassert(false, "Unexpected operator");
}
break;
}
case Token::Type::COLON: {
Token b = PopToken();
ssassert(PopToken().type == Token::Type::QUERY, "Expected ?");
Token c = PopToken();
r.value = c.value ? b.value : a.value;
break;
}
default:
ssassert(false, "Unexpected operator type");
}
stack.push_back(r);
}
void PluralExpr::Eval() {
while(true) {
Token t = Lex();
switch(t.type) {
case Token::Type::END:
case Token::Type::PAREN_RIGHT:
while(stack.size() > 1 &&
stack.end()[-2].type != Token::Type::PAREN_LEFT) {
Reduce();
}
if(t.type == Token::Type::PAREN_RIGHT) {
ssassert(stack.size() > 1, "Expected (");
stack.push_back(t);
}
return;
case Token::Type::PAREN_LEFT:
stack.push_back(t);
Eval();
if(stack.back().type != Token::Type::PAREN_RIGHT) {
ssassert(false, "Expected )");
}
stack.pop_back();
stack.erase(stack.end() - 2);
break;
case Token::Type::VALUE:
stack.push_back(t);
break;
case Token::Type::BINARY_OP:
case Token::Type::QUERY:
case Token::Type::COLON:
while(stack.size() > 1 &&
stack.end()[-2].type != Token::Type::PAREN_LEFT &&
t.Precedence() < stack.end()[-2].Precedence()) {
Reduce();
}
stack.push_back(t);
break;
}
}
}
unsigned PluralExpr::Eval(const std::string &s, unsigned n) {
PluralExpr expr = {};
expr.reader = ASCIIReader::From(s);
expr.value = n;
expr.Eval();
Token t = expr.PopToken();
ssassert(t.type == Token::Type::VALUE, "Expected a value");
return t.value;
}
//-----------------------------------------------------------------------------
// Gettext message keys
//-----------------------------------------------------------------------------
class TranslationKey {
public:
bool hasContext;
std::string context;
std::string ident;
};
struct TranslationKeyLess {
bool operator()(const TranslationKey &a, const TranslationKey &b) const {
return a.hasContext < b.hasContext ||
(a.hasContext == b.hasContext && a.context < b.context) ||
(a.hasContext == b.hasContext && a.context == b.context && a.ident < b.ident);
}
};
//-----------------------------------------------------------------------------
// Gettext .po file parsing
//-----------------------------------------------------------------------------
class GettextParser {
public:
ASCIIReader reader;
unsigned pluralCount;
std::string pluralExpr;
std::map<TranslationKey, std::vector<std::string>, TranslationKeyLess> messages;
void SkipSpace();
std::string ReadString();
void ParseHeader(const std::string &header);
void Parse();
};
void GettextParser::SkipSpace() {
while(!reader.AtEnd()) {
if(reader.TryChar('#')) {
reader.SkipUntilEol();
} else if(!reader.SkipSpace()) {
break;
}
}
}
std::string GettextParser::ReadString() {
SkipSpace();
reader.ExpectChar('"');
std::string result;
while(true) {
if(reader.AtEnd()) {
ssassert(false, "Unexpected EOF within a string");
} else if(reader.TryChar('\"')) {
SkipSpace();
if(!reader.TryChar('\"')) {
break;
}
} else if(reader.TryChar('\\')) {
if(reader.TryChar('\\')) {
result += '\\';
} else if(reader.TryChar('n')) {
result += '\n';
} else if(reader.TryChar('t')) {
result += '\t';
} else if(reader.TryChar('"')) {
result += '"';
} else {
ssassert(false, "Unexpected escape sequence");
}
} else {
result += reader.ReadChar();
}
}
return result;
}
void GettextParser::ParseHeader(const std::string &header) {
ASCIIReader reader = ASCIIReader::From(header);
while(!reader.AtEnd()) {
reader.SkipSpace();
if(reader.TryString("Plural-Forms:")) {
reader.SkipSpace();
reader.ExpectString("nplurals=");
reader.SkipSpace();
pluralCount = reader.ReadIntegerDecimal();
reader.SkipSpace();
reader.ExpectString(";");
reader.SkipSpace();
reader.ExpectString("plural=");
pluralExpr = reader.ReadUntilEol();
} else {
reader.SkipUntilEol();
}
}
}
void GettextParser::Parse() {
// Default to a single form, in case a header is missing.
pluralCount = 1;
pluralExpr = "0";
SkipSpace();
while(!reader.AtEnd()) {
TranslationKey key = {};
if(reader.TryString("msgctxt")) {
key.hasContext = true;
key.context = ReadString();
}
reader.ExpectString("msgid");
key.ident = ReadString();
if(reader.TryString("msgid_plural")) {
ReadString(); // we don't need it
}
std::vector<std::string> msgstrs;
while(reader.TryString("msgstr")) {
if(reader.TryChar('[')) {
unsigned index = reader.ReadIntegerDecimal();
reader.ExpectChar(']');
if(msgstrs.size() <= index) {
msgstrs.resize(index + 1);
}
msgstrs[index] = ReadString();
} else {
msgstrs.emplace_back(ReadString());
break;
}
}
if(key.ident == "") {
ssassert(msgstrs.size() == 1,
"Expected exactly one header msgstr");
ParseHeader(msgstrs[0]);
} else {
ssassert(msgstrs.size() == 1 ||
msgstrs.size() == pluralCount,
"Expected msgstr count to match plural form count");
messages.emplace(key, msgstrs);
}
}
}
//-----------------------------------------------------------------------------
// Translation management
//-----------------------------------------------------------------------------
class Translation {
public:
unsigned pluralCount;
std::string pluralExpr;
std::map<TranslationKey, std::vector<std::string>, TranslationKeyLess> messages;
static Translation From(const std::string &poData);
const std::string &Translate(const TranslationKey &key);
const std::string &TranslatePlural(const TranslationKey &key, unsigned n);
};
Translation Translation::From(const std::string &poData) {
GettextParser parser = {};
parser.reader = ASCIIReader::From(poData);
parser.Parse();
Translation trans = {};
trans.pluralCount = parser.pluralCount;
trans.pluralExpr = parser.pluralExpr;
trans.messages = parser.messages;
return trans;
}
const std::string &Translation::Translate(const TranslationKey &key) {
auto it = messages.find(key);
if(it == messages.end()) {
dbp("Missing (absent) translation for %s'%s'", key.context.c_str(), key.ident.c_str());
messages[key].emplace_back(key.ident);
it = messages.find(key);
}
if(it->second[0].empty()) {
dbp("Missing (empty) translation for %s'%s'", key.context.c_str(), key.ident.c_str());
it->second[0] = key.ident;
}
if(it->second.size() != 1) {
dbp("Incorrect use of translated message %s'%s'", key.context.c_str(), key.ident.c_str());
ssassert(false, "Using a message with a plural form without a number");
}
return it->second[0];
}
const std::string &Translation::TranslatePlural(const TranslationKey &key, unsigned n) {
unsigned pluralForm = PluralExpr::Eval(pluralExpr, n);
auto it = messages.find(key);
if(it == messages.end()) {
dbp("Missing (absent) translation for %s'%s'", key.context.c_str(), key.ident.c_str());
for(unsigned i = 0; i < pluralCount; i++) {
messages[key].emplace_back(key.ident);
}
it = messages.find(key);
}
if(it->second[pluralForm].empty()) {
dbp("Missing (empty) translation for %s'%s'[%d]",
key.context.c_str(), key.ident.c_str(), pluralForm);
it->second[pluralForm] = key.ident;
}
return it->second[pluralForm];
}
//-----------------------------------------------------------------------------
// Locale management
//-----------------------------------------------------------------------------
static std::set<Locale, LocaleLess> locales;
static std::map<Locale, Translation, LocaleLess> translations;
static Translation dummyTranslation;
static Translation *currentTranslation = &dummyTranslation;
const std::set<Locale, LocaleLess> &Locales() {
if(!locales.empty()) return locales;
std::string localeList = LoadString("locales.txt");
ASCIIReader reader = ASCIIReader::From(localeList);
while(!reader.AtEnd()) {
reader.SkipSpace();
if(reader.TryChar('#')) {
reader.SkipUntilEol();
continue;
}
std::smatch m;
reader.ExpectRegex(std::regex("([a-z]{2})-([A-Z]{2}),([0-9]{4}),(.+?)\n"), &m);
Locale locale = {};
locale.language = m.str(1);
locale.region = m.str(2);
locale.lcid = std::stoi(m.str(3), NULL, 16);
locale.displayName = m.str(4);
locales.emplace(locale);
}
return locales;
}
template<class Predicate>
bool SetLocale(Predicate pred) {
auto it = std::find_if(Locales().begin(), Locales().end(), pred);
if(it != locales.end()) {
std::string filename = "locales/" + it->language + "_" + it->region + ".po";
translations[*it] = Translation::From(LoadString(filename));
currentTranslation = &translations[*it];
RefreshLocale();
return true;
} else {
return false;
}
}
bool SetLocale(const std::string &name) {
return SetLocale([&](const Locale &locale) {
if(name == locale.language + "-" + locale.region) {
return true;
} else if(name == locale.language + "_" + locale.region) {
return true;
} else if(name == locale.language) {
return true;
} else {
return false;
}
});
}
bool SetLocale(uint16_t lcid) {
return SetLocale([&](const Locale &locale) {
return locale.lcid == lcid;
});
}
const std::string &Translate(const char *msgid) {
TranslationKey key = {};
key.ident = msgid;
return currentTranslation->Translate(key);
}
const std::string &Translate(const char *msgctxt, const char *msgid) {
TranslationKey key = {};
key.hasContext = true;
key.context = msgctxt;
key.ident = msgid;
return currentTranslation->Translate(key);
}
const std::string &TranslatePlural(const char *msgid, unsigned n) {
TranslationKey key = {};
key.ident = msgid;
return currentTranslation->TranslatePlural(key, n);
}
const std::string &TranslatePlural(const char *msgctxt, const char *msgid, unsigned n) {
TranslationKey key = {};
key.hasContext = true;
key.context = msgctxt;
key.ident = msgid;
return currentTranslation->TranslatePlural(key, n);
}
}