diff --git a/LICENSE b/LICENSE index 0972250..a3ee1e5 100644 --- a/LICENSE +++ b/LICENSE @@ -1,6 +1,6 @@ MIT License -Copyright (c) 2018 Cyano Hao +Copyright (c) 2018–2020 Cyano Hao Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal diff --git a/README.md b/README.md index 8a96bba..a0481aa 100644 --- a/README.md +++ b/README.md @@ -11,8 +11,6 @@ * `WarFontMerger-TC-版本号.7z`:繁体中文大字库(覆盖中日韩各国字符,字形采用台湾标准,体积稍大)。 * `WarFontMerger-Classic-版本号.7z`:传统字形大字库(覆盖中日韩各国字符,字形采用传统印刷体风格,体积稍大)。 -除了合并补全工具自带的字体库之外,[WMF 开源字体库](https://github.com/CyanoHao/WFM-Free-Font) 中的字体也经过验证可以用于该工具。 - ## 快速入门(Windows) ### 合并两个字体并补全 @@ -56,7 +54,6 @@ macOS 下的使用方法和 Windows 稍有不同。 ## 限制 -* 目前只支持 TrueType 曲线字体(TrueType 或 OpenType/TT,扩展名通常为 `.ttf`),暂不支持 PostScript 曲线字体(OpenType/CFF 或 OpenType/CID,扩展名通常为 `.otf`)。 * 不提供预编译的 32 位版本。WFM 不可避免地需要操纵汉字,而汉字是一个非常庞大的集合,读取并操作汉字需要巨大的内存,32 位程序极易因为超出内存上限而崩溃。 ## 编译和运行 @@ -110,20 +107,12 @@ otfccbuild base.otd -O2 -o 合并之后的字体.ttf rm *.otd ``` -## 开发计划 - -### 支持对每个字体自定义变换矩阵 - -这样可以更好地匹配原有字体的风格。例如,修改倾斜角度、压缩字符宽度,甚至旋转字符。 - -### 支持 PostScript 曲线字体 - -许多高质量的字体采用了 PostScript 曲线,封装为 OpenType/CFF 或 OpenType/CID 字体。由于 OpenType/CFF 或 OpenType/CID 的字体格式与 OpenType/TT 差别很大,目前还没办法支持。 - ## 感谢 [Belleve Invis](https://github.com/be5invis) 和[李阿玲](https://github.com/clerkma)编写的 [otfcc](https://github.com/caryll/otfcc) 用于解析和生成 OpenType 字体文件。 [Niels Lohmann](https://github.com/nlohmann) 的 [json](https://github.com/nlohmann/json) 库提供了非常漂亮的 C++ JSON 接口。本工具使用了修改版的 `json.hpp`,容许非标准编码的字符。 +TrueType 和 PostScript 曲线相互转换的算法来自 [AFDKO](https://github.com/adobe-type-tools/afdko) 和 [Fontello](https://github.com/fontello/cubic2quad)。这两个算法有[配合 otfcc 使用的独立版本](https://github.com/nowar-fonts/otfcc-quad2cubic),可用于 OpenType/TT 和 OpenType/CFF 字体的相互转换。 + Google 提供了大量的开源字体,Adobe 提供了高质量的[思源黑体](https://github.com/adobe-fonts/source-han-sans)。 diff --git a/build-mac64.bash b/build-mac64.bash index 0ae5b87..93320fd 100755 --- a/build-mac64.bash +++ b/build-mac64.bash @@ -4,8 +4,7 @@ source ./version.bash VERSION=$VERSION-mac64 -clang++ src/merge-otd.cpp src/merge-name.cpp src/iostream.cpp -Isrc/ -std=c++17 -O3 -o bin-mac64/merge-otd -strip bin-mac64/merge-otd +clang++ src/merge-otd.cpp src/merge-name.cpp src/ps2tt.cpp src/tt2ps.cpp src/iostream.cpp -Isrc/ -std=c++17 -O3 -s -o bin-mac64/merge-otd mkdir -p release cd release diff --git a/build-win32.bash b/build-win32.bash index 790a0e1..0760878 100644 --- a/build-win32.bash +++ b/build-win32.bash @@ -4,8 +4,7 @@ source ./version.bash VERSION=$VERSION-win32 -i686-w64-mingw32-g++ src/merge-otd.cpp src/merge-name.cpp src/iostream.cpp -Isrc/ -std=c++17 -O3 -static -Wl,--large-address-aware -o bin-win32/merge-otd.exe -strip bin-win32/merge-otd.exe +i686-w64-mingw32-g++ src/merge-otd.cpp src/merge-name.cpp src/ps2tt.cpp src/tt2ps.cpp src/iostream.cpp -Isrc/ -std=c++17 -O3 -static -s -Wl,--large-address-aware -o bin-win32/merge-otd.exe mkdir -p release cd release diff --git a/build-win64.bash b/build-win64.bash index 0ea5927..58710f2 100644 --- a/build-win64.bash +++ b/build-win64.bash @@ -4,8 +4,7 @@ source ./version.bash VERSION=$VERSION-win64 -x86_64-w64-mingw32-g++ src/merge-otd.cpp src/merge-name.cpp src/iostream.cpp -Isrc/ -std=c++17 -O3 -static -o bin-win64/merge-otd.exe -strip bin-win64/merge-otd.exe +x86_64-w64-mingw32-g++ src/merge-otd.cpp src/merge-name.cpp src/ps2tt.cpp src/tt2ps.cpp src/iostream.cpp -Isrc/ -std=c++17 -O3 -static -s -o bin-win64/merge-otd.exe mkdir -p release cd release diff --git a/src/merge-name.cpp b/src/merge-name.cpp index 2ebd960..eaa9770 100644 --- a/src/merge-name.cpp +++ b/src/merge-name.cpp @@ -438,7 +438,7 @@ void RemoveRedundantTable(vector &nametables) { } // Nowar Sans LCG + Nowar Sans CJK - if (nowarlcg != -1) { + if (nowarlcg != size_t(-1)) { bool hascjk = false; for (size_t i = 0; i < nametables.size(); i++) { json &table = nametables[i]; diff --git a/src/merge-otd.cpp b/src/merge-otd.cpp index 2d2e0ed..906c82f 100644 --- a/src/merge-otd.cpp +++ b/src/merge-otd.cpp @@ -17,10 +17,11 @@ #include "invisible.hpp" #include "merge-name.h" +#include "ps2tt.h" +#include "tt2ps.h" const char *usage = reinterpret_cast(u8"用法:\n\t%s 1.otd 2.otd [n.otd ...]\n"); const char *loadfilefail = reinterpret_cast(u8"读取文件 %s 失败\n"); -const char *mixedpostscript = reinterpret_cast(u8"暂不支持混用 TrueType 和 PostScript 轮廓字体"); using json = nlohmann::json; @@ -188,7 +189,7 @@ int main(int argc, char *u8argv[]) { try { auto s = LoadFile(u8argv[1]); base = json::parse(s); - } catch (std::runtime_error) { + } catch (const std::runtime_error &) { return EXIT_FAILURE; } basecff = IsPostScriptOutline(base); @@ -203,9 +204,11 @@ int main(int argc, char *u8argv[]) { } catch (std::runtime_error) { return EXIT_FAILURE; } - if (IsPostScriptOutline(ext) != basecff) { - nowide::cerr << mixedpostscript << std::endl; - return EXIT_FAILURE; + bool extcff = IsPostScriptOutline(ext); + if (basecff && !extcff) { + ext["glyf"] = Tt2Ps(ext["glyf"]); + } else if (!basecff && extcff) { + ext["glyf"] = Ps2Tt(ext["glyf"]); } RemoveBlankGlyph(ext); nametables.push_back(ext["name"]); diff --git a/src/point.hpp b/src/point.hpp new file mode 100644 index 0000000..acb954e --- /dev/null +++ b/src/point.hpp @@ -0,0 +1,75 @@ +#pragma once + +#include +#include + +struct Point +{ + double x; + double y; + + Point() : x(0), y(0) + { + } + Point(double x, double y) : x(x), y(y) + { + } + Point(const nlohmann::json &p) : x(p["x"]), y(p["y"]) + { + } + nlohmann::json ToJson(bool on) + { + return {{"x", x}, {"y", y}, {"on", on}}; + } +}; + +inline Point operator+(Point a, Point b) +{ + return {a.x + b.x, a.y + b.y}; +} + +inline Point operator-(Point a, Point b) +{ + return {a.x - b.x, a.y - b.y}; +} + +inline Point operator-(Point a) +{ + return {-a.x, -a.y}; +} + +inline Point operator/(Point a, double b) +{ + return {a.x / b, a.y / b}; +} + +inline Point operator*(Point a, double b) +{ + return {a.x * b, a.y * b}; +} + +inline Point operator*(double a, Point b) +{ + return {a * b.x, a * b.y}; +} + +// dot +inline double operator*(Point a, Point b) +{ + return a.x * b.x + a.y * b.y; +} + +inline double abs(Point a) +{ + return sqrt(a * a); +} + +inline void RoundInPlace(nlohmann::json &glyph) +{ + for (auto &contour : glyph["contours"]) + for (auto &point : contour) + { + point["x"] = int(round(double(point["x"]))); + point["y"] = int(round(double(point["y"]))); + } +} diff --git a/src/ps2tt.cpp b/src/ps2tt.cpp new file mode 100644 index 0000000..7e0f45a --- /dev/null +++ b/src/ps2tt.cpp @@ -0,0 +1,409 @@ +#define _USE_MATH_DEFINES + +#include +#include +#include +#include +#include +#include + +#include "point.hpp" +#include "ps2tt.h" + +using nlohmann::json; + +using Coeff2 = std::array; +using Coeff1 = std::array; +using Segment = std::array; +using SegmentQ = std::array; +using Solution = std::vector; + +namespace ConstructTtPath +{ +void Move(json &quadContour, Point p0) +{ + quadContour.push_back(p0.ToJson(true)); +} + +void Line(json &quadContour, Point p1) +{ + size_t length = quadContour.size(); + if (length >= 2 && quadContour[length - 2]["on"]) + { + // 2 lines, merge if the are collinear. + Point p2 = quadContour[length - 1]; + Point p3 = quadContour[length - 2]; + double a = p3.y - p1.y; + double b = p1.x - p3.x; + double c = p1.y * p3.x - p1.x * p3.y; + double distance = abs(a * p2.x + b * p2.y + c) / sqrt(a * a + b * b); + if (distance < 1) + quadContour.erase(length - 1); + } + quadContour.push_back(p1.ToJson(true)); +} + +void Curve(json &quadContour, Point p1, Point p2) +{ + size_t length = quadContour.size(); + Point p0 = quadContour[length - 1]; + if (abs((p0 + p2) / 2 - p1) < 1) + return Line(quadContour, p2); + if (length >= 2 && !quadContour[length - 2]["on"]) + { + // 2 curves, remove on-curve point if it is the center point + Point off = quadContour[length - 2]; + if (abs((p1 + off) / 2 - p0) < 1) + quadContour.erase(length - 1); + } + quadContour.push_back(p1.ToJson(false)); + quadContour.push_back(p2.ToJson(true)); +} + +// merge the last point and the first point +void Finish(json &quadContour) +{ + size_t length = quadContour.size(); + // quadContour[0] and quadContour[-1] are implicitly on-curve + if (length >= 2 && abs(Point(quadContour[0]) - quadContour[length - 1]) < 1) + { + quadContour.erase(length - 1); + length--; + } + if (length <= 2) + return; + if (quadContour[1]["on"] != quadContour[length - 1]["on"]) + // first point is tangent point, do nothing + return; + Point p1 = quadContour[1]; + Point p2 = quadContour[0]; + Point p3 = quadContour[length - 1]; + if (quadContour[1]["on"]) + { + // 2 lines, merge if the are collinear. + double a = p3.y - p1.y; + double b = p1.x - p3.x; + double c = p1.y * p3.x - p1.x * p3.y; + double distance = abs(a * p2.x + b * p2.y + c) / sqrt(a * a + b * b); + if (distance < 1) + quadContour.erase(0); + } + else if (abs((p1 + p3) / 2 - p2) < 1) + // 2 curves, remove on-curve point if it is the center point + quadContour.erase(0); +} +} // namespace ConstructTtPath + +/* point(t) = p1 (1-t)³ + c1 t (1-t)² + c2 t² (1-t) + p2 t³ + = a t³ + b t² + c t + d +*/ +inline Coeff2 CalcPowerCoefficients(Segment s) +{ + auto [p1, c1, c2, p2] = s; + Point a = (p2 - p1) + 3 * (c1 - c2); + Point b = 3 * (p1 + c2) - 6 * c1; + Point c = 3 * (c1 - p1); + Point d = p1; + return {d, c, b, a}; +} + +inline Point CalcPoint(double t, Coeff2 coeff) +{ + auto [d, c, b, a] = coeff; + return ((a * t + b) * t + c) * t + d; +} + +inline Point CalcPointQuad(double t, Coeff2 coeff) +{ + [[maybe_unused]] auto [c, b, a, _] = coeff; + return (a * t + b) * t + c; +} + +inline Point CalcPointDerivative(double t, Coeff2 coeff) +{ + [[maybe_unused]] auto [_, c, b, a] = coeff; + return (3 * a * t + 2 * b) * t + c; +} + +// a x² + b x + c +inline Solution QuadSolve(Coeff1 coeff) +{ + using T = Solution; + [[maybe_unused]] auto [c, b, a, _] = coeff; + if (!a) + return b ? T{-c / b} : T{}; + double delta = b * b - 4 * a * c; + return delta > 0 + ? T{(-b - sqrt(delta)) / (2 * a), (-b + sqrt(delta)) / (2 * a)} + : (delta ? T{-b / (2 * a)} : T{}); +} + +constexpr double curt(double x) +{ + return x < 0 ? -pow(-x, 1.0 / 3) : pow(x, 1.0 / 3); +} + +// a x³ + b x² + c x + d +inline Solution CubicSolve(Coeff1 coeff) +{ + auto [d, c, b, a] = coeff; + if (!a) + return QuadSolve(coeff); + // solve using Cardan's method + // http://www.nickalls.org/dick/papers/maths/cubic1993.pdf + // (doi:10.2307/3619777) + double xn = -b / (3 * a); // point of symmetry x coordinate + double yn = + ((a * xn + b) * xn + c) * xn + d; // point of symmetry y coordinate + double deltaSq = (b * b - 3 * a * c) / (9 * a * a); // delta^2 + double hSq = 4 * a * a * pow(deltaSq, 3); // h^2 + double d3 = yn * yn - hSq; + if (d3 > 0) + // 1 real root + return {xn + curt((-yn + sqrt(d3)) / (2 * a)) + + curt((-yn - sqrt(d3)) / (2 * a))}; + else if (d3 == 0) + // 2 real roots + return {xn - 2 * curt(yn / (2 * a)), xn + curt(yn / (2 * a))}; + else + { + // 3 real roots + double theta = acos(-yn / sqrt(hSq)) / 3; + double delta = sqrt(deltaSq); + return {xn + 2 * delta * cos(theta), + xn + 2 * delta * cos(theta + M_PI * 2 / 3), + xn + 2 * delta * cos(theta + M_PI * 4 / 3)}; + } +} + +/* f(t) = p1 (1-t)² + 2 c1 t (1-t) + p2 t² + = a t^2 + b t + c, t in [0, 1], + a = p1 + p2 - 2 * c1 + b = 2 * (c1 - p1) + c = p1 + + The distance between given point and quadratic curve is equal to + sqrt((f(t) - p)²), so these expression has zero derivative by t at + points where (f'(t), (f(t) - point)) = 0. + + Substituting quadratic curve as f(t) one could obtain a cubic equation + e3 t³ + e2 t² + e1 t + e0 = 0 with following coefficients: + e3 = 2 a² + e2 = 3 a b + e1 = b² + 2 a (c - p) + e0 = (c - p) b + + One of the roots of the equation from [0, 1], or t = 0 or t = 1 is a value of + t at which the distance between given point and quadratic Bezier curve has +minimum. + So to find the minimal distance one have to just pick the minimum value +of + the distance on set {t = 0, t = 1, t is root of the equation from [0, 1] }. +*/ +double MinDistanceToQuad(Point p, SegmentQ s) +{ + auto [p1, c1, p2] = s; + Point a = p1 + p2 - 2 * c1; + Point b = 2 * (c1 - p1); + Point c = p1; + Coeff1 e = {(c - p) * b, b * b + 2 * a * (c - p), 3 * a * b, 2 * a * a}; + Solution _candidates = CubicSolve(e); + Solution candidates = {0, 1}; + std::copy_if(_candidates.begin(), _candidates.end(), + std::back_inserter(candidates), + [](double t) { return t > 0 && t < 1; }); + + double minDistance = 1e9; + for (auto t : candidates) + { + double distance = abs(CalcPointQuad(t, {c, b, a, {}}) - p); + if (distance < minDistance) + minDistance = distance; + } + return minDistance; +} + +SegmentQ ProcessSegment(double t1, double t2, Coeff2 coeff) +{ + Point f1 = CalcPoint(t1, coeff); + Point f2 = CalcPoint(t2, coeff); + Point f1d = CalcPointDerivative(t1, coeff); + Point f2d = CalcPointDerivative(t2, coeff); + + // normal vector: p -- tangent vector + auto normal = [](Point p) { return Point{-p.y, p.x}; }; + + double d = f1d * normal(f2d); + if (abs(d) < 1e-6) + return {f1, (f1 + f2) / 2, f2}; + else + return {f1, (f1d * (f2 * normal(f2d)) + f2d * (f1 * -normal(f1d))) / d, + f2}; +} + +bool IsSegmentApproximationClose(double tmin, double tmax, Coeff2 coeff, + SegmentQ s, double error) +{ + int n = 4; + double dt = (tmax - tmin) / n; + for (double t = tmin + dt; t < tmax - 1e-6; t += dt) + { + Point p = CalcPoint(t, coeff); + if (MinDistanceToQuad(p, s) > error) + return false; + } + return true; +} + +/* Split cubic bézier curve into two cubic curves, see details here: + https://math.stackexchange.com/questions/877725 +*/ +static std::pair SubdivideCubic(double t, Segment s) +{ + auto [p1, c1, c2, p2] = s; + Point b = (1 - t) * p1 + t * c1; + Point _ = (1 - t) * c1 + t * c2; + Point f = (1 - t) * c2 + t * p2; + Point c = (1 - t) * b + t * _; + Point e = (1 - t) * _ + t * f; + Point d = (1 - t) * c + t * e; + return {{p1, b, c, d}, {d, e, f, p2}}; +} + +/* Find inflection points on a cubic curve, algorithm is similar to this one: + http://www.caffeineowl.com/graphics/2d/vectorial/cubic-inflexion.html +*/ +static Solution SolveInflections(Segment s) +{ + auto [p1, c1, c2, p2] = s; + double p = -(p2.x * (p1.y - 2 * c1.y + c2.y)) + + c2.x * (2 * p1.y - 3 * c1.y + p2.y) + + p1.x * (c1.y - 2 * c2.y + p2.y) - + c1.x * (p1.y - 3 * c2.y + 2 * p2.y); + double q = p2.x * (p1.y - c1.y) + 3 * c2.x * (-p1.y + c1.y) + + c1.x * (2 * p1.y - 3 * c2.y + p2.y) - + p1.x * (2 * c1.y - 3 * c2.y + p2.y); + double r = + c2.x * (p1.y - c1.y) + p1.x * (c1.y - c2.y) + c1.x * (-p1.y + c2.y); + + Solution result_ = QuadSolve({r, q, p, 0}); + Solution result; + std::copy_if(result_.begin(), result_.end(), std::back_inserter(result), + [](double t) { return t > 1e-6 && t < 1 - 1e-6; }); + std::sort(result.begin(), result.end()); + return result; +} + +// approximate cubic segment w/o inflections +static void ApproximateSimpleSegment(Segment s, json &quadContour, double error) +{ + auto [p1, c1, c2, p2] = s; + Coeff2 pc = CalcPowerCoefficients(s); + + std::vector apprx; + for (int segCount = 1; segCount <= 4; segCount++) + { + apprx = {}; + double dt = 1.0 / segCount; + bool isClose = true; + for (int i = 0; i < segCount; i++) + { + SegmentQ seg = ProcessSegment(i * dt, (i + 1) * dt, pc); + isClose = isClose && IsSegmentApproximationClose( + i * dt, (i + 1) * dt, pc, seg, error); + apprx.push_back(seg); + } + if (segCount == 1 && ((apprx[0][1] - p1) * (c1 - p1) < -1e-6 || + (apprx[0][1] - p2) * (c2 - p2) < -1e-6)) + // approximation concave, while the curve is convex (or vice versa) + continue; + if (isClose) + break; + } + + for (auto seg : apprx) + ConstructTtPath::Curve(quadContour, seg[1], seg[2]); +} + +static void ApproximateCurve(Segment s, json &quadContour, double error) +{ + Solution inflections = SolveInflections(s); + if (!inflections.size()) + return ApproximateSimpleSegment(s, quadContour, error); + Segment curve = s; + double prev = 0; + for (double i : inflections) + { + auto split = SubdivideCubic(1 - (1 - i) / (1 - prev), curve); + ApproximateSimpleSegment(split.first, quadContour, error); + curve = split.second; + prev = i; + } + ApproximateSimpleSegment(curve, quadContour, error); +} + +static json Convert(json glyph, double error) +{ + glyph.erase("stemH"); + glyph.erase("stemV"); + glyph.erase("hintMasks"); + glyph.erase("contourMasks"); + + for (json &contour : glyph["contours"]) + { + if (contour.size() <= 1) + continue; + + json quadContour = json::array(); + Segment s; + auto beg = contour.cbegin(); + auto end = --contour.cend(); + size_t cnt = contour.size(); + + // save initial on-curve point + json::const_iterator q = beg; + s[0] = *q; + ConstructTtPath::Move(quadContour, s[0]); + + // advance to next point, in reversed direction + auto advance = [beg, end](auto q) { return (q == beg) ? end : q - 1; }; + + while (cnt > 0) + { + q = advance(q); + if ((*q)["on"]) + { + s[0] = *q; + ConstructTtPath::Line(quadContour, s[0]); + cnt--; + } + else + { + s[1] = *q; + q--; // it’s safe here + s[2] = *q; + q = advance(q); + s[3] = *q; + ApproximateCurve(s, quadContour, error); + s[0] = s[3]; + cnt -= 3; + } + } + + ConstructTtPath::Finish(quadContour); + contour = quadContour; + } + + return glyph; +} + +json Ps2Tt(const json &glyf, double errorBound) +{ + json glyfQuad; + for (const auto &[name, glyph] : glyf.items()) + { + glyfQuad[name] = Convert(glyph, errorBound); + RoundInPlace(glyfQuad[name]); + } + return glyfQuad; +} diff --git a/src/ps2tt.h b/src/ps2tt.h new file mode 100644 index 0000000..129cece --- /dev/null +++ b/src/ps2tt.h @@ -0,0 +1,5 @@ +#pragma once + +#include + +nlohmann::json Ps2Tt(const nlohmann::json &glyf, double errorBound = 1); diff --git a/src/tt2ps.cpp b/src/tt2ps.cpp new file mode 100644 index 0000000..bfb7bc0 --- /dev/null +++ b/src/tt2ps.cpp @@ -0,0 +1,333 @@ +#include +#include +#include +#include + +#include "point.hpp" +#include "tt2ps.h" + +using nlohmann::json; + +static void TransformInPlace(json &glyph, double a, double b, double c, + double d, double dx, double dy) +{ + if (glyph.find("contours") != glyph.end()) + for (auto &contour : glyph["contours"]) + for (auto &point : contour) + { + double x = point["x"]; + double y = point["y"]; + point["x"] = a * x + c * y + dx; + point["y"] = b * x + d * y + dy; + } + // we have dereferenced the glyph. +} + +static json Dereference(json glyph, const json &glyf) +{ + if (glyph.find("references") == glyph.end()) + return glyph; + glyph["contours"] = json::array(); + + for (const auto &ref : glyph["references"]) + { + json target = glyf[std::string(ref["glyph"])]; + if (target.find("references") != target.end()) + target = Dereference(std::move(target), glyf); + TransformInPlace(target, ref["a"], ref["b"], ref["c"], ref["d"], + ref["x"], ref["y"]); + std::copy(target["contours"].begin(), target["contours"].end(), + std::back_inserter(glyph["contours"])); + } + + glyph.erase("references"); + return glyph; +} + +namespace ConstructCffPath +{ +void Move(json &cubicContour, Point p0) +{ + cubicContour.push_back(p0.ToJson(true)); +} + +void Line(json &cubicContour, Point p1) +{ + size_t length = cubicContour.size(); + if (length >= 2 && cubicContour[length - 2]["on"]) + { + // 2 lines, merge if the are collinear. + Point p2 = cubicContour[length - 1]; + Point p3 = cubicContour[length - 2]; + double a = p3.y - p1.y; + double b = p1.x - p3.x; + double c = p1.y * p3.x - p1.x * p3.y; + double distance = abs(a * p2.x + b * p2.y + c) / sqrt(a * a + b * b); + if (distance < 1) + cubicContour.erase(length - 1); + } + cubicContour.push_back(p1.ToJson(true)); +} + +void Curve(json &cubicContour, Point p1, Point p2, Point p3) +{ + cubicContour.push_back(p1.ToJson(false)); + cubicContour.push_back(p2.ToJson(false)); + cubicContour.push_back(p3.ToJson(true)); +} + +// merge the last point and the first point +void Finish(json &cubicContour) +{ + size_t length = cubicContour.size(); + // cubicContour[0] and cubicContour[-1] are implicitly on-curve + if (length >= 2 && + abs(Point(cubicContour[0]) - cubicContour[length - 1]) < 1) + { + cubicContour.erase(length - 1); + length--; + } + if (length >= 3 && cubicContour[1]["on"] && cubicContour[length - 1]["on"]) + { + // 2 lines, merge if the are collinear. + Point p1 = cubicContour[1]; + Point p2 = cubicContour[0]; + Point p3 = cubicContour[length - 1]; + double a = p3.y - p1.y; + double b = p1.x - p3.x; + double c = p1.y * p3.x - p1.x * p3.y; + double distance = abs(a * p2.x + b * p2.y + c) / sqrt(a * a + b * b); + if (distance < 1) + cubicContour.erase(0); + } +} +} // namespace ConstructCffPath + +static void SimpleCurve(Point *p, json &cubicContour) +{ + ConstructCffPath::Curve(cubicContour, (p[0] + 2 * p[1]) / 3, + (2 * p[1] + p[2]) / 3, p[2]); +} + +/* reimplemented afdko’s ttread::combinePair + + test if curve pair should be combined. + if true, combine curve and save to cubicContour, else save the first segment. + return 1 if curves combined else 0. +*/ +static int CombinePair(Point *p, json &cubicContour) +{ + double a = p[3].y - p[1].y; + double b = p[1].x - p[3].x; + if ((a != 0 || p[1].y != p[2].y) && (b != 0 || p[1].x != p[2].x)) + { + // Not a vertical or horizontal join... + double absq = a * a + b * b; + if (absq != 0) + { + double sr = a * (p[2].x - p[1].x) + b * (p[2].y - p[1].y); + if ((sr * sr) / absq < 1) + { + // ...that is straight... + if ((a * (p[0].x - p[1].x) + b * (p[0].y - p[1].y) < 0) == + (a * (p[4].x - p[1].x) + b * (p[4].y - p[1].y) < 0)) + { + // ...and without inflexion... + double d0 = (p[2].x - p[0].x) * (p[2].x - p[0].x) + + (p[2].y - p[0].y) * (p[2].y - p[0].y); + double d1 = (p[4].x - p[2].x) * (p[4].x - p[2].x) + + (p[4].y - p[2].y) * (p[4].y - p[2].y); + if (d0 <= 3 * d1 && d1 <= 3 * d0) + { + // ...and small segment length ratio; combine curve + ConstructCffPath::Curve(cubicContour, + (4 * p[1] - p[0]) / 3, + (4 * p[3] - p[4]) / 3, p[4]); + p[0] = p[4]; + return 1; + } + } + } + } + } + + // save first curve then replace it by second curve + SimpleCurve(p, cubicContour); + p[0] = p[2]; + p[1] = p[3]; + p[2] = p[4]; + + return 0; +} + +/* reimplemented afdko’s ttread::callbackApproxPath + + state sequence points + 0=off,1=on accumulated + 0 1 0 + 1 1 0 0-1 + 2 1 0 0 0-3 (p[2] is mid-point of p[1] and p[3]) + 3 1 0 1 0-2 + 4 1 0 1 0 0-3 +*/ +static json ConvertApprox(json glyph, const json &glyf) +{ + glyph = Dereference(std::move(glyph), glyf); + glyph.erase("instructions"); + glyph.erase("LTSH_yPel"); + + for (json &contour : glyph["contours"]) + { + if (contour.size() <= 1) + continue; + + json cubicContour = json::array(); + Point p[6]; // points: 0,2,4-on, 1,3-off, 5-tmp + json::const_iterator q; // current point + auto beg = contour.cbegin(); + auto end = --contour.cend(); + size_t cnt = contour.size(); + int state = 0; + + // save initial on-curve point + if ((*beg)["on"]) + { + q = beg; + p[0] = *q; + } + else if ((*end)["on"]) + { + q = end; + p[0] = *q; + } + else + { + // start at mid-point + q = beg; + cnt++; + p[0] = (Point(*beg) + *end) / 2; + } + ConstructCffPath::Move(cubicContour, p[0]); + + while (cnt--) + { + // advance to next point, in reversed direction + q = (q == beg) ? end : q - 1; + + if ((*q)["on"]) + { + // on-curve + switch (state) + { + case 0: + if (cnt > 0) + { + p[0] = *q; + ConstructCffPath::Line(cubicContour, p[0]); + // stay in state 0 + } + break; + case 1: + p[2] = *q; + state = 3; + break; + case 2: + p[4] = *q; + state = CombinePair(p, cubicContour) ? 0 : 3; + break; + case 3: + SimpleCurve(p, cubicContour); + if (cnt > 0) + { + p[0] = *q; + ConstructCffPath::Line(cubicContour, p[0]); + } + state = 0; + break; + case 4: + p[4] = *q; + state = CombinePair(p, cubicContour) ? 0 : 3; + break; + } + } + else + { + // off-curve + switch (state) + { + case 0: + p[1] = *q; + state = 1; + break; + case 1: + p[3] = *q; + p[2] = (p[1] + p[3]) / 2; + state = 2; + break; + case 2: + p[5] = *q; + p[4] = (p[3] + p[5]) / 2; + if (CombinePair(p, cubicContour)) + { + p[1] = p[5]; + state = 1; + } + else + { + p[3] = p[5]; + state = 4; + } + break; + case 3: + p[3] = *q; + state = 4; + break; + case 4: + p[5] = *q; + p[4] = (p[3] + p[5]) / 2; + if (CombinePair(p, cubicContour)) + { + p[1] = p[5]; + state = 1; + } + else + { + p[3] = p[5]; + state = 2; + } + break; + } + } + } + + // finish up + switch (state) + { + case 2: + p[3] = *q; + p[2] = (p[1] + p[3]) / 2; + [[fallthrough]]; + case 3: + case 4: + SimpleCurve(p, cubicContour); + break; + } + + ConstructCffPath::Finish(cubicContour); + contour = cubicContour; + } + + return glyph; +} + +json Tt2Ps(const json &glyf, bool roundToInt) +{ + json glyfCubic; + for (const auto &[name, glyph] : glyf.items()) + { + glyfCubic[name] = ConvertApprox(glyph, glyf); + if (roundToInt) + RoundInPlace(glyfCubic[name]); + } + return glyfCubic; +} diff --git a/src/tt2ps.h b/src/tt2ps.h new file mode 100644 index 0000000..9d0a113 --- /dev/null +++ b/src/tt2ps.h @@ -0,0 +1,5 @@ +#pragma once + +#include + +nlohmann::json Tt2Ps(const nlohmann::json &glyf, bool roundToInt = true); diff --git a/version.bash b/version.bash index 52709f5..624bade 100644 --- a/version.bash +++ b/version.bash @@ -1 +1 @@ -VERSION=0.3.3 +VERSION=1.0.0