Bug Summary

File:tools/ttf2lff/main.cpp
Warning:line 1479, 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-07-14-153100-5089-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 std::ostringstream stream;
574 stream << std::hex << std::nouppercase << std::setfill('0');
575 if (charCode <= 0xffff) {
576 stream << std::setw(4);
577 }
578 stream << charCode;
579 return stream.str();
580}
581
582bool canReferenceGlyph(unsigned int charCode)
583{
584 // LibreCAD's LFF header discovery currently extracts four hex digits.
585 return charCode <= 0xffff;
586}
587
588bool shouldWriteHeaderSymbol(FT_ULong charcode)
589{
590 if (charcode == 0x7f) {
591 return false;
592 }
593 if (charcode < 0x20) {
594 return false;
595 }
596 if (charcode >= 0x80 && charcode <= 0x9f) {
597 return false;
598 }
599 return true;
600}
601
602std::string serializePolyline(const Polyline& polyline)
603{
604 std::ostringstream stream;
605 for (size_t i = 0; i < polyline.vertices.size(); ++i) {
606 const auto& vertex = polyline.vertices[i];
607 if (i > 0) {
608 stream << ';';
609 }
610 stream << clearZeros(vertex.x) << ',' << clearZeros(vertex.y);
611 if (vertex.bulge != 0.0) {
612 stream << ",A" << clearZeros(vertex.bulge);
613 }
614 }
615 return stream.str();
616}
617
618struct PathEntry {
619 uint32_t id = 0;
620 int count = 0;
621};
622
623using PathMultiset = std::vector<PathEntry>;
624
625struct GlyphPathInfo {
626 PathMultiset paths;
627 std::vector<uint32_t> polylinePathIds;
628 int pathCount = 0;
629 size_t serializedBytes = 0;
630 size_t refBytes = 0;
631 uint32_t rarestPathId = std::numeric_limits<uint32_t>::max();
632 unsigned int charCode = 0;
633 bool refable = false;
634};
635
636uint32_t internPathId(const std::string& path,
637 std::unordered_map<std::string, uint32_t>& pathIds,
638 std::vector<std::string>& pathText)
639{
640 const auto found = pathIds.find(path);
641 if (found != pathIds.end()) {
642 return found->second;
643 }
644
645 const uint32_t id = static_cast<uint32_t>(pathText.size());
646 pathText.push_back(path);
647 pathIds.emplace(pathText.back(), id);
648 return id;
649}
650
651PathMultiset compactPathIds(std::vector<uint32_t>& ids)
652{
653 PathMultiset paths;
654 if (ids.empty()) {
655 return paths;
656 }
657
658 std::sort(ids.begin(), ids.end());
659 for (const uint32_t id : ids) {
660 if (!paths.empty() && paths.back().id == id) {
661 ++paths.back().count;
662 } else {
663 paths.push_back({id, 1});
664 }
665 }
666
667 return paths;
668}
669
670GlyphPathInfo buildGlyphPathInfo(const Glyph& glyph,
671 std::unordered_map<std::string, uint32_t>& pathIds,
672 std::vector<std::string>& pathText)
673{
674 GlyphPathInfo info;
675 info.charCode = glyph.charCode;
676 info.refable = canReferenceGlyph(glyph.charCode);
677 info.refBytes = 1 + formatCharCode(glyph.charCode).size() + 1;
678 info.polylinePathIds.reserve(glyph.polylines.size());
679
680 std::vector<uint32_t> ids;
681 for (const auto& polyline : glyph.polylines) {
682 if (polyline.vertices.size() < 2) {
683 info.polylinePathIds.push_back(std::numeric_limits<uint32_t>::max());
684 continue;
685 }
686 const uint32_t id = internPathId(serializePolyline(polyline), pathIds, pathText);
687 info.polylinePathIds.push_back(id);
688 ids.push_back(id);
689 ++info.pathCount;
690 info.serializedBytes += pathText[id].size() + 1;
691 }
692
693 info.paths = compactPathIds(ids);
694 return info;
695}
696
697int pathCount(const PathMultiset& paths)
698{
699 int count = 0;
700 for (const auto& entry : paths) {
701 count += entry.count;
702 }
703 return count;
704}
705
706bool containsPathMultiset(const PathMultiset& haystack, const PathMultiset& needle)
707{
708 size_t i = 0;
709 size_t j = 0;
710
711 while (i < haystack.size() && j < needle.size()) {
712 if (haystack[i].id < needle[j].id) {
713 ++i;
714 continue;
715 }
716 if (haystack[i].id > needle[j].id) {
717 return false;
718 }
719 if (haystack[i].count < needle[j].count) {
720 return false;
721 }
722 ++i;
723 ++j;
724 }
725
726 return j == needle.size();
727}
728
729void subtractPathMultiset(PathMultiset& haystack, const PathMultiset& needle)
730{
731 PathMultiset result;
732 result.reserve(haystack.size());
733
734 size_t i = 0;
735 size_t j = 0;
736 while (i < haystack.size()) {
737 PathEntry entry = haystack[i];
738 if (j < needle.size() && entry.id == needle[j].id) {
739 entry.count -= needle[j].count;
740 ++j;
741 } else if (j < needle.size() && entry.id > needle[j].id) {
742 ++j;
743 continue;
744 }
745 if (entry.count > 0) {
746 result.push_back(entry);
747 }
748 ++i;
749 }
750
751 haystack = std::move(result);
752}
753
754bool decrementPathEntry(PathMultiset& paths, uint32_t id)
755{
756 for (auto it = paths.begin(); it != paths.end(); ++it) {
757 if (it->id != id) {
758 continue;
759 }
760 --it->count;
761 if (it->count == 0) {
762 paths.erase(it);
763 }
764 return true;
765 }
766 return false;
767}
768
769bool removePolylineMultiset(Glyph& glyph, std::vector<uint32_t>& polylinePathIds, const PathMultiset& paths)
770{
771 PathMultiset remaining = paths;
772 std::vector<bool> remove(glyph.polylines.size(), false);
773 const uint32_t invalidPathId = std::numeric_limits<uint32_t>::max();
774
775 if (polylinePathIds.size() != glyph.polylines.size()) {
776 return false;
777 }
778 for (size_t i = 0; i < glyph.polylines.size(); ++i) {
779 const uint32_t id = polylinePathIds[i];
780 if (id != invalidPathId && decrementPathEntry(remaining, id)) {
781 remove[i] = true;
782 }
783 }
784
785 if (!remaining.empty()) {
786 return false;
787 }
788
789 std::vector<Polyline> kept;
790 std::vector<uint32_t> keptPathIds;
791 kept.reserve(glyph.polylines.size());
792 keptPathIds.reserve(polylinePathIds.size());
793 for (size_t i = 0; i < glyph.polylines.size(); ++i) {
794 if (!remove[i]) {
795 kept.push_back(std::move(glyph.polylines[i]));
796 keptPathIds.push_back(polylinePathIds[i]);
797 }
798 }
799
800 glyph.polylines = std::move(kept);
801 polylinePathIds = std::move(keptPathIds);
802 glyph.updateBoundingBox();
803 return true;
804}
805
806/**
807 * TUNING ALGORITHMS (from python-lff)
808 */
809
810/**
811 * ZeroVector: Remove two identical consecutive vertices
812 * This removes duplicate points that can occur from bezier curve approximations
813 */
814void applyZeroVector(Glyph& glyph) {
815 if (!tuningOptions.zeroVector) return;
816
817 for (auto& polyline : glyph.polylines) {
818 if (polyline.vertices.size() <= 1) continue;
819
820 std::vector<Vertex> filtered;
821 filtered.reserve(polyline.vertices.size());
822
823 for (const auto& v : polyline.vertices) {
824 if (filtered.empty()) {
825 filtered.push_back(v);
826 } else {
827 const Vertex& prev = filtered.back();
828 // Skip if identical to previous (using tolerance)
829 if (std::abs(prev.x - v.x) < 1e-9 &&
830 std::abs(prev.y - v.y) < 1e-9 &&
831 std::abs(prev.bulge - v.bulge) < 1e-9) {
832 continue; // Skip duplicate
833 }
834 filtered.push_back(v);
835 }
836 }
837 polyline.vertices = std::move(filtered);
838 polyline.updateBoundingBox();
839 }
840}
841
842/**
843 * RoundVertex: Round vertex coordinates to nearby grid
844 */
845void applyRoundVertex(Glyph& glyph) {
846 if (!tuningOptions.roundVertex) return;
847
848 for (auto& polyline : glyph.polylines) {
849 for (auto& v : polyline.vertices) {
850 v.x = roundToGrid(v.x);
851 v.y = roundToGrid(v.y);
852 if (v.bulge != 0.0) {
853 v.bulge = roundToGrid(v.bulge);
854 }
855 }
856 polyline.updateBoundingBox();
857 }
858}
859
860/**
861 * MergePath: Merge paths that share the same start/end vertex
862 * This combines adjacent polylines when they meet at the same point
863 */
864void applyMergePath(Glyph& glyph) {
865 if (!tuningOptions.mergePath) return;
866 if (glyph.polylines.size() <= 1) return;
867
868 // Simple merging: if two polylines share start/end, merge them
869 bool changed = true;
870 while (changed) {
871 changed = false;
872
873 // Rebuild connections map after each merge
874 std::map<std::pair<double, double>, std::vector<size_t>> connections;
875 for (size_t i = 0; i < glyph.polylines.size(); ++i) {
876 const auto& poly = glyph.polylines[i];
877 if (poly.vertices.size() < 2) continue;
878
879 const auto& first = poly.vertices.front();
880 const auto& last = poly.vertices.back();
881
882 connections[{roundToGrid(first.x), roundToGrid(first.y)}].push_back(i);
883 connections[{roundToGrid(last.x), roundToGrid(last.y)}].push_back(i);
884 }
885
886 for (auto it = connections.begin(); it != connections.end() && !changed; ++it) {
887 if (it->second.size() > 1) {
888 size_t idx1 = it->second[0];
889 size_t idx2 = it->second[1];
890
891 if (idx1 == idx2) continue;
892
893 Polyline mergedPoly;
894
895 const auto& p1 = glyph.polylines[idx1];
896 const auto& p2 = glyph.polylines[idx2];
897
898 double eps = tuningOptions.roundVertex ? tuningOptions.roundGrid : 1e-9;
899
900 bool p1StartsAtMatch = (std::abs(p1.vertices.front().x - it->first.first) < eps &&
901 std::abs(p1.vertices.front().y - it->first.second) < eps);
902 bool p1EndsAtMatch = (std::abs(p1.vertices.back().x - it->first.first) < eps &&
903 std::abs(p1.vertices.back().y - it->first.second) < eps);
904
905 if (p1StartsAtMatch && p1EndsAtMatch) {
906 mergedPoly = p1;
907 } else if (p1StartsAtMatch) {
908 for (auto rit = p1.vertices.rbegin(); rit != p1.vertices.rend(); ++rit) {
909 mergedPoly.vertices.push_back(*rit);
910 }
911 mergedPoly.vertices.insert(mergedPoly.vertices.end(), p2.vertices.begin() + 1, p2.vertices.end());
912 } else if (p1EndsAtMatch) {
913 mergedPoly.vertices = p1.vertices;
914 mergedPoly.vertices.insert(mergedPoly.vertices.end(), p2.vertices.begin() + 1, p2.vertices.end());
915 } else {
916 mergedPoly.vertices = p1.vertices;
917 mergedPoly.vertices.insert(mergedPoly.vertices.end(), p2.vertices.begin() + 1, p2.vertices.end());
918 }
919
920 if (idx1 < idx2) {
921 mergedPoly.updateBoundingBox();
922 glyph.polylines[idx1] = mergedPoly;
923 glyph.polylines.erase(glyph.polylines.begin() + idx2);
924 } else {
925 mergedPoly.updateBoundingBox();
926 glyph.polylines[idx2] = mergedPoly;
927 glyph.polylines.erase(glyph.polylines.begin() + idx1);
928 }
929
930 changed = true;
931 break;
932 }
933 }
934 }
935}
936
937/**
938 * Find duplicate glyphs and nested characters, replace with nested references
939 * Uses structural path-subset matching, because LFF Cxxxx references replay
940 * exact glyph paths rather than filled outline containment.
941 */
942size_t findAndReplaceNestedChars() {
943 if (!g_converter) return 0;
944 if (!tuningOptions.nestedChar) return 0;
945 auto& glyphBuffer = g_converter->getGlyphBuffer();
946 const size_t glyphCount = glyphBuffer.size();
947
948 if (glyphCount < 2) return 0;
949
950 std::unordered_map<std::string, uint32_t> pathIds;
951 std::vector<std::string> pathText;
952 std::vector<GlyphPathInfo> glyphInfos;
953 pathIds.reserve(glyphCount * 8);
954 pathText.reserve(glyphCount * 8);
955 glyphInfos.reserve(glyphCount);
956
957 for (size_t i = 0; i < glyphCount; ++i) {
958 glyphInfos.push_back(buildGlyphPathInfo(glyphBuffer[i], pathIds, pathText));
959 }
960
961 std::vector<int> pathFrequency(pathText.size(), 0);
962 for (const auto& info : glyphInfos) {
963 for (const auto& entry : info.paths) {
964 ++pathFrequency[entry.id];
965 }
966 }
967
968 // Exact containment filter: if candidate paths are a subset of a target,
969 // the candidate's rarest path must also be one of the target's paths.
970 std::vector<std::vector<size_t>> rarestPathIndex(pathText.size());
971 for (size_t i = 0; i < glyphInfos.size(); ++i) {
972 auto& info = glyphInfos[i];
973 if (info.paths.empty()) {
974 continue;
975 }
976
977 auto rarest = info.paths.front().id;
978 for (const auto& entry : info.paths) {
979 if (pathFrequency[entry.id] < pathFrequency[rarest] ||
980 (pathFrequency[entry.id] == pathFrequency[rarest] && entry.id < rarest)) {
981 rarest = entry.id;
982 }
983 }
984 info.rarestPathId = rarest;
985 rarestPathIndex[info.rarestPathId].push_back(i);
986 }
987
988 struct Candidate {
989 size_t index;
990 int savings;
991 int pathTotal;
992 unsigned int charCode;
993 };
994
995 size_t replacementCount = 0;
996 std::vector<unsigned int> seen(glyphCount, 0);
997 unsigned int seenStamp = 1;
998
999 for (size_t targetIndex = 0; targetIndex < glyphCount; ++targetIndex) {
1000 Glyph& target = glyphBuffer[targetIndex];
1001 const GlyphPathInfo& targetInfo = glyphInfos[targetIndex];
1002 PathMultiset activePaths = targetInfo.paths;
1003 if (activePaths.empty()) {
1004 continue;
1005 }
1006
1007 std::vector<uint32_t> activePolylinePathIds = targetInfo.polylinePathIds;
1008 std::vector<size_t> candidateIndexes;
1009 if (seenStamp == 0) {
1010 std::fill(seen.begin(), seen.end(), 0);
1011 seenStamp = 1;
1012 }
1013
1014 for (const auto& activeEntry : activePaths) {
1015 for (const size_t candidateIndex : rarestPathIndex[activeEntry.id]) {
1016 if (candidateIndex == targetIndex || seen[candidateIndex] == seenStamp) {
1017 continue;
1018 }
1019 seen[candidateIndex] = seenStamp;
1020 candidateIndexes.push_back(candidateIndex);
1021 }
1022 }
1023 ++seenStamp;
1024
1025 std::vector<Candidate> candidates;
1026 for (const size_t candidateIndex : candidateIndexes) {
1027 const GlyphPathInfo& candidateInfo = glyphInfos[candidateIndex];
1028 if (candidateInfo.paths.empty() || !candidateInfo.refable) {
1029 continue;
1030 }
1031 if (candidateInfo.pathCount > targetInfo.pathCount) {
1032 continue;
1033 }
1034 if (candidateInfo.pathCount == targetInfo.pathCount &&
1035 targetInfo.charCode <= candidateInfo.charCode) {
1036 continue;
1037 }
1038 if (!containsPathMultiset(activePaths, candidateInfo.paths)) {
1039 continue;
1040 }
1041 if (candidateInfo.serializedBytes <= candidateInfo.refBytes) {
1042 continue;
1043 }
1044
1045 const int savings = static_cast<int>(candidateInfo.serializedBytes - candidateInfo.refBytes);
1046 candidates.push_back({candidateIndex, savings, candidateInfo.pathCount, candidateInfo.charCode});
1047 }
1048
1049 std::sort(candidates.begin(), candidates.end(), [](const Candidate& left, const Candidate& right) {
1050 if (left.savings != right.savings) {
1051 return left.savings > right.savings;
1052 }
1053 if (left.pathTotal != right.pathTotal) {
1054 return left.pathTotal > right.pathTotal;
1055 }
1056 return left.charCode < right.charCode;
1057 });
1058
1059 int activeCount = targetInfo.pathCount;
1060 for (const Candidate& selected : candidates) {
1061 const GlyphPathInfo& selectedInfo = glyphInfos[selected.index];
1062 if (!containsPathMultiset(activePaths, selectedInfo.paths)) {
1063 continue;
1064 }
1065 if (activeCount == selectedInfo.pathCount && target.charCode <= selectedInfo.charCode) {
1066 continue;
1067 }
1068 if (!removePolylineMultiset(target, activePolylinePathIds, selectedInfo.paths)) {
1069 continue;
1070 }
1071
1072 target.references.insert(target.references.begin(), GlyphReference{selectedInfo.charCode});
1073 subtractPathMultiset(activePaths, selectedInfo.paths);
1074 activeCount -= selectedInfo.pathCount;
1075 ++replacementCount;
1076 if (activePaths.empty()) {
1077 break;
1078 }
1079 }
1080 }
1081
1082 return replacementCount;
1083}
1084
1085/**
1086 * NoComment: Remove comments from glyphs
1087 */
1088void applyNoComment(Glyph& glyph) {
1089 if (!tuningOptions.noComment) return;
1090 glyph.comment.clear();
1091 for (auto& polyline : glyph.polylines) {
1092 polyline.comment.clear();
1093 }
1094}
1095
1096/**
1097 * Apply all tuning algorithms to all glyphs
1098 */
1099void applyTuning() {
1100 if (!g_converter) return;
1101 auto& glyphBuffer = g_converter->getGlyphBuffer();
1102
1103 for (auto& glyph : glyphBuffer) {
1104 applyZeroVector(glyph);
1105 applyRoundVertex(glyph);
1106 applyMergePath(glyph);
1107 applyNoComment(glyph);
1108 }
1109}
1110
1111/**
1112 * Write a glyph to the output file
1113 */
1114void writeGlyph(std::ofstream& fp, const Glyph& glyph) {
1115 // Write glyph header
1116 if (glyph.symbol.empty()) {
1117 fp << "\n[#" << formatCharCode(glyph.charCode) << "]\n";
1118 } else {
1119 fp << "\n[#" << formatCharCode(glyph.charCode) << "] " << glyph.symbol << "\n";
1120 }
1121
1122 for (const auto& reference : glyph.references) {
1123 fp << "C" << formatCharCode(reference.charCode) << "\n";
1124 }
1125
1126 // Write polylines
1127 for (const auto& polyline : glyph.polylines) {
1128 if (polyline.isEmpty()) continue;
1129
1130 // Write comment if present and not suppressed
1131 if (!polyline.comment.empty() && !tuningOptions.noComment) {
1132 fp << "# " << polyline.comment << "\n";
1133 }
1134
1135 // Write vertices
1136 for (size_t i = 0; i < polyline.vertices.size(); ++i) {
1137 const auto& v = polyline.vertices[i];
1138 if (i > 0) {
1139 fp << ";";
1140 }
1141 fp << clearZeros(v.x) << "," << clearZeros(v.y);
1142 if (v.bulge != 0.0) {
1143 fp << ",A" << clearZeros(v.bulge);
1144 }
1145 }
1146 fp << "\n";
1147 }
1148}
1149
1150/**
1151 * Convert one single glyph (character, sign) into LFF format
1152 */
1153FT_Error convertGlyph(TTF2LFFConverter& converter, FT_ULong charcode, bool bufferOnly = false) {
1154 FT_Error error;
1155 FT_Glyph glyph_raw = nullptr;
1156
1157 FT_Face face = converter.getFace();
1158
1159 // load glyph
1160 error = FT_Load_Glyph(face,
1161 FT_Get_Char_Index(face, charcode),
1162 FT_LOAD_NO_BITMAP( 1L << 3 ) | FT_LOAD_NO_SCALE( 1L << 0 ));
1163 if (error) {
1164 std::cerr << "FT_Load_Glyph: " << FT_StrError(error) << std::endl;
1165 return error;
1166 }
1167
1168 error = FT_Get_Glyph(face->glyph, &glyph_raw);
1169 if (error) {
1170 std::cerr << "FT_Get_Glyph: " << FT_StrError(error) << std::endl;
1171 return error;
1172 }
1173
1174 if (face->glyph->format != ft_glyph_format_outlineFT_GLYPH_FORMAT_OUTLINE) {
1175 std::cerr << "Not an outline font\n";
1176 FT_Done_Glyph(glyph_raw);
1177 return 0;
1178 }
1179
1180 FTGlyphPtr glyph(glyph_raw);
1181 FT_OutlineGlyph og = (FT_OutlineGlyph)glyph.get();
1182
1183 auto& glyphBuffer = converter.getGlyphBuffer();
1184 auto& xMin = converter.getXMin();
1185 auto& nodes = converter.getNodes();
1186 auto& firstpass = converter.getFirstPass();
1187 auto& startcontour = converter.getStartContour();
1188 auto& factor = converter.getFactor();
1189
1190 if (bufferOnly) {
1191 // Create new glyph entry
1192 Glyph newGlyph;
1193 newGlyph.charCode = static_cast<unsigned int>(charcode);
1194
1195 // Try to get a printable unicode symbol for the header comment.
1196 if (shouldWriteHeaderSymbol(charcode) &&
1197 charcode <= 0x10FFFF && charcode != 0xFFFD &&
1198 !(charcode >= 0xD800 && charcode <= 0xDFFF)) {
1199 // Valid Unicode codepoint (not surrogate, not replacement char)
1200 std::string s;
1201 if (charcode < 0x80) {
1202 s += static_cast<char>(charcode);
1203 } else if (charcode < 0x800) {
1204 s += static_cast<char>(0xC0 | (charcode >> 6));
1205 s += static_cast<char>(0x80 | (charcode & 0x3F));
1206 } else if (charcode < 0x10000) {
1207 s += static_cast<char>(0xE0 | (charcode >> 12));
1208 s += static_cast<char>(0x80 | ((charcode >> 6) & 0x3F));
1209 s += static_cast<char>(0x80 | (charcode & 0x3F));
1210 } else {
1211 // 4-byte UTF-8 for characters ≥ U+10000
1212 s += static_cast<char>(0xF0 | (charcode >> 18));
1213 s += static_cast<char>(0x80 | ((charcode >> 12) & 0x3F));
1214 s += static_cast<char>(0x80 | ((charcode >> 6) & 0x3F));
1215 s += static_cast<char>(0x80 | (charcode & 0x3F));
1216 }
1217 newGlyph.symbol = s;
1218 }
1219
1220 FT_Error buildError = 0;
1221 newGlyph.polylines = buildPolylinesFromOutline(og->outline, nodes, factor, buildError);
1222 if (buildError) {
1223 std::cerr << "FT_Outline_Decompose: " << FT_StrError(buildError) << std::endl;
1224 return buildError;
1225 }
1226
1227 // Add to buffer
1228 glyphBuffer.push_back(newGlyph);
1229 } else {
1230 // Original direct-to-file output (for calibration)
1231 std::ofstream nullFile("/dev/null");
1232 if (nullFile.is_open()) {
1233 nullFile << "\n[#" << std::hex << std::setfill('0') << std::setw(4) << charcode << std::dec << "]\n";
1234
1235 xMin = 1000.0;
1236 firstpass = true;
1237 error = FT_Outline_Decompose(&(og->outline), &funcs, nullptr);
1238 if (error)
1239 std::cerr << "FT_Outline_Decompose: first pass: " << FT_StrError(error) << std::endl;
1240
1241 firstpass = false;
1242 startcontour = true;
1243 error = FT_Outline_Decompose(&(og->outline), &funcs, nullptr);
1244 nullFile << "\n";
1245 }
1246 }
1247
1248 return error;
1249}
1250
1251/**
1252 * Print usage information
1253 */
1254static void usage(int eval) {
1255 std::cout << "Usage: ttf2lff [options] <ttf file> <lff file>\n";
1256 std::cout << "\n";
1257 std::cout << "Convert TrueType font to LibreCAD Font Format (LFF)\n";
1258 std::cout << "\n";
1259 std::cout << "Arguments:\n";
1260 std::cout << " <ttf file> Input TrueType font file (.ttf, .otf)\n";
1261 std::cout << " <lff file> Output LFF font file\n";
1262 std::cout << "\n";
1263 std::cout << "Options:\n";
1264 std::cout << " -n, --nodes N Number of nodes for quadratic/cubic splines (default: 4)\n";
1265 std::cout << " -a, --author TEXT Author name for font metadata\n";
1266 std::cout << " -l, --letterspacing N Letter spacing (default: 3.0)\n";
1267 std::cout << " -w, --wordspacing N Word spacing (default: 6.75)\n";
1268 std::cout << " -f, --linespacing N Line spacing factor (default: 1.0)\n";
1269 std::cout << " -d, --precision N Decimal precision (default: 6)\n";
1270 std::cout << " -L, --license TEXT Font license\n";
1271 std::cout << "\n";
1272 std::cout << "Tuning options (from python-lff):\n";
1273 std::cout << " -z, --zerovector Remove duplicate consecutive vertices\n";
1274 std::cout << " -r, --round Round vertices to grid\n";
1275 std::cout << " -g, --grid N Grid size for rounding (default: 0.0001)\n";
1276 std::cout << " -m, --mergepath Merge paths with same start/end vertex\n";
1277 std::cout << " -e, --nestedchar Analyze for nested characters\n";
1278 std::cout << " -c, --nocomment Remove comments from glyphs\n";
1279 std::cout << " -t, --tuning Apply all tuning options\n";
1280 std::cout << "\n";
1281 std::cout << " -h, --help Show this help message\n";
1282 std::cout << "\n";
1283 std::cout << "Example:\n";
1284 std::cout << " ttf2lff -a \"John Doe\" -n 8 -z -r -m font.ttf output.lff\n";
1285 exit(eval);
1286}
1287
1288
1289/**
1290 * Main function
1291 */
1292int main(int argc, char* argv[]) {
1293 FT_Error error;
1294 std::string fTtf;
1295 std::string fLff;
1296
1297 // Default values
1298 int nodes = 4;
1299 std::string name = "Unknown";
1300 double letterSpacing = 3.0;
1301 double wordSpacing = 6.75;
1302 double lineSpacingFactor = 1.0;
1303 std::string author = "Unknown";
1304 std::string license = "Unknown";
1305 int precision = 6;
1306
1307 // Parse command line arguments
1308 for (int i = 1; i < argc; ++i) {
1309 std::string arg = argv[i];
1310
1311 if (arg == "-h" || arg == "--help") {
1312 usage(0);
1313 }
1314 else if (arg == "-n" || arg == "--nodes") {
1315 if (++i >= argc) { std::cerr << "Error: -n requires an argument\n"; return 1; }
1316 nodes = std::atoi(argv[i]);
1317 }
1318 else if (arg == "-a" || arg == "--author") {
1319 if (++i >= argc) { std::cerr << "Error: -a requires an argument\n"; return 1; }
1320 author = argv[i];
1321 }
1322 else if (arg == "-l" || arg == "--letterspacing") {
1323 if (++i >= argc) { std::cerr << "Error: -l requires an argument\n"; return 1; }
1324 letterSpacing = std::atof(argv[i]);
1325 }
1326 else if (arg == "-w" || arg == "--wordspacing") {
1327 if (++i >= argc) { std::cerr << "Error: -w requires an argument\n"; return 1; }
1328 wordSpacing = std::atof(argv[i]);
1329 }
1330 else if (arg == "-f" || arg == "--linespacing") {
1331 if (++i >= argc) { std::cerr << "Error: -f requires an argument\n"; return 1; }
1332 lineSpacingFactor = std::atof(argv[i]);
1333 }
1334 else if (arg == "-d" || arg == "--precision") {
1335 if (++i >= argc) { std::cerr << "Error: -d requires an argument\n"; return 1; }
1336 precision = std::atoi(argv[i]);
1337 }
1338 else if (arg == "-L" || arg == "--license") {
1339 if (++i >= argc) { std::cerr << "Error: -L requires an argument\n"; return 1; }
1340 license = argv[i];
1341 }
1342 else if (arg == "-z" || arg == "--zerovector") {
1343 tuningOptions.zeroVector = true;
1344 }
1345 else if (arg == "-r" || arg == "--round") {
1346 tuningOptions.roundVertex = true;
1347 }
1348 else if (arg == "-g" || arg == "--grid") {
1349 if (++i >= argc) { std::cerr << "Error: -g requires an argument\n"; return 1; }
1350 tuningOptions.roundGrid = std::atof(argv[i]);
1351 }
1352 else if (arg == "-m" || arg == "--mergepath") {
1353 tuningOptions.mergePath = true;
1354 }
1355 else if (arg == "-e" || arg == "--nestedchar") {
1356 tuningOptions.nestedChar = true;
1357 }
1358 else if (arg == "-c" || arg == "--nocomment") {
1359 tuningOptions.noComment = true;
1360 }
1361 else if (arg == "-t" || arg == "--tuning") {
1362 // Enable all tuning options
1363 tuningOptions.zeroVector = true;
1364 tuningOptions.roundVertex = true;
1365 tuningOptions.mergePath = true;
1366 tuningOptions.nestedChar = true;
1367 tuningOptions.noComment = true;
1368 }
1369 else if (arg[0] != '-') {
1370 // Assume first non-option is TTF file, second is LFF file
1371 fTtf = arg;
1372 if (++i < argc) {
1373 fLff = argv[i];
1374 }
1375 }
1376 else {
1377 std::cerr << "Unknown option: " << arg << "\n";
1378 usage(1);
1379 }
1380 }
1381
1382 if (fTtf.empty() || fLff.empty()) {
1383 std::cerr << "Error: Missing required arguments\n\n";
1384 usage(1);
1385 }
1386
1387 std::cout << "TTF file: " << fTtf << "\n";
1388 std::cout << "LFF file: " << fLff << "\n";
1389
1390 if (tuningOptions.zeroVector) std::cout << "Tuning: ZeroVector enabled\n";
1391 if (tuningOptions.roundVertex) std::cout << "Tuning: RoundVertex enabled (grid=" << tuningOptions.roundGrid << ")\n";
1392 if (tuningOptions.mergePath) std::cout << "Tuning: MergePath enabled\n";
1393 if (tuningOptions.nestedChar) std::cout << "Tuning: NestedChar enabled\n";
1394 if (tuningOptions.noComment) std::cout << "Tuning: NoComment enabled\n";
1395
1396 // Create converter with RAII
1397 TTF2LFFConverter converter;
1398 converter.getNodes() = nodes;
1399 converter.getPrecision() = precision;
1400
1401 // Initialize FreeType
1402 error = converter.initLibrary();
1403 if (error) {
1404 return 1;
1405 }
1406
1407 FT_Library library = converter.getLibrary();
1408 FT_Int major = 0, minor = 0, patch = 0;
1409 FT_Library_Version(library, &major, &minor, &patch);
1410 std::cerr << "FreeType version: " << major << '.' << minor << '.' << patch << std::endl;
1411
1412 // Load font
1413 error = converter.loadFace(fTtf);
1414 if (error) {
1415 return 1;
1416 }
1417
1418 FT_Face face = converter.getFace();
1419 std::cout << "Family: " << face->family_name << "\n";
1420 std::cout << "Style: " << face->style_name << "\n";
1421 std::cout << "Height: " << face->height << "\n";
1422 std::cout << "Ascender: " << face->ascender << "\n";
1423 std::cout << "Descender: " << face->descender << "\n";
1424 std::cout << "Faces: " << face->num_faces << "\n";
1425 std::cout << "Glyphs: " << face->num_glyphs << "\n";
1426 name = face->family_name;
1427
1428 // Determine scale factor by tracing 'A'
1429 converter.getYMax() = -1000;
1430 g_converter = &converter;
1431 convertGlyph(converter, 65, false); // Direct output for calibration
1432 converter.getFactor() = 1.0 / (1.0 / 9.0 * converter.getYMax());
1433 std::cout << "Factor: " << converter.getFactor() << "\n";
1434
1435 // Open output file
1436 if (!converter.openOutputFile(fLff)) {
1437 return 2;
1438 }
1439
1440 std::ofstream& outputFile = converter.getOutputFile();
1441
1442 // Set number format
1443 std::string numFormat = "%." + std::to_string(precision) + "f";
1444 converter.getNumFormat() = numFormat;
1445
1446 // Write font header
1447 outputFile << "# Format: LibreCAD Font 1\n";
1448 outputFile << "# Creator: ttf2lff with python-lff tuning\n";
1449 outputFile << "# Version: 1\n";
1450 outputFile << "# Name: " << name << "\n";
1451 outputFile << "# Encoding: UTF-8\n";
1452 outputFile << "# LetterSpacing: " << clearZeros(letterSpacing) << "\n";
1453 outputFile << "# WordSpacing: " << clearZeros(wordSpacing) << "\n";
1454 outputFile << "# LineSpacingFactor: " << clearZeros(lineSpacingFactor) << "\n";
1455
1456 time_t rawtime;
1457 time(&rawtime);
1458 struct tm* timeinfo = localtime(&rawtime);
1459 char buffer[12];
1460 strftime(buffer, sizeof(buffer), "%Y-%m-%d", timeinfo);
1461
1462 outputFile << "# Created: " << buffer << "\n";
1463 outputFile << "# Last modified: " << buffer << "\n";
1464 outputFile << "# Author: " << author << "\n";
1465 outputFile << "# License: " << license << "\n";
1466
1467 // Write tuning information as comments
1468 if (tuningOptions.zeroVector || tuningOptions.roundVertex ||
1469 tuningOptions.mergePath || tuningOptions.nestedChar || tuningOptions.noComment) {
1470 outputFile << "# Tuning: ";
1471 bool first = true;
1472 if (tuningOptions.zeroVector) { outputFile << (first ? "" : ", ") << "ZeroVector"; first = false; }
1473 if (tuningOptions.roundVertex) {
1474 outputFile << (first ? "" : ", ") << "RoundVertex(grid=" << std::fixed << std::setprecision(6) << tuningOptions.roundGrid << ")";
1475 first = false;
1476 }
1477 if (tuningOptions.mergePath) { outputFile << (first ? "" : ", ") << "MergePath"; first = false; }
1478 if (tuningOptions.nestedChar) { outputFile << (first ? "" : ", ") << "NestedChar"; first = false; }
1479 if (tuningOptions.noComment) { outputFile << (first ? "" : ", ") << "NoComment"; first = false; }
Value stored to 'first' is never read
1480 outputFile << "\n";
1481 }
1482
1483 outputFile << "\n";
1484
1485 // Convert all glyphs
1486 // First, collect all glyphs into buffer for tuning
1487 converter.getGlyphBuffer().clear();
1488
1489 FT_ULong charcode;
1490 FT_UInt gindex;
1491
1492 charcode = FT_Get_First_Char(face, &gindex);
1493 while (gindex != 0) {
1494 convertGlyph(converter, charcode, true); // Buffer the glyph
1495 charcode = FT_Get_Next_Char(face, charcode, &gindex);
1496 }
1497
1498 std::cout << "Converted " << converter.getGlyphBuffer().size() << " glyphs\n";
1499
1500 // Apply tuning algorithms
1501 if (tuningOptions.zeroVector || tuningOptions.roundVertex ||
1502 tuningOptions.mergePath || tuningOptions.nestedChar || tuningOptions.noComment) {
1503 std::cout << "Applying tuning algorithms...\n";
1504 applyTuning();
1505 std::cout << "Tuning complete\n";
1506 }
1507
1508 // Find and replace nested characters
1509 if (tuningOptions.nestedChar) {
1510 std::cout << "Finding nested characters...\n";
1511 const size_t nestedCount = findAndReplaceNestedChars();
1512 std::cout << "Nested character processing complete: " << nestedCount << " references\n";
1513 }
1514
1515 // Write buffered glyphs to file
1516 for (const auto& glyph : converter.getGlyphBuffer()) {
1517 writeGlyph(outputFile, glyph);
1518 }
1519
1520 converter.closeOutputFile();
1521 g_converter = nullptr;
1522
1523 std::cout << "Conversion complete: " << fLff << "\n";
1524 return 0;
1525}