Bug Summary

File:tools/ttf2lff/main.cpp
Warning:line 1487, column 90
Value stored to 'first' is never read

Annotated Source Code

Press '?' to see keyboard shortcuts

clang -cc1 -cc1 -triple x86_64-pc-linux-gnu -analyze -disable-free -clear-ast-before-backend -disable-llvm-verifier -discard-value-names -main-file-name main.cpp -analyzer-checker=core -analyzer-checker=apiModeling -analyzer-checker=unix -analyzer-checker=deadcode -analyzer-checker=cplusplus -analyzer-checker=security.insecureAPI.UncheckedReturn -analyzer-checker=security.insecureAPI.getpw -analyzer-checker=security.insecureAPI.gets -analyzer-checker=security.insecureAPI.mktemp -analyzer-checker=security.insecureAPI.mkstemp -analyzer-checker=security.insecureAPI.vfork -analyzer-checker=nullability.NullPassedToNonnull -analyzer-checker=nullability.NullReturnedFromNonnull -analyzer-output plist -w -setup-static-analyzer -mrelocation-model pic -pic-level 2 -fhalf-no-semantic-interposition -mframe-pointer=none -fmath-errno -ffp-contract=on -fno-rounding-math -mconstructor-aliases -funwind-tables=2 -target-cpu x86-64 -tune-cpu generic -debugger-tuning=gdb -fdebug-compilation-dir=/home/runner/work/LibreCAD/LibreCAD/tools/ttf2lff -fcoverage-compilation-dir=/home/runner/work/LibreCAD/LibreCAD/tools/ttf2lff -resource-dir /usr/lib/llvm-18/lib/clang/18 -D _REENTRANT -D MUPARSER_STATIC -D VERSION=0.0.0.2 -D QT_NO_DEBUG -I . -I /usr/include/freetype2 -I /usr/include/libpng16 -I ../../../Qt/6.9.0/gcc_64/mkspecs/linux-g++ -internal-isystem /usr/bin/../lib/gcc/x86_64-linux-gnu/14/../../../../include/c++/14 -internal-isystem /usr/bin/../lib/gcc/x86_64-linux-gnu/14/../../../../include/x86_64-linux-gnu/c++/14 -internal-isystem /usr/bin/../lib/gcc/x86_64-linux-gnu/14/../../../../include/c++/14/backward -internal-isystem /usr/lib/llvm-18/lib/clang/18/include -internal-isystem /usr/local/include -internal-isystem /usr/bin/../lib/gcc/x86_64-linux-gnu/14/../../../../x86_64-linux-gnu/include -internal-externc-isystem /usr/include/x86_64-linux-gnu -internal-externc-isystem /include -internal-externc-isystem /usr/include -O2 -std=gnu++1z -fdeprecated-macro -ferror-limit 19 -fgnuc-version=4.2.1 -fskip-odr-check-in-gmf -fcxx-exceptions -fexceptions -vectorize-loops -vectorize-slp -analyzer-output=html -faddrsig -D__GCC_HAVE_DWARF2_CFI_ASM=1 -o /home/runner/work/LibreCAD/LibreCAD/out/2026-08-04-154929-5069-1 -x c++ main.cpp
1/****************************************************************************
2** $Id: main.cpp $
3**
4** Copyright (C) 2001-2003 RibbonSoft. All rights reserved.
5** Copyright (C) 2011 Rallaz - rallazz@gmail.com
6** Copyright (C) 2025 LibreCAD (librecad.org)
7** Copyright (C) 2026 Dongxu Li (github.com/dxli)
8**
9** This file is part of the ttf2lff project.
10**
11** This file may be distributed and/or modified under the terms of the
12** GNU General Public License version 2 as published by the Free Software
13** Foundation and appearing in the file LICENSE.GPL included in the
14** packaging of this file.
15**
16** This file is provided AS IS with NO WARRANTY OF ANY KIND, INCLUDING THE
17** WARRANTY OF DESIGN, MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE.
18**
19** See http://www.ribbonsoft.com for further details.
20**
21** Contact info@ribbonsoft.com if any conditions of this licensing are
22** not clear to you.
23**
24**********************************************************************/
25
26#ifdef __APPLE__
27 #include <sys/types.h>
28#endif
29#ifdef __WIN32__
30 #include <time.h>
31#endif
32
33#include <algorithm>
34#include <cctype>
35#include <cerrno>
36#include <cmath>
37#include <cstdint>
38#include <cstdlib>
39#include <cstring>
40#include <fstream>
41#include <iomanip>
42#include <iostream>
43#include <limits>
44#include <map>
45#include <memory>
46#include <set>
47#include <sstream>
48#include <string>
49#include <unordered_map>
50#include <vector>
51
52#include <ft2build.h>
53#include FT_FREETYPE_H<freetype/freetype.h>
54#include FT_GLYPH_H<freetype/ftglyph.h>
55#include FT_MODULE_H<freetype/ftmodapi.h>
56#include FT_OUTLINE_H<freetype/ftoutln.h>
57
58// RAII wrappers for FreeType resources
59struct FTLibraryDeleter {
60 void operator()(FT_Library lib) const {
61 if (lib) FT_Done_FreeType(lib);
62 }
63};
64
65struct FTFaceDeleter {
66 void operator()(FT_Face f) const {
67 if (f) FT_Done_Face(f);
68 }
69};
70
71struct FTGlyphDeleter {
72 void operator()(FT_Glyph g) const {
73 if (g) FT_Done_Glyph(g);
74 }
75};
76
77using FTLibraryPtr = std::unique_ptr<std::remove_pointer_t<FT_Library>, FTLibraryDeleter>;
78using FTFacePtr = std::unique_ptr<std::remove_pointer_t<FT_Face>, FTFaceDeleter>;
79using FTGlyphPtr = std::unique_ptr<std::remove_pointer_t<FT_Glyph>, FTGlyphDeleter>;
80
81static std::string FT_StrError(FT_Error errnum)
82{
83 #undef __FTERRORS_H__
84 #define FT_ERRORDEF( e, v, s ) { e, s },
85 #define FT_ERROR_START_LIST {
86 #define FT_ERROR_END_LIST { 0, 0 } };
87
88 static const struct {
89 FT_Error errnum;
90 const char * errstr;
91 } ft_errtab[] =
92 #include FT_ERRORS_H<freetype/fterrors.h>
93
94 const FT_Error errno_max = (FT_Error)((sizeof(ft_errtab) / sizeof(ft_errtab[0])) - 2 /* FT_ERROR_END_LIST */);
95 if(errno(*__errno_location ()) > errno_max)
96 {
97 return "Internal error";
98 }
99
100 return std::string(ft_errtab[errnum].errstr);
101}
102
103// Data structures for glyph buffering
104struct Vertex {
105 double x;
106 double y;
107 double bulge; // Arc bulge factor (0.0 if not an arc)
108
109 Vertex() : x(0), y(0), bulge(0.0) {}
110 Vertex(double x_, double y_, double bulge_ = 0.0) : x(x_), y(y_), bulge(bulge_) {}
111
112 bool operator==(const Vertex& other) const {
113 // Use tolerance for floating point comparison
114 const double EPS = 1e-9;
115 return std::abs(x - other.x) < EPS &&
116 std::abs(y - other.y) < EPS &&
117 std::abs(bulge - other.bulge) < EPS;
118 }
119
120 bool operator!=(const Vertex& other) const {
121 return !(*this == other);
122 }
123};
124
125struct BoundingBox {
126 double xMin, yMin, xMax, yMax;
127 BoundingBox() : xMin(0), yMin(0), xMax(0), yMax(0) {}
128 BoundingBox(double xmin, double ymin, double xmax, double ymax)
129 : xMin(xmin), yMin(ymin), xMax(xmax), yMax(ymax) {}
130
131 bool contains(const BoundingBox& other) const {
132 return xMin <= other.xMin && yMin <= other.yMin &&
133 xMax >= other.xMax && yMax >= other.yMax;
134 }
135
136 bool overlaps(const BoundingBox& other) const {
137 return xMin < other.xMax && xMax > other.xMin &&
138 yMin < other.yMax && yMax > other.yMin;
139 }
140
141 double width() const { return xMax - xMin; }
142 double height() const { return yMax - yMin; }
143 double area() const { return width() * height(); }
144};
145
146struct Polyline {
147 std::vector<Vertex> vertices;
148 std::string comment;
149 BoundingBox bbox;
150
151 bool isEmpty() const { return vertices.empty(); }
152
153 void updateBoundingBox() {
154 if (vertices.empty()) {
155 bbox = BoundingBox();
156 return;
157 }
158 double xMin = vertices[0].x, yMin = vertices[0].y;
159 double xMax = vertices[0].x, yMax = vertices[0].y;
160 for (size_t i = 1; i < vertices.size(); ++i) {
161 xMin = std::min(xMin, vertices[i].x);
162 yMin = std::min(yMin, vertices[i].y);
163 xMax = std::max(xMax, vertices[i].x);
164 yMax = std::max(yMax, vertices[i].y);
165 }
166 bbox = BoundingBox(xMin, yMin, xMax, yMax);
167 }
168};
169
170struct GlyphReference {
171 unsigned int charCode = 0;
172};
173
174struct Glyph {
175 unsigned int charCode;
176 std::string symbol;
177 std::vector<GlyphReference> references;
178 std::vector<Polyline> polylines;
179 std::string comment;
180 BoundingBox bbox;
181
182 bool isEmpty() const { return references.empty() && polylines.empty(); }
183
184 void updateBoundingBox() {
185 if (polylines.empty()) {
186 bbox = BoundingBox();
187 return;
188 }
189 double xMin = polylines[0].bbox.xMin, yMin = polylines[0].bbox.yMin;
190 double xMax = polylines[0].bbox.xMax, yMax = polylines[0].bbox.yMax;
191 for (size_t i = 1; i < polylines.size(); ++i) {
192 xMin = std::min(xMin, polylines[i].bbox.xMin);
193 yMin = std::min(yMin, polylines[i].bbox.yMin);
194 xMax = std::max(xMax, polylines[i].bbox.xMax);
195 yMax = std::max(yMax, polylines[i].bbox.yMax);
196 }
197 bbox = BoundingBox(xMin, yMin, xMax, yMax);
198 }
199};
200
201// Encapsulated state for ttf2lff converter
202class TTF2LFFConverter {
203private:
204 FTLibraryPtr library;
205 FTFacePtr face;
206 std::ofstream outputFile;
207 double prevx;
208 double prevy;
209 bool firstpass;
210 bool startcontour;
211 float xMin;
212 int nodes, precision;
213 double factor;
214 int yMax;
215 std::string numFormat;
216 std::vector<Glyph> glyphBuffer;
217
218public:
219 TTF2LFFConverter()
220 : prevx(0), prevy(0), firstpass(false), startcontour(false),
221 xMin(0), nodes(4), precision(6), factor(0), yMax(-1000) {}
222
223 // Disable copy
224 TTF2LFFConverter(const TTF2LFFConverter&) = delete;
225 TTF2LFFConverter& operator=(const TTF2LFFConverter&) = delete;
226
227 // Enable move
228 TTF2LFFConverter(TTF2LFFConverter&&) = default;
229 TTF2LFFConverter& operator=(TTF2LFFConverter&&) = default;
230
231 ~TTF2LFFConverter() {
232 // Resources are automatically cleaned up by smart pointers
233 }
234
235 FT_Error initLibrary() {
236 FT_Library lib = nullptr;
237 FT_Error error = FT_Init_FreeType(&lib);
238 if (error) {
239 std::cerr << "FT_Init_FreeType: " << FT_StrError(error) << std::endl;
240 return error;
241 }
242 library.reset(lib);
243 return error;
244 }
245
246 FT_Error loadFace(const std::string& filename) {
247 FT_Face f = nullptr;
248 FT_Error error = FT_New_Face(library.get(), filename.c_str(), 0, &f);
249 if (error) {
250 std::cerr << "FT_New_Face: " << filename << ": " << FT_StrError(error) << std::endl;
251 return error;
252 }
253 face.reset(f);
254 return error;
255 }
256
257 bool openOutputFile(const std::string& filename) {
258 outputFile.open(filename, std::ios::binary);
259 if (!outputFile.is_open()) {
260 std::cerr << "Cannot open " << filename << ": " << strerror(errno(*__errno_location ())) << "\n";
261 return false;
262 }
263 return true;
264 }
265
266 void closeOutputFile() {
267 if (outputFile.is_open()) {
268 outputFile.close();
269 }
270 }
271
272 FT_Face getFace() const { return face.get(); }
273 FT_Library getLibrary() const { return library.get(); }
274 std::ofstream& getOutputFile() { return outputFile; }
275
276 // State accessors for callbacks
277 double& getPrevX() { return prevx; }
278 double& getPrevY() { return prevy; }
279 bool& getFirstPass() { return firstpass; }
280 bool& getStartContour() { return startcontour; }
281 float& getXMin() { return xMin; }
282 int& getNodes() { return nodes; }
283 int& getPrecision() { return precision; }
284 double& getFactor() { return factor; }
285 int& getYMax() { return yMax; }
286 std::string& getNumFormat() { return numFormat; }
287 std::vector<Glyph>& getGlyphBuffer() { return glyphBuffer; }
288};
289
290// Tuning algorithm flags (from python-lff)
291struct TuningOptions {
292 bool zeroVector = false; // Remove duplicate consecutive vertices
293 bool roundVertex = false; // Round vertices to grid
294 bool mergePath = false; // Merge paths with same vertex
295 bool nestedChar = false; // Find nested characters
296 bool noComment = false; // Remove glyph comments
297 double roundGrid = 0.0001; // Grid size for rounding
298};
299
300TuningOptions tuningOptions;
301
302// Forward declarations
303int moveTo(FT_Vector* to, void* /*fp*/);
304int lineTo(FT_Vector* to, void* /*fp*/);
305int conicTo(FT_Vector* /*control*/, FT_Vector* to, void* /*fp*/);
306int cubicTo(FT_Vector* control1, FT_Vector* control2, FT_Vector* to, void* /*fp*/);
307
308// Global converter instance (for callbacks)
309static TTF2LFFConverter* g_converter = nullptr;
310
311static const FT_Outline_Funcs funcs
312= {
313 (FT_Outline_MoveTo_FuncFT_Outline_MoveToFunc) moveTo,
314 (FT_Outline_LineTo_FuncFT_Outline_LineToFunc) lineTo,
315 (FT_Outline_ConicTo_FuncFT_Outline_ConicToFunc)conicTo,
316 (FT_Outline_CubicTo_FuncFT_Outline_CubicToFunc)cubicTo,
317 0, 0
318 };
319
320// Callback function implementations
321int moveTo(FT_Vector* to, void* /*fp*/) {
322 if (!g_converter) return 0;
323 auto& firstpass = g_converter->getFirstPass();
324 auto& xMin = g_converter->getXMin();
325 auto& prevx = g_converter->getPrevX();
326 auto& prevy = g_converter->getPrevY();
327
328 if (firstpass) {
329 if (to->x < xMin)
330 xMin = to->x;
331 } else {
332 prevx = to->x;
333 prevy = to->y;
334 }
335 return 0;
336}
337
338int lineTo(FT_Vector* to, void* /*fp*/) {
339 if (!g_converter) return 0;
340 auto& firstpass = g_converter->getFirstPass();
341 auto& xMin = g_converter->getXMin();
342 auto& prevx = g_converter->getPrevX();
343 auto& prevy = g_converter->getPrevY();
344 auto& yMax = g_converter->getYMax();
345
346 if (firstpass) {
347 if (to->x < xMin)
348 xMin = to->x;
349 } else {
350 prevx = to->x;
351 prevy = to->y;
352 if (to->y > yMax) {
353 yMax = to->y;
354 }
355 }
356 return 0;
357}
358
359int conicTo(FT_Vector* /*control*/, FT_Vector* to, void* /*fp*/) {
360 if (!g_converter) return 0;
361 auto& firstpass = g_converter->getFirstPass();
362 auto& xMin = g_converter->getXMin();
363 auto& prevx = g_converter->getPrevX();
364 auto& prevy = g_converter->getPrevY();
365 auto& yMax = g_converter->getYMax();
366
367 if (firstpass) {
368 if (to->x < xMin)
369 xMin = to->x;
370 } else {
371 prevx = to->x;
372 prevy = to->y;
373 if (to->y > yMax) {
374 yMax = to->y;
375 }
376 }
377 return 0;
378}
379
380int cubicTo(FT_Vector* /*control1*/, FT_Vector* /*control2*/, FT_Vector* to, void* /*fp*/) {
381 if (!g_converter) return 0;
382 auto& firstpass = g_converter->getFirstPass();
383 auto& xMin = g_converter->getXMin();
384 auto& prevx = g_converter->getPrevX();
385 auto& prevy = g_converter->getPrevY();
386 auto& yMax = g_converter->getYMax();
387
388 if (firstpass) {
389 if (to->x < xMin)
390 xMin = to->x;
391 } else {
392 prevx = to->x;
393 prevy = to->y;
394 if (to->y > yMax) {
395 yMax = to->y;
396 }
397 }
398 return 0;
399}
400
401struct OutlineBuildContext {
402 int nodes = 4;
403 bool hasCurrent = false;
404 FT_Vector currentPoint = {0, 0};
405 Polyline currentPolyline;
406 std::vector<Polyline> rawPolylines;
407};
408
409void finishCurrentPolyline(OutlineBuildContext& context)
410{
411 if (context.currentPolyline.vertices.size() >= 2) {
412 context.rawPolylines.push_back(context.currentPolyline);
413 }
414 context.currentPolyline = Polyline();
415}
416
417void appendOutlinePoint(OutlineBuildContext& context, double x, double y)
418{
419 context.currentPolyline.vertices.emplace_back(x, y, 0.0);
420}
421
422int buildMoveTo(FT_Vector* to, void* user)
423{
424 auto* context = static_cast<OutlineBuildContext*>(user);
425 finishCurrentPolyline(*context);
426 appendOutlinePoint(*context, to->x, to->y);
427 context->currentPoint = *to;
428 context->hasCurrent = true;
429 return 0;
430}
431
432int buildLineTo(FT_Vector* to, void* user)
433{
434 auto* context = static_cast<OutlineBuildContext*>(user);
435 appendOutlinePoint(*context, to->x, to->y);
436 context->currentPoint = *to;
437 context->hasCurrent = true;
438 return 0;
439}
440
441int buildConicTo(FT_Vector* control, FT_Vector* to, void* user)
442{
443 auto* context = static_cast<OutlineBuildContext*>(user);
444 const FT_Vector from = context->currentPoint;
445 const int steps = std::max(1, context->nodes);
446
447 for (int i = 1; i <= steps; ++i) {
448 const double t = static_cast<double>(i) / steps;
449 const double mt = 1.0 - t;
450 const double x = mt * mt * from.x + 2.0 * mt * t * control->x + t * t * to->x;
451 const double y = mt * mt * from.y + 2.0 * mt * t * control->y + t * t * to->y;
452 appendOutlinePoint(*context, x, y);
453 }
454
455 context->currentPoint = *to;
456 context->hasCurrent = true;
457 return 0;
458}
459
460int buildCubicTo(FT_Vector* control1, FT_Vector* control2, FT_Vector* to, void* user)
461{
462 auto* context = static_cast<OutlineBuildContext*>(user);
463 const FT_Vector from = context->currentPoint;
464 const int steps = std::max(1, context->nodes);
465
466 for (int i = 1; i <= steps; ++i) {
467 const double t = static_cast<double>(i) / steps;
468 const double mt = 1.0 - t;
469 const double x = mt * mt * mt * from.x
470 + 3.0 * mt * mt * t * control1->x
471 + 3.0 * mt * t * t * control2->x
472 + t * t * t * to->x;
473 const double y = mt * mt * mt * from.y
474 + 3.0 * mt * mt * t * control1->y
475 + 3.0 * mt * t * t * control2->y
476 + t * t * t * to->y;
477 appendOutlinePoint(*context, x, y);
478 }
479
480 context->currentPoint = *to;
481 context->hasCurrent = true;
482 return 0;
483}
484
485static const FT_Outline_Funcs glyphBuildFuncs = {
486 (FT_Outline_MoveTo_FuncFT_Outline_MoveToFunc) buildMoveTo,
487 (FT_Outline_LineTo_FuncFT_Outline_LineToFunc) buildLineTo,
488 (FT_Outline_ConicTo_FuncFT_Outline_ConicToFunc) buildConicTo,
489 (FT_Outline_CubicTo_FuncFT_Outline_CubicToFunc) buildCubicTo,
490 0,
491 0
492};
493
494std::vector<Polyline> buildPolylinesFromOutline(FT_Outline& outline, int nodes, double factor, FT_Error& error)
495{
496 OutlineBuildContext context;
497 context.nodes = nodes;
498
499 error = FT_Outline_Decompose(&outline, &glyphBuildFuncs, &context);
500 if (error) {
501 return {};
502 }
503
504 finishCurrentPolyline(context);
505 if (context.rawPolylines.empty()) {
506 return {};
507 }
508
509 double xMin = std::numeric_limits<double>::max();
510 for (const auto& polyline : context.rawPolylines) {
511 for (const auto& vertex : polyline.vertices) {
512 xMin = std::min(xMin, vertex.x);
513 }
514 }
515
516 std::vector<Polyline> polylines;
517 polylines.reserve(context.rawPolylines.size());
518 for (auto polyline : context.rawPolylines) {
519 for (auto& vertex : polyline.vertices) {
520 vertex.x = (vertex.x - xMin) * factor;
521 vertex.y *= factor;
522 }
523 polyline.updateBoundingBox();
524 polylines.push_back(std::move(polyline));
525 }
526
527 return polylines;
528}
529
530/**
531 * Format a number, removing trailing zeros
532 */
533std::string clearZeros(double num) {
534 if (!g_converter) {
535 // Fallback if converter not initialized
536 char buffer[50];
537 snprintf(buffer, sizeof(buffer), "%.6f", num);
538 std::string str = buffer;
539 int i = static_cast<int>(str.length()) - 1;
540 while (i > 1 && str.at(i) == '0') {
541 --i;
542 }
543 if (str.at(i) != '.')
544 ++i;
545 return str.substr(0, i);
546 }
547
548 int precision = g_converter->getPrecision();
549 const std::string& numFormat = g_converter->getNumFormat();
550
551 std::string numLine(precision + 10, '\0');
552 int len = snprintf(&numLine[0], precision + 10, numFormat.c_str(), num);
553 std::string str = numLine.substr(0, len);
554 int i = static_cast<int>(str.length()) - 1;
555 while (i > 1 && str.at(i) == '0') {
556 --i;
557 }
558 if (str.at(i) != '.')
559 ++i;
560 return str.substr(0, i);
561}
562
563/**
564 * Round a value to the nearest grid point
565 */
566double roundToGrid(double value) {
567 if (!tuningOptions.roundVertex) return value;
568 return std::round(value / tuningOptions.roundGrid) * tuningOptions.roundGrid;
569}
570
571std::string formatCharCode(unsigned int charCode)
572{
573 // LFF readers treat leading zeros as optional ([0020] == [20]), so
574 // write the minimal hex form to save a byte or two per glyph.
575 std::ostringstream stream;
576 stream << std::hex << std::nouppercase << charCode;
577 return stream.str();
578}
579
580bool canReferenceGlyph(unsigned int charCode)
581{
582 // Keep nested-glyph references within the BMP for maximum reader compatibility.
583 return charCode <= 0xffff;
584}
585
586bool shouldWriteHeaderSymbol(FT_ULong charcode)
587{
588 if (charcode == 0x7f) {
589 return false;
590 }
591 if (charcode < 0x20) {
592 return false;
593 }
594 if (charcode >= 0x80 && charcode <= 0x9f) {
595 return false;
596 }
597 return true;
598}
599
600std::string serializePolyline(const Polyline& polyline)
601{
602 std::ostringstream stream;
603 for (size_t i = 0; i < polyline.vertices.size(); ++i) {
604 const auto& vertex = polyline.vertices[i];
605 if (i > 0) {
606 stream << ';';
607 }
608 stream << clearZeros(vertex.x) << ',' << clearZeros(vertex.y);
609 if (vertex.bulge != 0.0) {
610 stream << ",A" << clearZeros(vertex.bulge);
611 }
612 }
613 return stream.str();
614}
615
616struct PathEntry {
617 uint32_t id = 0;
618 int count = 0;
619};
620
621using PathMultiset = std::vector<PathEntry>;
622
623struct GlyphPathInfo {
624 PathMultiset paths;
625 std::vector<uint32_t> polylinePathIds;
626 int pathCount = 0;
627 size_t serializedBytes = 0;
628 size_t refBytes = 0;
629 uint32_t rarestPathId = std::numeric_limits<uint32_t>::max();
630 unsigned int charCode = 0;
631 bool refable = false;
632};
633
634uint32_t internPathId(const std::string& path,
635 std::unordered_map<std::string, uint32_t>& pathIds,
636 std::vector<std::string>& pathText)
637{
638 const auto found = pathIds.find(path);
639 if (found != pathIds.end()) {
640 return found->second;
641 }
642
643 const uint32_t id = static_cast<uint32_t>(pathText.size());
644 pathText.push_back(path);
645 pathIds.emplace(pathText.back(), id);
646 return id;
647}
648
649PathMultiset compactPathIds(std::vector<uint32_t>& ids)
650{
651 PathMultiset paths;
652 if (ids.empty()) {
653 return paths;
654 }
655
656 std::sort(ids.begin(), ids.end());
657 for (const uint32_t id : ids) {
658 if (!paths.empty() && paths.back().id == id) {
659 ++paths.back().count;
660 } else {
661 paths.push_back({id, 1});
662 }
663 }
664
665 return paths;
666}
667
668GlyphPathInfo buildGlyphPathInfo(const Glyph& glyph,
669 std::unordered_map<std::string, uint32_t>& pathIds,
670 std::vector<std::string>& pathText)
671{
672 GlyphPathInfo info;
673 info.charCode = glyph.charCode;
674 info.refable = canReferenceGlyph(glyph.charCode);
675 info.refBytes = 1 + formatCharCode(glyph.charCode).size() + 1;
676 info.polylinePathIds.reserve(glyph.polylines.size());
677
678 std::vector<uint32_t> ids;
679 for (const auto& polyline : glyph.polylines) {
680 if (polyline.vertices.size() < 2) {
681 info.polylinePathIds.push_back(std::numeric_limits<uint32_t>::max());
682 continue;
683 }
684 const uint32_t id = internPathId(serializePolyline(polyline), pathIds, pathText);
685 info.polylinePathIds.push_back(id);
686 ids.push_back(id);
687 ++info.pathCount;
688 info.serializedBytes += pathText[id].size() + 1;
689 }
690
691 info.paths = compactPathIds(ids);
692 return info;
693}
694
695int pathCount(const PathMultiset& paths)
696{
697 int count = 0;
698 for (const auto& entry : paths) {
699 count += entry.count;
700 }
701 return count;
702}
703
704bool containsPathMultiset(const PathMultiset& haystack, const PathMultiset& needle)
705{
706 size_t i = 0;
707 size_t j = 0;
708
709 while (i < haystack.size() && j < needle.size()) {
710 if (haystack[i].id < needle[j].id) {
711 ++i;
712 continue;
713 }
714 if (haystack[i].id > needle[j].id) {
715 return false;
716 }
717 if (haystack[i].count < needle[j].count) {
718 return false;
719 }
720 ++i;
721 ++j;
722 }
723
724 return j == needle.size();
725}
726
727void subtractPathMultiset(PathMultiset& haystack, const PathMultiset& needle)
728{
729 PathMultiset result;
730 result.reserve(haystack.size());
731
732 size_t i = 0;
733 size_t j = 0;
734 while (i < haystack.size()) {
735 PathEntry entry = haystack[i];
736 if (j < needle.size() && entry.id == needle[j].id) {
737 entry.count -= needle[j].count;
738 ++j;
739 } else if (j < needle.size() && entry.id > needle[j].id) {
740 ++j;
741 continue;
742 }
743 if (entry.count > 0) {
744 result.push_back(entry);
745 }
746 ++i;
747 }
748
749 haystack = std::move(result);
750}
751
752bool decrementPathEntry(PathMultiset& paths, uint32_t id)
753{
754 for (auto it = paths.begin(); it != paths.end(); ++it) {
755 if (it->id != id) {
756 continue;
757 }
758 --it->count;
759 if (it->count == 0) {
760 paths.erase(it);
761 }
762 return true;
763 }
764 return false;
765}
766
767bool removePolylineMultiset(Glyph& glyph, std::vector<uint32_t>& polylinePathIds, const PathMultiset& paths)
768{
769 PathMultiset remaining = paths;
770 std::vector<bool> remove(glyph.polylines.size(), false);
771 const uint32_t invalidPathId = std::numeric_limits<uint32_t>::max();
772
773 if (polylinePathIds.size() != glyph.polylines.size()) {
774 return false;
775 }
776 for (size_t i = 0; i < glyph.polylines.size(); ++i) {
777 const uint32_t id = polylinePathIds[i];
778 if (id != invalidPathId && decrementPathEntry(remaining, id)) {
779 remove[i] = true;
780 }
781 }
782
783 if (!remaining.empty()) {
784 return false;
785 }
786
787 std::vector<Polyline> kept;
788 std::vector<uint32_t> keptPathIds;
789 kept.reserve(glyph.polylines.size());
790 keptPathIds.reserve(polylinePathIds.size());
791 for (size_t i = 0; i < glyph.polylines.size(); ++i) {
792 if (!remove[i]) {
793 kept.push_back(std::move(glyph.polylines[i]));
794 keptPathIds.push_back(polylinePathIds[i]);
795 }
796 }
797
798 glyph.polylines = std::move(kept);
799 polylinePathIds = std::move(keptPathIds);
800 glyph.updateBoundingBox();
801 return true;
802}
803
804/**
805 * TUNING ALGORITHMS (from python-lff)
806 */
807
808/**
809 * ZeroVector: Remove two identical consecutive vertices
810 * This removes duplicate points that can occur from bezier curve approximations
811 */
812void applyZeroVector(Glyph& glyph) {
813 if (!tuningOptions.zeroVector) return;
814
815 for (auto& polyline : glyph.polylines) {
816 if (polyline.vertices.size() <= 1) continue;
817
818 std::vector<Vertex> filtered;
819 filtered.reserve(polyline.vertices.size());
820
821 for (const auto& v : polyline.vertices) {
822 if (filtered.empty()) {
823 filtered.push_back(v);
824 } else {
825 const Vertex& prev = filtered.back();
826 // Skip if identical to previous (using tolerance)
827 if (std::abs(prev.x - v.x) < 1e-9 &&
828 std::abs(prev.y - v.y) < 1e-9 &&
829 std::abs(prev.bulge - v.bulge) < 1e-9) {
830 continue; // Skip duplicate
831 }
832 filtered.push_back(v);
833 }
834 }
835 polyline.vertices = std::move(filtered);
836 polyline.updateBoundingBox();
837 }
838}
839
840/**
841 * RoundVertex: Round vertex coordinates to nearby grid
842 */
843void applyRoundVertex(Glyph& glyph) {
844 if (!tuningOptions.roundVertex) return;
845
846 for (auto& polyline : glyph.polylines) {
847 for (auto& v : polyline.vertices) {
848 v.x = roundToGrid(v.x);
849 v.y = roundToGrid(v.y);
850 if (v.bulge != 0.0) {
851 v.bulge = roundToGrid(v.bulge);
852 }
853 }
854 polyline.updateBoundingBox();
855 }
856}
857
858/**
859 * MergePath: Merge paths that share the same start/end vertex
860 * This combines adjacent polylines when they meet at the same point
861 */
862void applyMergePath(Glyph& glyph) {
863 if (!tuningOptions.mergePath) return;
864 if (glyph.polylines.size() <= 1) return;
865
866 // Simple merging: if two polylines share start/end, merge them
867 bool changed = true;
868 while (changed) {
869 changed = false;
870
871 // Rebuild connections map after each merge
872 std::map<std::pair<double, double>, std::vector<size_t>> connections;
873 for (size_t i = 0; i < glyph.polylines.size(); ++i) {
874 const auto& poly = glyph.polylines[i];
875 if (poly.vertices.size() < 2) continue;
876
877 const auto& first = poly.vertices.front();
878 const auto& last = poly.vertices.back();
879
880 connections[{roundToGrid(first.x), roundToGrid(first.y)}].push_back(i);
881 connections[{roundToGrid(last.x), roundToGrid(last.y)}].push_back(i);
882 }
883
884 for (auto it = connections.begin(); it != connections.end() && !changed; ++it) {
885 if (it->second.size() > 1) {
886 size_t idx1 = it->second[0];
887 size_t idx2 = it->second[1];
888
889 if (idx1 == idx2) continue;
890
891 Polyline mergedPoly;
892
893 const auto& p1 = glyph.polylines[idx1];
894 const auto& p2 = glyph.polylines[idx2];
895
896 double eps = tuningOptions.roundVertex ? tuningOptions.roundGrid : 1e-9;
897
898 bool p1StartsAtMatch = (std::abs(p1.vertices.front().x - it->first.first) < eps &&
899 std::abs(p1.vertices.front().y - it->first.second) < eps);
900 bool p1EndsAtMatch = (std::abs(p1.vertices.back().x - it->first.first) < eps &&
901 std::abs(p1.vertices.back().y - it->first.second) < eps);
902
903 if (p1StartsAtMatch && p1EndsAtMatch) {
904 mergedPoly = p1;
905 } else if (p1StartsAtMatch) {
906 for (auto rit = p1.vertices.rbegin(); rit != p1.vertices.rend(); ++rit) {
907 mergedPoly.vertices.push_back(*rit);
908 }
909 mergedPoly.vertices.insert(mergedPoly.vertices.end(), p2.vertices.begin() + 1, p2.vertices.end());
910 } else if (p1EndsAtMatch) {
911 mergedPoly.vertices = p1.vertices;
912 mergedPoly.vertices.insert(mergedPoly.vertices.end(), p2.vertices.begin() + 1, p2.vertices.end());
913 } else {
914 mergedPoly.vertices = p1.vertices;
915 mergedPoly.vertices.insert(mergedPoly.vertices.end(), p2.vertices.begin() + 1, p2.vertices.end());
916 }
917
918 if (idx1 < idx2) {
919 mergedPoly.updateBoundingBox();
920 glyph.polylines[idx1] = mergedPoly;
921 glyph.polylines.erase(glyph.polylines.begin() + idx2);
922 } else {
923 mergedPoly.updateBoundingBox();
924 glyph.polylines[idx2] = mergedPoly;
925 glyph.polylines.erase(glyph.polylines.begin() + idx1);
926 }
927
928 changed = true;
929 break;
930 }
931 }
932 }
933}
934
935/**
936 * Find duplicate glyphs and nested characters, replace with nested references
937 * Uses structural path-subset matching, because LFF Cxxxx references replay
938 * exact glyph paths rather than filled outline containment.
939 */
940size_t findAndReplaceNestedChars() {
941 if (!g_converter) return 0;
942 if (!tuningOptions.nestedChar) return 0;
943 auto& glyphBuffer = g_converter->getGlyphBuffer();
944 const size_t glyphCount = glyphBuffer.size();
945
946 if (glyphCount < 2) return 0;
947
948 std::unordered_map<std::string, uint32_t> pathIds;
949 std::vector<std::string> pathText;
950 std::vector<GlyphPathInfo> glyphInfos;
951 pathIds.reserve(glyphCount * 8);
952 pathText.reserve(glyphCount * 8);
953 glyphInfos.reserve(glyphCount);
954
955 for (size_t i = 0; i < glyphCount; ++i) {
956 glyphInfos.push_back(buildGlyphPathInfo(glyphBuffer[i], pathIds, pathText));
957 }
958
959 std::vector<int> pathFrequency(pathText.size(), 0);
960 for (const auto& info : glyphInfos) {
961 for (const auto& entry : info.paths) {
962 ++pathFrequency[entry.id];
963 }
964 }
965
966 // Exact containment filter: if candidate paths are a subset of a target,
967 // the candidate's rarest path must also be one of the target's paths.
968 std::vector<std::vector<size_t>> rarestPathIndex(pathText.size());
969 for (size_t i = 0; i < glyphInfos.size(); ++i) {
970 auto& info = glyphInfos[i];
971 if (info.paths.empty()) {
972 continue;
973 }
974
975 auto rarest = info.paths.front().id;
976 for (const auto& entry : info.paths) {
977 if (pathFrequency[entry.id] < pathFrequency[rarest] ||
978 (pathFrequency[entry.id] == pathFrequency[rarest] && entry.id < rarest)) {
979 rarest = entry.id;
980 }
981 }
982 info.rarestPathId = rarest;
983 rarestPathIndex[info.rarestPathId].push_back(i);
984 }
985
986 struct Candidate {
987 size_t index;
988 int savings;
989 int pathTotal;
990 unsigned int charCode;
991 };
992
993 size_t replacementCount = 0;
994 std::vector<unsigned int> seen(glyphCount, 0);
995 unsigned int seenStamp = 1;
996
997 for (size_t targetIndex = 0; targetIndex < glyphCount; ++targetIndex) {
998 Glyph& target = glyphBuffer[targetIndex];
999 const GlyphPathInfo& targetInfo = glyphInfos[targetIndex];
1000 PathMultiset activePaths = targetInfo.paths;
1001 if (activePaths.empty()) {
1002 continue;
1003 }
1004
1005 std::vector<uint32_t> activePolylinePathIds = targetInfo.polylinePathIds;
1006 std::vector<size_t> candidateIndexes;
1007 if (seenStamp == 0) {
1008 std::fill(seen.begin(), seen.end(), 0);
1009 seenStamp = 1;
1010 }
1011
1012 for (const auto& activeEntry : activePaths) {
1013 for (const size_t candidateIndex : rarestPathIndex[activeEntry.id]) {
1014 if (candidateIndex == targetIndex || seen[candidateIndex] == seenStamp) {
1015 continue;
1016 }
1017 seen[candidateIndex] = seenStamp;
1018 candidateIndexes.push_back(candidateIndex);
1019 }
1020 }
1021 ++seenStamp;
1022
1023 std::vector<Candidate> candidates;
1024 for (const size_t candidateIndex : candidateIndexes) {
1025 const GlyphPathInfo& candidateInfo = glyphInfos[candidateIndex];
1026 if (candidateInfo.paths.empty() || !candidateInfo.refable) {
1027 continue;
1028 }
1029 if (candidateInfo.pathCount > targetInfo.pathCount) {
1030 continue;
1031 }
1032 if (candidateInfo.pathCount == targetInfo.pathCount &&
1033 targetInfo.charCode <= candidateInfo.charCode) {
1034 continue;
1035 }
1036 if (!containsPathMultiset(activePaths, candidateInfo.paths)) {
1037 continue;
1038 }
1039 if (candidateInfo.serializedBytes <= candidateInfo.refBytes) {
1040 continue;
1041 }
1042
1043 const int savings = static_cast<int>(candidateInfo.serializedBytes - candidateInfo.refBytes);
1044 candidates.push_back({candidateIndex, savings, candidateInfo.pathCount, candidateInfo.charCode});
1045 }
1046
1047 std::sort(candidates.begin(), candidates.end(), [](const Candidate& left, const Candidate& right) {
1048 if (left.savings != right.savings) {
1049 return left.savings > right.savings;
1050 }
1051 if (left.pathTotal != right.pathTotal) {
1052 return left.pathTotal > right.pathTotal;
1053 }
1054 return left.charCode < right.charCode;
1055 });
1056
1057 int activeCount = targetInfo.pathCount;
1058 for (const Candidate& selected : candidates) {
1059 const GlyphPathInfo& selectedInfo = glyphInfos[selected.index];
1060 if (!containsPathMultiset(activePaths, selectedInfo.paths)) {
1061 continue;
1062 }
1063 if (activeCount == selectedInfo.pathCount && target.charCode <= selectedInfo.charCode) {
1064 continue;
1065 }
1066 if (!removePolylineMultiset(target, activePolylinePathIds, selectedInfo.paths)) {
1067 continue;
1068 }
1069
1070 target.references.insert(target.references.begin(), GlyphReference{selectedInfo.charCode});
1071 subtractPathMultiset(activePaths, selectedInfo.paths);
1072 activeCount -= selectedInfo.pathCount;
1073 ++replacementCount;
1074 if (activePaths.empty()) {
1075 break;
1076 }
1077 }
1078 }
1079
1080 return replacementCount;
1081}
1082
1083/**
1084 * NoComment: Remove comments from glyphs
1085 */
1086void applyNoComment(Glyph& glyph) {
1087 if (!tuningOptions.noComment) return;
1088 glyph.comment.clear();
1089 for (auto& polyline : glyph.polylines) {
1090 polyline.comment.clear();
1091 }
1092}
1093
1094/**
1095 * Apply all tuning algorithms to all glyphs
1096 */
1097void applyTuning() {
1098 if (!g_converter) return;
1099 auto& glyphBuffer = g_converter->getGlyphBuffer();
1100
1101 for (auto& glyph : glyphBuffer) {
1102 applyZeroVector(glyph);
1103 applyRoundVertex(glyph);
1104 applyMergePath(glyph);
1105 applyNoComment(glyph);
1106 }
1107}
1108
1109/**
1110 * Write a glyph to the output file
1111 */
1112void writeGlyph(std::ofstream& fp, const Glyph& glyph) {
1113 // Write glyph header
1114 if (glyph.symbol.empty()) {
1115 fp << "\n[#" << formatCharCode(glyph.charCode) << "]\n";
1116 } else {
1117 fp << "\n[#" << formatCharCode(glyph.charCode) << "] " << glyph.symbol << "\n";
1118 }
1119
1120 for (const auto& reference : glyph.references) {
1121 fp << "C" << formatCharCode(reference.charCode) << "\n";
1122 }
1123
1124 // Write polylines
1125 for (const auto& polyline : glyph.polylines) {
1126 if (polyline.isEmpty()) continue;
1127
1128 // Write comment if present and not suppressed
1129 if (!polyline.comment.empty() && !tuningOptions.noComment) {
1130 fp << "# " << polyline.comment << "\n";
1131 }
1132
1133 // Write vertices
1134 for (size_t i = 0; i < polyline.vertices.size(); ++i) {
1135 const auto& v = polyline.vertices[i];
1136 if (i > 0) {
1137 fp << ";";
1138 }
1139 fp << clearZeros(v.x) << "," << clearZeros(v.y);
1140 if (v.bulge != 0.0) {
1141 fp << ",A" << clearZeros(v.bulge);
1142 }
1143 }
1144 fp << "\n";
1145 }
1146}
1147
1148/**
1149 * Convert one single glyph (character, sign) into LFF format
1150 */
1151FT_Error convertGlyph(TTF2LFFConverter& converter, FT_ULong charcode, bool bufferOnly = false) {
1152 FT_Error error;
1153 FT_Glyph glyph_raw = nullptr;
1154
1155 FT_Face face = converter.getFace();
1156
1157 // load glyph
1158 error = FT_Load_Glyph(face,
1159 FT_Get_Char_Index(face, charcode),
1160 FT_LOAD_NO_BITMAP( 1L << 3 ) | FT_LOAD_NO_SCALE( 1L << 0 ));
1161 if (error) {
1162 std::cerr << "FT_Load_Glyph: " << FT_StrError(error) << std::endl;
1163 return error;
1164 }
1165
1166 error = FT_Get_Glyph(face->glyph, &glyph_raw);
1167 if (error) {
1168 std::cerr << "FT_Get_Glyph: " << FT_StrError(error) << std::endl;
1169 return error;
1170 }
1171
1172 if (face->glyph->format != ft_glyph_format_outlineFT_GLYPH_FORMAT_OUTLINE) {
1173 std::cerr << "Not an outline font\n";
1174 FT_Done_Glyph(glyph_raw);
1175 return 0;
1176 }
1177
1178 FTGlyphPtr glyph(glyph_raw);
1179 FT_OutlineGlyph og = (FT_OutlineGlyph)glyph.get();
1180
1181 auto& glyphBuffer = converter.getGlyphBuffer();
1182 auto& xMin = converter.getXMin();
1183 auto& nodes = converter.getNodes();
1184 auto& firstpass = converter.getFirstPass();
1185 auto& startcontour = converter.getStartContour();
1186 auto& factor = converter.getFactor();
1187
1188 if (bufferOnly) {
1189 // Create new glyph entry
1190 Glyph newGlyph;
1191 newGlyph.charCode = static_cast<unsigned int>(charcode);
1192
1193 // Try to get a printable unicode symbol for the header comment.
1194 if (shouldWriteHeaderSymbol(charcode) &&
1195 charcode <= 0x10FFFF && charcode != 0xFFFD &&
1196 !(charcode >= 0xD800 && charcode <= 0xDFFF)) {
1197 // Valid Unicode codepoint (not surrogate, not replacement char)
1198 std::string s;
1199 if (charcode < 0x80) {
1200 s += static_cast<char>(charcode);
1201 } else if (charcode < 0x800) {
1202 s += static_cast<char>(0xC0 | (charcode >> 6));
1203 s += static_cast<char>(0x80 | (charcode & 0x3F));
1204 } else if (charcode < 0x10000) {
1205 s += static_cast<char>(0xE0 | (charcode >> 12));
1206 s += static_cast<char>(0x80 | ((charcode >> 6) & 0x3F));
1207 s += static_cast<char>(0x80 | (charcode & 0x3F));
1208 } else {
1209 // 4-byte UTF-8 for characters ≥ U+10000
1210 s += static_cast<char>(0xF0 | (charcode >> 18));
1211 s += static_cast<char>(0x80 | ((charcode >> 12) & 0x3F));
1212 s += static_cast<char>(0x80 | ((charcode >> 6) & 0x3F));
1213 s += static_cast<char>(0x80 | (charcode & 0x3F));
1214 }
1215 newGlyph.symbol = s;
1216 }
1217
1218 FT_Error buildError = 0;
1219 newGlyph.polylines = buildPolylinesFromOutline(og->outline, nodes, factor, buildError);
1220 if (buildError) {
1221 std::cerr << "FT_Outline_Decompose: " << FT_StrError(buildError) << std::endl;
1222 return buildError;
1223 }
1224
1225 // Add to buffer
1226 glyphBuffer.push_back(newGlyph);
1227 } else {
1228 // Original direct-to-file output (for calibration)
1229 std::ofstream nullFile("/dev/null");
1230 if (nullFile.is_open()) {
1231 nullFile << "\n[#" << std::hex << std::setfill('0') << std::setw(4) << charcode << std::dec << "]\n";
1232
1233 xMin = 1000.0;
1234 firstpass = true;
1235 error = FT_Outline_Decompose(&(og->outline), &funcs, nullptr);
1236 if (error)
1237 std::cerr << "FT_Outline_Decompose: first pass: " << FT_StrError(error) << std::endl;
1238
1239 firstpass = false;
1240 startcontour = true;
1241 error = FT_Outline_Decompose(&(og->outline), &funcs, nullptr);
1242 nullFile << "\n";
1243 }
1244 }
1245
1246 return error;
1247}
1248
1249/**
1250 * Print usage information
1251 */
1252static void usage(int eval) {
1253 std::cout << "Usage: ttf2lff [options] <ttf file> <lff file>\n";
1254 std::cout << "\n";
1255 std::cout << "Convert TrueType font to LibreCAD Font Format (LFF)\n";
1256 std::cout << "\n";
1257 std::cout << "Arguments:\n";
1258 std::cout << " <ttf file> Input TrueType font file (.ttf, .otf)\n";
1259 std::cout << " <lff file> Output LFF font file\n";
1260 std::cout << "\n";
1261 std::cout << "Options:\n";
1262 std::cout << " -n, --nodes N Number of nodes for quadratic/cubic splines (default: 4)\n";
1263 std::cout << " -a, --author TEXT Author name for font metadata\n";
1264 std::cout << " -l, --letterspacing N Letter spacing (default: 3.0)\n";
1265 std::cout << " -w, --wordspacing N Word spacing (default: 6.75)\n";
1266 std::cout << " -f, --linespacing N Line spacing factor (default: 1.0)\n";
1267 std::cout << " -d, --precision N Decimal digits after the point (default: 6, min: 1)\n";
1268 std::cout << " -L, --license TEXT Font license\n";
1269 std::cout << "\n";
1270 std::cout << "Tuning options (from python-lff):\n";
1271 std::cout << " -z, --zerovector Remove duplicate consecutive vertices\n";
1272 std::cout << " -r, --round Round vertices to grid\n";
1273 std::cout << " -g, --grid N Grid size for rounding (default: 0.0001)\n";
1274 std::cout << " -m, --mergepath Merge paths with same start/end vertex\n";
1275 std::cout << " -e, --nestedchar Analyze for nested characters\n";
1276 std::cout << " -c, --nocomment Remove comments from glyphs\n";
1277 std::cout << " -t, --tuning Apply all tuning options (implies -d 2)\n";
1278 std::cout << "\n";
1279 std::cout << " -h, --help Show this help message\n";
1280 std::cout << "\n";
1281 std::cout << "Example:\n";
1282 std::cout << " ttf2lff -a \"John Doe\" -n 8 -z -r -m font.ttf output.lff\n";
1283 exit(eval);
1284}
1285
1286
1287/**
1288 * Main function
1289 */
1290int main(int argc, char* argv[]) {
1291 FT_Error error;
1292 std::string fTtf;
1293 std::string fLff;
1294
1295 // Default values
1296 int nodes = 4;
1297 std::string name = "Unknown";
1298 double letterSpacing = 3.0;
1299 double wordSpacing = 6.75;
1300 double lineSpacingFactor = 1.0;
1301 std::string author = "Unknown";
1302 std::string license = "Unknown";
1303 int precision = 6;
1304 bool precisionExplicit = false;
1305
1306 // Parse command line arguments
1307 for (int i = 1; i < argc; ++i) {
1308 std::string arg = argv[i];
1309
1310 if (arg == "-h" || arg == "--help") {
1311 usage(0);
1312 }
1313 else if (arg == "-n" || arg == "--nodes") {
1314 if (++i >= argc) { std::cerr << "Error: -n requires an argument\n"; return 1; }
1315 nodes = std::atoi(argv[i]);
1316 }
1317 else if (arg == "-a" || arg == "--author") {
1318 if (++i >= argc) { std::cerr << "Error: -a requires an argument\n"; return 1; }
1319 author = argv[i];
1320 }
1321 else if (arg == "-l" || arg == "--letterspacing") {
1322 if (++i >= argc) { std::cerr << "Error: -l requires an argument\n"; return 1; }
1323 letterSpacing = std::atof(argv[i]);
1324 }
1325 else if (arg == "-w" || arg == "--wordspacing") {
1326 if (++i >= argc) { std::cerr << "Error: -w requires an argument\n"; return 1; }
1327 wordSpacing = std::atof(argv[i]);
1328 }
1329 else if (arg == "-f" || arg == "--linespacing") {
1330 if (++i >= argc) { std::cerr << "Error: -f requires an argument\n"; return 1; }
1331 lineSpacingFactor = std::atof(argv[i]);
1332 }
1333 else if (arg == "-d" || arg == "--precision") {
1334 if (++i >= argc) { std::cerr << "Error: -d requires an argument\n"; return 1; }
1335 precision = std::atoi(argv[i]);
1336 if (precision < 1) {
1337 std::cerr << "Error: -d/--precision requires a value >= 1\n"; return 1;
1338 }
1339 precisionExplicit = true;
1340 }
1341 else if (arg == "-L" || arg == "--license") {
1342 if (++i >= argc) { std::cerr << "Error: -L requires an argument\n"; return 1; }
1343 license = argv[i];
1344 }
1345 else if (arg == "-z" || arg == "--zerovector") {
1346 tuningOptions.zeroVector = true;
1347 }
1348 else if (arg == "-r" || arg == "--round") {
1349 tuningOptions.roundVertex = true;
1350 }
1351 else if (arg == "-g" || arg == "--grid") {
1352 if (++i >= argc) { std::cerr << "Error: -g requires an argument\n"; return 1; }
1353 tuningOptions.roundGrid = std::atof(argv[i]);
1354 }
1355 else if (arg == "-m" || arg == "--mergepath") {
1356 tuningOptions.mergePath = true;
1357 }
1358 else if (arg == "-e" || arg == "--nestedchar") {
1359 tuningOptions.nestedChar = true;
1360 }
1361 else if (arg == "-c" || arg == "--nocomment") {
1362 tuningOptions.noComment = true;
1363 }
1364 else if (arg == "-t" || arg == "--tuning") {
1365 // Enable all tuning options
1366 tuningOptions.zeroVector = true;
1367 tuningOptions.roundVertex = true;
1368 tuningOptions.mergePath = true;
1369 tuningOptions.nestedChar = true;
1370 tuningOptions.noComment = true;
1371 // Reduce coordinates to 2 decimals, unless -d/--precision was given
1372 if (!precisionExplicit) {
1373 precision = 2;
1374 }
1375 }
1376 else if (arg[0] != '-') {
1377 // Assume first non-option is TTF file, second is LFF file
1378 fTtf = arg;
1379 if (++i < argc) {
1380 fLff = argv[i];
1381 }
1382 }
1383 else {
1384 std::cerr << "Unknown option: " << arg << "\n";
1385 usage(1);
1386 }
1387 }
1388
1389 if (fTtf.empty() || fLff.empty()) {
1390 std::cerr << "Error: Missing required arguments\n\n";
1391 usage(1);
1392 }
1393
1394 std::cout << "TTF file: " << fTtf << "\n";
1395 std::cout << "LFF file: " << fLff << "\n";
1396
1397 if (tuningOptions.zeroVector) std::cout << "Tuning: ZeroVector enabled\n";
1398 if (tuningOptions.roundVertex) std::cout << "Tuning: RoundVertex enabled (grid=" << tuningOptions.roundGrid << ")\n";
1399 if (tuningOptions.mergePath) std::cout << "Tuning: MergePath enabled\n";
1400 if (tuningOptions.nestedChar) std::cout << "Tuning: NestedChar enabled\n";
1401 if (tuningOptions.noComment) std::cout << "Tuning: NoComment enabled\n";
1402 std::cout << "Precision: " << precision << " decimal digit(s)\n";
1403
1404 // Create converter with RAII
1405 TTF2LFFConverter converter;
1406 converter.getNodes() = nodes;
1407 converter.getPrecision() = precision;
1408
1409 // Initialize FreeType
1410 error = converter.initLibrary();
1411 if (error) {
1412 return 1;
1413 }
1414
1415 FT_Library library = converter.getLibrary();
1416 FT_Int major = 0, minor = 0, patch = 0;
1417 FT_Library_Version(library, &major, &minor, &patch);
1418 std::cerr << "FreeType version: " << major << '.' << minor << '.' << patch << std::endl;
1419
1420 // Load font
1421 error = converter.loadFace(fTtf);
1422 if (error) {
1423 return 1;
1424 }
1425
1426 FT_Face face = converter.getFace();
1427 std::cout << "Family: " << face->family_name << "\n";
1428 std::cout << "Style: " << face->style_name << "\n";
1429 std::cout << "Height: " << face->height << "\n";
1430 std::cout << "Ascender: " << face->ascender << "\n";
1431 std::cout << "Descender: " << face->descender << "\n";
1432 std::cout << "Faces: " << face->num_faces << "\n";
1433 std::cout << "Glyphs: " << face->num_glyphs << "\n";
1434 name = face->family_name;
1435
1436 // Determine scale factor by tracing 'A'
1437 converter.getYMax() = -1000;
1438 g_converter = &converter;
1439 convertGlyph(converter, 65, false); // Direct output for calibration
1440 converter.getFactor() = 1.0 / (1.0 / 9.0 * converter.getYMax());
1441 std::cout << "Factor: " << converter.getFactor() << "\n";
1442
1443 // Open output file
1444 if (!converter.openOutputFile(fLff)) {
1445 return 2;
1446 }
1447
1448 std::ofstream& outputFile = converter.getOutputFile();
1449
1450 // Set number format
1451 std::string numFormat = "%." + std::to_string(precision) + "f";
1452 converter.getNumFormat() = numFormat;
1453
1454 // Write font header
1455 outputFile << "# Format: LibreCAD Font 1\n";
1456 outputFile << "# Creator: ttf2lff with python-lff tuning\n";
1457 outputFile << "# Version: 1\n";
1458 outputFile << "# Name: " << name << "\n";
1459 outputFile << "# Encoding: UTF-8\n";
1460 outputFile << "# LetterSpacing: " << clearZeros(letterSpacing) << "\n";
1461 outputFile << "# WordSpacing: " << clearZeros(wordSpacing) << "\n";
1462 outputFile << "# LineSpacingFactor: " << clearZeros(lineSpacingFactor) << "\n";
1463
1464 time_t rawtime;
1465 time(&rawtime);
1466 struct tm* timeinfo = localtime(&rawtime);
1467 char buffer[12];
1468 strftime(buffer, sizeof(buffer), "%Y-%m-%d", timeinfo);
1469
1470 outputFile << "# Created: " << buffer << "\n";
1471 outputFile << "# Last modified: " << buffer << "\n";
1472 outputFile << "# Author: " << author << "\n";
1473 outputFile << "# License: " << license << "\n";
1474
1475 // Write tuning information as comments
1476 if (tuningOptions.zeroVector || tuningOptions.roundVertex ||
1477 tuningOptions.mergePath || tuningOptions.nestedChar || tuningOptions.noComment) {
1478 outputFile << "# Tuning: ";
1479 bool first = true;
1480 if (tuningOptions.zeroVector) { outputFile << (first ? "" : ", ") << "ZeroVector"; first = false; }
1481 if (tuningOptions.roundVertex) {
1482 outputFile << (first ? "" : ", ") << "RoundVertex(grid=" << std::fixed << std::setprecision(6) << tuningOptions.roundGrid << ")";
1483 first = false;
1484 }
1485 if (tuningOptions.mergePath) { outputFile << (first ? "" : ", ") << "MergePath"; first = false; }
1486 if (tuningOptions.nestedChar) { outputFile << (first ? "" : ", ") << "NestedChar"; first = false; }
1487 if (tuningOptions.noComment) { outputFile << (first ? "" : ", ") << "NoComment"; first = false; }
Value stored to 'first' is never read
1488 outputFile << "\n";
1489 }
1490
1491 outputFile << "\n";
1492
1493 // Convert all glyphs
1494 // First, collect all glyphs into buffer for tuning
1495 converter.getGlyphBuffer().clear();
1496
1497 FT_ULong charcode;
1498 FT_UInt gindex;
1499
1500 charcode = FT_Get_First_Char(face, &gindex);
1501 while (gindex != 0) {
1502 convertGlyph(converter, charcode, true); // Buffer the glyph
1503 charcode = FT_Get_Next_Char(face, charcode, &gindex);
1504 }
1505
1506 std::cout << "Converted " << converter.getGlyphBuffer().size() << " glyphs\n";
1507
1508 // Apply tuning algorithms
1509 if (tuningOptions.zeroVector || tuningOptions.roundVertex ||
1510 tuningOptions.mergePath || tuningOptions.nestedChar || tuningOptions.noComment) {
1511 std::cout << "Applying tuning algorithms...\n";
1512 applyTuning();
1513 std::cout << "Tuning complete\n";
1514 }
1515
1516 // Find and replace nested characters
1517 if (tuningOptions.nestedChar) {
1518 std::cout << "Finding nested characters...\n";
1519 const size_t nestedCount = findAndReplaceNestedChars();
1520 std::cout << "Nested character processing complete: " << nestedCount << " references\n";
1521 }
1522
1523 // Write buffered glyphs to file
1524 for (const auto& glyph : converter.getGlyphBuffer()) {
1525 writeGlyph(outputFile, glyph);
1526 }
1527
1528 converter.closeOutputFile();
1529 g_converter = nullptr;
1530
1531 std::cout << "Conversion complete: " << fLff << "\n";
1532 return 0;
1533}