diff --git a/CMakeLists.txt b/CMakeLists.txt --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -83,6 +83,16 @@ PURPOSE "Needed to build font configuration and installation tools" ) +if(FONTCONFIG_FOUND) + find_package(PkgConfig REQUIRED) + pkg_check_modules(HARFBUZZ REQUIRED harfbuzz) + set_package_properties(harfbuzz PROPERTIES DESCRIPTION "Font shaping library" + URL "https://www.freedesktop.org/wiki/Software/HarfBuzz/" + TYPE RECOMMENDED + PURPOSE "Needed to build font configuration tools" + ) +endif() + find_package(X11) set_package_properties(X11 PROPERTIES DESCRIPTION "X11 libraries" URL "http://www.x.org" diff --git a/kcms/fonts/CMakeLists.txt b/kcms/fonts/CMakeLists.txt --- a/kcms/fonts/CMakeLists.txt +++ b/kcms/fonts/CMakeLists.txt @@ -3,17 +3,30 @@ if(FONTCONFIG_FOUND) include_directories(${FONTCONFIG_INCLUDE_DIR}) + include_directories(${HARFBUZZ_INCLUDE_DIRS}) endif() include_directories(${FREETYPE_INCLUDE_DIRS}) + ########### next target ############### include_directories(../kfontinst/lib) -set(kcm_fonts_PART_SRCS ../krdb/krdb.cpp previewrenderengine.cpp previewimageprovider.cpp fonts.cpp ../kfontinst/lib/FcEngine.cpp) +set(kcm_fonts_PART_SRCS + fonts.cpp + ../krdb/krdb.cpp + ../kfontinst/lib/FcEngine.cpp + previewrenderengine.cpp + previewimageprovider.cpp + renderpreviewimageprovider.cpp + menupreview.cpp +) if(X11_FOUND) set(kcm_fonts_PART_SRCS ${kcm_fonts_PART_SRCS} ${libkxftconfig_SRCS}) + if(FONTCONFIG_FOUND) + set(kcm_fonts_PART_SRCS ${kcm_fonts_PART_SRCS} freetype-renderer.cpp) + endif() endif() set(klauncher_xml ${KINIT_DBUS_INTERFACES_DIR}/kf5_org.kde.KLauncher.xml) @@ -32,7 +45,10 @@ KF5::Declarative KF5::QuickAddons) if(FONTCONFIG_FOUND) - target_link_libraries(kcm_fonts ${FONTCONFIG_LIBRARIES}) + target_link_libraries(kcm_fonts + ${FONTCONFIG_LIBRARIES} + ${HARFBUZZ_LIBRARIES} + ) endif() target_link_libraries(kcm_fonts ${X11_LIBRARIES} ${X11_Xft_LIB}) diff --git a/kcms/fonts/fonts.cpp b/kcms/fonts/fonts.cpp --- a/kcms/fonts/fonts.cpp +++ b/kcms/fonts/fonts.cpp @@ -48,6 +48,7 @@ #include "../krdb/krdb.h" #include "previewimageprovider.h" +#include "renderpreviewimageprovider.h" /**** DLL Interface ****/ K_PLUGIN_FACTORY_WITH_JSON(KFontsFactory, "kcm_fonts.json", registerPlugin();) @@ -436,6 +437,7 @@ : KQuickAddons::ConfigModule(parent, args) , m_fontAASettings(new FontAASettings(this)) { + engine()->addImageProvider("menupreview", new MenuPreviewImageProvider); qApp->setAttribute(Qt::AA_DontCreateNativeWidgetSiblings); KAboutData* about = new KAboutData("kcm_fonts", i18n("Configure Fonts"), "0.1", QString(), KAboutLicense::LGPL); diff --git a/kcms/fonts/freetype-renderer.h b/kcms/fonts/freetype-renderer.h new file mode 100644 --- /dev/null +++ b/kcms/fonts/freetype-renderer.h @@ -0,0 +1,505 @@ +/* + * Copyright 2018 Max Harmathy + * + * This program is free software; you can redistribute it and/or modify it under + * the terms of the GNU General Public License as published by the Free Software + * Foundation; either version 2 of the License, or (at your option) any later + * version. + * + * This program is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS + * FOR A PARTICULAR PURPOSE. See the GNU General Public License for more + * details. + * + * You should have received a copy of the GNU General Public License along with + * this program; if not, see . + */ + +#ifndef FREETYPE_RENDERER_H +#define FREETYPE_RENDERER_H + +#include "kxftconfig.h" + +extern "C" { +#include +#include FT_FREETYPE_H +#include +#include +} + +#include +#include +#include +#include +#include + +/** + * @brief The FontManagement class is a wrapper around the Fontconfig library. + * + * Fontconfig is commonly used to manage font files on the system and has a complex configuration + * system. In particular it handles font substitution. It therefore can provide a font file for a + * given font specification, which is used in the retrievePath method. + */ +class FontManagement +{ +public: + /** + * FontManagement constructor initializes the Fontconfig library. + */ + FontManagement(); + + ~FontManagement(); + + FontManagement& operator=(const FontManagement&) = delete; + FontManagement(const FontManagement&) = delete; + + /** + * @brief retrievePath fetches the path of a specified font. + * + * Here the Fontconfig library is used to substitute the font according to the system settings + * and to locate the font file. The font file path can be used to load the font in FreeType. The + * path is copied from the Fontconfig data structures which are destroyed after this method + * call. + * + * @param font name to specify the font + * @return path to the font file + */ + const char* retrievePath(const char* font); + + /** + * @brief copyFcString helper method to retrieve the font path from Fontconfig data structures + * @param fcString pointer to the path in the data structure + * @return string copy of the path + */ + static inline const char* copyFcString(const FcChar8* fcString); + +private: + FcConfig* fontConfig; +}; + +/** + * Hold parameters for FreeType, which influence the rendering result. + */ +class FreeTypeParameters +{ +public: + /** + * Convert rendering options from the commonly used Fontconfig style into parameters for + * FreeType. FreeTypeParameters does not take ownership of the KXftConfig object. + */ + FreeTypeParameters(KXftConfig* options); + + int loadFlags; + FT_Render_Mode renderMode; + // FT_Library_SetLcdFilter +}; + +/** + * @brief The FreeTypeLibrary class + */ +class FreeTypeLibrary +{ +private: + FT_Library freetypeLib; + +public: + FreeTypeLibrary(); + virtual ~FreeTypeLibrary(); + + FT_Face getFontFace(const char* path); + + /** + * This inline function converts point size from float to internal integer representation used + * by FreeType. Keep in mind that point size isn't a discrete measure and therefore a float. + * @param pointSize size in typographic units + * @return internal integer representation used by FreeType + */ + static inline long convertPointSize(double pointSize); +}; + +/** + * @brief The RasteredGlyph class is the base class for rasters glyph data from FreeType. + * + * This class is an abstract class to provide a common interface to the different format of glyph + * data provided by FreeType. Depending on the parameters for font rendering FreeType uses different + * approached for storing the data, e.g. if anti-aliasing is turned off, glyphs are rendered in + * monochrome (handles by subclass MonochromeGlyph) using only one bit per pixel. + */ +class RasteredGlyph +{ + +protected: + /** + * @brief pitch is the length of one row of glyph data in bytes and signedness encoding + * direction. + * + * The pitch is copied from the FreeType glyph data structure. It can be positive or negative + * depending on the direction, in which the glyph is renders. See also FT_Bitmap in the FreeType + * documentation. An always positive length value is stored in rowLength. + */ + const int pitch; + + /** + * @brief rowLength is the length of one row of glyph data in bytes. + * + * In contrast to pitch this value is always positive and can be used to compute the required + * space of the glyph. + */ + const unsigned int rowLength; + + /** + * @brief width of the glyph in pixels. + */ + const unsigned int width; + + /** + * @brief height of the glyph in pixels. + */ + const unsigned int height; + +public: + /** + * @brief RasteredGlyph constructor + * + * The bitmap data structure has also a width and height (rows) field, however for sub-pixel + * rendered bitmaps the respective value will be in sub-pixels, i.e. three times larger. This + * should be handled by the constructor of the specialized sub-class. + * @param bitmap the rendering result from FreeType + * @param width the actual width in pixels + * @param height the actual height in pixels + */ + RasteredGlyph(FT_Bitmap* bitmap, uint width, uint height); + + virtual ~RasteredGlyph() = default; + + /** + * @brief paint method will paint the glyph to the canvas given at position (x,y). + * + * The rendering slightly differs depending on the given font rendering options. Therefore this + * is an abstract method, which has to be implemented by inheriting classes. For anti-aliased + * and sub-pixel based rendering the glyph has to be painted using alpha blending, since the + * FreeType data yields the coverage of a (sub-)pixel. + * @param canvas where the glyph is to be painted on. + * @param x leftmost coordinate where to start painting the glyph. + * @param y upmost coordinate where to start painting the glyph. + * @param pen color, in which the glyph will be painted. + */ + virtual void paint(QImage* canvas, int x, int y, const QColor& pen) = 0; + + /** + * @return the pixel height. + */ + unsigned int getHeight() const; + + /** + * @return the pixel width. + */ + unsigned int getWidth() const; +}; + +/** + * @brief The MonochromeGlyph class handles glyph data rendered without anti-aliasing. + * + * For every pixel there are only two possible states: either the glyph covers it, or not. Therefore + * the data is stored in a bitmap, where one bit denotes one pixel. + */ +class MonochromeGlyph : public RasteredGlyph +{ +protected: + /** + * @brief bitmap holds the pixel data of a rendered glyph, where every bit denotes a pixel. + */ + unsigned char* bitmap; + +public: + /** + * @brief MonochromeGlyph constructor. + * + * The width and height are passed from the FT_Bitmap input to the base constructor (@ref + * RasteredGlyph::RasteredGlyph). Side note: the width field in FT_Bitmap for monochrome glyph + * data gives the width in pixels (in contrast sub-pixel rendered glyph data). + * @param bitmap the rendering result from FreeType + */ + MonochromeGlyph(FT_Bitmap* bitmap); + + /** + * @brief ~MonochromeGlyph frees private bitmap data copied from FreeType bitmap. + */ + virtual ~MonochromeGlyph(); + + /** + * @copydoc RasteredGlyph::paint + * + * This will paint the monochrome glyph to the canvas. Since there is no anti-aliasing involved + * no alpha blending is needed. + */ + virtual void paint(QImage* canvas, int x, int y, const QColor& pen) override; + + /** + * @brief pixelAt is a helper function to access the bit data of a single pixel. + * + * This is necessary since not only one byte holds the data of eight pixels but also they are + * stored in most-significant bit order. + * @param x horizontal pixel position in the glyph bitmap + * @param y vertical pixel position in the glyph bitmap + * @param pitch see @ref RasteredGlyph::pitch + * @param buffer see @ref bitmap + * @return 1 if the pixel is covered by the glyph and 0 otherwise + */ + inline int pixelAt(uint x, uint y, int pitch, const unsigned char* buffer); +}; + +/** + * @brief The ByteDataGlyph class is an abstract class for glyph data, where pixel data is stored in + * bytes. + * + * This is the base class for anti-aliased and sub-pixel rendered glyph data. + */ +class ByteDataGlyph : public RasteredGlyph +{ +protected: + /** + * @brief bytemap stores the glyph data copied from the FreeType bitmap. + */ + QByteArray* bytemap; + +public: + /** + * @brief ByteDataGlyph constructor. + * @param bitmap see @ref RasteredGlyph::RasteredGlyph + * @param bytesPerPixel number of bytes used to store one pixel. For gray scale glyphs it would + * be set to 1, for sub-pixel rendered glyphs it would be 3 (one for each sub-pixel). + * @param width see @ref RasteredGlyph::RasteredGlyph + * @param height see @ref RasteredGlyph::RasteredGlyph + */ + ByteDataGlyph(FT_Bitmap* bitmap, uint bytesPerPixel, uint width, uint height); + + /** + * @brief ~ByteDataGlyph frees private byte array with data copied from FreeType bitmap. + */ + virtual ~ByteDataGlyph(); + + /** + * @copydoc RasteredGlyph::paint + */ + virtual void paint(QImage* canvas, int x, int y, const QColor& pen) override = 0; +}; + +/** + * @brief The GrayScaleGlyph class represents anti-aliased glyphs, rendered on pixel level. + */ +class GrayScaleGlyph : public ByteDataGlyph +{ +public: + /** + * @brief GrayScaleGlyph constructor. Parameters for base classes are initialized. + * @param bitmap see @ref RasteredGlyph::RasteredGlyph + */ + GrayScaleGlyph(FT_Bitmap* bitmap); + + /** + * @copydoc RasteredGlyph::paint + * + * Pen color will be alpha blended onto the background. + */ + virtual void paint(QImage* canvas, int x, int y, const QColor& pen) override; +}; + +/** + * @brief The AbstractSubPixelGlyph class is an abstract class for sub-pixel rendered glyphs. + * + * FreeType can render glyphs for utilizing sub-pixels of the displays panel to increase resolution. + * FreeType assumed a pixel geometry of three sub-pixels (stripes). The rendering result from + * FreeType is independent from the actual the sub-pixel color order. Usually LCD-Panels are + * oriented in a way, that sub-pixels have the order Red, Green, Blue from left to right. If a + * display is used in a turned position, the orientation and order of the sub-pixels change and + * glyphs have to be rendered and displayed differently. + * + * This class implements the common per sub-pixel alpha blending in the paint method. Since + * horizontal and vertical sub-pixel rendered glyphs are stored differently by FreeType the access + * is encapsulated in the sub-classes SubPixelGlyph and VerticalSubPixelGlyph, which implement the + * getValue method. + */ +class AbstractSubPixelGlyph : public ByteDataGlyph +{ +protected: + /** + * @brief This indicates if the sub-pixel order is turned around, i.e. BGR instead of RGB. + * + * Setting this to true will swap the offset for accessing red and blue sub-pixel information. + */ + bool reverse; + + /** + * @brief getValue retrieve the sub-pixel value of a pixel in the rendered glyph image. + * @param row in rendered glyph, y coordinate + * @param column in rendered glyph, x coordinate + * @param offset selector for sub-pixel is one out of 0, 1 or 2. + * Depending on the orientation 0 selects the left or top sub-pixel, 1 selects the middle + * sub-pixel and 2 selects the right or bottom sub-pixel. + * @return the area of the sub-pixel covered by the glyph in a range from 0 to 255, where 0 is + * not covered and 255 is fully covered. + */ + virtual unsigned char getValue(int row, int column, int offset) = 0; + +public: + AbstractSubPixelGlyph( + FT_Bitmap* bitmap, uint bytesPerPixel, uint width, uint height, bool reversed); + + /** + * @copydoc RasteredGlyph::paint + * + * Pen color will be alpha blended separately for every sub-pixel onto the background. + */ + virtual void paint(QImage* canvas, int x, int y, const QColor& pen) override; +}; + +/** + * @brief The SubPixelGlyph class represents sub-pixel rendered glyphs for horizontal sub-pixel + * orientation. + */ +class SubPixelGlyph : public AbstractSubPixelGlyph +{ +protected: + virtual inline unsigned char getValue(int row, int, int offset) override; + +public: + SubPixelGlyph(FT_Bitmap* bitmap, bool reversed); +}; + +/** + * @brief The VerticalSubPixelGlyph class represents sub-pixel rendered glyphs for vertical + * sub-pixel orientation. + */ +class VerticalSubPixelGlyph : public AbstractSubPixelGlyph +{ +protected: + virtual inline unsigned char getValue(int row, int column, int offset) override; + +public: + VerticalSubPixelGlyph(FT_Bitmap* bitmap, bool reversed); +}; + +/** + * @brief The GlyphData class holds metadata together with rendered glyph data. + * + * With the paint method it ca be used to directly put the rendered glyph on a QImage. + */ +class GlyphData +{ +private: + const int offsetX; + const int offsetY; + const int advanceX; + const int advanceY; + const int bearingLeft; + const int bearingTop; + + RasteredGlyph* pixelData; + +public: + /** + * @brief GlyphData constructor + * @param glyphPos is a Harfbuzz position data structure from a Harfbuzz font shaping run. + * @param glyphData is the data structure from FreeType containing the render results of a + * glyph. + * @param reversedSubpixel This is only relevant for sub-pixel rendered glyphs. Set this to true + * for bgr and vbgr sub-pixel order. See also @ref AbstractSubPixelGlyph::reverse. + */ + GlyphData(hb_glyph_position_t* glyphPos, + FT_GlyphSlotRec* glyphData, + bool reversedSubpixel = false); + int getOffsetX() const; + int getOffsetY() const; + int getBearingLeft() const; + int getBearingTop() const; + int getAdvanceX() const; + int getAdvanceY() const; + unsigned int getWidth() const; + unsigned int getHeight() const; + void paint(QImage* canvas, int x, int y, const QColor& pen); +}; + +/** + * @brief The FontShaping class contains all data provided by a font shaping run using Harfbuzz. + * + * In order to estimate the space needed by the text the actual rendering, i.e. the rasterization + * but not the painting to a surface, is conducted. + */ +class FontShaping +{ +private: + const char* path; + + unsigned int glyphCount; + GlyphData** glyphs; + unsigned int baseLineOffset; + QRect boundingBox; + +public: + /** + * @brief FontShaping constructor conducts the shaping and the rendering steps. + * @param freetypeLib a FreeTypeLibrary object + * @param fontManagement a FontManagement object + * @param text see @ref FreeTypeFontPreviewRenderer::renderText + * @param font see @ref FreeTypeFontPreviewRenderer::renderText + * @param pointSize see @ref FreeTypeFontPreviewRenderer::renderText + * @param options see @ref FreeTypeFontPreviewRenderer::renderText + */ + FontShaping(FreeTypeLibrary* freetypeLib, + FontManagement* fontManagement, + const char* text, + const char* font, + double pointSize, + KXftConfig* options); + + ~FontShaping(); + + unsigned int getGlyphCount() const; + GlyphData** getGlyphs() const; + unsigned int getBaseLineOffset() const; + QRect getBoundingBox() const; +}; + +/** + * The FreeTypeFontPreviewRenderer class provides the possibility to render Text with FreeType + * offside. + */ +class FreeTypeFontPreviewRenderer +{ +private: + FreeTypeLibrary* freeTypeLibrary; + FontManagement* fontManagement; + +public: + /** + * @brief FreeTypeFontPreviewRenderer + * + * The constructor initializes the library classes for Harfbuzz, FreeType and Fontconfig. + */ + explicit FreeTypeFontPreviewRenderer(); + virtual ~FreeTypeFontPreviewRenderer(); + + /** + * @brief Render text independent from render settings of the running session. + * + * The given text will be rendered offside using the options provided as parameters. This is + * intended to be presented to a user to give an impression, what rendering with the given + * parameters would look without the need to change the actual setting for the session. + * @param text string to render + * @param font in which the text should be rendered + * @param pointSize is the font size in typographic points + * @param options config object which contains anti-aliasing, hinting and sub-pixel settings + * @param background color + * @param pen writing color + * @return rendered text as QImage + */ + QImage renderText(const char* text, + const char* font, + double pointSize, + KXftConfig* options, + QColor background, + QColor pen); +}; + +#endif // FREETYPE_RENDERER_H diff --git a/kcms/fonts/freetype-renderer.cpp b/kcms/fonts/freetype-renderer.cpp new file mode 100644 --- /dev/null +++ b/kcms/fonts/freetype-renderer.cpp @@ -0,0 +1,631 @@ +/* + * Copyright 2018 Max Harmathy + * + * This program is free software; you can redistribute it and/or modify it under + * the terms of the GNU General Public License as published by the Free Software + * Foundation; either version 2 of the License, or (at your option) any later + * version. + * + * This program is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS + * FOR A PARTICULAR PURPOSE. See the GNU General Public License for more + * details. + * + * You should have received a copy of the GNU General Public License along with + * this program; if not, see . + */ + +/** @file + * Render font setting preview with FreeType & Co. + */ + +#include "freetype-renderer.h" + +extern "C" { +#include +} + +#include +#include +#include + +/** FreeType divides a pixel into 64 parts */ +#define PIXEL_FRACTION_FACTOR 64 + +/** FreeType uses typographic points defined as 1/72 inch */ +#define TYPOGRAHIC_POINTS_PER_INCH 72.0 + +#define FPX_FLOOR(x) ((x) & -64) +#define FPX_CEIL(x) (((x) + 63) & -64) +#define FPX_ROUND(x) (((x) + 32) & -64) +#define FPX_CONVERT(x) ((x) >> 6) + +/******************/ +/* FontManagement */ +/******************/ + +FontManagement::FontManagement() : fontConfig{ nullptr } +{ + FcBool fontconfigInit = FcInit(); + if (fontconfigInit == FcTrue) { + fontConfig = FcInitLoadConfigAndFonts(); + } +} + +FontManagement::~FontManagement() +{ + if (fontConfig) + free(fontConfig); +} + +const char* FontManagement::retrievePath(const char* font) +{ + auto pattern = FcNameParse(reinterpret_cast(font)); + FcConfigSubstitute(nullptr, pattern, FcMatchPattern); + FcDefaultSubstitute(pattern); + + // actual font substitution + FcResult fcResult; + auto match = FcFontMatch(fontConfig, pattern, &fcResult); + if (fcResult != FcResultMatch) { + FcPatternDestroy(match); + return ""; + } + + // grab font file path from result + FcChar8* fontFacePath; + if (FcPatternGetString(match, FC_FILE, 0, &fontFacePath) != FcResultMatch) { + FcPatternDestroy(pattern); + FcPatternDestroy(match); + return ""; + } + + // pull out path + auto path = copyFcString(fontFacePath); + + // and clean up + FcPatternDestroy(pattern); + FcPatternDestroy(match); + + return path; +} + +const char* FontManagement::copyFcString(const FcChar8* fcString) +{ + const char* tmp = reinterpret_cast(fcString); + char* result = new char[strlen(tmp) + 1]; + strcpy(result, tmp); + return result; +} + +/**********************/ +/* FreeTypeParameters */ +/**********************/ + +FreeTypeParameters::FreeTypeParameters(KXftConfig* options) + : loadFlags(FT_LOAD_DEFAULT), renderMode(FT_RENDER_MODE_NORMAL) +{ + KXftConfig::AntiAliasing::State antialiasing = options->getAntiAliasing(); + KXftConfig::Hint::Style hintStyle = KXftConfig::Hint::None; + options->getHintStyle(hintStyle); + KXftConfig::SubPixel::Type subpixelType = KXftConfig::SubPixel::None; + options->getSubPixelType(subpixelType); + + if (antialiasing == KXftConfig::AntiAliasing::Disabled) { + renderMode = FT_RENDER_MODE_MONO; + loadFlags |= FT_LOAD_MONOCHROME; + if (hintStyle == KXftConfig::Hint::None) { + loadFlags |= FT_LOAD_NO_HINTING; + } else { + loadFlags |= FT_LOAD_TARGET_MONO; + } + } else { + // bitmap fonts are disabled when anti-aliasing is used + loadFlags |= FT_LOAD_NO_BITMAP; + switch (hintStyle) { + case KXftConfig::Hint::NotSet: + case KXftConfig::Hint::None: + loadFlags |= FT_LOAD_NO_HINTING; + break; + case KXftConfig::Hint::Slight: + renderMode = FT_RENDER_MODE_LIGHT; + loadFlags |= FT_LOAD_TARGET_LIGHT; + break; + case KXftConfig::Hint::Medium: + loadFlags |= FT_LOAD_TARGET_LIGHT; + break; + case KXftConfig::Hint::Full: + // apply hinting appropriate for (sub-)pixel configuration + switch (subpixelType) { + case KXftConfig::SubPixel::NotSet: + case KXftConfig::SubPixel::None: + loadFlags |= FT_LOAD_TARGET_NORMAL; + break; + case KXftConfig::SubPixel::Rgb: + case KXftConfig::SubPixel::Bgr: + loadFlags |= FT_LOAD_TARGET_LCD; + break; + case KXftConfig::SubPixel::Vrgb: + case KXftConfig::SubPixel::Vbgr: + loadFlags |= FT_LOAD_TARGET_LCD_V; + break; + } + } + switch (subpixelType) { + case KXftConfig::SubPixel::NotSet: + case KXftConfig::SubPixel::None: + break; + case KXftConfig::SubPixel::Rgb: + case KXftConfig::SubPixel::Bgr: + renderMode = FT_RENDER_MODE_LCD; + break; + case KXftConfig::SubPixel::Vrgb: + case KXftConfig::SubPixel::Vbgr: + renderMode = FT_RENDER_MODE_LCD_V; + break; + } + } +} + +/*******************/ +/* FreeTypeLibrary */ +/*******************/ + +FreeTypeLibrary::FreeTypeLibrary() : freetypeLib{ nullptr } +{ + FT_Error freetypeInit = FT_Init_FreeType(&freetypeLib); + if (freetypeInit != 0) { + // FIXME + } +} + +FreeTypeLibrary::~FreeTypeLibrary() +{ + FT_Done_FreeType(freetypeLib); + freetypeLib = nullptr; +} + +FT_Face FreeTypeLibrary::getFontFace(const char* path) +{ + FT_Face fontFace; + auto error = FT_New_Face(freetypeLib, path, 0, &fontFace); + if (error) { + // FIXME + } + return fontFace; +} + +long FreeTypeLibrary::convertPointSize(double point_size) +{ + return static_cast(point_size * PIXEL_FRACTION_FACTOR); +} + +/***************/ +/* RasterGlyph */ +/***************/ + +RasteredGlyph::RasteredGlyph(FT_Bitmap* bitmap, uint width, uint height) + : pitch(bitmap->pitch) + , rowLength(static_cast(abs(pitch))) + , width(width) + , height(height) +{ +} + +unsigned int RasteredGlyph::getHeight() const +{ + return height; +} + +unsigned int RasteredGlyph::getWidth() const +{ + return width; +} + +/*******************/ +/* MonochromeGlyph */ +/*******************/ + +MonochromeGlyph::MonochromeGlyph(FT_Bitmap* bitmap) + : RasteredGlyph(bitmap, bitmap->width, bitmap->rows) +{ + auto size = height * rowLength; + this->bitmap = new unsigned char[size](); + memcpy(this->bitmap, bitmap->buffer, size); +} + +MonochromeGlyph::~MonochromeGlyph() +{ + delete[] bitmap; +} + +inline int MonochromeGlyph::pixelAt(uint x, uint y, int pitch, const unsigned char* buffer) +{ + uint index = y * static_cast(abs(pitch)) + x / 8; + uint position = 7 - x % 8; + unsigned char byte = buffer[index]; + return byte >> position & 0x1; +} + +void MonochromeGlyph::paint(QImage* canvas, int x, int y, const QColor& pen) +{ + for (uint glyphY = 0; glyphY < height; glyphY++) { + for (uint glyphX = 0; glyphX < width; glyphX++) { + if (pixelAt(glyphX, glyphY, pitch, bitmap)) { + int cursorX = static_cast(glyphX) + x; + int cursorY = static_cast(glyphY) + y; + canvas->setPixel(cursorX, cursorY, pen.rgb()); + } + } + } +} + +/*****************/ +/* ByteDataGlyph */ +/*****************/ + +ByteDataGlyph::ByteDataGlyph(FT_Bitmap* bitmap, uint bytesPerPixel, uint width, uint height) + : RasteredGlyph(bitmap, width, height) +{ + const char* buffer = reinterpret_cast(bitmap->buffer); + auto size = static_cast(height * rowLength * bytesPerPixel); + bytemap = new QByteArray(buffer, size); +} + +ByteDataGlyph::~ByteDataGlyph() +{ + delete bytemap; +} + +/******************/ +/* GrayScaleGlyph */ +/******************/ + +GrayScaleGlyph::GrayScaleGlyph(FT_Bitmap* bitmap) + : ByteDataGlyph(bitmap, 1, bitmap->width, bitmap->rows) +{ +} + +void GrayScaleGlyph::paint(QImage* canvas, int x, int y, const QColor& pen) +{ + int pen_r, pen_g, pen_b; + pen.getRgb(&pen_r, &pen_g, &pen_b); + for (int j = 0; static_cast(j) < height; ++j) { + for (int i = 0; static_cast(i) < width; ++i) { + int cursor_x = i + x; + int cursor_y = j + y; + + auto value = static_cast(bytemap->at(j * pitch + i)); + + int backgound_r, backgound_g, backgound_b; + canvas->pixelColor(cursor_x, cursor_y).getRgb(&backgound_r, &backgound_g, &backgound_b); + + int result_r = ((255 - value) * backgound_r + value * pen_r) / 255; + int result_g = ((255 - value) * backgound_g + value * pen_g) / 255; + int result_b = ((255 - value) * backgound_b + value * pen_b) / 255; + canvas->setPixel(cursor_x, cursor_y, qRgb(result_r, result_g, result_b)); + } + } +} + +/*************************/ +/* AbstractSubPixelGlyph */ +/*************************/ + +AbstractSubPixelGlyph::AbstractSubPixelGlyph( + FT_Bitmap* bitmap, uint bytesPerPixel, uint width, uint height, bool reversed) + : ByteDataGlyph(bitmap, bytesPerPixel, width, height), reverse(reversed) +{ +} + +void AbstractSubPixelGlyph::paint(QImage* canvas, int x, int y, const QColor& pen) +{ + int pen_r, pen_g, pen_b; + pen.getRgb(&pen_r, &pen_g, &pen_b); + + int offset_r = reverse ? 2 : 0; + int offset_g = 1; + int offset_b = reverse ? 0 : 2; + + for (int j = 0; static_cast(j) < height; ++j) { + for (int i = 0; static_cast(i) < width; ++i) { + int cursor_x = i + x; + int cursor_y = j + y; + + int backgound_r, backgound_g, backgound_b; + canvas->pixelColor(cursor_x, cursor_y).getRgb(&backgound_r, &backgound_g, &backgound_b); + + unsigned char value_r = getValue(j, i, offset_r); + unsigned char value_g = getValue(j, i, offset_g); + unsigned char value_b = getValue(j, i, offset_b); + + int result_r = ((255 - value_r) * backgound_r + value_r * pen_r) / 255; + int result_g = ((255 - value_g) * backgound_g + value_g * pen_g) / 255; + int result_b = ((255 - value_b) * backgound_b + value_b * pen_b) / 255; + canvas->setPixel(cursor_x, cursor_y, qRgb(result_r, result_g, result_b)); + } + } +} + +/*****************/ +/* SubPixelGlyph */ +/*****************/ + +SubPixelGlyph::SubPixelGlyph(FT_Bitmap* bitmap, bool reversed) + : AbstractSubPixelGlyph(bitmap, 3, bitmap->width / 3, bitmap->rows, reversed) +{ +} + +inline unsigned char SubPixelGlyph::getValue(int row, int column, int subPixelOffset) +{ + return static_cast(bytemap->at(row * pitch + 3 * column + subPixelOffset)); +} + +/*************************/ +/* VerticalSubPixelGlyph */ +/*************************/ + +VerticalSubPixelGlyph::VerticalSubPixelGlyph(FT_Bitmap* bitmap, bool reversed) + : AbstractSubPixelGlyph(bitmap, 3, bitmap->width, bitmap->rows / 3, reversed) +{ +} + +inline unsigned char VerticalSubPixelGlyph::getValue(int row, int column, int subPixelOffset) +{ + return static_cast(bytemap->at((3 * row + subPixelOffset) * pitch + column)); +} + +/*************/ +/* GlyphData */ +/*************/ + +int GlyphData::getOffsetX() const +{ + return offsetX; +} + +int GlyphData::getOffsetY() const +{ + return offsetY; +} + +GlyphData::GlyphData(hb_glyph_position_t* glyphPos, + FT_GlyphSlotRec* glyphData, + bool reversedSubpixel) + : offsetX(glyphPos->x_offset) + , offsetY(glyphPos->y_offset) + , advanceX(glyphPos->x_advance) + , advanceY(glyphPos->y_advance) + , bearingLeft(glyphData->bitmap_left) + , bearingTop(glyphData->bitmap_top) + , pixelData{ nullptr } +{ + + switch (glyphData->bitmap.pixel_mode) { + case FT_PIXEL_MODE_MONO: + pixelData = new MonochromeGlyph(&glyphData->bitmap); + break; + case FT_PIXEL_MODE_GRAY: + pixelData = new GrayScaleGlyph(&glyphData->bitmap); + break; + case FT_PIXEL_MODE_LCD: + pixelData = new SubPixelGlyph(&glyphData->bitmap, reversedSubpixel); + break; + case FT_PIXEL_MODE_LCD_V: + pixelData = new VerticalSubPixelGlyph(&glyphData->bitmap, reversedSubpixel); + break; + case FT_PIXEL_MODE_BGRA: + // TODO color emoji support + // Hint: bitmap would be pre-multiplied sRGB image in BGRA order + // see FT_PIXEL_MODE_BGRA in FreeType docs + break; + } +} + +int GlyphData::getBearingLeft() const +{ + return bearingLeft; +} + +int GlyphData::getBearingTop() const +{ + return bearingTop; +} + +int GlyphData::getAdvanceX() const +{ + return advanceX; +} + +int GlyphData::getAdvanceY() const +{ + return advanceY; +} + +unsigned int GlyphData::getWidth() const +{ + if (pixelData == nullptr) + return 0; + return pixelData->getWidth(); +} + +unsigned int GlyphData::getHeight() const +{ + if (pixelData == nullptr) + return 0; + return pixelData->getHeight(); +} + +void GlyphData::paint(QImage* canvas, int x, int y, const QColor& pen) +{ + if (pixelData == nullptr) + return; + pixelData->paint(canvas, x, y, pen); +} + +/***************/ +/* FontShaping */ +/***************/ + +FontShaping::FontShaping(FreeTypeLibrary* freetypeLib, + FontManagement* fontManagement, + const char* text, + const char* font, + double pointSize, + KXftConfig* options) +{ + auto harfbuzzBuffer = hb_buffer_create(); + + // we don't want to compute the length of an Unicode string + // -1 delegates length recognition to harfbuzz + hb_buffer_add_utf8(harfbuzzBuffer, text, -1, 0, -1); + hb_buffer_guess_segment_properties(harfbuzzBuffer); + + path = fontManagement->retrievePath(font); + auto fontFace = freetypeLib->getFontFace(path); + + auto ftSize = FreeTypeLibrary::convertPointSize(pointSize); + FT_Set_Char_Size(fontFace, 0, ftSize, 96, 96); + // TODO DPI + + auto parameters = FreeTypeParameters(options); + auto loadFlags = parameters.loadFlags; + auto renderMode = parameters.renderMode; + + auto hbFont = hb_ft_font_create(fontFace, nullptr); + hb_shape(hbFont, harfbuzzBuffer, nullptr, 0); + + glyphCount = 0; + hb_glyph_info_t* glyphInfo = hb_buffer_get_glyph_infos(harfbuzzBuffer, &glyphCount); + hb_glyph_position_t* glyphPos = hb_buffer_get_glyph_positions(harfbuzzBuffer, &glyphCount); + + glyphs = new GlyphData*[glyphCount]; + + // assume we have horizontal writing + baseLineOffset = 0; + unsigned int bottomExtend = 0; + int width = 0; + + FT_Vector cursor; + cursor.x = 0; + cursor.y = 0; + + for (unsigned int i = 0; i < glyphCount; ++i) { + FT_Set_Transform(fontFace, nullptr, &cursor); + + unsigned int glyphIndex = glyphInfo[i].codepoint; + FT_Load_Glyph(fontFace, glyphIndex, loadFlags); + auto glyphData = fontFace->glyph; + FT_Render_Glyph(glyphData, renderMode); + + KXftConfig::SubPixel::Type subpixel = KXftConfig::SubPixel::NotSet; + options->getSubPixelType(subpixel); + + glyphs[i] = new GlyphData(&glyphPos[i], glyphData, + subpixel == KXftConfig::SubPixel::Bgr + || subpixel == KXftConfig::SubPixel::Vbgr); + + int bearingTop = glyphs[i]->getBearingTop(); + + if (bearingTop >= 0) { + if (static_cast(bearingTop) > baseLineOffset) { + baseLineOffset = static_cast(bearingTop); + } + } + + auto absBearing = static_cast(abs(bearingTop)); + auto glyphBottomExtend = glyphs[i]->getHeight() - absBearing; + if (glyphBottomExtend > bottomExtend) { + bottomExtend = glyphBottomExtend; + } + + width += glyphs[i]->getAdvanceX(); + cursor.x = width; + } + // add width of last glyph + if (glyphCount) { + cursor.x += glyphs[glyphCount - 1]->getBearingLeft(); + cursor.x += glyphs[glyphCount - 1]->getWidth(); + } + boundingBox = QRect(0, 0, FPX_CONVERT(FPX_CEIL(cursor.x)), baseLineOffset + bottomExtend); + + // tidy up + hb_font_destroy(hbFont); + hb_buffer_destroy(harfbuzzBuffer); + FT_Done_Face(fontFace); +} + +FontShaping::~FontShaping() +{ + for (unsigned int i = 0; i < glyphCount; ++i) { + delete glyphs[i]; + } + delete[] glyphs; +} + +unsigned int FontShaping::getGlyphCount() const +{ + return glyphCount; +} + +GlyphData** FontShaping::getGlyphs() const +{ + return glyphs; +} + +unsigned int FontShaping::getBaseLineOffset() const +{ + return baseLineOffset; +} + +QRect FontShaping::getBoundingBox() const +{ + return boundingBox; +} + +/*******************************/ +/* FreeTypeFontPreviewRenderer */ +/*******************************/ + +QImage FreeTypeFontPreviewRenderer ::renderText(const char* text, + const char* font, + double pointSize, + KXftConfig* options, + QColor background, + QColor pen) +{ + FontShaping fontShaping(freeTypeLibrary, fontManagement, text, font, pointSize, options); + + auto width = fontShaping.getBoundingBox().width(); + auto height = fontShaping.getBoundingBox().height(); + + QImage canvas(width, height, QImage::Format_RGB888); + canvas.fill(background); + + int cursorX = 1; // FIXME position + + for (unsigned int i = 0; i < fontShaping.getGlyphCount(); ++i) { + GlyphData* data = fontShaping.getGlyphs()[i]; + int offset = fontShaping.getBaseLineOffset() - data->getBearingTop() + data->getOffsetY(); + + auto originX = data->getBearingLeft(); + + data->paint(&canvas, originX, offset, pen); + + cursorX += data->getAdvanceX(); + } + + return canvas; +} + +FreeTypeFontPreviewRenderer::FreeTypeFontPreviewRenderer() +{ + freeTypeLibrary = new FreeTypeLibrary(); + fontManagement = new FontManagement(); +} + +FreeTypeFontPreviewRenderer::~FreeTypeFontPreviewRenderer() +{ + delete fontManagement; + delete freeTypeLibrary; +} diff --git a/kcms/fonts/menupreview.h b/kcms/fonts/menupreview.h new file mode 100644 --- /dev/null +++ b/kcms/fonts/menupreview.h @@ -0,0 +1,138 @@ +/* + * Copyright 2018 Max Harmathy + * + * This program is free software; you can redistribute it and/or modify it under + * the terms of the GNU General Public License as published by the Free Software + * Foundation; either version 3 of the License, or (at your option) any later + * version. + * + * This program is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS + * FOR A PARTICULAR PURPOSE. See the GNU General Public License for more + * details. + * + * You should have received a copy of the GNU General Public License along with + * this program; if not, see . + */ + +#ifndef MENUPREVIEW_H +#define MENUPREVIEW_H + +#include "freetype-renderer.h" +#include "kxftconfig.h" + +#if defined(HAVE_FONTCONFIG) + +#include +#include +#include +#include +#include + +/** + * @brief The PreviewParameters is a helper class for communication between qml and + * QQuickImageProvider. + */ +class PreviewParameters +{ +public: + QString fontFamily; + double pointSize; + KXftConfig* options; + + PreviewParameters(const QString& fontFamily, double pointSize, KXftConfig* options); + PreviewParameters(const PreviewParameters& value); + virtual ~PreviewParameters(); + static PreviewParameters fromString(const QString& id); + QString toFormatetString(); +}; + +class EntryMockup +{ + QString label; + QString iconName; + +public: + EntryMockup(const QString& label, const QString& iconName); + + const QString& getIconName() const; + const QString& getLabel() const; +}; + +class MenuMockup +{ + QList entries; + +public: + void add(const EntryMockup& item); + QString getLabel(int index) const; + QString getIconName(int index) const; + static MenuMockup basicExample(); + int length() const; +}; + +class MenuPreviewArea; + +class MenuPreviewRenderer +{ +private: + FreeTypeFontPreviewRenderer renderer; + const int iconSize; + const int padding; + const QColor background; + +public: + MenuPreviewRenderer(const QColor& background, int iconSize = 16, int padding = 2); + QImage getImage(const PreviewParameters& parameters); +}; + +class MenuPreviewButton : public QPushButton +{ + Q_OBJECT +private: + PreviewParameters parameters; + void _update(); + +public: + MenuPreviewButton(const PreviewParameters& parameters, MenuPreviewArea* parent); + KXftConfig::AntiAliasing::State getAntialiasing(); + KXftConfig::Hint::Style getHint(); + KXftConfig::SubPixel::Type getSubPixel(); +public slots: + void setFont(const QString& fontFamily, double pointSize); +}; + +class MenuPreviewArea : public QWidget +{ + Q_OBJECT +private: + friend class MenuPreviewButton; + + QString fontFamily; + double pointSize; + + MenuPreviewRenderer renderer; + QButtonGroup group; + +public: + MenuPreviewArea(const QString& fontFamily, double pointSize, QWidget* parent = nullptr); + virtual ~MenuPreviewArea(); + +public slots: + void setFont(const QFont& font); + void setFontFamily(const QFont& font); + void setFontFamily(const QString& fontFamily); + void setPointSize(double pointSize); + void onSelectionChanged(QAbstractButton* button); + void reset(); + +signals: + void fontChanged(const QString& fontFamily, double pointSize); + void selectionChanged(KXftConfig::AntiAliasing::State antiAliasing, + KXftConfig::Hint::Style hint, + KXftConfig::SubPixel::Type subPixel); +}; + +#endif // defined(HAVE_FONTCONFIG) + +#endif // MENUPREVIEW_H diff --git a/kcms/fonts/menupreview.cpp b/kcms/fonts/menupreview.cpp new file mode 100644 --- /dev/null +++ b/kcms/fonts/menupreview.cpp @@ -0,0 +1,350 @@ +/* + * Copyright 2018 Max Harmathy + * + * This program is free software; you can redistribute it and/or modify it under + * the terms of the GNU General Public License as published by the Free Software + * Foundation; either version 2 of the License, or (at your option) any later + * version. + * + * This program is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS + * FOR A PARTICULAR PURPOSE. See the GNU General Public License for more + * details. + * + * You should have received a copy of the GNU General Public License along with + * this program; if not, see . + */ + +#include "menupreview.h" +#include "freetype-renderer.h" + +#if defined(HAVE_FONTCONFIG) + +#include +#include +#include +#include +#include + +namespace +{ +static const int MAX_PREVIEW_WIDTH = 120; +static const int MAX_PREVIEW_HIGHT = 240; +} + +PreviewParameters::PreviewParameters(const QString& fontFamily, + double pointSize, + KXftConfig* options) + : fontFamily(fontFamily), pointSize(pointSize), options(options) +{ +} + +PreviewParameters::PreviewParameters(const PreviewParameters& value) + : PreviewParameters(value.fontFamily, value.pointSize, new KXftConfig(*value.options)) +{ +} + +PreviewParameters::~PreviewParameters() +{ + delete options; +} + +PreviewParameters PreviewParameters::fromString(const QString& id) +{ + KXftConfig* options = new KXftConfig(); + auto fragments = id.split("/"); + QString fontFamily = ""; + double pointSize = 10; + if (fragments.length() >= 5) { + fontFamily = fragments[0]; + pointSize = fragments[1].toDouble(); + options->setAntiAliasing( + static_cast(fragments[2].toInt())); + options->setHintStyle(static_cast(fragments[3].toInt())); + options->setSubPixelType(static_cast(fragments[4].toInt())); + } + return PreviewParameters(fontFamily, pointSize, options); +} + +QString PreviewParameters::toFormatetString() +{ + auto typeface = QString("%1 %2").arg(fontFamily).arg(pointSize); + + auto aa = options->getAntiAliasing() == KXftConfig::AntiAliasing::Disabled; + auto antialiasing = aa ? "Disabled" : "Enabled"; + + KXftConfig::Hint::Style hintData; + options->getHintStyle(hintData); + auto hint = KXftConfig::toStr(hintData); + + KXftConfig::SubPixel::Type subpixelData; + options->getSubPixelType(subpixelData); + auto subpixel = KXftConfig::toStr(subpixelData); + + return QString("Typeface:\t%1\nAnti-Aliasing:\t%2\nHinting Style:\t%3\nSub-Pixel Order:\t%4") + .arg(typeface, antialiasing, hint, subpixel); +} + +EntryMockup::EntryMockup(const QString& label, const QString& iconName) + : label{ label }, iconName{ iconName } +{ +} + +const QString& EntryMockup::getIconName() const +{ + return iconName; +} + +const QString& EntryMockup::getLabel() const +{ + return label; +} + +void MenuMockup::add(const EntryMockup& item) +{ + entries.append(item); +} + +QString MenuMockup::getLabel(int index) const +{ + return entries.at(index).getLabel(); +} + +QString MenuMockup::getIconName(int index) const +{ + return entries.at(index).getIconName(); +} + +MenuMockup MenuMockup::basicExample() +{ + MenuMockup result; + result.add(EntryMockup("Office", "applications-office")); + result.add(EntryMockup("Internet", "applications-internet")); + result.add(EntryMockup("Multimedia", "applications-multimedia")); + result.add(EntryMockup("Graphics", "applications-graphics")); + result.add(EntryMockup("Accessories", "applications-accessories")); + result.add(EntryMockup("Development", "applications-development")); + result.add(EntryMockup("Settings", "preferences-system")); + result.add(EntryMockup("System", "applications-system")); + result.add(EntryMockup("Utilities", "applications-utilities")); + return result; +} + +int MenuMockup::length() const +{ + return entries.length(); +} + +MenuPreviewRenderer::MenuPreviewRenderer(const QColor& background, int iconSize, int padding) + : renderer(), iconSize(iconSize), padding(padding), background(background) +{ +} + +QImage MenuPreviewRenderer::getImage(const PreviewParameters& parameters) +{ + const auto menu = MenuMockup::basicExample(); + QList lables; + QList icons; + QSize dimensions(0, 2 * padding); + for (int i = 0; i < menu.length(); ++i) { + auto image = renderer.renderText(menu.getLabel(i).toLocal8Bit(), + parameters.fontFamily.toLocal8Bit(), parameters.pointSize, + parameters.options, background, Qt::black); + dimensions.rheight() += qMax(image.height(), iconSize) + 2 * padding; + dimensions.setWidth(qMax(dimensions.width(), image.width())); + lables.append(image); + icons.append(QIcon::fromTheme(menu.getIconName(i))); + } + dimensions.rwidth() += iconSize + 4 * padding; + QImage result(dimensions, QImage::Format_ARGB32); + result.fill(background); + QPainter painter(&result); + + for (int index = 0, y = padding; index < menu.length(); ++index) { + auto image = lables.at(index); + auto icon = icons.at(index).pixmap(iconSize, iconSize); + int heightOffset = (icon.height() - image.height()) / 2; + bool iconIsSmaller = heightOffset < 0; + if (iconIsSmaller) { + heightOffset = (-heightOffset); + } + + painter.drawPixmap( + QRectF(padding, y + (iconIsSmaller ? heightOffset : 0), iconSize, iconSize), icon, + QRectF(0, 0, iconSize, iconSize)); + + painter.drawImage(QRectF(iconSize + 3 * padding, y + (!iconIsSmaller ? heightOffset : 0), + image.width(), image.height()), + image, QRectF(0, 0, image.width(), image.height())); + y += qMax(image.height(), iconSize) + 2 * padding; + } + painter.end(); + return result; +} + +void MenuPreviewButton::_update() +{ + auto parent = dynamic_cast(this->parent()); + auto image = parent->renderer.getImage(parameters); + auto size = image.size(); + bool crop = false; + if (size.width() > MAX_PREVIEW_WIDTH) { + size.setWidth(MAX_PREVIEW_WIDTH); + crop = true; + } + if (size.height() > MAX_PREVIEW_HIGHT) { + size.setHeight(MAX_PREVIEW_HIGHT); + crop = true; + } + if (crop) { + image = image.copy(0, 0, size.width(), size.height()); + } + setIcon(QPixmap::fromImage(image)); + setIconSize(size); + setToolTip(parameters.toFormatetString()); +} + +MenuPreviewButton::MenuPreviewButton(const PreviewParameters& parameters, MenuPreviewArea* parent) + : QPushButton(parent), parameters(parameters) +{ + setCheckable(true); + _update(); +} + +KXftConfig::AntiAliasing::State MenuPreviewButton::getAntialiasing() +{ + return parameters.options->getAntiAliasing(); +} + +KXftConfig::Hint::Style MenuPreviewButton::getHint() +{ + KXftConfig::Hint::Style result; + parameters.options->getHintStyle(result); + return result; +} + +KXftConfig::SubPixel::Type MenuPreviewButton::getSubPixel() +{ + KXftConfig::SubPixel::Type result; + parameters.options->getSubPixelType(result); + return result; +} + +void MenuPreviewButton::setFont(const QString& fontFamily, double pointSize) +{ + parameters.fontFamily = fontFamily; + parameters.pointSize = pointSize; + _update(); +} + +void MenuPreviewArea::onSelectionChanged(QAbstractButton* button) +{ + if (MenuPreviewButton* my_button = dynamic_cast(button)) + emit selectionChanged(my_button->getAntialiasing(), my_button->getHint(), + my_button->getSubPixel()); +} + +void MenuPreviewArea::reset() +{ + auto button = group.checkedButton(); + if (button) { + // temporarily remove excluve flag to make reset possible + group.setExclusive(false); + button->setChecked(false); + group.setExclusive(true); + } +} + +MenuPreviewArea::MenuPreviewArea(const QString& fontFamily, double pointSize, QWidget* parent) + : QWidget(parent) + , fontFamily(fontFamily) + , pointSize(pointSize) + , renderer(QApplication::palette().background().color()) + , group() +{ + auto layout = new QGridLayout(this); + setLayout(layout); + + // add buttons + // start with anti-aliasing off + auto options = new KXftConfig(); + options->setAntiAliasing(KXftConfig::AntiAliasing::Disabled); + options->setHintStyle(KXftConfig::Hint::None); + auto button = new MenuPreviewButton(PreviewParameters(fontFamily, pointSize, options), this); + layout->addWidget(button, 0, 0); + group.addButton(button); + + // without anti-alising there is only hinting 'on' or 'off', so set it to full + options = new KXftConfig(*options); + options->setHintStyle(KXftConfig::Hint::Full); + button = new MenuPreviewButton(PreviewParameters(fontFamily, pointSize, options), this); + layout->addWidget(button, 1, 0); + group.addButton(button); + + int column = 1; + int row = 1; + // show subpixel off and RGB, which will fit in most cases + // TODO dynamically select actual subpixel order of the display + for (int i = 0; i < 2; ++i) { + auto subpixel = i == 0 ? KXftConfig::SubPixel::None : KXftConfig::SubPixel::Rgb; + // iterate over possible hinting settings + for (int hint = KXftConfig::Hint::None; hint <= KXftConfig::Hint::Full; ++hint) { + options = new KXftConfig(); + options->setAntiAliasing(KXftConfig::AntiAliasing::Enabled); + options->setSubPixelType(subpixel); + options->setHintStyle(static_cast(hint)); + button = new MenuPreviewButton(PreviewParameters(fontFamily, pointSize, options), this); + group.addButton(button); + if (row == 0) { + ++row; + } else { + --row; + ++column; + } + layout->addWidget(button, row, column); + } + } + for (auto button : group.buttons()) { + auto my_button = dynamic_cast(button); + connect(this, &MenuPreviewArea::fontChanged, my_button, &MenuPreviewButton::setFont); + } + group.setExclusive(true); + + connect(&group, SIGNAL(buttonClicked(QAbstractButton*)), this, + SLOT(onSelectionChanged(QAbstractButton*))); +} + +MenuPreviewArea::~MenuPreviewArea() +{ + for (auto button : group.buttons()) { + delete button; + } +} + +void MenuPreviewArea::setFont(const QFont& font) +{ + fontFamily = font.family(); + pointSize = font.pointSizeF(); + emit fontChanged(fontFamily, pointSize); +} + +void MenuPreviewArea::setFontFamily(const QFont& font) +{ + setFontFamily(font.family()); +} + +void MenuPreviewArea::setFontFamily(const QString& fontFamily) +{ + this->fontFamily = fontFamily; + emit fontChanged(fontFamily, pointSize); +} + +void MenuPreviewArea::setPointSize(double pointSize) +{ + this->pointSize = pointSize; + emit fontChanged(fontFamily, pointSize); +} + +#include "menupreview.moc" + +#endif // defined(HAVE_FONTCONFIG) diff --git a/kcms/fonts/package/contents/ui/SelectablePreview.qml b/kcms/fonts/package/contents/ui/SelectablePreview.qml new file mode 100644 --- /dev/null +++ b/kcms/fonts/package/contents/ui/SelectablePreview.qml @@ -0,0 +1,29 @@ +/* + * Copyright 2018 Max Harmathy + * + * This program is free software; you can redistribute it and/or modify it under + * the terms of the GNU General Public License as published by the Free Software + * Foundation; either version 2 of the License, or (at your option) any later + * version. + * + * This program is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS + * FOR A PARTICULAR PURPOSE. See the GNU General Public License for more + * details. + * + * You should have received a copy of the GNU General Public License along with + * this program; if not, see . + */ + +import QtQuick 2.11 +import QtQuick.Controls 2.3 + +Button { + property string fontFamily: "Sans" + property double fontSize: 10 + property int antialiasing: 0 + property int hintstyle: 0 + property int subpixel: 0 + icon.color: "transparent" // makes the actual image visible + icon.source: "image://menupreview/" + fontFamily + "/" + fontSize + "/" + antialiasing + "/" + hintstyle + "/" + subpixel +} diff --git a/kcms/fonts/package/contents/ui/main.qml b/kcms/fonts/package/contents/ui/main.qml --- a/kcms/fonts/package/contents/ui/main.qml +++ b/kcms/fonts/package/contents/ui/main.qml @@ -29,8 +29,70 @@ KCM.ConfigModule.quickHelp: i18n("Fonts") + Column { + id: easySettings + Layout.alignment: Qt.AlignHCenter + spacing: 8 + FontWidget { + id: generalFontWidget + label: i18n("General:") + category: "generalFont" + font: kcm.generalFont + } + QtControls.Label { + text: "Select suiting font rendering" + padding: 2 + } + QtControls.Frame { + id: previewFrame + Grid { + id: previewArea + rows: 2 + spacing: 4 + + Repeater { + model: 10 + SelectablePreview { + fontFamily: generalFontWidget.font.family + fontSize: generalFontWidget.font.pointSize + antialiasing: index % 5 == 0 ? 1 : 2 + hintstyle: index == 0 ? 0 : ((index + 2) % 4)+1 + subpixel: index % 5 <= 2 ? 1 : 2 + } + } + QtControls.ButtonGroup { + id: previewButtons + } + } + } + Row { + QtControls.Label { + text: "Expert Settings" + anchors.verticalCenter: parent.verticalCenter + } + QtControls.Button { + text: " + " + focusPolicy: Qt.TabFocus + display: QtControls.AbstractButton.TextOnly + Layout.alignment: Qt.AlignHCenter | Qt.AlignVCenter + onClicked: { + if (expertSettings.visible) { + //previewFrame.visible = 1 + formLayout.visible = 0 + roundButton.text = " + " + } else { + //previewFrame.visible = 0 + formLayout.visible = 1 + roundButton.text = " - " + } + } + } + } + } // Column + Kirigami.FormLayout { id: formLayout + visible: false readonly property int maxImplicitWidth: Math.max(adjustAllFontsButton.implicitWidth, Math.max(antiAliasingComboBox.implicitWidth, Math.max(excludeField.implicitWidth, Math.max(subPixelCombo.implicitWidth, hintingCombo.implicitWidth)))) QtControls.Button { @@ -41,12 +103,7 @@ onClicked: kcm.adjustAllFonts(); } - FontWidget { - id: generalFontWidget - label: i18n("General:") - category: "generalFont" - font: kcm.generalFont - } + FontWidget { label: i18n("Fixed width:") category: "fixedWidthFont" diff --git a/kcms/fonts/renderpreviewimageprovider.h b/kcms/fonts/renderpreviewimageprovider.h new file mode 100644 --- /dev/null +++ b/kcms/fonts/renderpreviewimageprovider.h @@ -0,0 +1,37 @@ +/* + * Copyright 2018 Max Harmathy + * + * This program is free software; you can redistribute it and/or modify it under + * the terms of the GNU General Public License as published by the Free Software + * Foundation; either version 2 of the License, or (at your option) any later + * version. + * + * This program is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS + * FOR A PARTICULAR PURPOSE. See the GNU General Public License for more + * details. + * + * You should have received a copy of the GNU General Public License along with + * this program; if not, see . + */ + +#ifndef RENERPREVIEWIMAGEPROVIDER_H +#define RENERPREVIEWIMAGEPROVIDER_H + +#include "menupreview.h" + +#include + +class MenuPreviewImageProvider : public QQuickImageProvider +{ +private: + MenuPreviewRenderer renderer; + +public: + MenuPreviewImageProvider(); + + QImage requestImage(const QString& id, QSize* size, const QSize& requestedSize) override; + QPixmap requestPixmap(const QString& id, QSize* size, const QSize& requestedSize) override; +}; + +#endif // RENERPREVIEWIMAGEPROVIDER_H diff --git a/kcms/fonts/renderpreviewimageprovider.cpp b/kcms/fonts/renderpreviewimageprovider.cpp new file mode 100644 --- /dev/null +++ b/kcms/fonts/renderpreviewimageprovider.cpp @@ -0,0 +1,44 @@ +/* + * Copyright 2018 Max Harmathy + * + * This program is free software; you can redistribute it and/or modify it under + * the terms of the GNU General Public License as published by the Free Software + * Foundation; either version 2 of the License, or (at your option) any later + * version. + * + * This program is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS + * FOR A PARTICULAR PURPOSE. See the GNU General Public License for more + * details. + * + * You should have received a copy of the GNU General Public License along with + * this program; if not, see . + */ + +#include "renderpreviewimageprovider.h" +#include "freetype-renderer.h" +#include "kxftconfig.h" + +#include +#include +#include + +MenuPreviewImageProvider::MenuPreviewImageProvider() + : QQuickImageProvider(QQuickImageProvider::Image), + renderer(Qt::white) +{ +} + +QImage +MenuPreviewImageProvider::requestImage(const QString& id, QSize* size, const QSize& requestedSize) +{ + auto parameters = PreviewParameters::fromString(id); + return renderer.getImage(parameters); +} + +QPixmap +MenuPreviewImageProvider::requestPixmap(const QString& id, QSize* size, const QSize& requestedSize) +{ + auto result = this->requestImage(id, size, requestedSize); + return QPixmap::fromImage(result); +}