mirror of
https://github.com/mirkobrombin/raven.git
synced 2026-10-30 21:35:06 +00:00
272 lines
13 KiB
C++
272 lines
13 KiB
C++
#include "text.h"
|
|
#include "include/core/SkBitmap.h"
|
|
#include "include/core/SkCanvas.h"
|
|
#include "include/core/SkFont.h"
|
|
#include "include/core/SkFontMetrics.h"
|
|
#include "include/core/SkFontMgr.h"
|
|
#include "include/core/SkPaint.h"
|
|
#include "include/core/SkTypeface.h"
|
|
#include "include/ports/SkFontMgr_empty.h"
|
|
#include <algorithm>
|
|
#include <cmath>
|
|
#include <initializer_list>
|
|
#include <stdexcept>
|
|
|
|
namespace raven {
|
|
|
|
static uint32_t field(std::span<const uint8_t> bytes,size_t at,size_t count=4)
|
|
{
|
|
uint32_t value=0;
|
|
for (size_t i=0;i<count;++i) value|=uint32_t(bytes[at+i])<<(i*8);
|
|
return value;
|
|
}
|
|
|
|
struct TextRenderer::State {
|
|
std::array<sk_sp<SkTypeface>,3> faces;
|
|
// Every bundled style: family (sans, mono, serif) by regular, bold, italic, bold italic.
|
|
std::array<std::array<sk_sp<SkTypeface>,4>,3> styled;
|
|
|
|
static bool contains_any(const std::string &name,std::initializer_list<const char *> words)
|
|
{
|
|
for (const auto *word : words)
|
|
if (name.find(word)!=name.npos) return true;
|
|
return false;
|
|
}
|
|
|
|
// Maps any logical font onto the bundled families and style, sizing it the way GDI does.
|
|
SkFont logical(std::span<const uint8_t,92> definition)
|
|
{
|
|
const auto height=int32_t(field(definition,0)),weight=int32_t(field(definition,16));
|
|
const bool italic=definition[20]!=0;
|
|
const uint8_t quality=definition[26],pitch=definition[27];
|
|
std::string name;
|
|
for (size_t i=0;i<32;++i) {
|
|
const auto c=field(definition,28+i*2,2); if (!c) break;
|
|
name.push_back(c<128 ? char(c>='A' && c<='Z' ? c+32 : c) : '?');
|
|
}
|
|
size_t family=0;
|
|
if (contains_any(name,{"courier","consolas","mono","fixed","lucida console","terminal","menlo","monaco","typewriter"}) || ((pitch&0x30)==0x30 && !contains_any(name,{"sans","arial","tahoma","segoe"})))
|
|
family=1;
|
|
else if (contains_any(name,{"times","serif","georgia","cambria","garamond","palatino","book antiqua","century"}) && !contains_any(name,{"sans"}))
|
|
family=2;
|
|
const size_t style=(weight>=600 ? 1U : 0U)|(italic ? 2U : 0U);
|
|
auto face=styled[family][style];
|
|
if (!face) face=styled[family][0];
|
|
if (!face) face=faces[family];
|
|
float size=height<0 ? float(-height) : height>0 ? float(height) : 12.0f;
|
|
SkFont probe(face,100.0f);
|
|
SkFontMetrics source; probe.getMetrics(&source);
|
|
if (height>0) {
|
|
// A positive height is the whole cell (ascent plus descent); scale so the rounded cell matches it.
|
|
const float cell=(-source.fAscent+source.fDescent)/100.0f;
|
|
size=float(height)/cell;
|
|
SkFont trial(face,size); SkFontMetrics measured; trial.getMetrics(&measured);
|
|
const float adjusted=float(height)/std::max(1.0f,float(std::lround(-measured.fAscent)+std::lround(measured.fDescent)))*size;
|
|
if (std::fabs(adjusted-size)<size*0.15f) size=adjusted;
|
|
}
|
|
SkFont result(face,std::max(size,1.0f));
|
|
result.setHinting(SkFontHinting::kFull); result.setSubpixel(false); result.setLinearMetrics(false);
|
|
result.setEdging(quality==3 ? SkFont::Edging::kAlias : SkFont::Edging::kAntiAlias);
|
|
return result;
|
|
}
|
|
|
|
std::pair<SkFont,size_t> font(std::span<const uint8_t,92> definition)
|
|
{
|
|
const auto height=int32_t(field(definition,0));
|
|
if (height>=0 || height < -256 || field(definition,4) || field(definition,8) || field(definition,12) ||
|
|
(field(definition,16)!=400 && field(definition,16)!=0) || definition[20] || definition[21] || definition[22] ||
|
|
definition[23] || definition[24] || definition[25] || definition[26]!=3 || definition[27]) throw uint32_t(120);
|
|
std::string name;
|
|
for (size_t i=0;i<32;++i) {
|
|
const auto c=field(definition,28+i*2,2); if (!c) break;
|
|
if (c>127) throw uint32_t(120);
|
|
name.push_back(char(c>='A' && c<='Z' ? c+32 : c));
|
|
}
|
|
size_t family;
|
|
if (name=="liberation sans" || name=="arial") family=0;
|
|
else if (name=="liberation mono" || name=="courier new") family=1;
|
|
else if (name=="liberation serif" || name=="times new roman") family=2;
|
|
else throw uint32_t(120);
|
|
SkFont result(faces[family],SkScalar(-height));
|
|
result.setHinting(SkFontHinting::kFull); result.setSubpixel(false); result.setLinearMetrics(false);
|
|
result.setEdging(SkFont::Edging::kAntiAlias);
|
|
return {result,family};
|
|
}
|
|
};
|
|
|
|
TextRenderer::TextRenderer(const std::string &directory) : state(std::make_unique<State>())
|
|
{
|
|
auto manager=SkFontMgr_New_Custom_Empty();
|
|
const std::array<const char *,3> files{"LiberationSans-Regular.ttf","LiberationMono-Regular.ttf","LiberationSerif-Regular.ttf"};
|
|
for (size_t i=0;i<files.size();++i) {
|
|
state->faces[i]=manager->makeFromFile((directory+"/"+files[i]).c_str());
|
|
if (!state->faces[i]) throw std::runtime_error("cannot load qualified native font file");
|
|
state->styled[i][0]=state->faces[i];
|
|
}
|
|
const std::array<std::array<const char *,3>,3> styles{{
|
|
{"LiberationSans-Bold.ttf","LiberationSans-Italic.ttf","LiberationSans-BoldItalic.ttf"},
|
|
{"LiberationMono-Bold.ttf","LiberationMono-Italic.ttf","LiberationMono-BoldItalic.ttf"},
|
|
{"LiberationSerif-Bold.ttf","LiberationSerif-Italic.ttf","LiberationSerif-BoldItalic.ttf"}}};
|
|
for (size_t family=0;family<3;++family)
|
|
for (size_t style=0;style<3;++style)
|
|
state->styled[family][style+1]=manager->makeFromFile((directory+"/"+styles[family][style]).c_str());
|
|
}
|
|
|
|
TextRenderer::~TextRenderer() = default;
|
|
|
|
TextMetrics TextRenderer::metrics(std::span<const uint8_t,92> definition)
|
|
{
|
|
auto [font,family]=state->font(definition); SkFontMetrics source; font.getMetrics(&source);
|
|
const auto rounded=[](float value) { return int32_t(std::lround(value)); };
|
|
const auto ascent=rounded(-source.fAscent),descent=rounded(source.fDescent);
|
|
std::array<uint8_t,96> table{};
|
|
if (state->faces[family]->getTableData(0x4f532f32,0,table.size(),table.data())!=table.size())
|
|
throw std::runtime_error("qualified font has no complete OS/2 metrics");
|
|
const auto character=[&](size_t at) { return uint16_t((uint16_t(table[at])<<8)|table[at+1]); };
|
|
return {ascent+descent,ascent,descent,rounded(-source.fAscent+source.fDescent-font.getSize()),rounded(source.fLeading),
|
|
rounded(source.fAvgCharWidth),rounded(source.fMaxCharWidth),character(64),character(66),31,character(92),
|
|
uint8_t(family==0 ? 39 : family==1 ? 54 : 23)};
|
|
}
|
|
|
|
static std::vector<SkGlyphID> glyphs(const SkFont &font,std::span<const uint16_t> text)
|
|
{
|
|
if (text.size()>4096) throw uint32_t(87);
|
|
for (const auto c : text)
|
|
if (!((c>=32 && c<=126) || (c>=0x00a0 && c<=0x024f) || (c>=0x0370 && c<=0x03ff) || (c>=0x1e00 && c<=0x1eff))) throw uint32_t(120);
|
|
std::vector<SkGlyphID> result(text.size());
|
|
const auto count=font.textToGlyphs(text.data(),text.size_bytes(),SkTextEncoding::kUTF16,result);
|
|
if (count!=text.size()) throw uint32_t(120);
|
|
for (const auto glyph : result) if (!glyph) throw uint32_t(120);
|
|
return result;
|
|
}
|
|
|
|
TextExtent TextRenderer::extent(std::span<const uint8_t,92> definition,std::span<const uint16_t> text)
|
|
{
|
|
auto [font,family]=state->font(definition); (void)family;
|
|
const auto ids=glyphs(font,text); std::vector<SkScalar> widths(ids.size()); font.getWidths(ids,widths);
|
|
float width=0; for (const auto value : widths) width+=value;
|
|
return {int32_t(std::lround(width)),metrics(definition).height};
|
|
}
|
|
|
|
std::vector<uint8_t> TextRenderer::mask(std::span<const uint8_t,92> definition,std::span<const uint16_t> text,
|
|
uint32_t width,uint32_t height,int32_t x,int32_t y,std::span<const std::array<int32_t,2>> positions)
|
|
{
|
|
if (!width || !height || width>8192 || height>8192 || uint64_t(width)*height>16*1024*1024) throw uint32_t(87);
|
|
auto [font,family]=state->font(definition); (void)family;
|
|
const auto ids=glyphs(font,text);
|
|
font.setEdging(SkFont::Edging::kAlias);
|
|
std::vector<uint8_t> pixels(size_t(width)*height*4);
|
|
SkBitmap bitmap;
|
|
if (!bitmap.installPixels(SkImageInfo::Make(int(width),int(height),kBGRA_8888_SkColorType,kOpaque_SkAlphaType),pixels.data(),size_t(width)*4))
|
|
throw std::bad_alloc();
|
|
SkCanvas canvas(bitmap); canvas.clear(SK_ColorWHITE); SkPaint paint; paint.setColor(SK_ColorBLACK);
|
|
const auto ascent=metrics(definition).ascent;
|
|
if(positions.empty())canvas.drawSimpleText(text.data(),text.size_bytes(),SkTextEncoding::kUTF16,SkScalar(x),SkScalar(int64_t(y)+ascent),font,paint);
|
|
else{
|
|
if(positions.size()!=ids.size())throw uint32_t(87);
|
|
std::vector<SkPoint> points;points.reserve(positions.size());
|
|
for(const auto &position:positions)points.push_back({SkScalar(int64_t(x)+position[0]),SkScalar(int64_t(y)+ascent+position[1])});
|
|
canvas.drawGlyphs({ids.data(),ids.size()},{points.data(),points.size()},{0,0},font,paint);
|
|
}
|
|
std::vector<uint8_t> coverage(size_t(width)*height);
|
|
for (size_t i=0;i<coverage.size();++i) {
|
|
if (pixels[i*4]!=0 && pixels[i*4]!=255) throw std::runtime_error("qualified monochrome text produced intermediate coverage");
|
|
coverage[i]=pixels[i*4]==0;
|
|
}
|
|
return coverage;
|
|
}
|
|
|
|
std::vector<int32_t> TextRenderer::advances(std::span<const uint8_t,92> definition,std::span<const uint16_t> text)
|
|
{
|
|
auto [font,family]=state->font(definition);(void)family;
|
|
const auto ids=glyphs(font,text);std::vector<SkScalar> widths(ids.size());font.getWidths(ids,widths);
|
|
std::vector<int32_t> result;result.reserve(widths.size());float total=0;
|
|
for(const auto width:widths){total+=width;result.push_back(int32_t(std::lround(total)));}
|
|
return result;
|
|
}
|
|
|
|
|
|
// The glyphs for UTF-16 text; characters the face lacks use its replacement glyph and controls draw nothing.
|
|
static std::vector<SkGlyphID> logical_glyphs(const SkFont &font,std::span<const uint16_t> text)
|
|
{
|
|
std::vector<SkGlyphID> result(text.size(),0);
|
|
std::vector<uint16_t> printable;
|
|
for (size_t i=0;i<text.size();++i) {
|
|
const uint16_t c=text[i];
|
|
printable.push_back(c<32 ? 32 : c);
|
|
}
|
|
if (!printable.empty()) {
|
|
// Surrogate pairs map to one glyph; the second unit keeps glyph 0 so positions stay unit-indexed.
|
|
for (size_t i=0;i<printable.size();++i) {
|
|
SkGlyphID id=0;
|
|
font.textToGlyphs(&printable[i],2,SkTextEncoding::kUTF16,{&id,1});
|
|
result[i]=id;
|
|
}
|
|
}
|
|
return result;
|
|
}
|
|
|
|
TextMetrics TextRenderer::logical_metrics(std::span<const uint8_t,92> definition)
|
|
{
|
|
// Fonts inside the qualified subset keep their measured OS/2 metrics.
|
|
try { return metrics(definition); } catch (const uint32_t &) {}
|
|
auto font=state->logical(definition); SkFontMetrics source; font.getMetrics(&source);
|
|
const auto rounded=[](float value) { return int32_t(std::lround(value)); };
|
|
const auto ascent=rounded(-source.fAscent),descent=rounded(source.fDescent);
|
|
const auto average=source.fAvgCharWidth>0 ? source.fAvgCharWidth : font.getSize()*0.5f;
|
|
const auto maximum=source.fMaxCharWidth>0 ? source.fMaxCharWidth : font.getSize();
|
|
// Fixed-pitch faces report no variable-pitch flag.
|
|
const auto *face=font.getTypeface();
|
|
const bool fixed=face==state->styled[1][0].get() || face==state->styled[1][1].get() ||
|
|
face==state->styled[1][2].get() || face==state->styled[1][3].get();
|
|
return {ascent+descent,ascent,descent,std::max(0,rounded(-source.fAscent+source.fDescent-font.getSize())),rounded(source.fLeading),
|
|
rounded(average),rounded(maximum),32,255,31,32,uint8_t(fixed ? 0x36 : 0x27)};
|
|
}
|
|
|
|
std::vector<int32_t> TextRenderer::logical_advances(std::span<const uint8_t,92> definition,std::span<const uint16_t> text)
|
|
{
|
|
auto font=state->logical(definition);
|
|
const auto ids=logical_glyphs(font,text);
|
|
std::vector<SkScalar> widths(ids.size(),0);
|
|
if (!ids.empty()) font.getWidths(ids,widths);
|
|
std::vector<int32_t> result; result.reserve(ids.size());
|
|
// Prefixes round the running float sum, like the qualified measurement, so widths never drift per glyph.
|
|
float total=0;
|
|
for (size_t i=0;i<ids.size();++i) {
|
|
if (text[i]>=32 && !(text[i]>=0xdc00 && text[i]<=0xdfff)) total+=widths[i];
|
|
result.push_back(int32_t(std::lround(total)));
|
|
}
|
|
return result;
|
|
}
|
|
|
|
std::vector<uint8_t> TextRenderer::render(std::span<const uint8_t,92> definition,std::span<const uint16_t> text,
|
|
uint32_t width,uint32_t height,int32_t origin_x,int32_t origin_y,std::span<const int32_t> offsets)
|
|
{
|
|
if (!width || !height || width>16384 || height>16384 || uint64_t(width)*height>32*1024*1024) throw uint32_t(87);
|
|
auto font=state->logical(definition);
|
|
const auto ids=logical_glyphs(font,text);
|
|
std::vector<uint8_t> alpha(size_t(width)*height);
|
|
SkBitmap bitmap;
|
|
if (!bitmap.installPixels(SkImageInfo::MakeA8(int(width),int(height)),alpha.data(),width)) throw std::bad_alloc();
|
|
SkCanvas canvas(bitmap); canvas.clear(SK_ColorTRANSPARENT);
|
|
SkPaint paint; paint.setColor(SK_ColorBLACK); paint.setAntiAlias(font.getEdging()!=SkFont::Edging::kAlias);
|
|
SkFontMetrics source; font.getMetrics(&source);
|
|
const auto baseline=SkScalar(origin_y)+SkScalar(std::lround(-source.fAscent));
|
|
std::vector<SkScalar> widths(ids.size(),0);
|
|
if (!ids.empty()) font.getWidths(ids,widths);
|
|
std::vector<SkGlyphID> drawn; std::vector<SkPoint> points;
|
|
SkScalar cursor=0;
|
|
for (size_t i=0;i<ids.size();++i) {
|
|
const SkScalar step=text[i]>=32 && !(text[i]>=0xdc00 && text[i]<=0xdfff) ? SkScalar(std::lround(widths[i])) : 0;
|
|
if (text[i]>=32 && ids[i]) {
|
|
drawn.push_back(ids[i]);
|
|
points.push_back({SkScalar(origin_x)+(offsets.empty() ? cursor : SkScalar(offsets[i])),baseline});
|
|
}
|
|
cursor+=step;
|
|
}
|
|
if (!drawn.empty()) canvas.drawGlyphs({drawn.data(),drawn.size()},{points.data(),points.size()},{0,0},font,paint);
|
|
return alpha;
|
|
}
|
|
|
|
}
|