From 66026f46e65cf2c22f2c1bac797a7bd90dcdc0f7 Mon Sep 17 00:00:00 2001 From: cbaakman Date: Fri, 5 Oct 2018 20:59:48 +0200 Subject: [PATCH] initial import --- .gitignore | 6 +- Makefile | 46 + README.md | 39 + build.cmd | 37 + data/sample1.svg | 7851 +++++++++++++++++++++++++++++++++++++++ data/sample2.svg | 492 +++ include/text-gl/error.h | 36 + include/text-gl/font.h | 146 + include/text-gl/image.h | 101 + include/text-gl/tex.h | 89 + include/text-gl/text.h | 93 + include/text-gl/utf8.h | 42 + src/error.cpp | 111 + src/image.cpp | 424 +++ src/parse.cpp | 837 +++++ src/tex.cpp | 164 + src/text.cpp | 307 ++ src/utf8.cpp | 131 + tests/encoding.cpp | 42 + tests/visual.cpp | 719 ++++ 20 files changed, 11712 insertions(+), 1 deletion(-) create mode 100644 Makefile create mode 100644 README.md create mode 100644 build.cmd create mode 100644 data/sample1.svg create mode 100644 data/sample2.svg create mode 100644 include/text-gl/error.h create mode 100644 include/text-gl/font.h create mode 100644 include/text-gl/image.h create mode 100644 include/text-gl/tex.h create mode 100644 include/text-gl/text.h create mode 100644 include/text-gl/utf8.h create mode 100644 src/error.cpp create mode 100644 src/image.cpp create mode 100644 src/parse.cpp create mode 100644 src/tex.cpp create mode 100644 src/text.cpp create mode 100644 src/utf8.cpp create mode 100644 tests/encoding.cpp create mode 100644 tests/visual.cpp diff --git a/.gitignore b/.gitignore index 259148f..7f23994 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,6 @@ +# vim swap files +*.swp + # Prerequisites *.d @@ -12,7 +15,7 @@ *.pch # Compiled Dynamic libraries -*.so +*.so* *.dylib *.dll @@ -27,6 +30,7 @@ *.lib # Executables +bin/test_* *.exe *.out *.app diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..c2e31a8 --- /dev/null +++ b/Makefile @@ -0,0 +1,46 @@ +CXX = g++ +CFLAGS = -std=c++17 +VERSION=1.0.0 +LIB_NAME=text-gl + + +all: lib/lib$(LIB_NAME).so.$(VERSION) + +clean: + rm -f bin/test_visual bin/test_encoding lib/lib$(LIB_NAME).so.$(VERSION) obj/*.o core + + +test: bin/test_visual bin/test_encoding + bin/test_encoding + bin/test_visual data/sample1.svg + bin/test_visual data/sample2.svg + + +bin/test_visual: tests/visual.cpp lib/lib$(LIB_NAME).so.$(VERSION) + mkdir -p bin + $(CXX) $(CFLAGS) -I include $^ -lboost_filesystem -lboost_system -lGL -lGLEW -lSDL2 -o $@ + + +bin/test_encoding: tests/encoding.cpp lib/lib$(LIB_NAME).so.$(VERSION) + mkdir -p bin + $(CXX) $(CFLAGS) -I include -fexec-charset=UTF-8 $^ -lboost_unit_test_framework -o $@ + + +lib/lib$(LIB_NAME).so.$(VERSION): obj/parse.o obj/image.o obj/utf8.o obj/error.o obj/tex.o obj/text.o + mkdir -p lib + $(CXX) $(CFLAGS) $^ -lGL -lxml2 -lcairo -o $@ -fPIC -shared + + +obj/%.o: src/%.cpp include/text-gl/font.h include/text-gl/text.h include/text-gl/utf8.h + mkdir -p obj + $(CXX) $(CFLAGS) -I include/text-gl -c $< -o $@ -fPIC + +install: + /usr/bin/install -d -m755 /usr/local/lib + /usr/bin/install -d -m755 /usr/local/include/text-gl + /usr/bin/install -m644 lib/lib$(LIB_NAME).so.$(VERSION) /usr/local/lib/lib$(LIB_NAME).so.$(VERSION) + ln -sf /usr/local/lib/lib$(LIB_NAME).so.$(VERSION) /usr/local/lib/lib$(LIB_NAME).so + /usr/bin/install -D include/xml-mesh/*.h /usr/local/include/xml-mesh/ + +uninstall: + rm -f /usr/local/lib/lib$(LIB_NAME).so* /usr/local/include/text-gl/*.h diff --git a/README.md b/README.md new file mode 100644 index 0000000..ff68f47 --- /dev/null +++ b/README.md @@ -0,0 +1,39 @@ +# Text GL 1.0.0 +A libray that facilitates working with text rendering in OpenGL. + +The only supported input font format is SVG. (for now) +See: https://www.w3.org/TR/SVG11/fonts.html + +See also the wiki at https://github.com/cbaakman/text-gl/wiki to find out how to import fonts and render text. + +## Contents +* An include dir with headers for the library +* A src dir, containing the implementation +* A test dir, containing a unit test and a visual test + +## Dependencies +* GNU/MinGW C++ compiler 4.7 or higher. +* LibXML 2.0 or higher: http://xmlsoft.org/ +* cairo 1.10.2 or higher: https://cairographics.org/ +* OpenGL 3.2 or higher, should be installed on your OS by default, if the hardware supports it. + +For the tests, also: +* Boost 1.68.0 or higher: https://www.boost.org/ +* GLM 0.9.9.2 or higher: https://glm.g-truc.net/0.9.9/index.html +* GLEW 2.1.0 or higher: http://glew.sourceforge.net/ +* LibSDL 2.0.8 or higher: https://www.libsdl.org/download-2.0.php + +## Building the Library +On linux, run 'make'. It will generate a .so file under 'lib'. + +On Windows run 'build.cmd'. It will generate a .dll file under 'bin' and an import .a under 'lib'. + +## Running the Tests +On Linux, run 'make test'. + +On Windows, the test is executed automatically when you build the library. + +## Installing +On Linux, run 'make install'. Or run 'make uninstall' to undo the installation. + +On Windows, add the .dll under 'bin', the import .a under 'lib' and the headers under 'include' to your build path. diff --git a/build.cmd b/build.cmd new file mode 100644 index 0000000..b7b211d --- /dev/null +++ b/build.cmd @@ -0,0 +1,37 @@ +set CXX=g++ +set CFLAGS=-std=c++17 +set VERSION=1.0.0 +set LIB_NAME=text-gl + + +if not exist obj (mkdir obj) +if not exist bin (mkdir bin) +if not exist lib (mkdir lib) + +del /Q /F /S obj\* bin\%LIB_NAME%-%VERSION%.dll lib\lib%LIB_NAME%.a + +:: Make the library. + +@for %%m in (parse image tex utf8 error text) do ( + %CXX% %CFLAGS% -I include\text-gl -c src\%%m.cpp -o obj\%%m.o -fPIC + + @if %ERRORLEVEL% neq 0 ( + goto end + ) +) + +%CXX% obj\parse.o obj\image.o obj\tex.o obj\utf8.o obj\error.o obj\text.o -lxml2 -lcairo -lopengl32 ^ +-o bin\%LIB_NAME%-%VERSION%.dll -shared -fPIC -Wl,--out-implib,lib\lib%LIB_NAME%.a +@if %ERRORLEVEL% neq 0 ( + goto end +) + +:: Make the tests. + +%CXX% %CFLAGS% -I include -fexec-charset=UTF-8 tests\encoding.cpp lib\lib%LIB_NAME%.a ^ +-lboost_unit_test_framework -o bin\test_encoding.exe && bin\test_encoding.exe + +%CXX% %CFLAGS% -I include tests\visual.cpp lib\lib%LIB_NAME%.a ^ +-lxml2 -lcairo -lopengl32 -lglew32 -lmingw32 -lSDL2main -lSDL2 -o bin\test_visual.exe && bin\test_visual.exe data\sample1.svg + +:end diff --git a/data/sample1.svg b/data/sample1.svg new file mode 100644 index 0000000..0bba63e --- /dev/null +++ b/data/sample1.svg @@ -0,0 +1,7851 @@ + + + + + +Created by FontForge 20170731 at Thu Jan 28 15:54:56 2016 + By Coos +Created by Coos Baakman,,,, with FontForge 2.0 (http://fontforge.sf.net) + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/data/sample2.svg b/data/sample2.svg new file mode 100644 index 0000000..2ff1e1c --- /dev/null +++ b/data/sample2.svg @@ -0,0 +1,492 @@ + + +Created in Inkscape by Coos Baakman, +https://inkscape.org/en/ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/include/text-gl/error.h b/include/text-gl/error.h new file mode 100644 index 0000000..20bd7ee --- /dev/null +++ b/include/text-gl/error.h @@ -0,0 +1,36 @@ +/* Copyright (C) 2018 Coos Baakman + This software is provided 'as-is', without any express or implied + warranty. In no event will the authors be held liable for any damages + arising from the use of this software. + Permission is granted to anyone to use this software for any purpose, + including commercial applications, and to alter it and redistribute it + freely, subject to the following restrictions: + 1. The origin of this software must not be misrepresented; you must not + claim that you wrote the original software. If you use this software + in a product, an acknowledgment in the product documentation would be + appreciated but is not required. + 2. Altered source versions must be plainly marked as such, and must not be + misrepresented as being the original software. + 3. This notice may not be removed or altered from any source distribution. +*/ + +#ifndef ERROR_H +#define ERROR_H + +#include + +#define ERRORBUF_SIZE 1024 + + +namespace TextGL +{ + class TextGLError: public std::exception + { + protected: + char buffer[ERRORBUF_SIZE]; + public: + const char *what(void) const noexcept; + }; +} + +#endif // ERROR_H diff --git a/include/text-gl/font.h b/include/text-gl/font.h new file mode 100644 index 0000000..cb0285a --- /dev/null +++ b/include/text-gl/font.h @@ -0,0 +1,146 @@ +/* Copyright (C) 2018 Coos Baakman + This software is provided 'as-is', without any express or implied + warranty. In no event will the authors be held liable for any damages + arising from the use of this software. + Permission is granted to anyone to use this software for any purpose, + including commercial applications, and to alter it and redistribute it + freely, subject to the following restrictions: + 1. The origin of this software must not be misrepresented; you must not + claim that you wrote the original software. If you use this software + in a product, an acknowledgment in the product documentation would be + appreciated but is not required. + 2. Altered source versions must be plainly marked as such, and must not be + misrepresented as being the original software. + 3. This notice may not be removed or altered from any source distribution. +*/ + +#ifndef FONT_H +#define FONT_H + +#include +#include +#include + +#include "utf8.h" + + +namespace TextGL +{ + struct GlyphMetrics + { + double bearingX, bearingY, + width, height, + advanceX; + }; + + struct FontBoundingBox + { + double left, bottom, right, top; + }; + + struct FontMetrics + { + double unitsPerEM, + ascent, descent; // Together, these two determine the height of one line. + + FontBoundingBox bbox; + }; + + + typedef std::unordered_map> KernTable; + + /** + * returns 0.0 if the combination doesn't exist. + */ + double GetKernValue(const KernTable &, const UTF8Char first, const UTF8Char second); + + enum GlyphPathElementType + { + ELEMENT_MOVETO, // uses x, y + ELEMENT_LINETO, // uses x, y + ELEMENT_CURVETO, // uses x1, y1, x2, y2, x, y + ELEMENT_ARCTO, // uses rx, ry, rotate(radians), largeArc, sweep, x, y + ELEMENT_CLOSEPATH // has no arguments + }; + + struct GlyphPathElement + { + GlyphPathElementType type; + + union + { + struct + { + double x1, y1, x2, y2; + }; + struct + { + double rx, ry, rotate; + bool largeArc, sweep; + }; + }; + double x, y; + }; + + struct GlyphData + { + GlyphMetrics mMetrics; + std::list mPath; + }; + + struct FontData + { + FontMetrics mMetrics; + std::unordered_map mGlyphs; + KernTable mHorizontalKernTable; + }; + + void ParseSVGFontData(std::istream &, FontData &); + + class FontParseError: public TextGLError + { + public: + FontParseError(const char *format, ...); + }; + + class MissingGlyphError: public TextGLError + { + public: + MissingGlyphError(const UTF8Char c); + }; + + struct Color + { + float r, g, b, a; + }; + + enum LineJoinType + { + LINEJOIN_MITER, LINEJOIN_ROUND, LINEJOIN_BEVEL + }; + + enum LineCapType + { + LINECAP_BUTT, LINECAP_ROUND, LINECAP_SQUARE + }; + + struct FontStyle + { + double size, strokeWidth; + Color fillColor, + strokeColor; + LineJoinType lineJoin; + LineCapType lineCap; + }; + + class Font + { + public: + virtual const FontMetrics *GetMetrics(void) const = 0; + virtual const FontStyle *GetStyle(void) const = 0; + virtual const KernTable *GetHorizontalKernTable(void) const = 0; + virtual const GlyphMetrics *GetGlyphMetrics(const UTF8Char) const = 0; + }; +} + +#endif // FONT_H diff --git a/include/text-gl/image.h b/include/text-gl/image.h new file mode 100644 index 0000000..98520f5 --- /dev/null +++ b/include/text-gl/image.h @@ -0,0 +1,101 @@ +/* Copyright (C) 2018 Coos Baakman + This software is provided 'as-is', without any express or implied + warranty. In no event will the authors be held liable for any damages + arising from the use of this software. + Permission is granted to anyone to use this software for any purpose, + including commercial applications, and to alter it and redistribute it + freely, subject to the following restrictions: + 1. The origin of this software must not be misrepresented; you must not + claim that you wrote the original software. If you use this software + in a product, an acknowledgment in the product documentation would be + appreciated but is not required. + 2. Altered source versions must be plainly marked as such, and must not be + misrepresented as being the original software. + 3. This notice may not be removed or altered from any source distribution. +*/ + +#ifndef IMAGE_H +#define IMAGE_H + +#include "font.h" + + +namespace TextGL +{ + class GLTextureGlyph; + class GLTextureFont; + + + enum ImageDataFormat + { + IMAGEFORMAT_RGBA32, + IMAGEFORMAT_ARGB32 + }; + + class Image + { + public: + virtual const void *GetData(void) const = 0; + virtual ImageDataFormat GetFormat(void) const = 0; + virtual void GetDimensions(size_t &w, size_t &h) const = 0; + }; + + class ImageGlyph + { + private: + GlyphMetrics mMetrics; // transformed by size + + Image *mImage; + + ImageGlyph(void); + ~ImageGlyph(void); + + void operator=(const ImageGlyph &) = delete; + ImageGlyph(const ImageGlyph &) = delete; + public: + const GlyphMetrics *GetMetrics(void) const; + + friend ImageGlyph *MakeImageGlyph(const FontData &, + const FontStyle &, + const GlyphData &); + friend GLTextureGlyph *MakeGLTextureGlyph(const ImageGlyph *); + friend void DestroyImageGlyph(ImageGlyph *); + }; + + class ImageFont: public Font + { + private: + FontMetrics mMetrics; // transformed by size + FontStyle style; + + std::unordered_map mGlyphs; + KernTable mHorizontalKernTable; // transformed by size + + ImageFont(void); + ~ImageFont(void); + + void operator=(const ImageFont &) = delete; + ImageFont(const ImageFont &) = delete; + public: + const FontStyle *GetStyle(void) const; + const FontMetrics *GetMetrics(void) const; + const KernTable *GetHorizontalKernTable(void) const; + const GlyphMetrics *GetGlyphMetrics(const UTF8Char) const; + const ImageGlyph *GetGlyph(const UTF8Char) const; + + friend ImageFont *MakeImageFont(const FontData &, const FontStyle &); + friend GLTextureFont *MakeGLTextureFont(const ImageFont *); + friend void DestroyImageFont(ImageFont *); + }; + + ImageFont *MakeImageFont(const FontData &, const FontStyle &); + void DestroyImageFont(ImageFont *); + + class FontImageError: public TextGLError + { + public: + FontImageError(const char *format, ...); + }; +} + +#endif // IMAGE_H diff --git a/include/text-gl/tex.h b/include/text-gl/tex.h new file mode 100644 index 0000000..d9a2efc --- /dev/null +++ b/include/text-gl/tex.h @@ -0,0 +1,89 @@ +/* Copyright (C) 2018 Coos Baakman + This software is provided 'as-is', without any express or implied + warranty. In no event will the authors be held liable for any damages + arising from the use of this software. + Permission is granted to anyone to use this software for any purpose, + including commercial applications, and to alter it and redistribute it + freely, subject to the following restrictions: + 1. The origin of this software must not be misrepresented; you must not + claim that you wrote the original software. If you use this software + in a product, an acknowledgment in the product documentation would be + appreciated but is not required. + 2. Altered source versions must be plainly marked as such, and must not be + misrepresented as being the original software. + 3. This notice may not be removed or altered from any source distribution. +*/ + + +#ifndef TEX_H +#define TEX_H + + +#include +#include + +#include "image.h" + + +namespace TextGL +{ + class GLTextureGlyph + { + private: + GlyphMetrics mMetrics; // transformed by size + + GLuint texture; + GLsizei textureWidth, textureHeight; // Never smaller than the metrics. + + GLTextureGlyph(void); + ~GLTextureGlyph(void); + + void operator=(const GLTextureGlyph &) = delete; + GLTextureGlyph(const GLTextureGlyph &) = delete; + public: + const GlyphMetrics *GetMetrics(void) const; + GLuint GetTexture(void) const; + void GetTextureDimensions(GLsizei &width, GLsizei &height) const; + + friend GLTextureGlyph *MakeGLTextureGlyph(const ImageGlyph *); + friend void DestroyGLTextureGlyph(GLTextureGlyph *); + }; + + class GLTextureFont: public Font + { + private: + FontMetrics mMetrics; // transformed by size + FontStyle style; + + std::unordered_map mGlyphs; + KernTable mHorizontalKernTable; // transformed by size + + GLTextureFont(void); + ~GLTextureFont(void); + + void operator=(const GLTextureFont &) = delete; + GLTextureFont(const GLTextureFont &) = delete; + public: + const FontMetrics *GetMetrics(void) const; + const FontStyle *GetStyle(void) const; + const GLTextureGlyph *GetGlyph(const UTF8Char) const; + const GlyphMetrics *GetGlyphMetrics(const UTF8Char) const; + const KernTable *GetHorizontalKernTable(void) const; + + friend GLTextureFont *MakeGLTextureFont(const ImageFont *); + friend void DestroyGLTextureFont(GLTextureFont *); + }; + + // A valid GL context is required to call these functions. + GLTextureFont *MakeGLTextureFont(const ImageFont *); + void DestroyGLTextureFont(GLTextureFont *); + + class GLError: public TextGLError + { + public: + GLError(const GLenum, const char *filename, const size_t line); + GLError(const char *format, ...); + }; +} + +#endif // TEX_H diff --git a/include/text-gl/text.h b/include/text-gl/text.h new file mode 100644 index 0000000..0c14b3e --- /dev/null +++ b/include/text-gl/text.h @@ -0,0 +1,93 @@ +/* Copyright (C) 2018 Coos Baakman + This software is provided 'as-is', without any express or implied + warranty. In no event will the authors be held liable for any damages + arising from the use of this software. + Permission is granted to anyone to use this software for any purpose, + including commercial applications, and to alter it and redistribute it + freely, subject to the following restrictions: + 1. The origin of this software must not be misrepresented; you must not + claim that you wrote the original software. If you use this software + in a product, an acknowledgment in the product documentation would be + appreciated but is not required. + 2. Altered source versions must be plainly marked as such, and must not be + misrepresented as being the original software. + 3. This notice may not be removed or altered from any source distribution. +*/ + +#ifndef TEXT_H +#define TEXT_H + +#include + +#include "tex.h" + +namespace TextGL +{ + struct TextSelectionDetails + { + // One dimensional, character position of the selection in the text string. + size_t startPosition, endPosition; + + GLfloat startX, endX, baseY, // should render the glyph at startX, baseY + ascent, descent; // relative to baseY + }; + + /** + * Might be thrown if the text doesn't fit within the given width. + */ + class TextFormatError: public TextGLError + { + public: + TextFormatError(const char *format, ...); + }; + + struct GlyphVertex + { + GLfloat x, y, tx, ty; + }; + + struct GlyphQuad + { + GlyphVertex vertices[4]; // counter clockwise + GLuint texture; + }; + + enum TextAlign + { + TEXTALIGN_LEFT, // extend right from startX + TEXTALIGN_CENTER, // extend around startX + TEXTALIGN_RIGHT // extend left from startX + }; + + struct TextParams + { + GLfloat startX, startY, + maxWidth, + lineSpacing; // between two baselines + TextAlign align; + }; + + GLfloat GetLineHeight(const GLTextureFont *); + + class GLTextLeftToRightIterator + { + protected: + virtual void OnGlyph(const UTF8Char c, const GlyphQuad &, const TextSelectionDetails &) {} + + virtual void OnLine(const TextSelectionDetails &) {} + public: + + /** + * NULL-terminated UTF-8 encoding is assumed for input sequence 'text'. + * + * Glyphs are placed from small x (left) to high x (right) + * and lines are placed from high y (up) to low y (down). + */ + void IterateText(const GLTextureFont *, const int8_t *text, + const TextParams &); + }; + + size_t CountLines(const Font *, const int8_t *text, const TextParams &); +} + +#endif // TEXT_H diff --git a/include/text-gl/utf8.h b/include/text-gl/utf8.h new file mode 100644 index 0000000..f7f5e54 --- /dev/null +++ b/include/text-gl/utf8.h @@ -0,0 +1,42 @@ +/* Copyright (C) 2018 Coos Baakman + This software is provided 'as-is', without any express or implied + warranty. In no event will the authors be held liable for any damages + arising from the use of this software. + Permission is granted to anyone to use this software for any purpose, + including commercial applications, and to alter it and redistribute it + freely, subject to the following restrictions: + 1. The origin of this software must not be misrepresented; you must not + claim that you wrote the original software. If you use this software + in a product, an acknowledgment in the product documentation would be + appreciated but is not required. + 2. Altered source versions must be plainly marked as such, and must not be + misrepresented as being the original software. + 3. This notice may not be removed or altered from any source distribution. +*/ + +#ifndef UTF8_H +#define UTF8_H + +#include +#include + +#include "error.h" + + +namespace TextGL +{ + typedef int32_t UTF8Char; // 4 byte placeholders + + const int8_t *NextUTF8Char(const int8_t *bytes, UTF8Char &c); + const int8_t *PrevUTF8Char(const int8_t *bytes, UTF8Char &c); + size_t CountCharsUTF8(const int8_t *start, const int8_t *end=NULL); + const int8_t *GetUTF8Position(const int8_t *bytes, const size_t characterNumber); + + class EncodingError: public TextGLError + { + public: + EncodingError(const char *format, ...); + }; +} + +#endif // UTF8_H diff --git a/src/error.cpp b/src/error.cpp new file mode 100644 index 0000000..e40dfc3 --- /dev/null +++ b/src/error.cpp @@ -0,0 +1,111 @@ +/* Copyright (C) 2018 Coos Baakman + This software is provided 'as-is', without any express or implied + warranty. In no event will the authors be held liable for any damages + arising from the use of this software. + Permission is granted to anyone to use this software for any purpose, + including commercial applications, and to alter it and redistribute it + freely, subject to the following restrictions: + 1. The origin of this software must not be misrepresented; you must not + claim that you wrote the original software. If you use this software + in a product, an acknowledgment in the product documentation would be + appreciated but is not required. + 2. Altered source versions must be plainly marked as such, and must not be + misrepresented as being the original software. + 3. This notice may not be removed or altered from any source distribution. +*/ + +#include +#include + +#include "text.h" + + +namespace TextGL +{ + FontParseError::FontParseError(const char *format, ...) + { + va_list pArgs; + va_start(pArgs, format); + + vsnprintf(buffer, ERRORBUF_SIZE, format, pArgs); + + va_end(pArgs); + } + FontImageError::FontImageError(const char *format, ...) + { + va_list pArgs; + va_start(pArgs, format); + + vsnprintf(buffer, ERRORBUF_SIZE, format, pArgs); + + va_end(pArgs); + } + EncodingError::EncodingError(const char *format, ...) + { + va_list pArgs; + va_start(pArgs, format); + + vsnprintf(buffer, ERRORBUF_SIZE, format, pArgs); + + va_end(pArgs); + } + TextFormatError::TextFormatError(const char *format, ...) + { + va_list pArgs; + va_start(pArgs, format); + + vsnprintf(buffer, ERRORBUF_SIZE, format, pArgs); + + va_end(pArgs); + } + MissingGlyphError::MissingGlyphError(const UTF8Char c) + { + snprintf(buffer, ERRORBUF_SIZE, "No glyph for \'%c\'", c); + } + GLError::GLError(const GLenum err, const char *filename, const size_t line) + { + switch (err) + { + case GL_NO_ERROR: + snprintf(buffer, ERRORBUF_SIZE, "GL_NO_ERROR at %s line %u", filename, line); + break; + case GL_INVALID_ENUM: + snprintf(buffer, ERRORBUF_SIZE, "GL_INVALID_ENUM at %s line %u", filename, line); + break; + case GL_INVALID_VALUE: + snprintf(buffer, ERRORBUF_SIZE, "GL_INVALID_VALUE at %s line %u", filename, line); + break; + case GL_INVALID_OPERATION: + snprintf(buffer, ERRORBUF_SIZE, "GL_INVALID_OPERATION at %s line %u", filename, line); + break; + case GL_INVALID_FRAMEBUFFER_OPERATION: + snprintf(buffer, ERRORBUF_SIZE, "GL_INVALID_FRAMEBUFFER_OPERATION at %s line %u", filename, line); + break; + case GL_OUT_OF_MEMORY: + snprintf(buffer, ERRORBUF_SIZE, "GL_OUT_OF_MEMORY at %s line %u", filename, line); + break; + case GL_STACK_UNDERFLOW: + snprintf(buffer, ERRORBUF_SIZE, "GL_STACK_UNDERFLOW at %s line %u", filename, line); + break; + case GL_STACK_OVERFLOW: + snprintf(buffer, ERRORBUF_SIZE, "GL_STACK_OVERFLOW at %s line %u", filename, line); + break; + default: + snprintf(buffer, ERRORBUF_SIZE, "unknown GL error 0x%x at %s line %u", err, filename, line); + break; + } + } + GLError::GLError(const char *format, ...) + { + va_list pArgs; + va_start(pArgs, format); + + vsnprintf(buffer, ERRORBUF_SIZE, format, pArgs); + + va_end(pArgs); + } + const char *TextGLError::what(void) const noexcept + { + return buffer; + } +} diff --git a/src/image.cpp b/src/image.cpp new file mode 100644 index 0000000..03f09d4 --- /dev/null +++ b/src/image.cpp @@ -0,0 +1,424 @@ +/* Copyright (C) 2018 Coos Baakman + This software is provided 'as-is', without any express or implied + warranty. In no event will the authors be held liable for any damages + arising from the use of this software. + Permission is granted to anyone to use this software for any purpose, + including commercial applications, and to alter it and redistribute it + freely, subject to the following restrictions: + 1. The origin of this software must not be misrepresented; you must not + claim that you wrote the original software. If you use this software + in a product, an acknowledgment in the product documentation would be + appreciated but is not required. + 2. Altered source versions must be plainly marked as such, and must not be + misrepresented as being the original software. + 3. This notice may not be removed or altered from any source distribution. +*/ + +#include +#include + +#include + +#include "image.h" + + +namespace TextGL +{ + class CairoImage: public Image + { + private: + cairo_surface_t *pSurface; + public: + CairoImage(const size_t w, const size_t h) + { + pSurface = cairo_image_surface_create(CAIRO_FORMAT_ARGB32, w, h); + + cairo_status_t status = cairo_surface_status(pSurface); + if (status != CAIRO_STATUS_SUCCESS) + { + throw FontImageError("%s while creating a cairo surface", cairo_status_to_string(status)); + } + } + + ~CairoImage(void) + { + cairo_surface_destroy(pSurface); + } + + const void *GetData(void) const + { + cairo_image_surface_get_data(pSurface); + } + + ImageDataFormat GetFormat(void) const + { + return IMAGEFORMAT_ARGB32; + } + + void GetDimensions(size_t &w, size_t &h) const + { + w = cairo_image_surface_get_width(pSurface); + h = cairo_image_surface_get_height(pSurface); + } + + friend CairoImage *MakeCairoGlyphImage(const FontData &fontData, + const FontStyle &style, + const GlyphData &glyphData); + }; + + void CairoArcTo(cairo_t *cr, const double currentX, const double currentY, + double rx, double ry, const double rotate, + const bool largeArc, const bool sweep, const double x, const double y) + { + if (rx == 0.0 || ry == 0.0) // means straight line + { + cairo_line_to(cr, x, y); + return; + } + else if (x == currentX && y == currentY) + return; + + double radiiRatio = ry / rx, + + dx = x - currentX, + dy = y - currentY, + + // Transform the target point from elipse to circle + xe = dx * cos(-rotate) - dy * sin(-rotate), + ye =(dy * cos(-rotate) + dx * sin(-rotate)) / radiiRatio, + + // angle between the line from current to target point and the x axis + angle = atan2(ye, xe); + + // Move the target point onto the x axis + // The current point was already on the x-axis + xe = sqrt(xe * xe + ye * ye); + ye = 0.0; + + // Update the first radius if it is too small + rx = std::max(rx, xe / 2); + + // Find one circle centre + double xc = xe / 2, + yc = sqrt(rx * rx - xc * xc); + + // fix for a glitch, appearing on some machines: + if (rx == xc) + yc = 0.0; + + // Use the flags to pick a circle center + if (!(largeArc != sweep)) + yc = -yc; + + // Rotate the target point and the center back to their original circle positions + + double sinAngle = sin(angle), + cosAngle = cos(angle); + + ye = xe * sinAngle; + xe = xe * cosAngle; + + double ax = xc * cosAngle - yc * sinAngle, + ay = yc * cosAngle + xc * sinAngle; + xc = ax; + yc = ay; + + // Find the drawing angles, from center to current and target points on circle: + double angle1 = atan2(0.0 - yc, 0.0 - xc), // current is shifted to 0,0 + angle2 = atan2( ye - yc, xe - xc); + + cairo_save(cr); + cairo_translate(cr, currentX, currentY); + cairo_rotate(cr, rotate); + cairo_scale(cr, 1.0, radiiRatio); + + if (sweep) + { + cairo_arc(cr, xc, yc, rx, angle1, angle2); + } + else + { + cairo_arc_negative(cr, xc, yc, rx, angle1, angle2); + } + + cairo_restore(cr); + } + + void PathToCairo(const std::list &path, cairo_t *cr) + { + double currentX = 0.0, + currentY = 0.0; + for (const GlyphPathElement &el : path) + { + switch (el.type) + { + case ELEMENT_MOVETO: + cairo_move_to(cr, el.x, el.y); + break; + case ELEMENT_LINETO: + cairo_line_to(cr, el.x, el.y); + break; + case ELEMENT_CURVETO: + cairo_curve_to(cr, el.x1, el.y1, el.x2, el.y2, el.x, el.y); + break; + case ELEMENT_ARCTO: + CairoArcTo(cr, currentX, currentY, + el.rx, el.ry, + el.rotate, el.largeArc, el.sweep, + el.x, el.y); + break; + case ELEMENT_CLOSEPATH: + cairo_close_path(cr); + break; + default: + throw FontImageError("Unsupported path element: %x", el.type); + } + + currentX = el.x; + currentY = el.y; + } + } + + /** + * Don't set scale if the stroke width has to be transformed in cairo space. + */ + void CairoDrawPath(cairo_t *cr, const FontStyle &style, const double scale=1.0) + { + if (style.fillColor.a > 0.0) + { + cairo_set_source_rgba(cr, style.fillColor.r, style.fillColor.g, style.fillColor.b, style.fillColor.a); + + cairo_fill_preserve(cr); + } + + if (style.strokeWidth > 0.0 && style.strokeColor.a > 0.0) + { + cairo_set_source_rgba(cr, style.strokeColor.r, style.strokeColor.g, style.strokeColor.b, style.strokeColor.a); + cairo_set_line_width(cr, style.strokeWidth / scale); + + switch (style.lineJoin) + { + case LINEJOIN_MITER: + cairo_set_line_join(cr, CAIRO_LINE_JOIN_MITER); + break; + case LINEJOIN_ROUND: + cairo_set_line_join(cr, CAIRO_LINE_JOIN_ROUND); + break; + case LINEJOIN_BEVEL: + cairo_set_line_join(cr, CAIRO_LINE_JOIN_BEVEL); + break; + default: + throw FontImageError("Unsupported line join type: %x", style.lineJoin); + } + + switch (style.lineCap) + { + case LINECAP_BUTT: + cairo_set_line_cap(cr, CAIRO_LINE_CAP_BUTT); + break; + case LINECAP_ROUND: + cairo_set_line_cap(cr, CAIRO_LINE_CAP_ROUND); + break; + case LINECAP_SQUARE: + cairo_set_line_cap(cr, CAIRO_LINE_CAP_SQUARE); + break; + default: + throw FontImageError("Unsupported line cap type: %x", style.lineCap); + } + + cairo_stroke(cr); + } + } + + CairoImage *MakeCairoGlyphImage(const FontData &fontData, + const FontStyle &style, + const GlyphData &glyphData) + { + double scale = style.size / fontData.mMetrics.unitsPerEM; + + /* Cairo surfaces and OpenGL textures have integer dimensions, but + * glyph bouding boxes consist floating points. Make sure the bounding box fits onto the texture: + */ + int w = (int)ceil((fontData.mMetrics.bbox.right - fontData.mMetrics.bbox.left) * scale), + h = (int)ceil((fontData.mMetrics.bbox.top - fontData.mMetrics.bbox.bottom) * scale); + + CairoImage *pCairoImage = new CairoImage(w, h); + + cairo_t *cr = cairo_create(pCairoImage->pSurface); + + cairo_status_t status = cairo_status(cr); + if (status != CAIRO_STATUS_SUCCESS) + { + delete pCairoImage; + throw FontImageError("%s while creating a cairo context", cairo_status_to_string(status)); + } + + cairo_set_antialias(cr, CAIRO_ANTIALIAS_BEST); + + // Within the cairo surface, move to the glyph's coordinate system. + cairo_scale(cr, scale, scale); + cairo_translate(cr, -fontData.mMetrics.bbox.left, -fontData.mMetrics.bbox.bottom); + + #ifdef DEBUG + // Draw a red rectangle to indicate the bounding box. + cairo_set_source_rgb(cr, 1.0, 0.0, 0.0); + cairo_rectangle(cr, fontData.mMetrics.bbox.left, fontData.mMetrics.bbox.bottom, + fontData.mMetrics.bbox.right - fontData.mMetrics.bbox.left, + fontData.mMetrics.bbox.top - fontData.mMetrics.bbox.bottom); + cairo_set_line_width(cr, 1.0); + cairo_stroke(cr); + #endif // DEBUG + + // Set the path in cairo. + PathToCairo(glyphData.mPath, cr); + + // Fill it in, according to the font style. + CairoDrawPath(cr, style, scale); + cairo_surface_flush(pCairoImage->pSurface); + + // Don't need this anymore, we're done drawing to the surface. + cairo_destroy(cr); + + return pCairoImage; + } + + void ScaleGlyphMetrics(const GlyphMetrics &metricsSrc, const double scale, GlyphMetrics &metricsDest) + { + metricsDest.bearingX = metricsSrc.bearingX * scale; + metricsDest.bearingY = metricsSrc.bearingY * scale; + metricsDest.width = metricsSrc.width * scale; + metricsDest.height = metricsSrc.height * scale; + metricsDest.advanceX = metricsSrc.advanceX * scale; + } + + void DestroyImageGlyph(ImageGlyph *p) + { + delete p->mImage; + delete p; + } + + ImageGlyph *MakeImageGlyph(const FontData &fontData, + const FontStyle &style, + const GlyphData &glyphData) + { + double scale = style.size / fontData.mMetrics.unitsPerEM; + + ImageGlyph *pImageGlyph = new ImageGlyph; + + pImageGlyph->mImage = MakeCairoGlyphImage(fontData, style, glyphData); + + ScaleGlyphMetrics(glyphData.mMetrics, scale, pImageGlyph->mMetrics); + + return pImageGlyph; + } + + void ScaleFontMetrics(const FontMetrics &metricsSrc, const double scale, FontMetrics &metricsDest) + { + metricsDest.unitsPerEM = metricsSrc.unitsPerEM * scale; + metricsDest.ascent = metricsSrc.ascent * scale; + metricsDest.descent = metricsSrc.descent * scale; + + metricsDest.bbox.left = metricsSrc.bbox.left * scale; + metricsDest.bbox.right = metricsSrc.bbox.right * scale; + metricsDest.bbox.top = metricsSrc.bbox.top * scale; + metricsDest.bbox.bottom = metricsSrc.bbox.bottom * scale; + } + + void ScaleKernTable(const KernTable &tableSrc, const double scale, KernTable &tableDest) + { + for (const std::pair> &pair1 : tableSrc) + { + UTF8Char c1 = std::get<0>(pair1); + std::unordered_map m2 = std::get<1>(pair1); + for (const std::pair &pair2 : m2) + { + UTF8Char c2 = std::get<0>(pair2); + + double k = std::get<1>(pair2); + + tableDest[c1][c2] = k * scale; + } + } + } + + ImageFont *MakeImageFont(const FontData &fontData, const FontStyle &style) + { + double scale = style.size / fontData.mMetrics.unitsPerEM; + + ImageFont *pImageFont = new ImageFont; + + ScaleFontMetrics(fontData.mMetrics, scale, pImageFont->mMetrics); + + ScaleKernTable(fontData.mHorizontalKernTable, scale, pImageFont->mHorizontalKernTable); + + for (const std::pair &pair : fontData.mGlyphs) + { + UTF8Char c = std::get<0>(pair); + + try + { + ImageGlyph *pGlyph = MakeImageGlyph(fontData, style, fontData.mGlyphs.at(c)); + pImageFont->mGlyphs[c] = pGlyph; + } + catch (...) + { + DestroyImageFont(pImageFont); + std::rethrow_exception(std::current_exception()); + } + } + + return pImageFont; + } + void DestroyImageFont(ImageFont *p) + { + if (p == NULL) + return; + + for (auto &pair : p->mGlyphs) + { + DestroyImageGlyph(std::get<1>(pair)); + } + + delete p; + } + + ImageGlyph::ImageGlyph(void) + { + } + ImageGlyph::~ImageGlyph(void) + { + } + ImageFont::ImageFont(void) + { + } + ImageFont::~ImageFont(void) + { + } + const FontStyle *ImageFont::GetStyle(void) const + { + return &style; + } + const FontMetrics *ImageFont::GetMetrics(void) const + { + return &mMetrics; + } + const KernTable *ImageFont::GetHorizontalKernTable(void) const + { + return &mHorizontalKernTable; + } + const ImageGlyph *ImageFont::GetGlyph(const UTF8Char c) const + { + if (mGlyphs.find(c) == mGlyphs.end()) + throw MissingGlyphError(c); + + return mGlyphs.at(c); + } + const GlyphMetrics *ImageFont::GetGlyphMetrics(const UTF8Char c) const + { + return GetGlyph(c)->GetMetrics(); + } + const GlyphMetrics *ImageGlyph::GetMetrics(void) const + { + return &mMetrics; + } +} diff --git a/src/parse.cpp b/src/parse.cpp new file mode 100644 index 0000000..de01a42 --- /dev/null +++ b/src/parse.cpp @@ -0,0 +1,837 @@ +/* Copyright (C) 2018 Coos Baakman + This software is provided 'as-is', without any express or implied + warranty. In no event will the authors be held liable for any damages + arising from the use of this software. + Permission is granted to anyone to use this software for any purpose, + including commercial applications, and to alter it and redistribute it + freely, subject to the following restrictions: + 1. The origin of this software must not be misrepresented; you must not + claim that you wrote the original software. If you use this software + in a product, an acknowledgment in the product documentation would be + appreciated but is not required. + 2. Altered source versions must be plainly marked as such, and must not be + misrepresented as being the original software. + 3. This notice may not be removed or altered from any source distribution. +*/ + +#include +#include + +#include +#include +#include + +#include "font.h" + + +# define PI 3.14159265358979323846 + +#define HAS_ID(map, id) (map.find(id) != map.end()) + +namespace TextGL +{ + class XMLChildIterator : public std::iterator + { + private: + xmlNodePtr pChildren; + public: + XMLChildIterator(xmlNodePtr p): pChildren(p) {} + XMLChildIterator(const XMLChildIterator &it): pChildren(it.pChildren) {} + + XMLChildIterator &operator++(void) + { + pChildren = pChildren->next; + return *this; + } + XMLChildIterator operator++(int) + { + XMLChildIterator tmp(*this); + operator++(); + return tmp; + } + bool operator==(const XMLChildIterator &other) const + { + return pChildren == other.pChildren; + } + bool operator!=(const XMLChildIterator &other) const + { + return pChildren != other.pChildren; + } + xmlNodePtr operator*(void) + { + return pChildren; + } + }; + + class XMLChildIterable + { + private: + xmlNodePtr pParent; + public: + XMLChildIterable(xmlNodePtr p): pParent(p) {} + + XMLChildIterator begin(void) + { + return XMLChildIterator(pParent->children); + } + + XMLChildIterator end(void) + { + return XMLChildIterator(nullptr); + } + }; + + bool HasChild(xmlNodePtr pTag, const char *tagName) + { + for (xmlNodePtr pChild : XMLChildIterable(pTag)) + { + if (boost::iequals((const char *)pChild->name, tagName)) + return true; + } + return false; + } + + xmlNodePtr FindChild(xmlNodePtr pParent, const char *tagName) + { + for (xmlNodePtr pChild : XMLChildIterable(pParent)) + { + if (boost::iequals((const char *)pChild->name, tagName)) + return pChild; + } + + throw FontParseError("No %s tag found in %s tag", tagName, (const char *)pParent->name); + } + + std::list IterFindChildren(xmlNodePtr pParent, const char *tagName) + { + std::list l; + + for (xmlNodePtr pChild : XMLChildIterable(pParent)) + { + if (boost::iequals((const char *)pChild->name, tagName)) + l.push_back(pChild); + } + + return l; + } + + xmlDocPtr ParseXML(std::istream &is) + { + const size_t bufSize = 1024; + std::streamsize res; + char buf[bufSize]; + int wellFormed; + + xmlParserCtxtPtr pCtxt; + xmlDocPtr pDoc; + + // Read the first 4 bytes. + is.read(buf, 4); + if (!is.good()) + throw FontParseError("Error reading the first xml bytes!"); + res = is.gcount(); + + // Create a progressive parsing context. + pCtxt = xmlCreatePushParserCtxt(NULL, NULL, buf, res, NULL); + if (!pCtxt) + throw FontParseError("Failed to create parser context!"); + + // Loop on the input, getting the document data. + while (is.good()) + { + is.read(buf, bufSize); + res = is.gcount(); + + xmlParseChunk(pCtxt, buf, res, 0); + } + + // There is no more input, indicate the parsing is finished. + xmlParseChunk(pCtxt, buf, 0, 1); + + // Check if it was well formed. + pDoc = pCtxt->myDoc; + wellFormed = pCtxt->wellFormed; + xmlFreeParserCtxt(pCtxt); + + if (!wellFormed) + { + xmlFreeDoc(pDoc); + throw FontParseError("xml document is not well formed"); + } + + return pDoc; + } + + /** + * Locale independent, always uses a dot as decimal separator. + * + * returns the pointer to the string after the number. + */ + const char *ParseDouble(const char *in, double &out) + { + double d = 10.0f; + int digit, ndigit = 0; + + // start from zero + out = 0.0; + + const char *p = in; + while (*p) + { + if (isdigit(*p)) + { + digit = (*p - '0'); + + if (d > 1.0) // left from period + { + out *= d; + out += digit; + } + else // right from period, decimal + { + out += d * digit; + d *= 0.1; + } + ndigit++; + } + else if (tolower(*p) == 'e') + { + // exponent + + // if no digits precede the exponent, assume 1 + if (ndigit <= 0) + out = 1.0; + + p++; + if (*p == '+') // '+' just means positive power, default + p++; // skip it, don't give it to atoi + + int e = atoi(p); + + out = out * pow(10, e); + + // read past exponent number + if (*p == '-') + p++; + + while (isdigit(*p)) + p++; + + return p; + } + else if (*p == '.') + { + // expect decimal digits after this + + d = 0.1; + } + else if (*p == '-') + { + // negative number + double v; + p = ParseDouble(p + 1, v); + + out = -v; + + return p; + } + else + { + // To assume success, must have read at least one digit + if (ndigit > 0) + return p; + else + return nullptr; + } + p++; + } + + return p; + } + + void ParseUnicodeAttrib(const xmlNodePtr pTag, const char *key, UTF8Char &c) + { + xmlChar *pS = xmlGetProp(pTag, (const xmlChar *)key); + if (pS == nullptr) + throw FontParseError("Missing %s attribute: %s", (const char *)pTag->name, key); + int len = xmlStrlen(pS); + if (len > 4) + throw FontParseError("%s attribute %s has length %s. Expecting unicode", (const char *)pTag->name, key, len); + + // libxml2 automatically converts the html code to utf-8. + + if (*NextUTF8Char((const int8_t *)pS, c) != NULL) + throw FontParseError("Cannot read string %s as utf-8", pS); + + xmlFree(pS); + } + + void ParseStringAttrib(const xmlNodePtr pTag, const char *key, std::string &s) + { + xmlChar *pS = xmlGetProp(pTag, (const xmlChar *)key); + if (pS == nullptr) + throw FontParseError("Missing %s attribute: %s", (const char *)pTag->name, key); + + s.assign((const char *)pS); + xmlFree(pS); + } + + void ParseDoubleAttrib(const xmlNodePtr pTag, const char *key, double &d) + { + xmlChar *pS = xmlGetProp(pTag, (const xmlChar *)key); + if (pS == nullptr) + throw FontParseError("Missing %s attribute: %s", (const char *)pTag->name, key); + + if (ParseDouble((const char *)pS, d) == NULL) + throw FontParseError("Cannot convert string %s to number", pS); + xmlFree(pS); + } + + void ParseBoundingBoxAttrib(const xmlNodePtr pTag, FontBoundingBox &bbox) + { + std::string s; + ParseStringAttrib(pTag, "bbox", s); + + double numbers[4]; + size_t i; + const char *p = s.c_str(); + for (i = 0; i < 4; i++) + { + while (isspace(*p)) p++; + if ((p = ParseDouble(p, numbers[i])) == NULL) + throw FontParseError("bbox attribute doesn't contain 4 numbers"); + } + + bbox.left = numbers[0]; + bbox.bottom = numbers[1]; + bbox.right = numbers[2]; + bbox.top = numbers[3]; + } + + /** + * Parses n comma/space separated floats from the given string and + * returns a pointer to the text after it. + */ + const char *SVGParsePathDoubles(const int n, const char *text, double outs []) + { + int i; + for (i = 0; i < n; i++) + { + if (!*text) + return NULL; + + while (isspace(*text) || *text == ',') + { + text++; + if (!*text) + return NULL; + } + + text = ParseDouble(text, outs[i]); + if (!text) + return NULL; + } + + return text; + } + + /** + * This function, used to draw quadratic curves in cairo, is + * based on code from cairosvg(http://cairosvg.org/) + */ + void Quadratic2Bezier(double &x1, double &y1, double &x2, double &y2, const double x, const double y) + { + double xq1 = (x2) * 2 / 3 + (x1) / 3, + yq1 = (y2) * 2 / 3 + (y1) / 3, + xq2 = (x2) * 2 / 3 + x / 3, + yq2 = (y2) * 2 / 3 + y / 3; + + x1 = xq1; + y1 = yq1; + x2 = xq2; + y2 = yq2; + } + + void ParseSVGPath(const char *d, std::list &path) + { + const char *nd; + double ds[6], + qx1, qy1, qx2, qy2; + + char prevSymbol, symbol = 'm'; + bool upper = false; + GlyphPathElement el; + + while (*d) + { + prevSymbol = symbol; + + // Get the next path symbol: + + while (isspace(*d)) + d++; + + upper = isupper(*d); // upper is absolute, lower is relative + symbol = tolower(*d); + d++; + + // Take the approriate action for the symbol: + switch (symbol) + { + case 'z': // closepath + + el.type = ELEMENT_CLOSEPATH; + path.push_back(el); + break; + + case 'm': // moveto(x y)+ + + while ((nd = SVGParsePathDoubles(2, d, ds))) + { + if (upper) + { + el.x = ds[0]; + el.y = ds[1]; + } + else + { + el.x += ds[0]; + el.y += ds[1]; + } + + el.type = ELEMENT_MOVETO; + path.push_back(el); + + d = nd; + } + break; + + case 'l': // lineto(x y)+ + + while ((nd = SVGParsePathDoubles(2, d, ds))) + { + if (upper) + { + el.x = ds[0]; + el.y = ds[1]; + } + else + { + el.x += ds[0]; + el.y += ds[1]; + } + + el.type = ELEMENT_LINETO; + path.push_back(el); + + d = nd; + } + break; + + case 'h': // horizontal lineto x+ + + while ((nd = SVGParsePathDoubles(1, d, ds))) + { + if (upper) + { + el.x = ds[0]; + } + else + { + el.x += ds[0]; + } + + el.type = ELEMENT_LINETO; + path.push_back(el); + + d = nd; + } + break; + + case 'v': // vertical lineto y+ + + while ((nd = SVGParsePathDoubles(1, d, ds))) + { + if (upper) + { + el.y = ds[0]; + } + else + { + el.y += ds[0]; + } + + el.type = ELEMENT_LINETO; + path.push_back(el); + + d = nd; + } + break; + + case 'c': // curveto(x1 y1 x2 y2 x y)+ + + while ((nd = SVGParsePathDoubles(6, d, ds))) + { + if (upper) + { + el.x1 = ds[0]; el.y1 = ds[1]; + el.x2 = ds[2]; el.y2 = ds[3]; + el.x = ds[4]; el.y = ds[5]; + } + else + { + el.x1 = el.x + ds[0]; el.y1 = el.y + ds[1]; + el.x2 = el.x + ds[2]; el.y2 = el.y + ds[3]; + el.x += ds[4]; el.y += ds[5]; + } + + el.type = ELEMENT_CURVETO; + path.push_back(el); + + d = nd; + } + break; + + case 's': // shorthand/smooth curveto(x2 y2 x y)+ + + while ((nd = SVGParsePathDoubles(4, d, ds))) + { + if (prevSymbol == 's' || prevSymbol == 'c') + { + el.x1 = el.x + (el.x - el.x2); + el.y1 = el.y + (el.y - el.y2); + } + else + { + el.x1 = el.x; + el.y1 = el.y; + } + prevSymbol = symbol; + + if (upper) + { + el.x2 = ds[0]; + el.y2 = ds[1]; + el.x = ds[2]; + el.y = ds[3]; + } + else + { + el.x2 = el.x + ds[0]; + el.y2 = el.y + ds[1]; + el.x += ds[2]; + el.y += ds[3]; + } + + el.type = ELEMENT_CURVETO; + path.push_back(el); + + d = nd; + } + break; + + case 'q': // quadratic Bezier curveto(x1 y1 x y)+ + + while ((nd = SVGParsePathDoubles(4, d, ds))) + { + el.x1 = el.x; + el.y1 = el.y; + if (upper) + { + el.x2 = ds[0]; + el.y2 = ds[1]; + el.x = ds[2]; + el.y = ds[3]; + } + else + { + el.x2 = el.x + ds[0]; + el.y2 = el.y + ds[1]; + el.x += ds[2]; + el.y += ds[3]; + } + + // Cairo doesn't do quadratic, so fake it: + + qx1 = el.x1; + qy1 = el.y1; + qx2 = el.x2; + qy2 = el.y2; + Quadratic2Bezier(qx1, qy1, qx2, qy2, el.x, el.y); + el.x1 = qx1; + el.y1 = qy1; + el.x2 = qx2; + el.y2 = qy2; + + el.type = ELEMENT_CURVETO; + path.push_back(el); + + d = nd; + } + break; + + case 't': // Shorthand/smooth quadratic Bézier curveto(x y)+ + + while ((nd = SVGParsePathDoubles(2, d, ds))) + { + el.x1 = el.x; + el.y1 = el.y; + if (prevSymbol == 't' || prevSymbol == 'q') + { + el.x2 = el.x + (el.x - el.x2); + el.y2 = el.y + (el.y - el.y2); + } + else + { + el.x2 = el.x; + el.y2 = el.y; + } + + if (upper) + { + el.x = ds[0]; + el.y = ds[1]; + } + else + { + el.x += ds[0]; + el.y += ds[1]; + } + + // Cairo doesn't do quadratic, so fake it: + + qx1 = el.x1; + qy1 = el.y1; + qx2 = el.x2; + qy2 = el.y2; + Quadratic2Bezier(qx1, qy1, qx2, qy2, el.x, el.y); + + el.type = ELEMENT_CURVETO; + path.push_back(el); + + d = nd; + } + break; + + case 'a': // elliptical arc(rx ry x-axis-rotation large-arc-flag sweep-flag x y)+ + + while (isspace(*d)) + d++; + + while (isdigit(*d) || *d == '-') + { + nd = SVGParsePathDoubles(3, d, ds); + if (!nd) + { + throw FontParseError("arc incomplete. Missing first 3 floats in %s", d); + } + + el.rx = ds[0]; + el.ry = ds[1]; + el.rotate = ds[2] * PI / 180; // convert degrees to radians + + while (isspace(*nd) || *nd == ',') + nd++; + + if (!isdigit(*nd)) + { + throw FontParseError("arc incomplete. Missing large arc digit in %s", d); + } + + el.largeArc = *nd != '0'; + + do { + nd++; + } while (isspace(*nd) || *nd == ','); + + if (!isdigit(*nd)) + { + throw FontParseError("arc incomplete. Missing sweep digit in %s", d); + } + + el.sweep = *nd != '0'; + + do + { + nd++; + } + while (isspace(*nd) || *nd == ','); + + nd = SVGParsePathDoubles(2, nd, ds); + if (!nd) + { + throw FontParseError("arc incomplete. Missing last two floats in %s", d); + } + + while (isspace(*nd)) + nd++; + + d = nd; + + // end points: + if (upper) + { + el.x = ds[0]; + el.y = ds[1]; + } + else + { + el.x += ds[0]; + el.y += ds[1]; + } + + el.type = ELEMENT_ARCTO; + path.push_back(el); + } // end loop + + break; + } + } + } + + void ParseGlyphTag(const xmlNodePtr pGlyphTag, const GlyphMetrics &defaults, + FontData &fontData, + std::unordered_map &namesToCharacters) + { + if (!xmlHasProp(pGlyphTag, (const xmlChar *)"unicode")) + return; + + UTF8Char c; + ParseUnicodeAttrib(pGlyphTag, "unicode", c); + + if (xmlHasProp(pGlyphTag, (const xmlChar *)"glyph-name")) + { + std::string name; + ParseStringAttrib(pGlyphTag, "glyph-name", name); + + namesToCharacters[name] = c; + } + + fontData.mGlyphs[c].mMetrics = defaults; + if (xmlHasProp(pGlyphTag, (const xmlChar *)"horiz-adv-x")) + ParseDoubleAttrib(pGlyphTag, "horiz-adv-x", fontData.mGlyphs[c].mMetrics.advanceX); + if (xmlHasProp(pGlyphTag, (const xmlChar *)"horiz-origin-x")) + ParseDoubleAttrib(pGlyphTag, "horiz-origin-x", fontData.mGlyphs[c].mMetrics.bearingX); + if (xmlHasProp(pGlyphTag, (const xmlChar *)"horiz-origin-y")) + ParseDoubleAttrib(pGlyphTag, "horiz-origin-y", fontData.mGlyphs[c].mMetrics.bearingY); + + std::string d = ""; + if (xmlHasProp(pGlyphTag, (const xmlChar *)"d")) // 'd' might be missing for a whitespace glyph + ParseStringAttrib(pGlyphTag, "d", d); + ParseSVGPath(d.c_str(), fontData.mGlyphs[c].mPath); + } + + void ParseGlyphNameListAttrib(xmlNodePtr pTag, const char *id, std::list &names) + { + std::string value; + ParseStringAttrib(pTag, id, value); + + boost::split(names, value, boost::is_any_of(",")); + } + + void ParseGlyphUnicodeListAttrib(xmlNodePtr pTag, const char *id, std::list &characters) + { + std::string value; + ParseStringAttrib(pTag, id, value); + + std::list reprs; + boost::split(reprs, value, boost::is_any_of(",")); + for (const std::string &repr : reprs) + { + UTF8Char c; + if (*NextUTF8Char((const int8_t *)repr.c_str(), c) != NULL) + throw FontParseError("Error interpreting %s attribute %s %s as utf-8", (const char *)pTag->name, id, repr.c_str()); + characters.push_back(c); + } + } + + void ParseHKernTag(const xmlNodePtr pHKernTag, + const std::unordered_map &namesToCharacters, + FontData &fontData) + { + double k; + ParseDoubleAttrib(pHKernTag, "k", k); + + std::list g1, g2; + std::list u1, u2; + + if (xmlHasProp(pHKernTag, (const xmlChar *)"g1")) + { + ParseGlyphNameListAttrib(pHKernTag, "g1", g1); + for (std::string &name : g1) + { + if (!HAS_ID(namesToCharacters, name)) + throw FontParseError("No such glyph: %s", name); + u1.push_back(namesToCharacters.at(name)); + } + } + if (xmlHasProp(pHKernTag, (const xmlChar *)"g2")) + { + ParseGlyphNameListAttrib(pHKernTag, "g2", g2); + for (std::string &name : g2) + { + if (!HAS_ID(namesToCharacters, name)) + throw FontParseError("No such glyph: %s", name); + u2.push_back(namesToCharacters.at(name)); + } + } + if (xmlHasProp(pHKernTag, (const xmlChar *)"u1")) + ParseGlyphUnicodeListAttrib(pHKernTag, "u1", u1); + if (xmlHasProp(pHKernTag, (const xmlChar *)"u2")) + ParseGlyphUnicodeListAttrib(pHKernTag, "u2", u2); + + for (const UTF8Char c1 : u1) + for (const UTF8Char c2 : u2) + fontData.mHorizontalKernTable[c1][c2] = k; + } + + void ParseSVGFontData(std::istream &is, FontData &fontData) + { + xmlDocPtr pDoc = ParseXML(is); + + try + { + xmlNodePtr pRoot = xmlDocGetRootElement(pDoc); + if(pRoot == nullptr) + throw FontParseError("no root element found in xml tree"); + + if (!boost::iequals((const char *)pRoot->name, "svg")) + throw FontParseError("no root element is not \"svg\""); + + xmlNodePtr pDefsTag = FindChild(pRoot, "defs"), + pFontTag = FindChild(pDefsTag, "font"), + pFaceTag = FindChild(pFontTag, "font-face"); + + ParseDoubleAttrib(pFaceTag, "ascent", fontData.mMetrics.ascent); + ParseDoubleAttrib(pFaceTag, "descent", fontData.mMetrics.descent); + ParseDoubleAttrib(pFaceTag, "units-per-em", fontData.mMetrics.unitsPerEM); + + ParseBoundingBoxAttrib(pFaceTag, fontData.mMetrics.bbox); + + GlyphMetrics defaultGlyphMetrics = {0.0f, 0.0f, 0.0f, + fontData.mMetrics.bbox.top - fontData.mMetrics.bbox.bottom, + fontData.mMetrics.bbox.right - fontData.mMetrics.bbox.left}; + if (xmlHasProp(pFontTag, (const xmlChar *)"horiz-adv-x")) + ParseDoubleAttrib(pFontTag, "horiz-adv-x", defaultGlyphMetrics.advanceX); + if (xmlHasProp(pFontTag, (const xmlChar *)"horiz-origin-x")) + ParseDoubleAttrib(pFontTag, "horiz-origin-x", defaultGlyphMetrics.bearingX); + if (xmlHasProp(pFontTag, (const xmlChar *)"horiz-origin-y")) + ParseDoubleAttrib(pFontTag, "horiz-origin-y", defaultGlyphMetrics.bearingY); + + std::unordered_map namesToCharacters; + + for (xmlNodePtr pGlyphTag : IterFindChildren(pFontTag, "glyph")) + ParseGlyphTag(pGlyphTag, defaultGlyphMetrics, fontData, namesToCharacters); + + for (xmlNodePtr pHKernTag : IterFindChildren(pFontTag, "hkern")) + ParseHKernTag(pHKernTag, namesToCharacters, fontData); + } + catch(...) + { + xmlFreeDoc(pDoc); + + std::rethrow_exception(std::current_exception()); + } + xmlFreeDoc(pDoc); + } +} diff --git a/src/tex.cpp b/src/tex.cpp new file mode 100644 index 0000000..e3d3786 --- /dev/null +++ b/src/tex.cpp @@ -0,0 +1,164 @@ +/* Copyright (C) 2018 Coos Baakman + This software is provided 'as-is', without any express or implied + warranty. In no event will the authors be held liable for any damages + arising from the use of this software. + Permission is granted to anyone to use this software for any purpose, + including commercial applications, and to alter it and redistribute it + freely, subject to the following restrictions: + 1. The origin of this software must not be misrepresented; you must not + claim that you wrote the original software. If you use this software + in a product, an acknowledgment in the product documentation would be + appreciated but is not required. + 2. Altered source versions must be plainly marked as such, and must not be + misrepresented as being the original software. + 3. This notice may not be removed or altered from any source distribution. +*/ + +#include "tex.h" + +#ifdef DEBUG + #define CHECK_GL() { GLenum err = glGetError(); if (err != GL_NO_ERROR) throw FontGLError(err, __FILE__, __LINE__); } +#else + #define CHECK_GL() +#endif + +namespace TextGL +{ + GLTextureGlyph::GLTextureGlyph(void) + { + } + GLTextureGlyph::~GLTextureGlyph(void) + { + } + GLTextureFont::GLTextureFont(void) + { + } + GLTextureFont::~GLTextureFont(void) + { + } + void DestroyGLTextureGlyph(GLTextureGlyph *pTextureGlyph) + { + glDeleteTextures(1, &(pTextureGlyph->texture)); + CHECK_GL(); + + delete pTextureGlyph; + } + GLTextureGlyph *MakeGLTextureGlyph(const ImageGlyph *pImageGlyph) + { + GLTextureGlyph *pTextureGlyph = new GLTextureGlyph; + pTextureGlyph->mMetrics = pImageGlyph->mMetrics; + + size_t w, h; + pImageGlyph->mImage->GetDimensions(w, h); + pTextureGlyph->textureWidth = w; + pTextureGlyph->textureHeight = h; + + glGenTextures(1, &(pTextureGlyph->texture)); + CHECK_GL(); + + if (pTextureGlyph->texture == NULL) + throw GLError("No GL texture was generated"); + + glBindTexture(GL_TEXTURE_2D, pTextureGlyph->texture); + CHECK_GL(); + + glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR); + CHECK_GL(); + + glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR); + CHECK_GL(); + + glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP); + CHECK_GL(); + + glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP); + CHECK_GL(); + + switch (pImageGlyph->mImage->GetFormat()) + { + case IMAGEFORMAT_RGBA32: + glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA8, + pTextureGlyph->textureWidth, pTextureGlyph->textureHeight, + 0, GL_RGBA, GL_UNSIGNED_INT_8_8_8_8, + pImageGlyph->mImage->GetData()); + break; + case IMAGEFORMAT_ARGB32: + glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA8, + pTextureGlyph->textureWidth, pTextureGlyph->textureHeight, + 0, GL_BGRA, GL_UNSIGNED_INT_8_8_8_8_REV, + pImageGlyph->mImage->GetData()); + break; + default: + throw FontImageError("Unsupported image format: %x", pImageGlyph->mImage->GetFormat()); + } + CHECK_GL(); + + glBindTexture(GL_TEXTURE_2D, NULL); + CHECK_GL(); + + return pTextureGlyph; + } + + GLTextureFont *MakeGLTextureFont(const ImageFont *pImageFont) + { + GLTextureFont *pTextureFont = new GLTextureFont; + pTextureFont->mHorizontalKernTable = pImageFont->mHorizontalKernTable; + pTextureFont->mMetrics = pImageFont->mMetrics; + pTextureFont->style = pImageFont->style; + + for (const auto &pair : pImageFont->mGlyphs) + { + const UTF8Char c = std::get<0>(pair); + const ImageGlyph *pImageGlyph = std::get<1>(pair); + + pTextureFont->mGlyphs[c] = MakeGLTextureGlyph(pImageGlyph); + } + + return pTextureFont; + } + void DestroyGLTextureFont(GLTextureFont *pTextureFont) + { + for (const auto &pair : pTextureFont->mGlyphs) + { + DestroyGLTextureGlyph(std::get<1>(pair)); + } + + delete pTextureFont; + } + const GlyphMetrics *GLTextureGlyph::GetMetrics(void) const + { + return &mMetrics; + } + GLuint GLTextureGlyph::GetTexture(void) const + { + return texture; + } + void GLTextureGlyph::GetTextureDimensions(GLsizei &width, GLsizei &height) const + { + width = textureWidth; + height = textureHeight; + } + const FontMetrics *GLTextureFont::GetMetrics(void) const + { + return &mMetrics; + } + const FontStyle *GLTextureFont::GetStyle(void) const + { + return &style; + } + const GLTextureGlyph *GLTextureFont::GetGlyph(const UTF8Char c) const + { + if (mGlyphs.find(c) == mGlyphs.end()) + throw MissingGlyphError(c); + + return mGlyphs.at(c); + } + const GlyphMetrics *GLTextureFont::GetGlyphMetrics(const UTF8Char c) const + { + return GetGlyph(c)->GetMetrics(); + } + const KernTable *GLTextureFont::GetHorizontalKernTable(void) const + { + return &mHorizontalKernTable; + } +} diff --git a/src/text.cpp b/src/text.cpp new file mode 100644 index 0000000..be0f23f --- /dev/null +++ b/src/text.cpp @@ -0,0 +1,307 @@ +/* Copyright (C) 2018 Coos Baakman + This software is provided 'as-is', without any express or implied + warranty. In no event will the authors be held liable for any damages + arising from the use of this software. + Permission is granted to anyone to use this software for any purpose, + including commercial applications, and to alter it and redistribute it + freely, subject to the following restrictions: + 1. The origin of this software must not be misrepresented; you must not + claim that you wrote the original software. If you use this software + in a product, an acknowledgment in the product documentation would be + appreciated but is not required. + 2. Altered source versions must be plainly marked as such, and must not be + misrepresented as being the original software. + 3. This notice may not be removed or altered from any source distribution. +*/ + +#include "text.h" + + +namespace TextGL +{ + void SetTextSelection(const GLTextureFont *pFont, + const size_t startPosition, const size_t endPosition, + const GLfloat startX, const GLfloat endX, const GLfloat baseY, + TextSelectionDetails &details) + { + details.startPosition = startPosition; + details.endPosition = endPosition; + details.startX = startX; + details.endX = endX; + details.baseY = baseY; + details.ascent = pFont->GetMetrics()->ascent; + details.descent = pFont->GetMetrics()->descent; + } + + void SetGlyphQuad(const GLTextureFont *pFont, const UTF8Char c, + const GLfloat x, const GLfloat y, + GlyphQuad &quad) + { + const GLTextureGlyph *pGlyph = pFont->GetGlyph(c); + const GlyphMetrics *pGlyphMetrics = pGlyph->GetMetrics(); + const FontMetrics *pFontMetrics = pFont->GetMetrics(); + + quad.texture = pGlyph->GetTexture(); + + GLsizei tw, th; + pGlyph->GetTextureDimensions(tw, th); + + // top left + quad.vertices[3].x = x + pFontMetrics->bbox.left + pGlyphMetrics->bearingX; + quad.vertices[3].y = y + pFontMetrics->bbox.top + pGlyphMetrics->bearingY; + quad.vertices[3].tx = 0.0f; + quad.vertices[3].ty = 1.0f; + + // top right + quad.vertices[2].x = quad.vertices[3].x + tw; + quad.vertices[2].y = quad.vertices[3].y; + quad.vertices[2].tx = 1.0f; + quad.vertices[2].ty = 1.0f; + + // bottom right + quad.vertices[1].x = quad.vertices[2].x; + quad.vertices[1].y = quad.vertices[2].y - th; + quad.vertices[1].tx = 1.0f; + quad.vertices[1].ty = 0.0f; + + // bottom left + quad.vertices[0].x = quad.vertices[3].x; + quad.vertices[0].y = quad.vertices[3].y - th; + quad.vertices[0].tx = 0.0f; + quad.vertices[0].ty = 0.0f; + } + + double GetKernValue(const KernTable &kernTable, const UTF8Char c1, const UTF8Char c2) + { + if (kernTable.find(c1) == kernTable.end()) + return 0.0; + + const auto &m2 = kernTable.at(c1); + + if (m2.find(c2) == m2.end()) + return 0.0; + + return m2.at(c2); + } + + bool IsSpace(const UTF8Char c) + { + return c == ' ' || c == '\t'; + } + + const int8_t *SkipSpaces(const int8_t *p) + { + const int8_t *next; + UTF8Char c; + + while (true) + { + next = NextUTF8Char(p, c); + if (c == NULL || !IsSpace(c)) + return p; + else + p = next; + } + } + + bool AtLineEnding(const int8_t *p, const int8_t *&past) + { + UTF8Char c; + + past = NextUTF8Char(p, c); + if (c == '\n') + return true; + else if (c == '\r') // Detect windows line endings. + { + past = NextUTF8Char(p, c); + if (c == '\n') + return true; + } + return false; + } + + bool AtStringEnding(const int8_t *p) + { + UTF8Char c; + + NextUTF8Char(p, c); + + return c == NULL; + } + + GLfloat NextWordWidth(const Font *pFont, const int8_t *text, const int8_t *&pEnd) + { + GLfloat w = 0.0f; + UTF8Char c, cPrev = NULL; + const int8_t *p = text, *next; + + // First read all the whitespaces preceeding the word. + while (true) + { + next = NextUTF8Char(p, c); + if (AtStringEnding(p) || AtLineEnding(p, next)) + { + // Don't count " \n" as a word. + pEnd = p; + return 0.0f; + } + else if (!IsSpace(c)) + break; + + if (cPrev != NULL) + w += GetKernValue(*(pFont->GetHorizontalKernTable()), cPrev, c); + + w += pFont->GetGlyphMetrics(c)->advanceX; + + p = next; + cPrev = c; + } + + // Next read until the first whitespace. + while (true) + { + next = NextUTF8Char(p, c); + if (AtStringEnding(p) || AtLineEnding(p, next) || IsSpace(c)) + { + pEnd = p; + return w; + } + + if (cPrev != NULL) + w += GetKernValue(*(pFont->GetHorizontalKernTable()), cPrev, c); + + w += pFont->GetGlyphMetrics(c)->advanceX; + + p = next; + cPrev = c; + } + } + + GLfloat NextLineWidth(const Font *pFont, const int8_t *text, const GLfloat maxLineWidth, const int8_t *&pLineEnd) + { + GLfloat lineWidth = 0.0f, wordWidth; + const int8_t *p = SkipSpaces(text), + *pWordEnd, *past; + UTF8Char c = NULL; + while (true) + { + wordWidth = NextWordWidth(pFont, p, pWordEnd); + if (wordWidth > maxLineWidth) + throw TextFormatError("Next word of \"%s\" doesn't fit in line width %f", text, maxLineWidth); + else if ((lineWidth + wordWidth) > maxLineWidth) + { + pLineEnd = p; + return lineWidth; + } + + lineWidth += wordWidth; + p = pWordEnd; + + // Check what character ended the word. + if (AtStringEnding(p) || AtLineEnding(p, past)) + { + pLineEnd = p; + return lineWidth; + } + } + } + + void GLTextLeftToRightIterator::IterateText(const GLTextureFont *pFont, const int8_t *text, const TextParams ¶ms) + { + GlyphQuad quad; + TextSelectionDetails glyphSelection, lineSelection; + GLfloat x, y = params.startY, x0, + lineWidth; + + size_t count; + const int8_t *p = text, *next, *pLineEnd; + UTF8Char c, cPrev; + while (!AtStringEnding(p)) + { + lineWidth = NextLineWidth(pFont, p, params.maxWidth, pLineEnd); + cPrev = NULL; + if (params.align == TEXTALIGN_CENTER) + x = params.startX - lineWidth / 2; + + else if (params.align == TEXTALIGN_RIGHT) + x = params.startX - lineWidth; + + else // default TEXTALIGN_LEFT + x = params.startX; + + p = SkipSpaces(p); + + SetTextSelection(pFont, + CountCharsUTF8(text, p), CountCharsUTF8(text, pLineEnd), + x, x + lineWidth, y, lineSelection); + OnLine(lineSelection); + + while (p < pLineEnd) + { + x0 = x; + + next = NextUTF8Char(p, c); + + if (cPrev != NULL) + x += GetKernValue(*(pFont->GetHorizontalKernTable()), cPrev, c); + + SetGlyphQuad(pFont, c, x, y, quad); + + x += pFont->GetGlyph(c)->GetMetrics()->advanceX; + + SetTextSelection(pFont, + CountCharsUTF8(text, p), CountCharsUTF8(text, next), + x0, x, y, glyphSelection); + OnGlyph(c, quad, glyphSelection); + + cPrev = c; + p = next; + } + + // See what character ended the line. + if (AtStringEnding(p)) + return; + + else if (AtLineEnding(p, next)) + p = next; + + // Otherwise it was just a whitespace. + + y -= params.lineSpacing; + } + } + + size_t CountLines(const Font *pFont, const int8_t *text, const TextParams ¶ms) + { + const int8_t *p = text, *next; + size_t count = 0; + + while (!AtStringEnding(p)) + { + count++; + + NextLineWidth(pFont, p, params.maxWidth, next); + + p = next; + + // See what character ended the line. + if (AtStringEnding(p)) + break; + + else if (AtLineEnding(p, next)) + p = next; + + // Otherwise it was just a whitespace. + } + + return count; + } + + GLfloat GetLineHeight(const GLTextureFont *pFont) + { + const FontMetrics *pMetrics = pFont->GetMetrics(); + return pMetrics->ascent - pMetrics->descent; + } +} + + diff --git a/src/utf8.cpp b/src/utf8.cpp new file mode 100644 index 0000000..33d356a --- /dev/null +++ b/src/utf8.cpp @@ -0,0 +1,131 @@ +/* Copyright (C) 2018 Coos Baakman + This software is provided 'as-is', without any express or implied + warranty. In no event will the authors be held liable for any damages + arising from the use of this software. + Permission is granted to anyone to use this software for any purpose, + including commercial applications, and to alter it and redistribute it + freely, subject to the following restrictions: + 1. The origin of this software must not be misrepresented; you must not + claim that you wrote the original software. If you use this software + in a product, an acknowledgment in the product documentation would be + appreciated but is not required. + 2. Altered source versions must be plainly marked as such, and must not be + misrepresented as being the original software. + 3. This notice may not be removed or altered from any source distribution. +*/ + +#include + +#include "utf8.h" + +namespace TextGL +{ + int CountSuccessiveLeftBits(const int8_t byte) + { + int n = 0; + while (n < 8 && (byte & (0b0000000010000000 >> n))) + n ++; + + return n; + } + const int8_t *NextUTF8Char(const int8_t *bytes, UTF8Char &ch) + { + size_t nBytes, + i; + + /* + The first bits of the first byte determine the length + of the utf-8 character. The number of bytes equals the + number of uninterrupted 1 bits at the beginning. + The remaining first byte bits are considered coding. + ???????? : 0 bytes + 1??????? : 1 bytes + 11?????? : 2 bytes + 110????? : stop, assume 2 byte code! + ^^^^^^ + */ + + nBytes = CountSuccessiveLeftBits(bytes[0]); + + // Always include the first byte. + ch = 0x000000ff & bytes[0]; + + if (nBytes == 0) + { + // Assume ascii. + return bytes + 1; + } + + /* + Complement the unicode identifier from the remaining encoding bits. + All but the first UTF-8 byte must start in 10.., so six coding + bits per byte: + .. 10?????? 10?????? 10?????? .. + */ + for (i = 1; i < nBytes; i++) + { + if ((bytes[i] & 0b11000000) != 0b10000000) + { + throw EncodingError("utf-8 byte %u (0x%x) not starting in 10.. !", (i + 1), bytes[i]); + } + + ch = (ch << 8) | 0x000000ff & bytes[i]; + } + + // Move to the next utf-8 character pointer. + return bytes + nBytes; + } + const int8_t *PrevUTF8Char(const int8_t *bytes, UTF8Char &ch) + { + size_t nBytes = 0, + nBits; + bool beginFound = false; + int8_t byte; + + ch = 0x00000000; + while (!beginFound) + { + // Take one byte: + nBytes++; + byte = *(bytes - nBytes); + + // is it 10?????? + beginFound = (byte & 0b11000000) != 0b10000000; + + ch |= (0x000000ff & byte) << (8 * (nBytes - 1)); + } + + // Only ascii chars are allowed to start in a 0-bit. + nBits = CountSuccessiveLeftBits(byte); + if (nBits != nBytes && nBytes > 1) + { + throw EncodingError("%u successive bits, but %u bytes", nBits, nBytes); + } + + return bytes - nBytes; + } + const int8_t *GetUTF8Position(const int8_t *bytes, const size_t n) + { + size_t i = 0; + UTF8Char ch; + while (i < n) + { + bytes = NextUTF8Char(bytes, ch); + i++; + } + return bytes; + } + size_t CountCharsUTF8(const int8_t *bytes, const int8_t *end) + { + size_t n = 0; + UTF8Char ch; + while (*bytes) + { + bytes = NextUTF8Char(bytes, ch); + n++; + if (end and bytes >= end) + return n; + } + return n; + } +} diff --git a/tests/encoding.cpp b/tests/encoding.cpp new file mode 100644 index 0000000..2ea9899 --- /dev/null +++ b/tests/encoding.cpp @@ -0,0 +1,42 @@ +#define BOOST_TEST_DYN_LINK +#define BOOST_TEST_MODULE TestEncoding +#include + +#include + + +using namespace TextGL; + +BOOST_AUTO_TEST_CASE(prev_test) +{ + const int8_t text[] = "БejЖbaba"; + UTF8Char characters[8]; + + size_t i = 8; + const int8_t *p = GetUTF8Position(text, i); + + BOOST_CHECK_EQUAL(*p, NULL); // string termination + + for (; p > text; i--, p = PrevUTF8Char(p, characters[i])); + + BOOST_CHECK_EQUAL(characters[0], 'Б'); + BOOST_CHECK_EQUAL(characters[1], 'e'); + BOOST_CHECK_EQUAL(characters[3], 'Ж'); + BOOST_CHECK_EQUAL(characters[7], 'a'); +} + +BOOST_AUTO_TEST_CASE(next_test) +{ + const int8_t text[] = "БejЖbaba"; + UTF8Char characters[8]; + + size_t i = 0; + const int8_t *p; + + for (p = text; *p; p = NextUTF8Char(p, characters[i]), i++); + + BOOST_CHECK_EQUAL(characters[0], 'Б'); + BOOST_CHECK_EQUAL(characters[1], 'e'); + BOOST_CHECK_EQUAL(characters[3], 'Ж'); + BOOST_CHECK_EQUAL(characters[7], 'a'); +} diff --git a/tests/visual.cpp b/tests/visual.cpp new file mode 100644 index 0000000..0745e11 --- /dev/null +++ b/tests/visual.cpp @@ -0,0 +1,719 @@ +#include +#include +#include +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include + +using namespace TextGL; +using namespace glm; + + +#define CHECK_GL() { GLenum err = glGetError(); if (err != GL_NO_ERROR) throw GLError(err, __FILE__, __LINE__); } + + +#define VERTEX_POSITION_INDEX 0 +#define VERTEX_TEXCOORDS_INDEX 1 + + +class MessageError: public std::exception +{ + protected: + std::string message; + public: + const char *what(void) const noexcept + { + return message.c_str(); + } +}; + +class ShaderError: public MessageError +{ + public: + ShaderError(const boost::format &fmt) + { + message = fmt.str(); + } +}; + +class InitError: public MessageError +{ + public: + InitError(const boost::format &fmt) + { + message = fmt.str(); + } + InitError(const std::string &msg) + { + message = msg; + } +}; + +class RenderError: public MessageError +{ + public: + RenderError(const std::string & msg) + { + message = msg; + } +}; + + +GLuint CreateShader(const std::string &source, GLenum type) +{ + GLint result; + int logLength; + + GLuint shader = glCreateShader(type); + CHECK_GL(); + + const char *pSource = source.c_str(); + glShaderSource(shader, 1, &pSource, NULL); + CHECK_GL(); + + glCompileShader(shader); + CHECK_GL(); + + glGetShaderiv(shader, GL_COMPILE_STATUS, &result); + CHECK_GL(); + + if (result != GL_TRUE) + { + glGetShaderiv(shader, GL_INFO_LOG_LENGTH, &logLength); + CHECK_GL(); + + char *errorString = new char[logLength + 1]; + glGetShaderInfoLog(shader, logLength, NULL, errorString); + CHECK_GL(); + + ShaderError error(boost::format("error while compiling shader: %1%") % errorString); + delete[] errorString; + + glDeleteShader(shader); + CHECK_GL(); + + throw error; + } + + return shader; +} + +GLuint LinkShaderProgram(const GLuint vertexShader, + const GLuint fragmentShader, + const std::map &vertexAttribLocations) +{ + GLint result; + int logLength; + GLuint program; + + program = glCreateProgram(); + CHECK_GL(); + + glAttachShader(program, vertexShader); + CHECK_GL(); + + glAttachShader(program, fragmentShader); + CHECK_GL(); + + for (const auto &pair : vertexAttribLocations) + { + glBindAttribLocation(program, std::get<0>(pair), + std::get<1>(pair).c_str()); + CHECK_GL(); + } + + glLinkProgram(program); + CHECK_GL(); + + glGetProgramiv(program, GL_LINK_STATUS, &result); + CHECK_GL(); + + if (result != GL_TRUE) + { + glGetProgramiv(program, GL_INFO_LOG_LENGTH, &logLength); + CHECK_GL(); + + char *errorString = new char[logLength + 1]; + glGetProgramInfoLog(program, logLength, NULL, errorString); + CHECK_GL(); + + ShaderError error(boost::format("error while linking shader: %1%") % errorString); + delete[] errorString; + + glDeleteProgram(program); + CHECK_GL(); + + throw error; + } + + return program; +} + +const char glyphVertexShaderSrc[] = R"shader( +#version 150 + +in vec2 position; +in vec2 texCoords; + +out VertexData +{ + vec2 texCoords; +} vertexOut; + +uniform mat4 projectionMatrix; + +void main() +{ + gl_Position = projectionMatrix * vec4(position, 0.0, 1.0); + vertexOut.texCoords = texCoords; +} +)shader", + glyphFragmentShaderSrc[] = R"shader( +#version 150 + +uniform sampler2D tex; + +in VertexData +{ + vec2 texCoords; +} vertexIn; + +out vec4 fragColor; + +void main() +{ + fragColor = texture(tex, vertexIn.texCoords); +} + +)shader", + selectionVertexShaderSrc[] = R"shader( +#version 150 + +uniform mat4 projectionMatrix; + +in vec2 position; + +void main() +{ + gl_Position = projectionMatrix * vec4(position, 0.0, 1.0); +} +)shader", + selectionFragmentShaderSrc[] = R"shader( +#version 150 + +out vec4 fragColor; + +void main() +{ + fragColor = vec4(1.0, 0.0, 0.0, 1.0); +} +)shader"; + + +class TextBeamTracer: public GLTextLeftToRightIterator +{ + private: + vec3 beam[2]; + + vec2 selectionQuad[4]; + + protected: + void OnGlyph(const UTF8Char c, const GlyphQuad &, const TextSelectionDetails &details) + { + GLfloat bottomY = details.baseY + details.descent, + topY = details.baseY + details.ascent; + + vec3 p0(details.startX, topY, 0.0f), + p1(details.endX, topY, 0.0f), + p2(details.endX, bottomY, 0.0f), + p3(details.startX, bottomY, 0.0f), + p; + + if (intersectLineTriangle(beam[0], beam[1] - beam[0], p0, p1, p2, p) + || intersectLineTriangle(beam[0], beam[1] - beam[0], p0, p2, p3, p)) + { + selectionQuad[0].x = p0.x; + selectionQuad[0].y = p0.y; + selectionQuad[1].x = p1.x; + selectionQuad[1].y = p1.y; + selectionQuad[2].x = p2.x; + selectionQuad[2].y = p2.y; + selectionQuad[3].x = p3.x; + selectionQuad[3].y = p3.y; + } + } + public: + void SetBeam(const vec3 &p0, const vec3 &p1) + { + beam[0] = p0; + beam[1] = p1; + } + const vec2 *GetQuad(void) + { + return selectionQuad; + } +}; + + +class TextRenderer: public GLTextLeftToRightIterator +{ + private: + GLuint vboID, + shaderProgram; + mat4 projection; + protected: + void OnGlyph(const UTF8Char c, const GlyphQuad &quad, const TextSelectionDetails &details) + { + glBindBuffer(GL_ARRAY_BUFFER, vboID); + CHECK_GL(); + + glEnableVertexAttribArray(VERTEX_POSITION_INDEX); + CHECK_GL(); + glEnableVertexAttribArray(VERTEX_TEXCOORDS_INDEX); + CHECK_GL(); + glVertexAttribPointer(VERTEX_POSITION_INDEX, 2, GL_FLOAT, GL_FALSE, sizeof(GlyphVertex), 0); + CHECK_GL(); + glVertexAttribPointer(VERTEX_TEXCOORDS_INDEX, 2, GL_FLOAT, GL_FALSE, sizeof(GlyphVertex), (GLvoid *)(2 * sizeof(GLfloat))); + CHECK_GL(); + + // Fill the buffer. + + GlyphVertex *pVertexBuffer = (GlyphVertex *)glMapBuffer(GL_ARRAY_BUFFER, GL_WRITE_ONLY); + CHECK_GL(); + + pVertexBuffer[0] = quad.vertices[0]; + pVertexBuffer[1] = quad.vertices[1]; + pVertexBuffer[2] = quad.vertices[3]; + pVertexBuffer[3] = quad.vertices[2]; + + glUnmapBuffer(GL_ARRAY_BUFFER); + CHECK_GL(); + + // Draw the buffer. + + glUseProgram(shaderProgram); + CHECK_GL(); + + GLint location = glGetUniformLocation(shaderProgram, "projectionMatrix"); + if (location < 0) + throw RenderError("projection matrix location not found"); + + glUniformMatrix4fv(location, 1, GL_FALSE, value_ptr(projection)); + CHECK_GL(); + + glActiveTexture(GL_TEXTURE0); + CHECK_GL(); + + glBindTexture(GL_TEXTURE_2D, quad.texture); + CHECK_GL(); + + glDrawArrays(GL_TRIANGLE_STRIP, 0, 4); + CHECK_GL(); + + glDisableVertexAttribArray(VERTEX_POSITION_INDEX); + CHECK_GL(); + glDisableVertexAttribArray(VERTEX_TEXCOORDS_INDEX); + CHECK_GL(); + + glBindBuffer(GL_ARRAY_BUFFER, NULL); + CHECK_GL(); + } + public: + void SetProjection(const mat4 &prj) + { + projection = prj; + } + + void InitGL(void) + { + glGenBuffers(1, &vboID); + CHECK_GL(); + + if (vboID == NULL) + throw InitError("No vertex buffer was generated"); + + glBindBuffer(GL_ARRAY_BUFFER, vboID); + CHECK_GL(); + + glBufferData(GL_ARRAY_BUFFER, 4 * sizeof(GlyphVertex), NULL, GL_DYNAMIC_DRAW); + CHECK_GL(); + + glBindBuffer(GL_ARRAY_BUFFER, NULL); + CHECK_GL(); + + GLuint vertexShader = CreateShader(glyphVertexShaderSrc, GL_VERTEX_SHADER), + fragmentShader = CreateShader(glyphFragmentShaderSrc, GL_FRAGMENT_SHADER); + + std::map vertexAttribLocations; + vertexAttribLocations[VERTEX_POSITION_INDEX] = "position"; + vertexAttribLocations[VERTEX_TEXCOORDS_INDEX] = "texCoords"; + + shaderProgram = LinkShaderProgram(vertexShader, fragmentShader, vertexAttribLocations); + + glDeleteShader(vertexShader); + CHECK_GL(); + + glDeleteShader(fragmentShader); + CHECK_GL(); + } + void FreeGL(void) + { + glDeleteProgram(shaderProgram); + CHECK_GL(); + + glDeleteBuffers(1, &vboID); + CHECK_GL(); + } +}; + +typedef vec2 SelectionVertex; + +class SelectionRenderer +{ + private: + GLuint vboID, + shaderProgram; + mat4 projection; + public: + void InitGL(void) + { + glGenBuffers(1, &vboID); + CHECK_GL(); + + if (vboID == NULL) + throw InitError("No vertex buffer was generated"); + + glBindBuffer(GL_ARRAY_BUFFER, vboID); + CHECK_GL(); + + glBufferData(GL_ARRAY_BUFFER, 4 * sizeof(SelectionVertex), NULL, GL_DYNAMIC_DRAW); + CHECK_GL(); + + glBindBuffer(GL_ARRAY_BUFFER, NULL); + CHECK_GL(); + + GLuint vertexShader = CreateShader(selectionVertexShaderSrc, GL_VERTEX_SHADER), + fragmentShader = CreateShader(selectionFragmentShaderSrc, GL_FRAGMENT_SHADER); + + std::map vertexAttribLocations; + vertexAttribLocations[VERTEX_POSITION_INDEX] = "position"; + + shaderProgram = LinkShaderProgram(vertexShader, fragmentShader, vertexAttribLocations); + + glDeleteShader(vertexShader); + CHECK_GL(); + + glDeleteShader(fragmentShader); + CHECK_GL(); + } + void FreeGL(void) + { + glDeleteBuffers(1, &vboID); + CHECK_GL(); + + glDeleteProgram(shaderProgram); + CHECK_GL(); + } + void Render(const SelectionVertex *quad, const mat4 &projection) + { + glBindBuffer(GL_ARRAY_BUFFER, vboID); + CHECK_GL(); + + glEnableVertexAttribArray(VERTEX_POSITION_INDEX); + CHECK_GL(); + glVertexAttribPointer(VERTEX_POSITION_INDEX, 2, GL_FLOAT, GL_FALSE, sizeof(SelectionVertex), 0); + CHECK_GL(); + + // Fill the buffer. + + SelectionVertex *pVertexBuffer = (SelectionVertex *)glMapBuffer(GL_ARRAY_BUFFER, GL_WRITE_ONLY); + CHECK_GL(); + + pVertexBuffer[0] = quad[0]; + pVertexBuffer[1] = quad[1]; + pVertexBuffer[2] = quad[2]; + pVertexBuffer[3] = quad[3]; + + glUnmapBuffer(GL_ARRAY_BUFFER); + CHECK_GL(); + + // Draw the buffer. + + glUseProgram(shaderProgram); + CHECK_GL(); + + GLint location = glGetUniformLocation(shaderProgram, "projectionMatrix"); + if (location < 0) + throw RenderError("projection matrix location not found"); + + glUniformMatrix4fv(location, 1, GL_FALSE, value_ptr(projection)); + CHECK_GL(); + + glDrawArrays(GL_LINE_LOOP, 0, 4); + CHECK_GL(); + + glDisableVertexAttribArray(VERTEX_POSITION_INDEX); + CHECK_GL(); + + glBindBuffer(GL_ARRAY_BUFFER, NULL); + CHECK_GL(); + } +}; + +const int8_t displayText[] = R"text(Once upon a time, there was a big man. He had very big hands and legs. He had giant eyes. However, the biggest was his chest. But his head was even bigger. + +Upon a day, the big man went to the butcher. He asked: do you have eggplants? The butcher answered: "sorry, all out". And then the man became so unhappy that he cried himself to death... +And then he came back as a ghost, but he couldn't fly. So the ghost fell into the water and drowned. The end?! +)text"; + +class DemoApp +{ +private: + SDL_Window *mainWindow; + SDL_GLContext mainGLContext; + + bool running; + + GLfloat angle; + GLTextureFont *pFont; + TextRenderer mTextRenderer; + SelectionRenderer mSelectionRenderer; + TextBeamTracer mBeamTracer; + TextParams textParams; +public: + void Init(const std::shared_ptr pImageFont, const TextParams ¶ms) + { + textParams = params; + + int error = SDL_Init(SDL_INIT_EVERYTHING); + if (error != 0) + throw InitError(boost::format("Unable to initialize SDL: %1%") % SDL_GetError()); + + SDL_GL_SetAttribute(SDL_GL_RED_SIZE, 8); + SDL_GL_SetAttribute(SDL_GL_GREEN_SIZE, 8); + SDL_GL_SetAttribute(SDL_GL_BLUE_SIZE, 8); + SDL_GL_SetAttribute(SDL_GL_ALPHA_SIZE, 8); + SDL_GL_SetAttribute(SDL_GL_STENCIL_SIZE, 8); + SDL_GL_SetAttribute(SDL_GL_DEPTH_SIZE, 24); + SDL_GL_SetAttribute(SDL_GL_DOUBLEBUFFER, 1); + SDL_GL_SetAttribute(SDL_GL_MULTISAMPLEBUFFERS, 1); + SDL_GL_SetAttribute(SDL_GL_MULTISAMPLESAMPLES, 4); + + Uint32 flags = SDL_WINDOW_SHOWN | SDL_WINDOW_OPENGL; + mainWindow = SDL_CreateWindow("Text Test", + SDL_WINDOWPOS_UNDEFINED, SDL_WINDOWPOS_UNDEFINED, + 800, 600, flags); + if (!mainWindow) + throw InitError(boost::format("SDL_CreateWindow failed: %1%") % SDL_GetError()); + + mainGLContext = SDL_GL_CreateContext(mainWindow); + if (!mainGLContext) + throw InitError(boost::format("Failed to create a GL context: %1%") % SDL_GetError()); + + GLenum err = glewInit(); + if (GLEW_OK != err) + throw InitError(boost::format("glewInit failed: %1%") % glewGetErrorString(err)); + + if (!GLEW_VERSION_3_2) + throw InitError("OpenGL 3.2 is not supported"); + + + pFont = MakeGLTextureFont(pImageFont.get()); + + mTextRenderer.InitGL(); + mSelectionRenderer.InitGL(); + } + + void Destroy(void) + { + mTextRenderer.FreeGL(); + mSelectionRenderer.FreeGL(); + + DestroyGLTextureFont(pFont); + + SDL_GL_DeleteContext(mainGLContext); + SDL_DestroyWindow(mainWindow); + SDL_Quit(); + } + + void GetTextProjection(mat4 &matProject) + { + int screenWidth, screenHeight; + SDL_GL_GetDrawableSize(mainWindow, &screenWidth, &screenHeight); + + matProject = perspective(pi() / 4, GLfloat(screenWidth) / GLfloat(screenHeight), 0.1f, 2000.0f); + matProject = translate(matProject, vec3(0.0f, 0.0, -1000.0f)); + matProject = rotate(matProject, angle, vec3(0.0f, 1.0f, 0.0f)); + } + + void OnMouseClick(const SDL_MouseButtonEvent &event) + { + GLfloat textX, textY; + if (event.button == SDL_BUTTON_LEFT) + { + int screenWidth, screenHeight; + SDL_GL_GetDrawableSize(mainWindow, &screenWidth, &screenHeight); + + vec4 viewPort; + glGetFloatv(GL_VIEWPORT, value_ptr(viewPort)); + + mat4 matProject; + GetTextProjection(matProject); + + vec3 mouseWindowOrigin(event.x, screenHeight - event.y, 0.0f), + dir(0.0f, 0.0f, 10000.0f); + + vec3 mouseModelPosition0 = unProject(mouseWindowOrigin, mat4(), matProject, viewPort), + mouseModelPosition1 = unProject(mouseWindowOrigin + dir, mat4(), matProject, viewPort); + + mBeamTracer.SetBeam(mouseModelPosition0, mouseModelPosition1); + mBeamTracer.IterateText(pFont, displayText, textParams); + } + } + + void HandleEvent(const SDL_Event &event) + { + if (event.type == SDL_QUIT) + running = false; + else if (event.type == SDL_MOUSEBUTTONDOWN) + OnMouseClick(event.button); + } + + void Render(void) + { + int screenWidth, screenHeight; + SDL_GL_GetDrawableSize(mainWindow, &screenWidth, &screenHeight); + + glViewport(0, 0, screenWidth, screenHeight); + + mat4 matProject; + GetTextProjection(matProject); + + glClearColor(1.0f, 1.0f, 1.0f, 1.0f); + CHECK_GL(); + + glClear(GL_COLOR_BUFFER_BIT); + CHECK_GL(); + + glEnable(GL_BLEND); + CHECK_GL(); + + glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA); + CHECK_GL(); + + glDepthMask(GL_FALSE); + CHECK_GL(); + + // Render text. + mTextRenderer.SetProjection(matProject); + mTextRenderer.IterateText(pFont, displayText, textParams); + + // Render selection. + mSelectionRenderer.Render(mBeamTracer.GetQuad(), matProject); + } + + void RunDemo(const std::shared_ptr pImageFont, const TextParams ¶ms) + { + Init(pImageFont, params); + + boost::posix_time::ptime tStart = boost::posix_time::microsec_clock::local_time(), + tNow; + boost::posix_time::time_duration delta; + running = true; + while (running) + { + try + { + SDL_Event event; + while (SDL_PollEvent(&event)) { + HandleEvent(event); + } + + tNow = boost::posix_time::microsec_clock::local_time(); + delta = (tNow - tStart); + angle = 0.3f * sin(float(delta.total_milliseconds()) / 1000); + + Render(); + + SDL_GL_SwapWindow(mainWindow); + } + catch (...) + { + Destroy(); + std::rethrow_exception(std::current_exception()); + } + } + + Destroy(); + } +}; + + +int main(int argc, char **argv) +{ + FontStyle style; + style.size = 32.0; + style.strokeWidth = 2.0; + style.fillColor = {1.0, 1.0, 1.0, 1.0}; + style.strokeColor = {0.0, 0.0, 0.0, 1.0}; + style.lineJoin = LINEJOIN_MITER; + style.lineCap = LINECAP_SQUARE; + + TextParams params; + params.startX = 0.0f; + params.startY = 250.0f; + params.maxWidth = 800.0f; + params.lineSpacing = 40.0f; + params.align = TEXTALIGN_CENTER; + + FontData fontData; + std::shared_ptr pImageFont = NULL; + + DemoApp app; + + std::ifstream is; + + if (argc < 2) + { + std::cerr << boost::format("Usage: %1% font_path") % argv[0] << std::endl; + return 1; + } + + try + { + is.open(argv[1]); + if (!is.good()) + { + std::cerr << "Error opening " << argv[1] << std::endl; + return 1; + } + + ParseSVGFontData(is, fontData); + + is.close(); + + pImageFont = std::shared_ptr(MakeImageFont(fontData, style), DestroyImageFont); + + // Put the text in the middle. + params.startY = 0.0f + (params.lineSpacing * CountLines(pImageFont.get(), displayText, params)) / 2; + + app.RunDemo(pImageFont, params); + } + catch (const std::exception &e) + { + std::cerr << e.what() << std::endl; + return 1; + } + + return 0; +}