mirror of
https://github.com/mapbox/node-fontnik.git
synced 2026-01-25 16:02:44 +00:00
678 lines
22 KiB
C++
678 lines
22 KiB
C++
// fontnik
|
|
#include "glyphs.hpp"
|
|
|
|
// node
|
|
#include <node_buffer.h>
|
|
#include <nan.h>
|
|
|
|
#include "agg_curves.h"
|
|
|
|
// boost
|
|
#pragma GCC diagnostic push
|
|
#pragma GCC diagnostic ignored "-Wshadow"
|
|
#pragma GCC diagnostic ignored "-Wunused-local-typedef"
|
|
#include <boost/geometry.hpp>
|
|
#include <boost/geometry/geometries/point.hpp>
|
|
#include <boost/geometry/geometries/box.hpp>
|
|
#include <boost/geometry/index/rtree.hpp>
|
|
#pragma GCC diagnostic pop
|
|
|
|
// std
|
|
#include <cmath> // std::sqrt
|
|
|
|
namespace bg = boost::geometry;
|
|
namespace bgm = bg::model;
|
|
namespace bgi = bg::index;
|
|
typedef bgm::point<float, 2, bg::cs::cartesian> Point;
|
|
typedef bgm::box<Point> Box;
|
|
typedef std::vector<Point> Points;
|
|
typedef std::vector<Points> Rings;
|
|
typedef std::pair<Point, Point> SegmentPair;
|
|
typedef std::pair<Box, SegmentPair> SegmentValue;
|
|
typedef bgi::rtree<SegmentValue, bgi::rstar<16>> Tree;
|
|
|
|
namespace node_fontnik {
|
|
|
|
struct FaceMetadata {
|
|
std::string family_name;
|
|
std::string style_name;
|
|
std::vector<int> points;
|
|
FaceMetadata(std::string const& _family_name,
|
|
std::string const& _style_name,
|
|
std::vector<int> && _points) :
|
|
family_name(_family_name),
|
|
style_name(_style_name),
|
|
points(std::move(_points)) {}
|
|
FaceMetadata(std::string const& _family_name,
|
|
std::vector<int> && _points) :
|
|
family_name(_family_name),
|
|
points(std::move(_points)) {}
|
|
};
|
|
|
|
struct LoadBaton {
|
|
Nan::Persistent<v8::Function> callback;
|
|
Nan::Persistent<v8::Object> buffer;
|
|
const char * font_data;
|
|
std::size_t font_size;
|
|
std::string error_name;
|
|
std::vector<FaceMetadata> faces;
|
|
uv_work_t request;
|
|
LoadBaton(v8::Local<v8::Object> buf,
|
|
v8::Local<v8::Value> cb) :
|
|
font_data(node::Buffer::Data(buf)),
|
|
font_size(node::Buffer::Length(buf)),
|
|
error_name(),
|
|
faces(),
|
|
request() {
|
|
request.data = this;
|
|
callback.Reset(cb.As<v8::Function>());
|
|
buffer.Reset(buf.As<v8::Object>());
|
|
}
|
|
~LoadBaton() {
|
|
callback.Reset();
|
|
buffer.Reset();
|
|
}
|
|
};
|
|
|
|
struct RangeBaton {
|
|
Nan::Persistent<v8::Function> callback;
|
|
Nan::Persistent<v8::Object> buffer;
|
|
const char* font_data;
|
|
std::size_t font_size;
|
|
std::string error_name;
|
|
std::uint32_t start;
|
|
std::uint32_t end;
|
|
std::vector<std::uint32_t> chars;
|
|
std::string message;
|
|
uv_work_t request;
|
|
RangeBaton(v8::Local<v8::Object> buf,
|
|
v8::Local<v8::Value> cb,
|
|
std::uint32_t _start,
|
|
std::uint32_t _end) :
|
|
font_data(node::Buffer::Data(buf)),
|
|
font_size(node::Buffer::Length(buf)),
|
|
error_name(),
|
|
start(_start),
|
|
end(_end),
|
|
chars(),
|
|
message(),
|
|
request() {
|
|
request.data = this;
|
|
callback.Reset(cb.As<v8::Function>());
|
|
buffer.Reset(buf.As<v8::Object>());
|
|
}
|
|
~RangeBaton() {
|
|
callback.Reset();
|
|
buffer.Reset();
|
|
}
|
|
};
|
|
|
|
NAN_METHOD(Load) {
|
|
// Validate arguments.
|
|
if (!info[0]->IsObject()) {
|
|
return Nan::ThrowTypeError("First argument must be a font buffer");
|
|
}
|
|
v8::Local<v8::Object> obj = info[0]->ToObject();
|
|
if (obj->IsNull() || obj->IsUndefined() || !node::Buffer::HasInstance(obj)) {
|
|
return Nan::ThrowTypeError("First argument must be a font buffer");
|
|
}
|
|
|
|
if (info.Length() < 2 || !info[1]->IsFunction()) {
|
|
return Nan::ThrowTypeError("Callback must be a function");
|
|
}
|
|
|
|
LoadBaton* baton = new LoadBaton(obj,info[1]);
|
|
uv_queue_work(uv_default_loop(), &baton->request, LoadAsync, (uv_after_work_cb)AfterLoad);
|
|
}
|
|
|
|
NAN_METHOD(Range) {
|
|
// Validate arguments.
|
|
if (info.Length() < 1 || !info[0]->IsObject()) {
|
|
return Nan::ThrowTypeError("First argument must be an object of options");
|
|
}
|
|
|
|
v8::Local<v8::Object> options = info[0].As<v8::Object>();
|
|
v8::Local<v8::Value> font_buffer = options->Get(Nan::New<v8::String>("font").ToLocalChecked());
|
|
if (!font_buffer->IsObject()) {
|
|
return Nan::ThrowTypeError("Font buffer is not an object");
|
|
}
|
|
v8::Local<v8::Object> obj = font_buffer->ToObject();
|
|
v8::Local<v8::Value> start = options->Get(Nan::New<v8::String>("start").ToLocalChecked());
|
|
v8::Local<v8::Value> end = options->Get(Nan::New<v8::String>("end").ToLocalChecked());
|
|
|
|
if (obj->IsNull() || obj->IsUndefined() || !node::Buffer::HasInstance(obj)) {
|
|
return Nan::ThrowTypeError("First argument must be a font buffer");
|
|
}
|
|
|
|
if (!start->IsNumber() || start->IntegerValue() < 0) {
|
|
return Nan::ThrowTypeError("option `start` must be a number from 0-65535");
|
|
}
|
|
|
|
if (!end->IsNumber() || end->IntegerValue() > 65535) {
|
|
return Nan::ThrowTypeError("option `end` must be a number from 0-65535");
|
|
}
|
|
|
|
if (end->IntegerValue() < start->IntegerValue()) {
|
|
return Nan::ThrowTypeError("`start` must be less than or equal to `end`");
|
|
}
|
|
|
|
if (info.Length() < 2 || !info[1]->IsFunction()) {
|
|
return Nan::ThrowTypeError("Callback must be a function");
|
|
}
|
|
|
|
RangeBaton* baton = new RangeBaton(obj,
|
|
info[1],
|
|
start->IntegerValue(),
|
|
end->IntegerValue());
|
|
uv_queue_work(uv_default_loop(), &baton->request, RangeAsync, (uv_after_work_cb)AfterRange);
|
|
|
|
NanReturnUndefined();
|
|
}
|
|
|
|
struct ft_library_guard {
|
|
ft_library_guard(FT_Library * lib) :
|
|
library_(lib) {}
|
|
|
|
~ft_library_guard() {
|
|
if (library_) FT_Done_FreeType(*library_);
|
|
}
|
|
|
|
FT_Library * library_;
|
|
};
|
|
|
|
struct ft_glyph_guard {
|
|
ft_glyph_guard(FT_Glyph * glyph) :
|
|
glyph_(glyph) {}
|
|
|
|
~ft_glyph_guard() {
|
|
if (glyph_) FT_Done_Glyph(*glyph_);
|
|
}
|
|
|
|
FT_Glyph * glyph_;
|
|
};
|
|
|
|
void LoadAsync(uv_work_t* req) {
|
|
LoadBaton* baton = static_cast<LoadBaton*>(req->data);
|
|
|
|
FT_Library library = nullptr;
|
|
ft_library_guard library_guard(&library);
|
|
FT_Error error = FT_Init_FreeType(&library);
|
|
if (error) {
|
|
/* LCOV_EXCL_START */
|
|
baton->error_name = std::string("could not open FreeType library");
|
|
return;
|
|
/* LCOV_EXCL_END */
|
|
}
|
|
FT_Face ft_face = 0;
|
|
int num_faces = 0;
|
|
for (int i = 0; ft_face == 0 || i < num_faces; ++i) {
|
|
FT_Error face_error = FT_New_Memory_Face(library, reinterpret_cast<FT_Byte const*>(baton->font_data), static_cast<FT_Long>(baton->font_size), i, &ft_face);
|
|
if (face_error) {
|
|
baton->error_name = std::string("could not open font file");
|
|
return;
|
|
}
|
|
std::set<int> points;
|
|
if (num_faces == 0)
|
|
num_faces = ft_face->num_faces;
|
|
FT_ULong charcode;
|
|
FT_UInt gindex;
|
|
charcode = FT_Get_First_Char(ft_face, &gindex);
|
|
while (gindex != 0) {
|
|
charcode = FT_Get_Next_Char(ft_face, charcode, &gindex);
|
|
if (charcode != 0) points.emplace(charcode);
|
|
}
|
|
|
|
std::vector<int> points_vec(points.begin(), points.end());
|
|
|
|
if (ft_face->style_name) {
|
|
baton->faces.emplace_back(ft_face->family_name, ft_face->style_name, std::move(points_vec));
|
|
} else {
|
|
baton->faces.emplace_back(ft_face->family_name, std::move(points_vec));
|
|
}
|
|
|
|
if (ft_face) {
|
|
FT_Done_Face(ft_face);
|
|
}
|
|
}
|
|
};
|
|
|
|
void AfterLoad(uv_work_t* req) {
|
|
Nan::HandleScope scope;
|
|
|
|
LoadBaton* baton = static_cast<LoadBaton*>(req->data);
|
|
|
|
if (!baton->error_name.empty()) {
|
|
v8::Local<v8::Value> argv[1] = { Nan::Error(baton->error_name.c_str()) };
|
|
Nan::MakeCallback(Nan::GetCurrentContext()->Global(), Nan::New(baton->callback), 1, argv);
|
|
} else {
|
|
v8::Local<v8::Array> js_faces = Nan::New<v8::Array>(baton->faces.size());
|
|
unsigned idx = 0;
|
|
for (auto const& face : baton->faces) {
|
|
v8::Local<v8::Object> js_face = Nan::New<v8::Object>();
|
|
js_face->Set(Nan::New("family_name").ToLocalChecked(), Nan::New(face.family_name).ToLocalChecked());
|
|
if (!face.style_name.empty()) js_face->Set(Nan::New("style_name").ToLocalChecked(), Nan::New(face.style_name).ToLocalChecked());
|
|
v8::Local<v8::Array> js_points = Nan::New<v8::Array>(face.points.size());
|
|
unsigned p_idx = 0;
|
|
for (auto const& pt : face.points) {
|
|
js_points->Set(p_idx++,Nan::New(pt));
|
|
}
|
|
js_face->Set(Nan::New("points").ToLocalChecked(), js_points);
|
|
js_faces->Set(idx++,js_face);
|
|
}
|
|
v8::Local<v8::Value> argv[2] = { Nan::Null(), js_faces };
|
|
Nan::MakeCallback(Nan::GetCurrentContext()->Global(), Nan::New(baton->callback), 2, argv);
|
|
}
|
|
delete baton;
|
|
};
|
|
|
|
void RangeAsync(uv_work_t* req) {
|
|
RangeBaton* baton = static_cast<RangeBaton*>(req->data);
|
|
|
|
unsigned array_size = baton->end - baton->start;
|
|
baton->chars.reserve(array_size);
|
|
for (unsigned i=baton->start; i <= baton->end; i++) {
|
|
baton->chars.emplace_back(i);
|
|
}
|
|
|
|
FT_Library library = nullptr;
|
|
ft_library_guard library_guard(&library);
|
|
FT_Error error = FT_Init_FreeType(&library);
|
|
if (error) {
|
|
/* LCOV_EXCL_START */
|
|
baton->error_name = std::string("could not open FreeType library");
|
|
return;
|
|
/* LCOV_EXCL_END */
|
|
}
|
|
|
|
FT_Face ft_face = 0;
|
|
|
|
mbgl::glyphs::Glyphs glyphs;
|
|
|
|
int num_faces = 0;
|
|
for (int i = 0; ft_face == 0 || i < num_faces; ++i) {
|
|
FT_Error face_error = FT_New_Memory_Face(library, reinterpret_cast<FT_Byte const*>(baton->font_data), static_cast<FT_Long>(baton->font_size), i, &ft_face);
|
|
if (face_error) {
|
|
baton->error_name = std::string("could not open font");
|
|
return;
|
|
}
|
|
|
|
mbgl::glyphs::Face *mutable_face = glyphs.add_faces();
|
|
// mutable_face->set_range(std::to_string(baton->start) + "-" + std::to_string(baton->end));
|
|
mutable_face->set_family_name(ft_face->family_name);
|
|
|
|
if (ft_face->style_name) {
|
|
mutable_face->set_style_name(ft_face->style_name);
|
|
}
|
|
|
|
mutable_face->set_ascender(ft_face->ascender);
|
|
mutable_face->set_descender(ft_face->descender);
|
|
mutable_face->set_line_height(ft_face->height);
|
|
|
|
// Add metadata to face.
|
|
mbgl::glyphs::Face::Metadata mutable_metadata = mutable_face->metadata();
|
|
mutable_metadata.set_size(char_size);
|
|
mutable_metadata.set_buffer(buffer_size);
|
|
mutable_metadata.set_cutoff(cutoff_size);
|
|
mutable_metadata.set_scale(scale_factor);
|
|
mutable_metadata.set_granularity(granularity);
|
|
mutable_metadata.set_offset(offset_size);
|
|
mutable_metadata.set_radius(radius_size);
|
|
|
|
// Set character sizes.
|
|
double size = char_size * scale_factor;
|
|
FT_Set_Char_Size(ft_face,0,(FT_F26Dot6)(size * (1<<6)),0,0);
|
|
|
|
for (std::vector<uint32_t>::size_type x = 0; x != baton->chars.size(); x++) {
|
|
FT_ULong char_code = baton->chars[x];
|
|
glyph_info glyph;
|
|
|
|
// Get FreeType face from face_ptr.
|
|
FT_UInt char_index = FT_Get_Char_Index(ft_face, char_code);
|
|
|
|
if (!char_index) continue;
|
|
|
|
glyph.glyph_index = char_index;
|
|
RenderSDF(glyph, char_size, buffer_size, cutoff_size, ft_face);
|
|
|
|
// Add glyph to face.
|
|
mbgl::glyphs::Glyph *mutable_glyph = mutable_face->add_glyphs();
|
|
mutable_glyph->set_id(char_code);
|
|
mutable_glyph->set_width(glyph.width);
|
|
mutable_glyph->set_height(glyph.height);
|
|
mutable_glyph->set_left(glyph.left);
|
|
mutable_glyph->set_top(glyph.top);
|
|
mutable_glyph->set_advance(glyph.advance);
|
|
|
|
if (glyph.width > 0) {
|
|
mutable_glyph->set_bitmap(glyph.bitmap);
|
|
}
|
|
|
|
}
|
|
|
|
if (ft_face) {
|
|
FT_Done_Face(ft_face);
|
|
}
|
|
}
|
|
|
|
baton->message = glyphs.SerializeAsString();
|
|
}
|
|
|
|
void AfterRange(uv_work_t* req) {
|
|
Nan::HandleScope scope;
|
|
|
|
RangeBaton* baton = static_cast<RangeBaton*>(req->data);
|
|
|
|
if (!baton->error_name.empty()) {
|
|
v8::Local<v8::Value> argv[1] = { Nan::Error(baton->error_name.c_str()) };
|
|
Nan::MakeCallback(Nan::GetCurrentContext()->Global(), Nan::New(baton->callback), 1, argv);
|
|
} else {
|
|
v8::Local<v8::Array> js_faces = Nan::New<v8::Array>();
|
|
unsigned idx = 0;
|
|
v8::Local<v8::Value> argv[2] = { Nan::Null(), Nan::CopyBuffer(baton->message.data(), baton->message.size()).ToLocalChecked() };
|
|
Nan::MakeCallback(Nan::GetCurrentContext()->Global(), Nan::New(baton->callback), 2, argv);
|
|
}
|
|
|
|
delete baton;
|
|
};
|
|
|
|
struct User {
|
|
Rings rings;
|
|
Points ring;
|
|
};
|
|
|
|
void CloseRing(Points &ring) {
|
|
const Point &first = ring.front();
|
|
const Point &last = ring.back();
|
|
|
|
if (first.get<0>() != last.get<0>() ||
|
|
first.get<1>() != last.get<1>()) {
|
|
ring.push_back(first);
|
|
}
|
|
}
|
|
|
|
int MoveTo(const FT_Vector *to, void *ptr) {
|
|
User *user = (User*)ptr;
|
|
if (!user->ring.empty()) {
|
|
CloseRing(user->ring);
|
|
user->rings.push_back(user->ring);
|
|
user->ring.clear();
|
|
}
|
|
user->ring.push_back(Point { float(to->x) / 64, float(to->y) / 64 });
|
|
return 0;
|
|
}
|
|
|
|
int LineTo(const FT_Vector *to, void *ptr) {
|
|
User *user = (User*)ptr;
|
|
user->ring.push_back(Point { float(to->x) / 64, float(to->y) / 64 });
|
|
return 0;
|
|
}
|
|
|
|
int ConicTo(const FT_Vector *control,
|
|
const FT_Vector *to,
|
|
void *ptr) {
|
|
User *user = (User*)ptr;
|
|
|
|
Point prev = user->ring.back();
|
|
|
|
// pop off last point, duplicate of first point in bezier curve
|
|
user->ring.pop_back();
|
|
|
|
agg_fontnik::curve3_div curve(prev.get<0>(), prev.get<1>(),
|
|
float(control->x) / 64, float(control->y) / 64,
|
|
float(to->x) / 64, float(to->y) / 64);
|
|
|
|
curve.rewind(0);
|
|
double x, y;
|
|
unsigned cmd;
|
|
|
|
while (agg_fontnik::path_cmd_stop != (cmd = curve.vertex(&x, &y))) {
|
|
user->ring.push_back(Point {x, y});
|
|
}
|
|
|
|
return 0;
|
|
}
|
|
|
|
int CubicTo(const FT_Vector *c1,
|
|
const FT_Vector *c2,
|
|
const FT_Vector *to,
|
|
void *ptr) {
|
|
User *user = (User*)ptr;
|
|
|
|
Point prev = user->ring.back();
|
|
|
|
// pop off last point, duplicate of first point in bezier curve
|
|
user->ring.pop_back();
|
|
|
|
agg_fontnik::curve4_div curve(prev.get<0>(), prev.get<1>(),
|
|
float(c1->x) / 64, float(c1->y) / 64,
|
|
float(c2->x) / 64, float(c2->y) / 64,
|
|
float(to->x) / 64, float(to->y) / 64);
|
|
|
|
curve.rewind(0);
|
|
double x, y;
|
|
unsigned cmd;
|
|
|
|
while (agg_fontnik::path_cmd_stop != (cmd = curve.vertex(&x, &y))) {
|
|
user->ring.push_back(Point {x, y});
|
|
}
|
|
|
|
return 0;
|
|
}
|
|
|
|
// point in polygon ray casting algorithm
|
|
bool PolyContainsPoint(const Rings &rings, const Point &p) {
|
|
bool c = false;
|
|
|
|
for (const Points &ring : rings) {
|
|
auto p1 = ring.begin();
|
|
auto p2 = p1 + 1;
|
|
|
|
for (; p2 != ring.end(); p1++, p2++) {
|
|
if (((p1->get<1>() > p.get<1>()) != (p2->get<1>() > p.get<1>())) && (p.get<0>() < (p2->get<0>() - p1->get<0>()) * (p.get<1>() - p1->get<1>()) / (p2->get<1>() - p1->get<1>()) + p1->get<0>())) {
|
|
c = !c;
|
|
}
|
|
}
|
|
}
|
|
|
|
return c;
|
|
}
|
|
|
|
double SquaredDistance(const Point &v, const Point &w) {
|
|
const double a = v.get<0>() - w.get<0>();
|
|
const double b = v.get<1>() - w.get<1>();
|
|
return a * a + b * b;
|
|
}
|
|
|
|
Point ProjectPointOnLineSegment(const Point &p,
|
|
const Point &v,
|
|
const Point &w) {
|
|
const double l2 = SquaredDistance(v, w);
|
|
if (l2 == 0) return v;
|
|
|
|
const double t = ((p.get<0>() - v.get<0>()) * (w.get<0>() - v.get<0>()) + (p.get<1>() - v.get<1>()) * (w.get<1>() - v.get<1>())) / l2;
|
|
if (t < 0) return v;
|
|
if (t > 1) return w;
|
|
|
|
return Point {
|
|
v.get<0>() + t * (w.get<0>() - v.get<0>()),
|
|
v.get<1>() + t * (w.get<1>() - v.get<1>())
|
|
};
|
|
}
|
|
|
|
double SquaredDistanceToLineSegment(const Point &p,
|
|
const Point &v,
|
|
const Point &w) {
|
|
const Point s = ProjectPointOnLineSegment(p, v, w);
|
|
return SquaredDistance(p, s);
|
|
}
|
|
|
|
double MinDistanceToLineSegment(const Tree &tree,
|
|
const Point &p,
|
|
int radius) {
|
|
const int squared_radius = radius * radius;
|
|
|
|
std::vector<SegmentValue> results;
|
|
tree.query(bgi::intersects(
|
|
Box{
|
|
Point{p.get<0>() - radius, p.get<1>() - radius},
|
|
Point{p.get<0>() + radius, p.get<1>() + radius}
|
|
}),
|
|
std::back_inserter(results));
|
|
|
|
double sqaured_distance = std::numeric_limits<double>::infinity();
|
|
|
|
for (const auto &value : results) {
|
|
const SegmentPair &segment = value.second;
|
|
const double dist = SquaredDistanceToLineSegment(p,
|
|
segment.first,
|
|
segment.second);
|
|
if (dist < sqaured_distance && dist < squared_radius) {
|
|
sqaured_distance = dist;
|
|
}
|
|
}
|
|
|
|
return std::sqrt(sqaured_distance);
|
|
}
|
|
|
|
void RenderSDF(glyph_info &glyph,
|
|
int size,
|
|
int buffer,
|
|
float cutoff,
|
|
FT_Face ft_face) {
|
|
if (FT_Load_Glyph (ft_face, glyph.glyph_index, FT_LOAD_NO_HINTING)) {
|
|
return;
|
|
}
|
|
|
|
FT_Glyph ft_glyph = nullptr;
|
|
ft_glyph_guard glyph_guard(&ft_glyph);
|
|
if (FT_Get_Glyph(ft_face->glyph, &ft_glyph)) return;
|
|
|
|
int advance = ft_face->glyph->metrics.horiAdvance / 64;
|
|
int ascender = ft_face->size->metrics.ascender / 64;
|
|
int descender = ft_face->size->metrics.descender / 64;
|
|
|
|
glyph.line_height = ft_face->size->metrics.height;
|
|
glyph.advance = advance;
|
|
glyph.ascender = ascender;
|
|
glyph.descender = descender;
|
|
|
|
FT_Outline_Funcs func_interface = {
|
|
.move_to = &MoveTo,
|
|
.line_to = &LineTo,
|
|
.conic_to = &ConicTo,
|
|
.cubic_to = &CubicTo,
|
|
.shift = 0,
|
|
.delta = 0
|
|
};
|
|
|
|
User user;
|
|
|
|
// Decompose outline into bezier curves and line segments
|
|
FT_Outline outline = ((FT_OutlineGlyph)ft_glyph)->outline;
|
|
if (FT_Outline_Decompose(&outline, &func_interface, &user)) return;
|
|
|
|
if (!user.ring.empty()) {
|
|
CloseRing(user.ring);
|
|
user.rings.push_back(user.ring);
|
|
}
|
|
|
|
if (user.rings.empty()) return;
|
|
|
|
// Calculate the real glyph bbox.
|
|
double bbox_xmin = std::numeric_limits<double>::infinity(),
|
|
bbox_ymin = std::numeric_limits<double>::infinity();
|
|
|
|
double bbox_xmax = -std::numeric_limits<double>::infinity(),
|
|
bbox_ymax = -std::numeric_limits<double>::infinity();
|
|
|
|
for (const Points &ring : user.rings) {
|
|
for (const Point &point : ring) {
|
|
if (point.get<0>() > bbox_xmax) bbox_xmax = point.get<0>();
|
|
if (point.get<0>() < bbox_xmin) bbox_xmin = point.get<0>();
|
|
if (point.get<1>() > bbox_ymax) bbox_ymax = point.get<1>();
|
|
if (point.get<1>() < bbox_ymin) bbox_ymin = point.get<1>();
|
|
}
|
|
}
|
|
|
|
bbox_xmin = std::round(bbox_xmin);
|
|
bbox_ymin = std::round(bbox_ymin);
|
|
bbox_xmax = std::round(bbox_xmax);
|
|
bbox_ymax = std::round(bbox_ymax);
|
|
|
|
// Offset so that glyph outlines are in the bounding box.
|
|
for (Points &ring : user.rings) {
|
|
for (Point &point : ring) {
|
|
point.set<0>(point.get<0>() + -bbox_xmin + buffer);
|
|
point.set<1>(point.get<1>() + -bbox_ymin + buffer);
|
|
}
|
|
}
|
|
|
|
if (bbox_xmax - bbox_xmin == 0 || bbox_ymax - bbox_ymin == 0) return;
|
|
|
|
glyph.left = bbox_xmin;
|
|
glyph.top = bbox_ymax;
|
|
glyph.width = bbox_xmax - bbox_xmin;
|
|
glyph.height = bbox_ymax - bbox_ymin;
|
|
|
|
Tree tree;
|
|
float offset = offset_size;
|
|
int radius = radius_size;
|
|
|
|
for (const Points &ring : user.rings) {
|
|
auto p1 = ring.begin();
|
|
auto p2 = p1 + 1;
|
|
|
|
for (; p2 != ring.end(); p1++, p2++) {
|
|
const int segment_x1 = std::min(p1->get<0>(), p2->get<0>());
|
|
const int segment_x2 = std::max(p1->get<0>(), p2->get<0>());
|
|
const int segment_y1 = std::min(p1->get<1>(), p2->get<1>());
|
|
const int segment_y2 = std::max(p1->get<1>(), p2->get<1>());
|
|
|
|
tree.insert(SegmentValue {
|
|
Box {
|
|
Point {segment_x1, segment_y1},
|
|
Point {segment_x2, segment_y2}
|
|
},
|
|
SegmentPair {
|
|
Point {p1->get<0>(), p1->get<1>()},
|
|
Point {p2->get<0>(), p2->get<1>()}
|
|
}
|
|
});
|
|
}
|
|
}
|
|
|
|
// Loop over every pixel and determine the positive/negative distance to the outline.
|
|
unsigned int buffered_width = glyph.width + 2 * buffer;
|
|
unsigned int buffered_height = glyph.height + 2 * buffer;
|
|
unsigned int bitmap_size = buffered_width * buffered_height;
|
|
glyph.bitmap.resize(bitmap_size);
|
|
|
|
for (unsigned int y = 0; y < buffered_height; y++) {
|
|
for (unsigned int x = 0; x < buffered_width; x++) {
|
|
unsigned int ypos = buffered_height - y - 1;
|
|
unsigned int i = ypos * buffered_width + x;
|
|
|
|
double d = MinDistanceToLineSegment(tree, Point {x + offset, y + offset}, radius) * (256 / radius);
|
|
|
|
// Invert if point is inside.
|
|
const bool inside = PolyContainsPoint(user.rings, Point {x + offset, y + offset});
|
|
if (inside) {
|
|
d = -d;
|
|
}
|
|
|
|
// Shift the 0 so that we can fit a few negative values
|
|
// into our 8 bits.
|
|
d += cutoff * 256;
|
|
|
|
// Clamp to 0-255 to prevent overflows or underflows.
|
|
int n = d > 255 ? 255 : d;
|
|
n = n < 0 ? 0 : n;
|
|
n = ((255 - n) / granularity) * granularity;
|
|
|
|
glyph.bitmap[i] = static_cast<char>(n);
|
|
}
|
|
}
|
|
}
|
|
|
|
} // ns node_fontnik
|