Repository: ayoy/fontedit Branch: develop Commit: 9fdf7bfce507 Files: 82 Total size: 335.1 KB Directory structure: gitextract_t8uykv9z/ ├── .gitignore ├── .gitmodules ├── CHANGELOG.md ├── CMakeLists.txt ├── CODE_OF_CONDUCT.md ├── LICENSE ├── README.md ├── app/ │ ├── CMakeLists.txt │ ├── addglyphdialog.cpp │ ├── addglyphdialog.h │ ├── addglyphdialog.ui │ ├── assets.qrc │ ├── cmake_uninstall.cmake.in │ ├── command.h │ ├── common/ │ │ ├── CMakeLists.txt │ │ ├── common.h │ │ ├── f2b_qt_compat.cpp │ │ └── f2b_qt_compat.h │ ├── fontfaceviewmodel.cpp │ ├── fontfaceviewmodel.h │ ├── global.h │ ├── l10n/ │ │ ├── fontedit_en.qm │ │ └── fontedit_en.ts │ ├── macos/ │ │ ├── Info.plist │ │ └── fontedit.icns │ ├── main.cpp │ ├── mainwindow.cpp │ ├── mainwindow.h │ ├── mainwindow.ui │ ├── mainwindowmodel.cpp │ ├── mainwindowmodel.h │ ├── qfontfacereader.cpp │ ├── qfontfacereader.h │ ├── semver.hpp │ ├── sourcecoderunnable.cpp │ ├── sourcecoderunnable.h │ ├── ui/ │ │ ├── CMakeLists.txt │ │ ├── aboutdialog.cpp │ │ ├── aboutdialog.h │ │ ├── aboutdialog.ui │ │ ├── batchpixelchange.h │ │ ├── facewidget.cpp │ │ ├── facewidget.h │ │ ├── focuswidget.cpp │ │ ├── focuswidget.h │ │ ├── glyphgraphicsview.cpp │ │ ├── glyphgraphicsview.h │ │ ├── glyphinfowidget.cpp │ │ ├── glyphinfowidget.h │ │ ├── glyphwidget.cpp │ │ └── glyphwidget.h │ ├── updatehelper.cpp │ ├── updatehelper.h │ ├── utf8/ │ │ ├── CMakeLists.txt │ │ ├── utf8/ │ │ │ ├── checked.h │ │ │ ├── core.h │ │ │ └── unchecked.h │ │ └── utf8.h │ ├── win/ │ │ └── fontedit.rc │ └── x11/ │ └── fontedit.desktop ├── fontedit.iss ├── lib/ │ ├── CMakeLists.txt │ ├── src/ │ │ ├── CMakeLists.txt │ │ ├── f2b.h │ │ ├── fontdata.cpp │ │ ├── fontdata.h │ │ ├── fontsourcecodegenerator.cpp │ │ ├── fontsourcecodegenerator.h │ │ ├── format.h │ │ ├── include/ │ │ │ └── f2b.h │ │ └── sourcecode.h │ └── test/ │ ├── CMakeLists.txt │ ├── fontface_test.cpp │ ├── glyph_test.cpp │ └── sourcecode_test.cpp └── test/ ├── CMakeLists.txt ├── assets/ │ ├── jetbrains260.fontedit │ ├── monaco8-subset.c-test │ ├── monaco8.c-test │ └── monaco8.fontedit ├── f2b_qt_compat_test.cpp └── sourcecodegeneration_test.cpp ================================================ FILE CONTENTS ================================================ ================================================ FILE: .gitignore ================================================ # C++ objects and libs *.slo *.lo *.o *.a *.la *.lai *.so *.dll *.dylib # Qt-es object_script.*.Release object_script.*.Debug *_plugin_import.cpp /.qmake.cache /.qmake.stash *.pro.user *.pro.user.* *.qbs.user *.qbs.user.* *.moc moc_*.cpp moc_*.h qrc_*.cpp ui_*.h *.qmlc *.jsc Makefile* *build-* # Qt unit tests target_wrapper.* # QtCreator *.autosave # QtCreator Qml *.qmlproject.user *.qmlproject.user.* # QtCreator CMake CMakeLists.txt.user* .DS_Store .idea *~ .*~ *.cbp build ================================================ FILE: .gitmodules ================================================ [submodule "gsl"] path = gsl url = https://github.com/microsoft/GSL.git [submodule "gtest"] path = gtest url = https://github.com/google/googletest.git ================================================ FILE: CHANGELOG.md ================================================ # Changelog ## 1.1.0 - 2020-05-31 * Export subset of font characters (only the characters that you really need in the font) - helps to reduce font size when you only use a bunch of characters and not the whole ASCII table * Exported source code is wrapped at around 80 columns * Added pseudocode in the generated source code, explaining how to retrieve individual font glyphs * More user-friendly Undo/Redo functionality * Added checking for updates on-demand from the menu and automatically at start-up (the latter can be disabled) ## 1.0.0 - 2020-03-20 * First public release * Importing system fonts * Editing font glyphs * Adding new glyphs * Exporting source code * Saving current state of the font document ================================================ FILE: CMakeLists.txt ================================================ cmake_minimum_required(VERSION 3.9) project(FontEdit) set(CMAKE_PROJECT_VERSION_MAJOR 1) set(CMAKE_PROJECT_VERSION_MINOR 1) set(CMAKE_PROJECT_VERSION_PATCH 0) set(APP_VERSION 1.1.0) set(APP_BUILD 4) set(APP_YEAR 2020) set(CMAKE_CXX_STANDARD 17) set(CMAKE_CXX_STANDARD_REQUIRED ON) set(BUILD_TESTS ON) set(CMAKE_RUNTIME_OUTPUT_DIRECTORY ${CMAKE_CURRENT_BINARY_DIR}) find_package(Qt5 COMPONENTS Widgets REQUIRED) enable_testing() add_subdirectory(gtest EXCLUDE_FROM_ALL) add_subdirectory(gsl EXCLUDE_FROM_ALL) add_subdirectory(lib) add_subdirectory(app) if(${BUILD_TESTS}) add_subdirectory(test) endif() if (APPLE) add_custom_command(OUTPUT "${APP_TARGET_NAME}.dmg" COMMAND macdeployqt ARGS "${APP_TARGET_NAME}.app" "-libpath=/lib" "-dmg" MAIN_DEPENDENCY ${APP_TARGET_NAME}.app WORKING_DIRECTORY "${CMAKE_CURRENT_BINARY_DIR}") add_custom_target(dmg DEPENDS "${CMAKE_CURRENT_BINARY_DIR}/${APP_TARGET_NAME}.dmg" ) elseif (WIN32) message(STATUS "ADDING WINDOWS INSTALLER TARGET") add_custom_command(OUTPUT "win" COMMAND windeployqt.exe ARGS "${APP_TARGET_NAME}.exe" "--dir" "win" WORKING_DIRECTORY "${CMAKE_CURRENT_BINARY_DIR}") add_custom_target(installer DEPENDS "${CMAKE_CURRENT_BINARY_DIR}/win" ) endif () set(CPACK_PACKAGE_VERSION "1.1.0") set(CPACK_PACKAGE_CONTACT "dominik@kapusta.cc") set(CPACK_PROJECT_HOMEPAGE_URL "https://kapusta.cc") set(CPACK_DEBIAN_FILE_NAME DEB-DEFAULT) set(CPACK_DEBIAN_PACKAGE_NAME "fontedit") set(CPACK_DEBIAN_PACKAGE_RELEASE 3) set(CPACK_DEBIAN_PACKAGE_DESCRIPTION "Edit and convert fonts to byte arrays suitable for use in embedded systems' displays") set(CPACK_DEBIAN_PACKAGE_SHLIBDEPS ON) include(CPack) ================================================ FILE: CODE_OF_CONDUCT.md ================================================ # Contributor Covenant Code of Conduct ## Our Pledge In the interest of fostering an open and welcoming environment, we as contributors and maintainers pledge to making participation in our project and our community a harassment-free experience for everyone, regardless of age, body size, disability, ethnicity, sex characteristics, gender identity and expression, level of experience, education, socio-economic status, nationality, personal appearance, race, religion, or sexual identity and orientation. ## Our Standards Examples of behavior that contributes to creating a positive environment include: * Using welcoming and inclusive language * Being respectful of differing viewpoints and experiences * Gracefully accepting constructive criticism * Focusing on what is best for the community * Showing empathy towards other community members Examples of unacceptable behavior by participants include: * The use of sexualized language or imagery and unwelcome sexual attention or advances * Trolling, insulting/derogatory comments, and personal or political attacks * Public or private harassment * Publishing others' private information, such as a physical or electronic address, without explicit permission * Other conduct which could reasonably be considered inappropriate in a professional setting ## Our Responsibilities Project maintainers are responsible for clarifying the standards of acceptable behavior and are expected to take appropriate and fair corrective action in response to any instances of unacceptable behavior. Project maintainers have the right and responsibility to remove, edit, or reject comments, commits, code, wiki edits, issues, and other contributions that are not aligned to this Code of Conduct, or to ban temporarily or permanently any contributor for other behaviors that they deem inappropriate, threatening, offensive, or harmful. ## Scope This Code of Conduct applies both within project spaces and in public spaces when an individual is representing the project or its community. Examples of representing a project or community include using an official project e-mail address, posting via an official social media account, or acting as an appointed representative at an online or offline event. Representation of a project may be further defined and clarified by project maintainers. ## Enforcement Instances of abusive, harassing, or otherwise unacceptable behavior may be reported by contacting the project team at dominik@kapusta.cc. All complaints will be reviewed and investigated and will result in a response that is deemed necessary and appropriate to the circumstances. The project team is obligated to maintain confidentiality with regard to the reporter of an incident. Further details of specific enforcement policies may be posted separately. Project maintainers who do not follow or enforce the Code of Conduct in good faith may face temporary or permanent repercussions as determined by other members of the project's leadership. ## Attribution This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4, available at https://www.contributor-covenant.org/version/1/4/code-of-conduct.html [homepage]: https://www.contributor-covenant.org For answers to common questions about this code of conduct, see https://www.contributor-covenant.org/faq ================================================ FILE: LICENSE ================================================ GNU GENERAL PUBLIC LICENSE Version 3, 29 June 2007 Copyright (C) 2007 Free Software Foundation, Inc. Everyone is permitted to copy and distribute verbatim copies of this license document, but changing it is not allowed. Preamble The GNU General Public License is a free, copyleft license for software and other kinds of works. The licenses for most software and other practical works are designed to take away your freedom to share and change the works. By contrast, the GNU General Public License is intended to guarantee your freedom to share and change all versions of a program--to make sure it remains free software for all its users. We, the Free Software Foundation, use the GNU General Public License for most of our software; it applies also to any other work released this way by its authors. You can apply it to your programs, too. When we speak of free software, we are referring to freedom, not price. Our General Public Licenses are designed to make sure that you have the freedom to distribute copies of free software (and charge for them if you wish), that you receive source code or can get it if you want it, that you can change the software or use pieces of it in new free programs, and that you know you can do these things. To protect your rights, we need to prevent others from denying you these rights or asking you to surrender the rights. Therefore, you have certain responsibilities if you distribute copies of the software, or if you modify it: responsibilities to respect the freedom of others. For example, if you distribute copies of such a program, whether gratis or for a fee, you must pass on to the recipients the same freedoms that you received. You must make sure that they, too, receive or can get the source code. And you must show them these terms so they know their rights. Developers that use the GNU GPL protect your rights with two steps: (1) assert copyright on the software, and (2) offer you this License giving you legal permission to copy, distribute and/or modify it. For the developers' and authors' protection, the GPL clearly explains that there is no warranty for this free software. For both users' and authors' sake, the GPL requires that modified versions be marked as changed, so that their problems will not be attributed erroneously to authors of previous versions. Some devices are designed to deny users access to install or run modified versions of the software inside them, although the manufacturer can do so. This is fundamentally incompatible with the aim of protecting users' freedom to change the software. The systematic pattern of such abuse occurs in the area of products for individuals to use, which is precisely where it is most unacceptable. Therefore, we have designed this version of the GPL to prohibit the practice for those products. If such problems arise substantially in other domains, we stand ready to extend this provision to those domains in future versions of the GPL, as needed to protect the freedom of users. Finally, every program is threatened constantly by software patents. States should not allow patents to restrict development and use of software on general-purpose computers, but in those that do, we wish to avoid the special danger that patents applied to a free program could make it effectively proprietary. To prevent this, the GPL assures that patents cannot be used to render the program non-free. The precise terms and conditions for copying, distribution and modification follow. TERMS AND CONDITIONS 0. Definitions. "This License" refers to version 3 of the GNU General Public License. "Copyright" also means copyright-like laws that apply to other kinds of works, such as semiconductor masks. "The Program" refers to any copyrightable work licensed under this License. Each licensee is addressed as "you". "Licensees" and "recipients" may be individuals or organizations. To "modify" a work means to copy from or adapt all or part of the work in a fashion requiring copyright permission, other than the making of an exact copy. The resulting work is called a "modified version" of the earlier work or a work "based on" the earlier work. A "covered work" means either the unmodified Program or a work based on the Program. To "propagate" a work means to do anything with it that, without permission, would make you directly or secondarily liable for infringement under applicable copyright law, except executing it on a computer or modifying a private copy. Propagation includes copying, distribution (with or without modification), making available to the public, and in some countries other activities as well. To "convey" a work means any kind of propagation that enables other parties to make or receive copies. Mere interaction with a user through a computer network, with no transfer of a copy, is not conveying. An interactive user interface displays "Appropriate Legal Notices" to the extent that it includes a convenient and prominently visible feature that (1) displays an appropriate copyright notice, and (2) tells the user that there is no warranty for the work (except to the extent that warranties are provided), that licensees may convey the work under this License, and how to view a copy of this License. If the interface presents a list of user commands or options, such as a menu, a prominent item in the list meets this criterion. 1. Source Code. The "source code" for a work means the preferred form of the work for making modifications to it. "Object code" means any non-source form of a work. A "Standard Interface" means an interface that either is an official standard defined by a recognized standards body, or, in the case of interfaces specified for a particular programming language, one that is widely used among developers working in that language. The "System Libraries" of an executable work include anything, other than the work as a whole, that (a) is included in the normal form of packaging a Major Component, but which is not part of that Major Component, and (b) serves only to enable use of the work with that Major Component, or to implement a Standard Interface for which an implementation is available to the public in source code form. A "Major Component", in this context, means a major essential component (kernel, window system, and so on) of the specific operating system (if any) on which the executable work runs, or a compiler used to produce the work, or an object code interpreter used to run it. The "Corresponding Source" for a work in object code form means all the source code needed to generate, install, and (for an executable work) run the object code and to modify the work, including scripts to control those activities. However, it does not include the work's System Libraries, or general-purpose tools or generally available free programs which are used unmodified in performing those activities but which are not part of the work. For example, Corresponding Source includes interface definition files associated with source files for the work, and the source code for shared libraries and dynamically linked subprograms that the work is specifically designed to require, such as by intimate data communication or control flow between those subprograms and other parts of the work. The Corresponding Source need not include anything that users can regenerate automatically from other parts of the Corresponding Source. The Corresponding Source for a work in source code form is that same work. 2. Basic Permissions. All rights granted under this License are granted for the term of copyright on the Program, and are irrevocable provided the stated conditions are met. This License explicitly affirms your unlimited permission to run the unmodified Program. The output from running a covered work is covered by this License only if the output, given its content, constitutes a covered work. This License acknowledges your rights of fair use or other equivalent, as provided by copyright law. You may make, run and propagate covered works that you do not convey, without conditions so long as your license otherwise remains in force. You may convey covered works to others for the sole purpose of having them make modifications exclusively for you, or provide you with facilities for running those works, provided that you comply with the terms of this License in conveying all material for which you do not control copyright. Those thus making or running the covered works for you must do so exclusively on your behalf, under your direction and control, on terms that prohibit them from making any copies of your copyrighted material outside their relationship with you. Conveying under any other circumstances is permitted solely under the conditions stated below. Sublicensing is not allowed; section 10 makes it unnecessary. 3. Protecting Users' Legal Rights From Anti-Circumvention Law. No covered work shall be deemed part of an effective technological measure under any applicable law fulfilling obligations under article 11 of the WIPO copyright treaty adopted on 20 December 1996, or similar laws prohibiting or restricting circumvention of such measures. When you convey a covered work, you waive any legal power to forbid circumvention of technological measures to the extent such circumvention is effected by exercising rights under this License with respect to the covered work, and you disclaim any intention to limit operation or modification of the work as a means of enforcing, against the work's users, your or third parties' legal rights to forbid circumvention of technological measures. 4. Conveying Verbatim Copies. You may convey verbatim copies of the Program's source code as you receive it, in any medium, provided that you conspicuously and appropriately publish on each copy an appropriate copyright notice; keep intact all notices stating that this License and any non-permissive terms added in accord with section 7 apply to the code; keep intact all notices of the absence of any warranty; and give all recipients a copy of this License along with the Program. You may charge any price or no price for each copy that you convey, and you may offer support or warranty protection for a fee. 5. Conveying Modified Source Versions. You may convey a work based on the Program, or the modifications to produce it from the Program, in the form of source code under the terms of section 4, provided that you also meet all of these conditions: a) The work must carry prominent notices stating that you modified it, and giving a relevant date. b) The work must carry prominent notices stating that it is released under this License and any conditions added under section 7. This requirement modifies the requirement in section 4 to "keep intact all notices". c) You must license the entire work, as a whole, under this License to anyone who comes into possession of a copy. This License will therefore apply, along with any applicable section 7 additional terms, to the whole of the work, and all its parts, regardless of how they are packaged. This License gives no permission to license the work in any other way, but it does not invalidate such permission if you have separately received it. d) If the work has interactive user interfaces, each must display Appropriate Legal Notices; however, if the Program has interactive interfaces that do not display Appropriate Legal Notices, your work need not make them do so. A compilation of a covered work with other separate and independent works, which are not by their nature extensions of the covered work, and which are not combined with it such as to form a larger program, in or on a volume of a storage or distribution medium, is called an "aggregate" if the compilation and its resulting copyright are not used to limit the access or legal rights of the compilation's users beyond what the individual works permit. Inclusion of a covered work in an aggregate does not cause this License to apply to the other parts of the aggregate. 6. Conveying Non-Source Forms. You may convey a covered work in object code form under the terms of sections 4 and 5, provided that you also convey the machine-readable Corresponding Source under the terms of this License, in one of these ways: a) Convey the object code in, or embodied in, a physical product (including a physical distribution medium), accompanied by the Corresponding Source fixed on a durable physical medium customarily used for software interchange. b) Convey the object code in, or embodied in, a physical product (including a physical distribution medium), accompanied by a written offer, valid for at least three years and valid for as long as you offer spare parts or customer support for that product model, to give anyone who possesses the object code either (1) a copy of the Corresponding Source for all the software in the product that is covered by this License, on a durable physical medium customarily used for software interchange, for a price no more than your reasonable cost of physically performing this conveying of source, or (2) access to copy the Corresponding Source from a network server at no charge. c) Convey individual copies of the object code with a copy of the written offer to provide the Corresponding Source. This alternative is allowed only occasionally and noncommercially, and only if you received the object code with such an offer, in accord with subsection 6b. d) Convey the object code by offering access from a designated place (gratis or for a charge), and offer equivalent access to the Corresponding Source in the same way through the same place at no further charge. You need not require recipients to copy the Corresponding Source along with the object code. If the place to copy the object code is a network server, the Corresponding Source may be on a different server (operated by you or a third party) that supports equivalent copying facilities, provided you maintain clear directions next to the object code saying where to find the Corresponding Source. Regardless of what server hosts the Corresponding Source, you remain obligated to ensure that it is available for as long as needed to satisfy these requirements. e) Convey the object code using peer-to-peer transmission, provided you inform other peers where the object code and Corresponding Source of the work are being offered to the general public at no charge under subsection 6d. A separable portion of the object code, whose source code is excluded from the Corresponding Source as a System Library, need not be included in conveying the object code work. A "User Product" is either (1) a "consumer product", which means any tangible personal property which is normally used for personal, family, or household purposes, or (2) anything designed or sold for incorporation into a dwelling. In determining whether a product is a consumer product, doubtful cases shall be resolved in favor of coverage. For a particular product received by a particular user, "normally used" refers to a typical or common use of that class of product, regardless of the status of the particular user or of the way in which the particular user actually uses, or expects or is expected to use, the product. A product is a consumer product regardless of whether the product has substantial commercial, industrial or non-consumer uses, unless such uses represent the only significant mode of use of the product. "Installation Information" for a User Product means any methods, procedures, authorization keys, or other information required to install and execute modified versions of a covered work in that User Product from a modified version of its Corresponding Source. The information must suffice to ensure that the continued functioning of the modified object code is in no case prevented or interfered with solely because modification has been made. If you convey an object code work under this section in, or with, or specifically for use in, a User Product, and the conveying occurs as part of a transaction in which the right of possession and use of the User Product is transferred to the recipient in perpetuity or for a fixed term (regardless of how the transaction is characterized), the Corresponding Source conveyed under this section must be accompanied by the Installation Information. But this requirement does not apply if neither you nor any third party retains the ability to install modified object code on the User Product (for example, the work has been installed in ROM). The requirement to provide Installation Information does not include a requirement to continue to provide support service, warranty, or updates for a work that has been modified or installed by the recipient, or for the User Product in which it has been modified or installed. Access to a network may be denied when the modification itself materially and adversely affects the operation of the network or violates the rules and protocols for communication across the network. Corresponding Source conveyed, and Installation Information provided, in accord with this section must be in a format that is publicly documented (and with an implementation available to the public in source code form), and must require no special password or key for unpacking, reading or copying. 7. Additional Terms. "Additional permissions" are terms that supplement the terms of this License by making exceptions from one or more of its conditions. Additional permissions that are applicable to the entire Program shall be treated as though they were included in this License, to the extent that they are valid under applicable law. If additional permissions apply only to part of the Program, that part may be used separately under those permissions, but the entire Program remains governed by this License without regard to the additional permissions. When you convey a copy of a covered work, you may at your option remove any additional permissions from that copy, or from any part of it. (Additional permissions may be written to require their own removal in certain cases when you modify the work.) You may place additional permissions on material, added by you to a covered work, for which you have or can give appropriate copyright permission. Notwithstanding any other provision of this License, for material you add to a covered work, you may (if authorized by the copyright holders of that material) supplement the terms of this License with terms: a) Disclaiming warranty or limiting liability differently from the terms of sections 15 and 16 of this License; or b) Requiring preservation of specified reasonable legal notices or author attributions in that material or in the Appropriate Legal Notices displayed by works containing it; or c) Prohibiting misrepresentation of the origin of that material, or requiring that modified versions of such material be marked in reasonable ways as different from the original version; or d) Limiting the use for publicity purposes of names of licensors or authors of the material; or e) Declining to grant rights under trademark law for use of some trade names, trademarks, or service marks; or f) Requiring indemnification of licensors and authors of that material by anyone who conveys the material (or modified versions of it) with contractual assumptions of liability to the recipient, for any liability that these contractual assumptions directly impose on those licensors and authors. All other non-permissive additional terms are considered "further restrictions" within the meaning of section 10. If the Program as you received it, or any part of it, contains a notice stating that it is governed by this License along with a term that is a further restriction, you may remove that term. If a license document contains a further restriction but permits relicensing or conveying under this License, you may add to a covered work material governed by the terms of that license document, provided that the further restriction does not survive such relicensing or conveying. If you add terms to a covered work in accord with this section, you must place, in the relevant source files, a statement of the additional terms that apply to those files, or a notice indicating where to find the applicable terms. Additional terms, permissive or non-permissive, may be stated in the form of a separately written license, or stated as exceptions; the above requirements apply either way. 8. Termination. You may not propagate or modify a covered work except as expressly provided under this License. Any attempt otherwise to propagate or modify it is void, and will automatically terminate your rights under this License (including any patent licenses granted under the third paragraph of section 11). However, if you cease all violation of this License, then your license from a particular copyright holder is reinstated (a) provisionally, unless and until the copyright holder explicitly and finally terminates your license, and (b) permanently, if the copyright holder fails to notify you of the violation by some reasonable means prior to 60 days after the cessation. Moreover, your license from a particular copyright holder is reinstated permanently if the copyright holder notifies you of the violation by some reasonable means, this is the first time you have received notice of violation of this License (for any work) from that copyright holder, and you cure the violation prior to 30 days after your receipt of the notice. Termination of your rights under this section does not terminate the licenses of parties who have received copies or rights from you under this License. If your rights have been terminated and not permanently reinstated, you do not qualify to receive new licenses for the same material under section 10. 9. Acceptance Not Required for Having Copies. You are not required to accept this License in order to receive or run a copy of the Program. Ancillary propagation of a covered work occurring solely as a consequence of using peer-to-peer transmission to receive a copy likewise does not require acceptance. However, nothing other than this License grants you permission to propagate or modify any covered work. These actions infringe copyright if you do not accept this License. Therefore, by modifying or propagating a covered work, you indicate your acceptance of this License to do so. 10. Automatic Licensing of Downstream Recipients. Each time you convey a covered work, the recipient automatically receives a license from the original licensors, to run, modify and propagate that work, subject to this License. You are not responsible for enforcing compliance by third parties with this License. An "entity transaction" is a transaction transferring control of an organization, or substantially all assets of one, or subdividing an organization, or merging organizations. If propagation of a covered work results from an entity transaction, each party to that transaction who receives a copy of the work also receives whatever licenses to the work the party's predecessor in interest had or could give under the previous paragraph, plus a right to possession of the Corresponding Source of the work from the predecessor in interest, if the predecessor has it or can get it with reasonable efforts. You may not impose any further restrictions on the exercise of the rights granted or affirmed under this License. For example, you may not impose a license fee, royalty, or other charge for exercise of rights granted under this License, and you may not initiate litigation (including a cross-claim or counterclaim in a lawsuit) alleging that any patent claim is infringed by making, using, selling, offering for sale, or importing the Program or any portion of it. 11. Patents. A "contributor" is a copyright holder who authorizes use under this License of the Program or a work on which the Program is based. The work thus licensed is called the contributor's "contributor version". A contributor's "essential patent claims" are all patent claims owned or controlled by the contributor, whether already acquired or hereafter acquired, that would be infringed by some manner, permitted by this License, of making, using, or selling its contributor version, but do not include claims that would be infringed only as a consequence of further modification of the contributor version. For purposes of this definition, "control" includes the right to grant patent sublicenses in a manner consistent with the requirements of this License. Each contributor grants you a non-exclusive, worldwide, royalty-free patent license under the contributor's essential patent claims, to make, use, sell, offer for sale, import and otherwise run, modify and propagate the contents of its contributor version. In the following three paragraphs, a "patent license" is any express agreement or commitment, however denominated, not to enforce a patent (such as an express permission to practice a patent or covenant not to sue for patent infringement). To "grant" such a patent license to a party means to make such an agreement or commitment not to enforce a patent against the party. If you convey a covered work, knowingly relying on a patent license, and the Corresponding Source of the work is not available for anyone to copy, free of charge and under the terms of this License, through a publicly available network server or other readily accessible means, then you must either (1) cause the Corresponding Source to be so available, or (2) arrange to deprive yourself of the benefit of the patent license for this particular work, or (3) arrange, in a manner consistent with the requirements of this License, to extend the patent license to downstream recipients. "Knowingly relying" means you have actual knowledge that, but for the patent license, your conveying the covered work in a country, or your recipient's use of the covered work in a country, would infringe one or more identifiable patents in that country that you have reason to believe are valid. If, pursuant to or in connection with a single transaction or arrangement, you convey, or propagate by procuring conveyance of, a covered work, and grant a patent license to some of the parties receiving the covered work authorizing them to use, propagate, modify or convey a specific copy of the covered work, then the patent license you grant is automatically extended to all recipients of the covered work and works based on it. A patent license is "discriminatory" if it does not include within the scope of its coverage, prohibits the exercise of, or is conditioned on the non-exercise of one or more of the rights that are specifically granted under this License. You may not convey a covered work if you are a party to an arrangement with a third party that is in the business of distributing software, under which you make payment to the third party based on the extent of your activity of conveying the work, and under which the third party grants, to any of the parties who would receive the covered work from you, a discriminatory patent license (a) in connection with copies of the covered work conveyed by you (or copies made from those copies), or (b) primarily for and in connection with specific products or compilations that contain the covered work, unless you entered into that arrangement, or that patent license was granted, prior to 28 March 2007. Nothing in this License shall be construed as excluding or limiting any implied license or other defenses to infringement that may otherwise be available to you under applicable patent law. 12. No Surrender of Others' Freedom. If conditions are imposed on you (whether by court order, agreement or otherwise) that contradict the conditions of this License, they do not excuse you from the conditions of this License. If you cannot convey a covered work so as to satisfy simultaneously your obligations under this License and any other pertinent obligations, then as a consequence you may not convey it at all. For example, if you agree to terms that obligate you to collect a royalty for further conveying from those to whom you convey the Program, the only way you could satisfy both those terms and this License would be to refrain entirely from conveying the Program. 13. Use with the GNU Affero General Public License. Notwithstanding any other provision of this License, you have permission to link or combine any covered work with a work licensed under version 3 of the GNU Affero General Public License into a single combined work, and to convey the resulting work. The terms of this License will continue to apply to the part which is the covered work, but the special requirements of the GNU Affero General Public License, section 13, concerning interaction through a network will apply to the combination as such. 14. Revised Versions of this License. The Free Software Foundation may publish revised and/or new versions of the GNU General Public License from time to time. Such new versions will be similar in spirit to the present version, but may differ in detail to address new problems or concerns. Each version is given a distinguishing version number. If the Program specifies that a certain numbered version of the GNU General Public License "or any later version" applies to it, you have the option of following the terms and conditions either of that numbered version or of any later version published by the Free Software Foundation. If the Program does not specify a version number of the GNU General Public License, you may choose any version ever published by the Free Software Foundation. If the Program specifies that a proxy can decide which future versions of the GNU General Public License can be used, that proxy's public statement of acceptance of a version permanently authorizes you to choose that version for the Program. Later license versions may give you additional or different permissions. However, no additional obligations are imposed on any author or copyright holder as a result of your choosing to follow a later version. 15. Disclaimer of Warranty. THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING, REPAIR OR CORRECTION. 16. Limitation of Liability. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS), EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH DAMAGES. 17. Interpretation of Sections 15 and 16. If the disclaimer of warranty and limitation of liability provided above cannot be given local legal effect according to their terms, reviewing courts shall apply local law that most closely approximates an absolute waiver of all civil liability in connection with the Program, unless a warranty or assumption of liability accompanies a copy of the Program in return for a fee. END OF TERMS AND CONDITIONS How to Apply These Terms to Your New Programs If you develop a new program, and you want it to be of the greatest possible use to the public, the best way to achieve this is to make it free software which everyone can redistribute and change under these terms. To do so, attach the following notices to the program. It is safest to attach them to the start of each source file to most effectively state the exclusion of warranty; and each file should have at least the "copyright" line and a pointer to where the full notice is found. Copyright (C) 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 . Also add information on how to contact you by electronic and paper mail. If the program does terminal interaction, make it output a short notice like this when it starts in an interactive mode: Copyright (C) This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'. This is free software, and you are welcome to redistribute it under certain conditions; type `show c' for details. The hypothetical commands `show w' and `show c' should show the appropriate parts of the General Public License. Of course, your program's commands might be different; for a GUI interface, you would use an "about box". You should also get your employer (if you work as a programmer) or school, if any, to sign a "copyright disclaimer" for the program, if necessary. For more information on this, and how to apply and follow the GNU GPL, see . The GNU General Public License does not permit incorporating your program into proprietary programs. If your program is a subroutine library, you may consider it more useful to permit linking proprietary applications with the library. If this is what you want to do, use the GNU Lesser General Public License instead of this License. But first, please read . ================================================ FILE: README.md ================================================ # FontEdit FontEdit is a desktop application that allows you to convert general-purpose fixed-width desktop fonts to byte array representation that's suitable for use in embedded systems displays. It's written in C++ with Qt UI and was tested on Windows, Linux and MacOS. Read more about it in [the blog post](https://kapusta.cc/2019/03/20/fontedit/). ![FontEdit](https://kapusta.cc/assets/fontedit/imported_font.png) ## Features With FontEdit you can: * import fonts from the operating system - to load a custom font, you should first register it in your OS, * edit individual font glyphs after importing - automatic import is a best-effort operation and although the font should be usable right after importing, you might want to tweak it so that it looks better, * add new glyphs to a font document - either by copying an existing glyph, starting from scratch or adding a glyph from a character you input (useful for adding non-ASCII characters to your font), * export the font as source code (in a form of byte array) suitable for Arduino, C/C++ or Python, * save your progress to a file - the font document file is cross-platform so you can e.g. import and edit it on MacOS and then move to RPi and export the code from there, * as of 1.1.0 you can do partial exports, i.e. export only a bunch of font characters that you really need for your application (read more in [this blog post](https://kapusta.cc/2020/05/31/fontedit-1-1-0)). ### Font Editor You can edit font glyphs with a minimal editor that's controlled with a mouse and keyboard. Click and drag the mouse to set pixels (making them black), hold Alt or Ctrl (⌘) to erase. Use touchpad scroll (mouse wheel) with Ctrl (⌘) to zoom the editor canvas. You can also reset the current glyph or the whole font to their initial state (from latest save). The editor supports Undo/Redo for most operations. ### Source Code Export The font data can be exported to: * a C file (also suitable for use with C++), * an Arduino-specific C file (using PROGMEM), * a Python list or bytes object (both compatible with Python 2.x/3.x and MicroPython). You can switch between MSB and LSB mode, invert all the bits, and conditionally include line spacings in font definition (not recommended unless you have a very good reason for it). The tab size can be configured. ## Getting FontEdit ### Packages The [Releases GitHub page](https://github.com/ayoy/fontedit/releases) contains packages for: * Ubuntu/Debian (amd64), * Raspbian Buster (armhf), * MacOS, * Windows. ### Building from source Prerequisites: * Qt (tested with >= 5.9) * cmake (3.9 or newer) * C++ compiler that supports C++17 Follow these steps to build the app from the source code: 1. Clone the Git repository: ``` $ git clone https://github.com/ayoy/fontedit $ cd fontedit ``` 2. Check out Git submodules: ``` $ git submodule update --init ``` 3. Build with CMake: ``` $ mkdir build $ cd build $ cmake -DCMAKE_BUILD_TYPE=Release .. $ make ``` 4. (Optionally) Install on Linux with: `make install` or create a dmg image on MacOS with `make dmg`. ## Bugs, ideas, improvements Please report bugs and feature requests via [GitHub Issues](https://github.com/ayoy/fontedit/issues) or as a [pull request](https://github.com/ayoy/fontedit/pulls). ## License © 2020 Dominik Kapusta This app is distributed in accordance with GPL v3. See [LICENSE](https://github.com/ayoy/fontedit/blob/master/LICENSE) for details. The app uses icons from [www.flaticon.com](https://www.flaticon.com) made by [Smashicons](https://www.flaticon.com/authors/smashicons), [Freepik](https://www.flaticon.com/authors/freepik) and [Pixel perfect](https://www.flaticon.com/authors/pixel-perfect). ================================================ FILE: app/CMakeLists.txt ================================================ cmake_minimum_required(VERSION 3.9) if (UNIX AND NOT APPLE) project(fontedit LANGUAGES CXX) else() project(FontEdit LANGUAGES CXX) endif() set(APP_TARGET_NAME ${PROJECT_NAME} PARENT_SCOPE) set(CMAKE_INCLUDE_CURRENT_DIR ON) set(CMAKE_AUTOUIC ON) set(CMAKE_AUTOMOC ON) set(CMAKE_AUTORCC ON) set(CMAKE_CXX_STANDARD 17) set(CMAKE_CXX_STANDARD_REQUIRED ON) find_package(Qt5 COMPONENTS Core Gui Widgets Network REQUIRED) find_package(Qt5LinguistTools) add_subdirectory(utf8) add_subdirectory(common) add_subdirectory(ui) set(SRC_FILES addglyphdialog.cpp addglyphdialog.h addglyphdialog.ui command.h fontfaceviewmodel.cpp fontfaceviewmodel.h global.h mainwindow.cpp mainwindow.h mainwindow.ui mainwindowmodel.cpp mainwindowmodel.h qfontfacereader.cpp qfontfacereader.h semver.hpp sourcecoderunnable.cpp sourcecoderunnable.h updatehelper.cpp updatehelper.h ) add_library(appbundle ${SRC_FILES}) target_include_directories(appbundle PUBLIC ${CMAKE_CURRENT_SOURCE_DIR} utf8 common ui) target_link_libraries(appbundle PUBLIC Qt5::Widgets Qt5::Core Qt5::Network common ui font2bytes GSL) if (APPLE) message(STATUS "Building MacOS X Bundle") add_executable(${PROJECT_NAME} MACOSX_BUNDLE main.cpp assets.qrc ) set(CMAKE_OSX_DEPLOYMENT_TARGET 10.14) set_target_properties(${PROJECT_NAME} PROPERTIES MACOSX_BUNDLE_INFO_PLIST ${PROJECT_SOURCE_DIR}/macos/Info.plist) set_target_properties(${PROJECT_NAME} PROPERTIES MACOSX_BUNDLE_ICON_FILE fontedit.icns) set(RESOURCES_DIR ${CMAKE_RUNTIME_OUTPUT_DIRECTORY}/${PROJECT_NAME}.app/Contents/Resources) file(MAKE_DIRECTORY ${RESOURCES_DIR}) file(COPY ${PROJECT_SOURCE_DIR}/macos/fontedit.icns DESTINATION ${RESOURCES_DIR}) elseif(WIN32) set(OPENSSL_ROOT_DIR "${Qt5_DIR}/../../../../Tools/OpenSSL/Win_x64" CACHE STRING "OpenSSL dir") include(FindOpenSSL) add_executable(${PROJECT_NAME} WIN32 main.cpp assets.qrc win/fontedit.rc ) target_link_libraries(${PROJECT_NAME} PRIVATE OpenSSL::SSL OpenSSL::Crypto) else() add_executable(${PROJECT_NAME} main.cpp assets.qrc ) if (UNIX) install(TARGETS ${PROJECT_NAME} RUNTIME DESTINATION bin LIBRARY DESTINATION lib) install(FILES x11/fontedit.desktop DESTINATION share/applications) install(DIRECTORY x11/icons DESTINATION share) # uninstall target if(NOT TARGET uninstall) configure_file( "${CMAKE_CURRENT_SOURCE_DIR}/cmake_uninstall.cmake.in" "${CMAKE_CURRENT_BINARY_DIR}/cmake_uninstall.cmake" IMMEDIATE @ONLY) add_custom_target(uninstall COMMAND ${CMAKE_COMMAND} -P ${CMAKE_CURRENT_BINARY_DIR}/cmake_uninstall.cmake) endif() endif() endif() target_include_directories(${PROJECT_NAME} PRIVATE appbundle) target_link_libraries(${PROJECT_NAME} PRIVATE appbundle) target_compile_definitions(${PROJECT_NAME} PRIVATE VERSION="${APP_VERSION}" BUILD="${APP_BUILD}" YEAR="${APP_YEAR}") target_compile_definitions(appbundle PRIVATE VERSION="${APP_VERSION}" BUILD="${APP_BUILD}" YEAR="${APP_YEAR}") target_compile_definitions(ui PRIVATE VERSION="${APP_VERSION}" BUILD="${APP_BUILD}" YEAR="${APP_YEAR}") ================================================ FILE: app/addglyphdialog.cpp ================================================ #include "addglyphdialog.h" #include "./ui_addglyphdialog.h" #include "facewidget.h" #include "qfontfacereader.h" AddGlyphDialog::AddGlyphDialog(const FontFaceViewModel& faceViewModel, QWidget *parent) : QDialog(parent), ui_ { new Ui::AddGlyphDialog } { ui_->setupUi(this); faceScene_->setBackgroundBrush(QBrush(Qt::lightGray)); faceWidget_ = new FaceWidget(7); ui_->titleLabel->setText(tr("Add a new Glyph to %1").arg(faceViewModel.faceInfo().fontName)); ui_->faceGraphicsView->setScene(faceScene_.get()); ui_->faceGraphicsView->scene()->addItem(faceWidget_); faceWidget_->load(faceViewModel.face(), f2b::font::margins {}); connect(faceWidget_, &FaceWidget::currentGlyphIndexChanged, ui_->copyRadio, &QRadioButton::click); connect(faceWidget_, &FaceWidget::currentGlyphIndexChanged, [&, faceViewModel](std::optional index) { if (index.has_value()) newGlyph_ = faceViewModel.face().glyph_at(index.value()); }); connect(ui_->buttonBox, &QDialogButtonBox::accepted, [&, faceViewModel] { if (ui_->emptyRadio->isChecked()) { newGlyph_ = f2b::font::glyph { faceViewModel.face().glyphs_size() }; } else if (ui_->characterRadio->isChecked()) { QFontFaceReader adapter { faceViewModel.font().value(), ui_->characterLineEdit->text().toStdString(), faceViewModel.face().glyphs_size() }; newGlyph_ = f2b::font::face(adapter).glyph_at(0); } emit glyphSelected(newGlyph_); }); if (faceViewModel.font().has_value()) { ui_->characterRadio->setEnabled(true); ui_->characterErrorLabel->setVisible(false); ui_->characterLineEdit->setVisible(true); } else { ui_->characterRadio->setEnabled(false); ui_->characterErrorLabel->setVisible(true); ui_->characterLineEdit->setVisible(false); } } AddGlyphDialog::~AddGlyphDialog() { delete ui_; } ================================================ FILE: app/addglyphdialog.h ================================================ #ifndef ADDGLYPHDIALOG_H #define ADDGLYPHDIALOG_H #include #include #include #include #include "fontfaceviewmodel.h" #include "f2b.h" namespace Ui { class AddGlyphDialog; } class FaceWidget; class AddGlyphDialog : public QDialog { Q_OBJECT public: explicit AddGlyphDialog(const FontFaceViewModel& faceViewModel, QWidget *parent = nullptr); ~AddGlyphDialog(); signals: void glyphSelected(const std::optional& glyph); private: Ui::AddGlyphDialog *ui_; FaceWidget *faceWidget_ { nullptr }; std::unique_ptr faceScene_ { std::make_unique() }; std::optional newGlyph_ {}; }; #endif // ADDGLYPHDIALOG_H ================================================ FILE: app/addglyphdialog.ui ================================================ AddGlyphDialog 0 0 624 539 624 16777215 Add New Glyph 8 Adding Glyph for Add empty Glyph true true Initialize with font character: false Current document font not found in system true 1 Type character here Qt::Horizontal 40 20 Copy existing Glyph: true Qt::Horizontal QDialogButtonBox::Cancel|QDialogButtonBox::Ok emptyRadio characterRadio characterLineEdit copyRadio faceGraphicsView buttonBox rejected() AddGlyphDialog reject() 328 534 286 274 characterLineEdit textChanged(QString) characterRadio click() 583 86 179 78 buttonBox accepted() AddGlyphDialog accept() 378 522 8 344 ================================================ FILE: app/assets.qrc ================================================ assets/code.svg assets/copy.svg assets/delete.svg assets/paste.svg assets/print.svg assets/redo.svg assets/reset.svg assets/save.svg assets/undo.svg assets/font.svg assets/clear.svg assets/settings.svg assets/open.svg assets/add-glyph.svg assets/delete-glyph.svg assets/icon/fontedit256.png assets/icon/fontedit96.png assets/icon/fontedit96@2x.png l10n/fontedit_en.qm ================================================ FILE: app/cmake_uninstall.cmake.in ================================================ if(NOT EXISTS "@CMAKE_BINARY_DIR@/install_manifest.txt") message(FATAL_ERROR "Cannot find install manifest: @CMAKE_BINARY_DIR@/install_manifest.txt") endif(NOT EXISTS "@CMAKE_BINARY_DIR@/install_manifest.txt") file(READ "@CMAKE_BINARY_DIR@/install_manifest.txt" files) string(REGEX REPLACE "\n" ";" files "${files}") foreach(file ${files}) message(STATUS "Uninstalling $ENV{DESTDIR}${file}") if(IS_SYMLINK "$ENV{DESTDIR}${file}" OR EXISTS "$ENV{DESTDIR}${file}") exec_program( "@CMAKE_COMMAND@" ARGS "-E remove \"$ENV{DESTDIR}${file}\"" OUTPUT_VARIABLE rm_out RETURN_VALUE rm_retval ) if(NOT "${rm_retval}" STREQUAL 0) message(FATAL_ERROR "Problem when removing $ENV{DESTDIR}${file}") endif(NOT "${rm_retval}" STREQUAL 0) else(IS_SYMLINK "$ENV{DESTDIR}${file}" OR EXISTS "$ENV{DESTDIR}${file}") message(STATUS "File $ENV{DESTDIR}${file} does not exist.") endif(IS_SYMLINK "$ENV{DESTDIR}${file}" OR EXISTS "$ENV{DESTDIR}${file}") endforeach(file) ================================================ FILE: app/command.h ================================================ #ifndef COMMAND_H #define COMMAND_H #include #include "facewidget.h" #include "mainwindowmodel.h" class Command : public QUndoCommand { public: Command(const QString& name, std::function undo, std::function redo, QUndoCommand *parent = nullptr) : QUndoCommand(name, parent), undo_ { undo }, redo_ { redo } {} void undo() override { undo_(); } void redo() override { redo_(); } int id() const override { return -1; } protected: std::function undo_; std::function redo_; }; class SwitchActiveGlyphCommand : public QUndoCommand { public: SwitchActiveGlyphCommand(FaceWidget* faceWidget, MainWindowModel* viewModel, std::size_t fromIndex, std::size_t toIndex, QUndoCommand *parent = nullptr) : QUndoCommand(QObject::tr("Switch Active Glyph"), parent), faceWidget_ { faceWidget }, viewModel_ { viewModel }, fromIndex_ { fromIndex }, toIndex_ { toIndex } {} void undo() override { faceWidget_->setCurrentGlyphIndex(fromIndex_); viewModel_->setActiveGlyphIndex(fromIndex_); } void redo() override { faceWidget_->setCurrentGlyphIndex(toIndex_); viewModel_->setActiveGlyphIndex(toIndex_); } bool isObsolete() const { return fromIndex_ == toIndex_; } int id() const override { return 0xa5b939e9; } bool mergeWith(const QUndoCommand *other) override { if (other->id() != id()) return false; auto otherCommand = static_cast(other); toIndex_ = otherCommand->toIndex_; return true; } void setToIndex(std::size_t index) { toIndex_ = index; } private: FaceWidget* faceWidget_; MainWindowModel* viewModel_; std::size_t fromIndex_; std::size_t toIndex_; }; #endif // COMMAND_H ================================================ FILE: app/common/CMakeLists.txt ================================================ cmake_minimum_required(VERSION 3.9) set(CMAKE_AUTOUIC OFF) set(CMAKE_AUTOMOC OFF) set(CMAKE_AUTORCC OFF) project(common LANGUAGES CXX) find_package(Qt5 COMPONENTS Core Gui Widgets REQUIRED) add_library(common common.h f2b_qt_compat.cpp f2b_qt_compat.h) target_include_directories(common PUBLIC ${CMAKE_CURRENT_SOURCE_DIR}) target_link_libraries(common PRIVATE Qt5::Widgets Qt5::Core font2bytes) ================================================ FILE: app/common/common.h ================================================ #ifndef COMMON_H #define COMMON_H #include namespace Color { static constexpr QRgb activeGlyph = 0xff000000; static constexpr QRgb inactiveGlyph = 0xffe2e2e2; static constexpr QRgb glyphMargin = 0xffe2e2e2; static constexpr QRgb inactiveText = 0xffc8c8c8; } #if defined(Q_OS_MAC) static const QString consoleFontName = "Monaco"; #elif defined(Q_OS_WIN) static const QString consoleFontName = "Consolas"; #else static const QString consoleFontName = "Monaco"; #endif #endif // COMMON_H ================================================ FILE: app/common/f2b_qt_compat.cpp ================================================ #include "f2b_qt_compat.h" static constexpr quint32 font_glyph_magic_number = 0x92588c12; static constexpr quint32 font_face_magic_number = 0x03f59a82; static constexpr quint32 font_glyph_version = 1; static constexpr quint32 font_face_version = 2; using namespace f2b; QDataStream& operator<<(QDataStream& s, const font::glyph& glyph) { s << font_glyph_magic_number; s << font_glyph_version; s.setVersion(QDataStream::Qt_5_7); s << (quint32) glyph.size().width; s << (quint32) glyph.size().height; s << glyph.pixels(); return s; } QDataStream& operator>>(QDataStream& s, font::glyph& glyph) { quint32 magic_number; quint32 version; s >> magic_number >> version; if (magic_number == font_glyph_magic_number && version == font_glyph_version) { s.setVersion(QDataStream::Qt_5_7); quint32 width, height; s >> width >> height; std::vector pixels; pixels.reserve(width * height); s >> pixels; glyph = font::glyph({width, height}, pixels); } return s; } QDataStream& operator<<(QDataStream& s, const font::face& face) { s << font_face_magic_number; s << font_face_version; s.setVersion(QDataStream::Qt_5_7); s << (quint32) face.glyphs_size().width; s << (quint32) face.glyphs_size().height; s << face.glyphs(); s << face.exported_glyph_ids(); return s; } QDataStream& operator>>(QDataStream& s, font::face& face) { quint32 magic_number; quint32 version; s >> magic_number >> version; if (magic_number == font_face_magic_number) { s.setVersion(QDataStream::Qt_5_7); quint32 width, height; s >> width >> height; std::vector glyphs; s >> glyphs; std::set exported_glyph_ids; if (version < 2) { for (std::size_t i = 0; i < glyphs.size(); i++) { exported_glyph_ids.insert(i); } } else { s >> exported_glyph_ids; } face = font::face({width, height}, glyphs, exported_glyph_ids); } return s; } QVariant to_qvariant(const source_code::indentation& i) { if (std::holds_alternative(i)) { return QVariant(-1); } else if (std::holds_alternative(i)) { return QVariant((uint)std::get(i).num_spaces); } return QVariant(); } source_code::indentation from_qvariant(const QVariant& v) { bool ok; auto intValue = v.toInt(&ok); if (ok && intValue == -1) { return source_code::tab {}; } auto uintValue = v.toUInt(&ok); if (ok) { return source_code::space { uintValue }; } return source_code::tab {}; } ================================================ FILE: app/common/f2b_qt_compat.h ================================================ #ifndef F2B_QT_COMPAT_H #define F2B_QT_COMPAT_H #include "f2b.h" #include #include #include #include #include #include "common.h" #include #include #include #include #include namespace f2b { namespace font { inline font::point point_with_qpoint(const QPoint &p) { return { static_cast(qMax(0, p.x())), static_cast(qMax(0, p.y())) }; } inline QPoint qpoint_with_point(const font::point &p) { return QPoint { static_cast(p.x), static_cast(p.y) }; } inline font::glyph_size size_with_qsize(const QSize &s) { return { static_cast(qMax(0, s.width())), static_cast(qMax(0, s.height())) }; } inline QSize qsize_with_size(const font::glyph_size &s) { return QSize { static_cast(s.width), static_cast(s.height) }; } inline QImage glyph_preview_image(const font::glyph &g, font::margins m) { auto useful_glyph_size = g.size(); useful_glyph_size.height -= m.top + m.bottom; auto useful_image_size = qsize_with_size(useful_glyph_size); QImage image(useful_image_size, QImage::Format_Mono); image.fill(1); for (std::vector::size_type y = 0; y < useful_glyph_size.height; ++y) { for (std::vector::size_type x = 0; x < useful_glyph_size.width; ++x) { if (g.is_pixel_set({x, y + m.top})) { image.setPixel(x, y, 0); } } } return image; } } // namespace Font } // namespace f2b static constexpr quint32 std_optional_magic_number = 0x46b13680; static constexpr quint32 std_vector_magic_number = 0x30612113; static constexpr quint32 std_set_magic_number = 0x254c2e1e; static constexpr quint32 std_unordered_map_magic_number = 0xc9eb6edf; static constexpr quint32 std_optional_version = 1; static constexpr quint32 std_vector_version = 1; static constexpr quint32 std_set_version = 1; static constexpr quint32 std_unordered_map_version = 1; template inline QDataStream& operator<<(QDataStream& s, const std::optional& opt) { s << std_optional_magic_number; s << std_optional_version; s.setVersion(QDataStream::Qt_5_7); if (opt.has_value()) { s << true << QVariant(opt.value()); } else { s << false; } return s; } template inline QDataStream& operator>>(QDataStream& s, std::optional& opt) { quint32 magic_number; quint32 version; s >> magic_number >> version; if (magic_number == std_optional_magic_number && version == std_optional_version) { s.setVersion(QDataStream::Qt_5_7); bool has_value; s >> has_value; QVariant value; if (has_value) { s >> value; opt = value.value(); } else { opt = {}; } } return s; } template inline QDataStream& operator<<(QDataStream& s, const std::vector& vec) { s << std_vector_magic_number; s << std_vector_version; s.setVersion(QDataStream::Qt_5_7); s << (quint32) vec.size(); for (const auto& value : vec) { s << value; } return s; } template inline QDataStream& operator>>(QDataStream& s, std::vector& vec) { quint32 magic_number; quint32 version; s >> magic_number >> version; if (magic_number == std_vector_magic_number && version == std_vector_version) { s.setVersion(QDataStream::Qt_5_7); quint32 size; s >> size; vec.clear(); vec.reserve(size); T value; for (quint32 i = 0; i < size; ++i) { s >> value; vec.push_back(value); } } return s; } template inline QDataStream& operator<<(QDataStream& s, const std::set& set) { s << std_set_magic_number; s << std_set_version; s.setVersion(QDataStream::Qt_5_7); s << (quint32) set.size(); for (const auto& value : set) { s << value; } return s; } template inline QDataStream& operator>>(QDataStream& s, std::set& set) { quint32 magic_number; quint32 version; s >> magic_number >> version; if (magic_number == std_set_magic_number && version == std_set_version) { s.setVersion(QDataStream::Qt_5_7); quint32 size; s >> size; set.clear(); T value; for (quint32 i = 0; i < size; ++i) { s >> value; set.insert(value); } } return s; } template<> inline QDataStream& operator<<(QDataStream& s, const std::set& set) { s << std_set_magic_number; s << std_set_version; s.setVersion(QDataStream::Qt_5_7); s << (quint32) set.size(); for (const auto& value : set) { s << (quint32) value; } return s; } template<> inline QDataStream& operator>>(QDataStream& s, std::set& set) { quint32 magic_number; quint32 version; s >> magic_number >> version; if (magic_number == std_set_magic_number && version == std_set_version) { s.setVersion(QDataStream::Qt_5_7); quint32 size; s >> size; set.clear(); quint32 value; for (quint32 i = 0; i < size; ++i) { s >> value; set.insert(static_cast(value)); } } return s; } template inline QDataStream& operator<<(QDataStream& s, const std::unordered_map& map) { s << std_unordered_map_magic_number; s << std_unordered_map_version; s.setVersion(QDataStream::Qt_5_7); s << (quint32) map.size(); for (const auto& [k,v] : map) { s << k; s << v; } return s; } template inline QDataStream& operator<<(QDataStream& s, const std::unordered_map& map) { s << std_unordered_map_magic_number; s << std_unordered_map_version; s.setVersion(QDataStream::Qt_5_7); s << (quint32) map.size(); for (const auto& [k,v] : map) { s << (quint32) k; s << v; } return s; } template inline QDataStream& operator>>(QDataStream& s, std::unordered_map& map) { quint32 magic_number; quint32 version; s >> magic_number >> version; if (magic_number == std_unordered_map_magic_number && version == std_unordered_map_version) { s.setVersion(QDataStream::Qt_5_7); quint32 size; s >> size; map.clear(); map.reserve(size); K key; V value; for (quint32 i = 0; i < size; ++i) { s >> key; s >> value; map[key] = value; } } return s; } template inline QDataStream& operator>>(QDataStream& s, std::unordered_map& map) { quint32 magic_number; quint32 version; s >> magic_number >> version; if (magic_number == std_unordered_map_magic_number && version == std_unordered_map_version) { s.setVersion(QDataStream::Qt_5_7); quint32 size; s >> size; map.clear(); map.reserve(size); quint32 key; V value; for (quint32 i = 0; i < size; ++i) { s >> key; s >> value; map[static_cast(key)] = value; } } return s; } QDataStream& operator<<(QDataStream& s, const f2b::font::glyph& glyph); QDataStream& operator>>(QDataStream& s, f2b::font::glyph& glyph); QDataStream& operator<<(QDataStream& s, const f2b::font::face& face); QDataStream& operator>>(QDataStream& s, f2b::font::face& face); QVariant to_qvariant(const f2b::source_code::indentation& i); f2b::source_code::indentation from_qvariant(const QVariant& v); #endif // F2B_QT_COMPAT_H ================================================ FILE: app/fontfaceviewmodel.cpp ================================================ #include "fontfaceviewmodel.h" #include "f2b.h" #include "f2b_qt_compat.h" #include "qfontfacereader.h" #include #include #include #include #include #include f2b::font::face import_face(const QFont &font) { QFontFaceReader adapter(font); return f2b::font::face(adapter); } QString font_name(const QFont &font) { QStringList s; s << QString("%1 %2pt").arg(font.family()).arg(QString::number(font.pointSize())); if (font.bold()) { s << "Bold"; } if (font.italic()) { s << "Italic"; } if (font.underline()) { s << "Underline"; } if (font.strikeOut()) { s << "Strikeout"; } return s.join(", "); } FontFaceViewModel::FontFaceViewModel(const QString& documentFilePath) { QFile f(documentFilePath); if (!f.exists() || !f.permissions().testFlag(QFileDevice::ReadUser)) { throw std::runtime_error { "Unable to open file " + documentFilePath.toStdString() }; } f.open(QIODevice::ReadOnly); QDataStream s(&f); s >> *this; f.close(); isDirty_ = false; } FontFaceViewModel::FontFaceViewModel(f2b::font::face face, std::optional name) noexcept : face_ { face }, name_ { name }, originalMargins_ { face.calculate_margins() } { } FontFaceViewModel::FontFaceViewModel(const QFont &font) : FontFaceViewModel(import_face(font), font_name(font)) { font_ = font; isDirty_ = true; } void FontFaceViewModel::saveToFile(const QString &documentPath) { QFile f(documentPath); QFile directory(QFileInfo(documentPath).path()); if (!directory.permissions().testFlag(QFileDevice::WriteUser) || (f.exists() && !f.permissions().testFlag(QFileDevice::WriteUser))) { throw std::runtime_error { "Unable to write to file: " + documentPath.toStdString() }; } f.open(QIODevice::WriteOnly); QDataStream s(&f); s << *this; f.close(); isDirty_ = false; } FaceInfo FontFaceViewModel::faceInfo() const { auto fontName = name_.has_value() ? name_.value() : QObject::tr("Custom font"); auto size = face_.glyphs_size(); size.height -= originalMargins_.top + originalMargins_.bottom; return { fontName, face_.glyphs_size(), size, face_.num_glyphs(), face_.exported_glyph_ids().size() }; } void FontFaceViewModel::modifyGlyph(std::size_t index, const f2b::font::glyph &new_glyph) { doModifyGlyph(index, [&](f2b::font::glyph &glyph) { glyph = new_glyph; }); } void FontFaceViewModel::modifyGlyph(std::size_t index, const BatchPixelChange &change, BatchPixelChange::ChangeType changeType) { doModifyGlyph(index, [&](f2b::font::glyph& glyph) { change.apply(glyph, changeType); }); } void FontFaceViewModel::doModifyGlyph(std::size_t idx, std::function change) { f2b::font::glyph& glyph { face_.glyph_at(idx) }; bool first_change = false; if (originalGlyphs_.count(idx) == 0) { qDebug() << "active_glyph non-const cache miss"; originalGlyphs_.insert({ idx, glyph }); first_change = true; } else { qDebug() << "active_glyph non-const cache hit"; } change(glyph); isDirty_ = true; // remove glyph from originals when restoring initial state if (!first_change && glyph == originalGlyphs_.at(idx)) { originalGlyphs_.erase(idx); } } void FontFaceViewModel::reset() { face_ = originalFace(); originalGlyphs_.clear(); } void FontFaceViewModel::resetGlyph(std::size_t index) { if (isGlyphModified(index)) { face_.set_glyph(originalGlyphs_.at(index), index); originalGlyphs_.erase(activeGlyphIndex_.value()); isDirty_ = true; } } void FontFaceViewModel::appendGlyph(f2b::font::glyph newGlyph) { face_.append_glyph(std::move(newGlyph)); isDirty_ = true; } void FontFaceViewModel::deleteGlyph(std::size_t index) { if (index == face_.num_glyphs() - 1) { if (activeGlyphIndex_.has_value() && activeGlyphIndex_.value() == face_.num_glyphs() - 1) { activeGlyphIndex_ = face_.num_glyphs() - 2; } face_.delete_last_glyph(); } else { face_.clear_glyph(index); } isDirty_ = true; } f2b::font::face FontFaceViewModel::originalFace() const noexcept { f2b::font::face f = face_; for (const auto& pair : originalGlyphs_) { f.set_glyph(pair.second, pair.first); } return f; } static constexpr auto fontfaceviewmodel_magic_number = 0x1c22f998; static constexpr auto fontfaceviewmodel_version = 2; QDataStream& operator<<(QDataStream& s, const FontFaceViewModel &vm) { s << (quint32) fontfaceviewmodel_magic_number; s << (qint32) fontfaceviewmodel_version; s.setVersion(QDataStream::Qt_5_7); s << vm.face_; s << vm.name_; s << (quint32) vm.originalMargins_.top << (quint32) vm.originalMargins_.bottom; s << vm.font_; return s; } QDataStream& operator>>(QDataStream& s, FontFaceViewModel& vm) { quint32 magic_number; quint32 version; s >> magic_number >> version; if (magic_number == fontfaceviewmodel_magic_number && version <= fontfaceviewmodel_version) { s.setVersion(QDataStream::Qt_5_7); s >> vm.face_; s >> vm.name_; quint32 top, bottom; s >> top >> bottom; vm.originalMargins_ = { top, bottom }; vm.originalGlyphs_ = {}; vm.activeGlyphIndex_ = {}; if (version <= 2) { s >> vm.font_; } } return s; } ================================================ FILE: app/fontfaceviewmodel.h ================================================ #ifndef FONTFACEVIEWMODEL_H #define FONTFACEVIEWMODEL_H #include #include "f2b.h" #include #include #include #include #include "batchpixelchange.h" struct FaceInfo { QString fontName; f2b::font::glyph_size size; f2b::font::glyph_size sizeWithoutMargins; std::size_t numberOfGlyphs; std::size_t numberOfExportedGlyphs; }; class FontFaceViewModel { public: explicit FontFaceViewModel() = default; explicit FontFaceViewModel(const QString& documentPath); explicit FontFaceViewModel(f2b::font::face face, std::optional name) noexcept; explicit FontFaceViewModel(const QFont& font); void saveToFile(const QString& documentPath); std::optional font() const noexcept { return font_; } const f2b::font::face& face() const noexcept { return face_; } f2b::font::face& face() noexcept { return face_; } FaceInfo faceInfo() const; f2b::font::face originalFace() const noexcept; f2b::font::margins originalFaceMargins() const noexcept { return originalMargins_; } void setGlyphExportedState(std::size_t idx, bool isExported) { if (idx >= face_.num_glyphs()) { throw std::out_of_range("Active glyph index higher than number of glyphs."); } if (isExported) { face_.exported_glyph_ids().insert(idx); } else { face_.exported_glyph_ids().erase(idx); } isDirty_ = true; } void setActiveGlyphIndex(std::optional idx) { if (idx.has_value() && idx.value() >= face_.num_glyphs()) { throw std::out_of_range("Active glyph index higher than number of glyphs."); } activeGlyphIndex_ = idx; } std::optional activeGlyphIndex() const noexcept { return activeGlyphIndex_; } std::optional activeGlyph() const { if (activeGlyphIndex_.has_value()) { return face_.glyph_at(activeGlyphIndex_.value()); } return {}; } void resetActiveGlyph() { if (!activeGlyphIndex_.has_value()) { return; } resetGlyph(activeGlyphIndex_.value()); } void reset(); void resetGlyph(std::size_t index); void modifyGlyph(std::size_t index, const f2b::font::glyph& new_glyph); void modifyGlyph(std::size_t index, const BatchPixelChange& change, BatchPixelChange::ChangeType changeType = BatchPixelChange::ChangeType::Normal); void appendGlyph(f2b::font::glyph newGlyph); void deleteGlyph(std::size_t index); bool isModified() const { return originalGlyphs_.size() > 0; } bool isGlyphModified(std::size_t idx) const { // modified glyphs have their unmodified counterparts stored in originalGlyphs_. return originalGlyphs_.count(idx) == 1; } bool isModifiedSinceSave() const { return isDirty_; } private: void doModifyGlyph(std::size_t idx, std::function change); f2b::font::face face_; std::optional name_; f2b::font::margins originalMargins_; // this holds copies of unmodified glyphs once they are edited. std::unordered_map originalGlyphs_; std::optional font_; // not persisted std::optional activeGlyphIndex_; bool isDirty_ { false }; friend QDataStream& operator<<(QDataStream&, const FontFaceViewModel&); friend QDataStream& operator>>(QDataStream& s, FontFaceViewModel& vm); }; QDataStream& operator<<(QDataStream& s, const FontFaceViewModel& vm); QDataStream& operator>>(QDataStream& s, FontFaceViewModel& vm); #endif // FONTFACEVIEWMODEL_H ================================================ FILE: app/global.h ================================================ #ifndef GLOBAL_H #define GLOBAL_H #include namespace Global { static const QString organization_name = "Dominik Kapusta"; static const QString organization_domain = "kapusta.cc"; static const QString application_name = "FontEdit"; static constexpr std::string_view application_version = VERSION; } #endif // GLOBAL_H ================================================ FILE: app/l10n/fontedit_en.ts ================================================ AboutDialog About FontEdit <html><head/><body><p><span style=" font-size:18pt;">FontEdit </span><span style=" font-size:13pt; font-weight:400;">v##version## build ##build##</span></p><p><span style=" font-size:13pt; font-weight:400;">Copyright ##year## Dominik Kapusta</span></p><p align="center"><a href="https://github.com/ayoy/fontedit"><span style=" font-size:13pt; font-weight:400; text-decoration: underline; color:#0068da;">Get Source Code</span></a><span style=" font-size:13pt; font-weight:400;">•</span><a href="https://github.com/ayoy/fontedit/issues"><span style=" font-size:13pt; font-weight:400; text-decoration: underline; color:#0068da;">Report a Bug</span></a></p><p><span style=" font-size:13pt; font-weight:400;">This program is distributed under the terms of </span><a href="https://www.gnu.org/licenses/gpl-3.0.en.html"><span style=" font-size:13pt; font-weight:400; text-decoration: underline; color:#0068da;">General Public License v3</span></a><span style=" font-size:13pt;">.</span></p><p><span style=" font-size:13pt; font-weight:400;"><br/>Icons made by </span><a href="https://www.flaticon.com/authors/smashicons"><span style=" font-size:13pt; font-weight:400; text-decoration: underline; color:#0068da;">Smashicons</span></a><span style=" font-size:13pt; font-weight:400;"> and </span><a href="https://www.flaticon.com/authors/freepik"><span style=" font-size:13pt; font-weight:400; text-decoration: underline; color:#0068da;">Freepik</span></a><span style=" font-size:13pt; font-weight:400;"> from </span><a href="https://www.flaticon.com/"><span style=" font-size:13pt; font-weight:400; text-decoration: underline; color:#0068da;">www.flaticon.com</span></a><span style=" font-size:13pt; font-weight:400;">.</span><a href="https://www.flaticon.com/"><span style=" font-size:13pt; font-weight:400; text-decoration: underline; color:#0068da;"><br/></span></a></p></body></html> AddGlyphDialog Add New Glyph Adding Glyph for Add empty Glyph Initialize with font character: Current document font not found in system Type character here Copy existing Glyph: Add a new Glyph to %1 GlyphInfoWidget Exported MainWindow Edit Font Ctrl+R TextLabel Source Code Updating source code... Output Format Misc Options Invert Bits Reverse Bits (MSB) Print FontEdit Toggle exported state by pressing Space on a selected glyph Show non-exported Glyphs Export Selected Glyphs Export All Glyphs Include Line Spacing Font Array Name font Indentation Export Source Code... File Edit Help View New Ctrl+N Import Font Reset Glyph Export... Save Ctrl+S Reset Font Print... Ctrl+P Save As... Ctrl+Shift+S Add Glyph Ctrl+Shift+N Quit Ctrl+Q Copy Glyph Paste Glyph Open... Ctrl+O Recent files Close About FontEdit Delete Glyph Check for Updates... Check for updates at start-up Update Available FontEdit %1 is available (you have %2). Get the new version from GitHub Releases Page. Go to Releases Page Dismiss No Update Available You're using the latest version of FontEdit Start by importing a system font or opening an existing document Select a glyph on the right to edit it Click and drag to paint, hold Alt or Ctrl to erase. Select Font Open Document You can only delete Glyphs at the end of the list. This Glyph will be cleared instead of deleted (to ensure that other Glyphs' indexes are unchanged). Clear Glyph Save Document Error Don't Save Cancel Do you want to save the changes you made? Your changes will be lost if you don't save them. Toggle Glyph Exported Size (full): %1x%2px Size (adjusted): %1x%2px %n Glyph(s) %n Glyph %n Glyphs %n to export %n to export %n to export Edit Glyph Are you sure you want to reset all changes to the font? This operation cannot be undone. Save Source Code Source code successfully exported. Unable to write to file: MainWindowModel Tab %n Space(s) %n Space %n Spaces New Document Edited QObject Switch Active Glyph Custom font ================================================ FILE: app/macos/Info.plist ================================================ CFBundleDevelopmentRegion English CFBundleExecutable FontEdit CFBundleGetInfoString CFBundleIconFile fontedit.icns CFBundleIdentifier cc.kapusta.FontEdit CFBundleInfoDictionaryVersion 6.0 CFBundleLongVersionString CFBundleName CFBundlePackageType APPL CFBundleShortVersionString 1.1.0 CFBundleSignature ???? CFBundleVersion 4 CSResourcesFileMapped LSMinimumSystemVersion 10.14 NSHumanReadableCopyright NSPrincipalClass NSApplication NSHighResolutionCapable True NSRequiresAquaSystemAppearance ================================================ FILE: app/main.cpp ================================================ #include "mainwindow.h" #include "global.h" #include #include int main(int argc, char *argv[]) { QApplication a(argc, argv); QApplication::setOrganizationName(Global::organization_name); QApplication::setOrganizationDomain(Global::organization_domain); QApplication::setApplicationName(Global::application_name); QApplication::setApplicationVersion(QString::fromStdString(std::string(Global::application_version))); QTranslator myappTranslator; myappTranslator.load(":/l10n/fontedit_en.qm"); a.installTranslator(&myappTranslator); a.setAttribute(Qt::AA_UseHighDpiPixmaps); MainWindow w; w.show(); return QApplication::exec(); } ================================================ FILE: app/mainwindow.cpp ================================================ #include "mainwindow.h" #include "./ui_mainwindow.h" #include "f2b.h" #include "facewidget.h" #include "fontfaceviewmodel.h" #include "command.h" #include "aboutdialog.h" #include "addglyphdialog.h" #include "common.h" #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include static constexpr auto codeTabIndex = 1; static constexpr auto exportAllButtonIndex = -3; static constexpr auto fileFilter = "FontEdit documents (*.fontedit)"; MainWindow::MainWindow(QWidget *parent) : QMainWindow(parent), ui_ { new Ui::MainWindow }, statusLabel_ { new QLabel() } { ui_->setupUi(this); initUI(); setupActions(); updateUI(viewModel_->uiState()); connectUpdateHelper(); connectUIInputs(); connectViewModelOutputs(); viewModel_->restoreSession(); ui_->statusBar->addPermanentWidget(statusLabel_); updateHelper_->checkForUpdatesIfNeeded(); } MainWindow::~MainWindow() { delete ui_; } void MainWindow::connectUpdateHelper() { connect(updateHelper_.get(), &UpdateHelper::updateAvailable, [&] (UpdateHelper::Update update) { showUpdateDialog(update); }); connect(updateHelper_.get(), &UpdateHelper::updateNotAvailable, [&] { showUpdateDialog({}); }); } void MainWindow::connectUIInputs() { connect(ui_->actionAbout, &QAction::triggered, this, &MainWindow::showAboutDialog); connect(ui_->actionImport_Font, &QAction::triggered, this, &MainWindow::showFontDialog); connect(ui_->actionOpen, &QAction::triggered, this, &MainWindow::showOpenDocumentDialog); connect(ui_->actionAdd_Glyph, &QAction::triggered, this, &MainWindow::showAddGlyphDialog); connect(ui_->actionDelete_Glyph, &QAction::triggered, this, &MainWindow::showDeleteGlyphDialog); connect(ui_->actionReset_Glyph, &QAction::triggered, this, &MainWindow::resetCurrentGlyph); connect(ui_->actionReset_Font, &QAction::triggered, this, &MainWindow::resetFont); connect(ui_->actionSave, &QAction::triggered, this, &MainWindow::save); connect(ui_->actionSave_As, &QAction::triggered, this, &MainWindow::saveAs); connect(ui_->actionClose, &QAction::triggered, this, &MainWindow::showCloseDocumentDialogIfNeeded); connect(ui_->actionExport, &QAction::triggered, this, &MainWindow::exportSourceCode); connect(ui_->exportButton, &QPushButton::clicked, this, &MainWindow::exportSourceCode); connect(ui_->actionQuit, &QAction::triggered, this, &MainWindow::close); connect(ui_->tabWidget, &QTabWidget::currentChanged, [&](int index) { if (index == codeTabIndex) { displaySourceCode(); viewModel_->registerInputEvent(UIState::InterfaceAction::ActionTabCode); } else { viewModel_->registerInputEvent(UIState::InterfaceAction::ActionTabEdit); } }); connect(ui_->exportMethodButtonGroup, QOverload::of(&QButtonGroup::buttonClicked), [&](int buttonID) { viewModel_->setExportAllEnabled(buttonID == exportAllButtonIndex); }); connect(ui_->invertBitsCheckBox, &QCheckBox::stateChanged, [&](int state) { viewModel_->setInvertBits(state == Qt::Checked); }); connect(ui_->bitNumberingCheckBox, &QCheckBox::stateChanged, [&](int state) { viewModel_->setMSBEnabled(state == Qt::Checked); }); connect(ui_->lineSpacingCheckBox, &QCheckBox::stateChanged, [&](int state) { viewModel_->setIncludeLineSpacing(state == Qt::Checked); }); connect(ui_->formatComboBox, &QComboBox::currentTextChanged, viewModel_.get(), &MainWindowModel::setOutputFormat); connect(ui_->indentationComboBox, &QComboBox::currentTextChanged, viewModel_.get(), &MainWindowModel::setIndentation); connect(ui_->fontArrayNameEdit, &QLineEdit::textChanged, [&](const QString& fontArrayName) { auto fontName = fontArrayName.isEmpty() ? ui_->fontArrayNameEdit->placeholderText() : std::move(fontArrayName); debounceFontNameChanged(fontName); }); connect(ui_->actionCheck_for_Updates, &QAction::triggered, [&] { updateHelper_->checkForUpdates(true); }); } void MainWindow::connectViewModelOutputs() { connect(viewModel_.get(), &MainWindowModel::documentTitleChanged, [&](const QString& title) { setWindowTitle(QString("FontEdit (%1)").arg(title)); }); connect(viewModel_.get(), &MainWindowModel::uiStateChanged, this, &MainWindow::updateUI); connect(viewModel_.get(), &MainWindowModel::faceLoaded, [&](f2b::font::face& face) { undoStack_->clear(); displayFace(face); }); connect(viewModel_.get(), &MainWindowModel::documentError, this, &MainWindow::displayError); connect(viewModel_.get(), &MainWindowModel::activeGlyphChanged, [&](std::optional glyph) { if (glyph.has_value()) { displayGlyph(glyph.value()); } else if (auto g = glyphWidget_.get()) { ui_->glyphGraphicsView->scene()->removeItem(g); glyphWidget_.release(); } }); connect(viewModel_.get(), &MainWindowModel::sourceCodeUpdating, [&]() { // ui_->stackedWidget->setCurrentWidget(ui_->spinnerContainer); }); connect(viewModel_.get(), &MainWindowModel::sourceCodeChanged, [&]() { if (ui_->tabWidget->currentIndex() == codeTabIndex) { displaySourceCode(); } }); connect(viewModel_.get(), &MainWindowModel::documentClosed, this, &MainWindow::closeCurrentDocument); } void MainWindow::initUI() { // hide not implemented UI ui_->actionCopy_Glyph->setVisible(false); ui_->actionPaste_Glyph->setVisible(false); ui_->copyButton->setVisible(false); ui_->pasteButton->setVisible(false); ui_->printButton->setVisible(false); ui_->actionPrint->setVisible(false); ui_->actionNew->setVisible(false); faceScene_->setBackgroundBrush(QBrush(Qt::lightGray)); ui_->faceGraphicsView->setScene(faceScene_.get()); ui_->actionShow_non_exported_Glyphs->setChecked(viewModel_->shouldShowNonExportedGlyphs()); ui_->showNonExportedGlyphsCheckBox->setCheckState(viewModel_->shouldShowNonExportedGlyphs()); ui_->showNonExportedGlyphsCheckBox->setVisible(false); ui_->faceInfoSeparator->setVisible(false); ui_->faceInfoLabel->setVisible(false); auto scrollBarWidth = ui_->faceGraphicsView->verticalScrollBar()->sizeHint().width(); auto faceViewWidth = static_cast(FaceWidget::cell_width) * 3 + scrollBarWidth; ui_->faceGraphicsView->setMinimumSize({ faceViewWidth, ui_->faceGraphicsView->minimumSize().height() }); ui_->exportAllButton->setChecked(viewModel_->exportAllEnabled()); ui_->exportSubsetButton->setChecked(!viewModel_->exportAllEnabled()); ui_->invertBitsCheckBox->setCheckState(viewModel_->invertBits()); ui_->bitNumberingCheckBox->setCheckState(viewModel_->msbEnabled()); ui_->lineSpacingCheckBox->setCheckState(viewModel_->includeLineSpacing()); for (const auto& [identifier, name] : viewModel_->outputFormats().toStdMap()) { ui_->formatComboBox->addItem(name, identifier); } for (const auto& [indent, name] : viewModel_->indentationStyles()) { ui_->indentationComboBox->addItem(name); } ui_->formatComboBox->setCurrentText(viewModel_->outputFormat()); ui_->indentationComboBox->setCurrentText(viewModel_->indentationStyleCaption()); QFont f(consoleFontName, 12); f.setStyleHint(QFont::TypeWriter); ui_->sourceCodeTextBrowser->setFont(f); } void MainWindow::showUpdateDialog(std::optional update) { QMessageBox messageBox; messageBox.setCheckBox(new QCheckBox(tr("Check for updates at start-up"))); messageBox.checkBox()->setChecked(updateHelper_->shouldCheckAtStartup()); connect(messageBox.checkBox(), &QCheckBox::toggled, [&] (bool isChecked) { updateHelper_->setShouldCheckAtStartup(isChecked); }); if (update.has_value()) { auto updateInfo = update.value(); messageBox.setText(tr("Update Available")); messageBox.setInformativeText(tr("FontEdit %1 is available (you have %2).\nGet the new version from GitHub Releases Page.") .arg(updateInfo.latestVersion, updateInfo.currentVersion)); messageBox.setDetailedText(updateInfo.releaseNotes); auto visitPageButton = messageBox.addButton(tr("Go to Releases Page"), QMessageBox::YesRole); messageBox.addButton(tr("Dismiss"), QMessageBox::NoRole); messageBox.setDefaultButton(visitPageButton); messageBox.exec(); if (messageBox.clickedButton() == visitPageButton) { QDesktopServices::openUrl(updateInfo.webpageURL); } } else { messageBox.setText(tr("No Update Available")); messageBox.setInformativeText(tr("You're using the latest version of FontEdit")); messageBox.exec(); } } void MainWindow::closeCurrentDocument() { ui_->showNonExportedGlyphsCheckBox->setVisible(false); ui_->faceInfoSeparator->setVisible(false); ui_->faceInfoLabel->setVisible(false); if (faceWidget_ != nullptr) { faceScene_->removeItem(faceWidget_); delete faceWidget_; faceWidget_ = nullptr; } if (auto g = glyphWidget_.get()) { ui_->glyphGraphicsView->scene()->removeItem(g); glyphWidget_.release(); } undoStack_->clear(); updateResetActions(); } void MainWindow::setupActions() { auto undo = undoStack_->createUndoAction(this); undo->setIcon(QIcon {":/toolbar/assets/undo.svg"}); undo->setShortcut(QKeySequence::Undo); auto redo = undoStack_->createRedoAction(this); redo->setIcon(QIcon {":/toolbar/assets/redo.svg"}); redo->setShortcut(QKeySequence::Redo); ui_->openButton->setDefaultAction(ui_->actionOpen); ui_->importFontButton->setDefaultAction(ui_->actionImport_Font); ui_->addGlyphButton->setDefaultAction(ui_->actionAdd_Glyph); ui_->deleteGlyphButton->setDefaultAction(ui_->actionDelete_Glyph); ui_->saveButton->setDefaultAction(ui_->actionSave); ui_->copyButton->setDefaultAction(ui_->actionCopy_Glyph); ui_->pasteButton->setDefaultAction(ui_->actionPaste_Glyph); ui_->undoButton->setDefaultAction(undo); ui_->redoButton->setDefaultAction(redo); ui_->resetGlyphButton->setDefaultAction(ui_->actionReset_Glyph); ui_->resetFontButton->setDefaultAction(ui_->actionReset_Font); ui_->actionReset_Glyph->setEnabled(false); ui_->actionReset_Font->setEnabled(false); ui_->menuEdit->insertAction(ui_->actionCopy_Glyph, undo); ui_->menuEdit->insertAction(ui_->actionCopy_Glyph, redo); } void MainWindow::updateUI(UIState uiState) { ui_->tabWidget->setTabEnabled(1, uiState.actions[UIState::InterfaceAction::ActionTabCode]); ui_->actionAdd_Glyph->setEnabled(uiState.actions[UIState::InterfaceAction::ActionAddGlyph]); ui_->actionDelete_Glyph->setEnabled(uiState.actions[UIState::InterfaceAction::ActionDeleteGlyph]); ui_->actionSave->setEnabled(uiState.actions[UIState::InterfaceAction::ActionSave]); ui_->actionSave_As->setEnabled(uiState.actions[UIState::InterfaceAction::ActionSave]); ui_->actionClose->setEnabled(uiState.actions[UIState::InterfaceAction::ActionClose]); ui_->actionCopy_Glyph->setEnabled(uiState.actions[UIState::InterfaceAction::ActionCopy]); ui_->actionPaste_Glyph->setEnabled(uiState.actions[UIState::InterfaceAction::ActionPaste]); ui_->actionExport->setEnabled(uiState.actions[UIState::InterfaceAction::ActionExport]); ui_->actionPrint->setEnabled(uiState.actions[UIState::InterfaceAction::ActionPrint]); switch (uiState.statusBarMessage) { case UIState::MessageIdle: statusLabel_->setText(tr("Start by importing a system font or opening an existing document")); break; case UIState::MessageLoadedFace: statusLabel_->setText(tr("Select a glyph on the right to edit it")); break; case UIState::MessageLoadedGlyph: statusLabel_->setText(tr("Click and drag to paint, hold Alt or Ctrl to erase.")); break; } if (ui_->tabWidget->currentIndex() == codeTabIndex) { statusLabel_->setVisible(false); } else { statusLabel_->setVisible(true); } ui_->statusBar->clearMessage(); } QString MainWindow::defaultDialogDirectory() const { QString directoryPath = viewModel_->lastVisitedDirectory(); if (directoryPath.isNull()) { directoryPath = QStandardPaths::standardLocations(QStandardPaths::DocumentsLocation).last(); } return directoryPath; } void MainWindow::showAboutDialog() { auto about = new AboutDialog(this); about->show(); } void MainWindow::showFontDialog() { switch (promptToSaveDirtyDocument()) { case Save: save(); case DontSave: break; case Cancel: return; } bool ok; QFont f(consoleFontName, 24); f.setStyleHint(QFont::TypeWriter); f = QFontDialog::getFont(&ok, f, this, tr("Select Font"), QFontDialog::MonospacedFonts | QFontDialog::DontUseNativeDialog); if (ok) { qDebug() << "selected font:" << f; viewModel_->importFont(f); } } void MainWindow::showOpenDocumentDialog() { switch (promptToSaveDirtyDocument()) { case Save: save(); case DontSave: break; case Cancel: return; } QString fileName = QFileDialog::getOpenFileName(this, tr("Open Document"), std::move(defaultDialogDirectory()), tr(fileFilter)); if (!fileName.isNull()) viewModel_->openDocument(fileName); } void MainWindow::showCloseDocumentDialogIfNeeded() { switch (promptToSaveDirtyDocument()) { case Save: save(); case DontSave: break; case Cancel: return; } viewModel_->closeCurrentDocument(); } void MainWindow::showAddGlyphDialog() { auto addGlyph = new AddGlyphDialog(*viewModel_->faceModel(), this); addGlyph->show(); connect(addGlyph, &AddGlyphDialog::glyphSelected, [&](const std::optional& glyph) { if (glyph.has_value()) { auto numberOfGlyphs = viewModel_->faceModel()->face().num_glyphs(); auto activeGlyphIndex = viewModel_->faceModel()->activeGlyphIndex(); pushUndoCommand(new Command(tr("Add Glyph"), [&, numberOfGlyphs, activeGlyphIndex] { viewModel_->deleteGlyph(numberOfGlyphs); viewModel_->setActiveGlyphIndex(activeGlyphIndex); displayFace(viewModel_->faceModel()->face()); }, [&, glyph] { viewModel_->appendGlyph(glyph.value()); viewModel_->setActiveGlyphIndex(viewModel_->faceModel()->face().num_glyphs()-1); displayFace(viewModel_->faceModel()->face()); })); } }); } void MainWindow::showDeleteGlyphDialog() { auto currentIndex = viewModel_->faceModel()->activeGlyphIndex(); auto isLastGlyph = currentIndex.value() == viewModel_->faceModel()->face().num_glyphs() - 1; if (!isLastGlyph) { QMessageBox::information(this, tr("Delete Glyph"), tr("You can only delete Glyphs at the end of the list. " "This Glyph will be cleared instead of deleted " "(to ensure that other Glyphs' indexes are unchanged)."), QMessageBox::StandardButton::Ok); } auto glyph = viewModel_->faceModel()->activeGlyph(); if (glyph.has_value()) { auto commandName = isLastGlyph ? tr("Delete Glyph") : tr("Clear Glyph"); pushUndoCommand(new Command(commandName, [&, currentIndex, isLastGlyph, glyph] { if (isLastGlyph) { viewModel_->appendGlyph(glyph.value()); viewModel_->setActiveGlyphIndex(viewModel_->faceModel()->face().num_glyphs()-1); displayFace(viewModel_->faceModel()->face()); } else { viewModel_->modifyGlyph(currentIndex.value(), glyph.value()); faceWidget_->updateGlyphInfo(currentIndex.value(), viewModel_->faceModel()->activeGlyph().value()); displayGlyph(viewModel_->faceModel()->activeGlyph().value()); } }, [&, currentIndex, isLastGlyph] { viewModel_->deleteGlyph(currentIndex.value()); if (isLastGlyph) { displayFace(viewModel_->faceModel()->face()); } else { faceWidget_->updateGlyphInfo(currentIndex.value(), viewModel_->faceModel()->activeGlyph().value()); displayGlyph(viewModel_->faceModel()->activeGlyph().value()); } })); } } void MainWindow::save() { auto currentPath = viewModel_->currentDocumentPath(); if (currentPath.has_value()) { viewModel_->saveDocument(currentPath.value()); } else { saveAs(); } } void MainWindow::saveAs() { QString fileName = QFileDialog::getSaveFileName(this, tr("Save Document"), std::move(defaultDialogDirectory()), tr(fileFilter)); if (!fileName.isNull()) viewModel_->saveDocument(fileName); } void MainWindow::displayError(const QString &error) { QMessageBox::critical(this, tr("Error"), error, QMessageBox::StandardButton::Ok); } void MainWindow::closeEvent(QCloseEvent *event) { switch (promptToSaveDirtyDocument()) { case Save: save(); case DontSave: event->accept(); break; case Cancel: event->ignore(); } } MainWindow::SavePromptButton MainWindow::promptToSaveDirtyDocument() { if (viewModel_->faceModel() == nullptr || !viewModel_->faceModel()->isModifiedSinceSave()) { return DontSave; // ignore this dialog and move on } QStringList buttons { tr("Save"), tr("Don't Save"), tr("Cancel") }; auto ret = QMessageBox::information(this, "", tr("Do you want to save the changes you made? Your changes will be lost if you don't save them."), buttons[0], buttons[1], buttons[2], 0, 2); return static_cast(ret); } void MainWindow::displayFace(f2b::font::face& face) { if (faceWidget_ == nullptr) { faceWidget_ = new FaceWidget(); faceWidget_->setShowsNonExportedItems(viewModel_->shouldShowNonExportedGlyphs()); ui_->faceGraphicsView->scene()->addItem(faceWidget_); connect(faceWidget_, &FaceWidget::currentGlyphIndexChanged, this, &MainWindow::switchActiveGlyph); connect(faceWidget_, &FaceWidget::glyphExportedStateChanged, this, &MainWindow::setGlyphExported); connect(ui_->actionShow_non_exported_Glyphs, &QAction::toggled, [&](bool checked) { viewModel_->setShouldShowNonExportedGlyphs(checked); faceWidget_->setShowsNonExportedItems(checked); QApplication::processEvents(); ui_->faceGraphicsView->setSceneRect(faceWidget_->rect()); }); } auto margins = viewModel_->faceModel()->originalFaceMargins(); faceWidget_->load(face, margins); ui_->showNonExportedGlyphsCheckBox->setVisible(true); ui_->faceInfoSeparator->setVisible(true); auto faceInfo = viewModel_->faceModel()->faceInfo(); updateFaceInfoLabel(faceInfo); updateDefaultFontName(faceInfo); ui_->faceInfoLabel->setVisible(true); auto glyph = viewModel_->faceModel()->activeGlyph(); if (glyph.has_value()) { displayGlyph(glyph.value()); faceWidget_->setCurrentGlyphIndex(viewModel_->faceModel()->activeGlyphIndex()); viewModel_->setActiveGlyphIndex(viewModel_->faceModel()->activeGlyphIndex()); } else if (auto g = glyphWidget_.get()) { ui_->glyphGraphicsView->scene()->removeItem(g); glyphWidget_.release(); } updateResetActions(); } void MainWindow::setGlyphExported(std::size_t index, bool isExported) { pushUndoCommand(new Command(tr("Toggle Glyph Exported"), [&, index, isExported] { viewModel_->setGlyphExported(index, !isExported); auto faceModel = viewModel_->faceModel(); updateFaceInfoLabel(faceModel->faceInfo()); if (!faceWidget_->showsNonExportedItems()) { auto margins = faceModel->originalFaceMargins(); faceWidget_->load(faceModel->face(), margins); faceWidget_->setCurrentGlyphIndex(index); glyphWidget_->load(faceModel->face().glyph_at(index), margins); } else { faceWidget_->updateGlyphInfo(index, {}, !isExported); } }, [&, index, isExported] { auto faceModel = viewModel_->faceModel(); auto shouldUpdateCurrentIndex = !isExported && !faceWidget_->showsNonExportedItems(); std::optional nextIndex {}; if (shouldUpdateCurrentIndex) { // Find index of the next exported item auto i = std::next(faceModel->face().exported_glyph_ids().find(index)); if (i != faceModel->face().exported_glyph_ids().end()) { nextIndex = *i; } } viewModel_->setGlyphExported(index, isExported); updateFaceInfoLabel(faceModel->faceInfo()); if (shouldUpdateCurrentIndex) { auto margins = faceModel->originalFaceMargins(); faceWidget_->load(faceModel->face(), margins); faceWidget_->setCurrentGlyphIndex(nextIndex); if (nextIndex.has_value()) { glyphWidget_->load(faceModel->face().glyph_at(nextIndex.value()), margins); } } else { faceWidget_->updateGlyphInfo(index, {}, isExported); } })); } void MainWindow::updateFaceInfoLabel(const FaceInfo &faceInfo) { QStringList lines; lines << faceInfo.fontName; lines << tr("Size (full): %1x%2px").arg(faceInfo.size.width).arg(faceInfo.size.height); lines << tr("Size (adjusted): %1x%2px").arg(faceInfo.sizeWithoutMargins.width).arg(faceInfo.sizeWithoutMargins.height); lines << QString("%1, %2").arg(tr("%n Glyph(s)", "", faceInfo.numberOfGlyphs), tr("%n to export", "", faceInfo.numberOfExportedGlyphs)); ui_->faceInfoLabel->setText(lines.join("\n")); } void MainWindow::updateDefaultFontName(const FaceInfo &faceInfo) { auto fontName = faceInfo.fontName.toLower(); fontName.remove(','); fontName.replace(' ', '_'); ui_->fontArrayNameEdit->setText(fontName); } void MainWindow::displayGlyph(const f2b::font::glyph& glyph) { auto margins = viewModel_->faceModel()->originalFaceMargins(); if (!glyphWidget_.get()) { glyphWidget_ = std::make_unique(glyph, margins); ui_->glyphGraphicsView->scene()->addItem(glyphWidget_.get()); connect(glyphWidget_.get(), &GlyphWidget::pixelsChanged, this, &MainWindow::editGlyph); } else { glyphWidget_->load(glyph, margins); } updateResetActions(); ui_->glyphGraphicsView->fitInView(glyphWidget_->boundingRect(), Qt::KeepAspectRatio); } void MainWindow::editGlyph(const BatchPixelChange& change) { auto currentIndex = viewModel_->faceModel()->activeGlyphIndex(); if (currentIndex.has_value()) { auto applyChange = [&, currentIndex, change](BatchPixelChange::ChangeType type) -> std::function { return [&, currentIndex, change, type] { viewModel_->modifyGlyph(currentIndex.value(), change, type); updateResetActions(); glyphWidget_->applyChange(change, type); faceWidget_->updateGlyphInfo(currentIndex.value(), viewModel_->faceModel()->activeGlyph().value()); viewModel_->updateDocumentTitle(); }; }; pushUndoCommand(new Command(tr("Edit Glyph"), applyChange(BatchPixelChange::ChangeType::Reverse), applyChange(BatchPixelChange::ChangeType::Normal))); } } void MainWindow::switchActiveGlyph(std::optional newIndex) { auto currentIndex = viewModel_->faceModel()->activeGlyphIndex(); if (currentIndex == newIndex) { return; } if (currentIndex.has_value() && newIndex.has_value()) { if (!pendingSwitchGlyphCommand_) { pendingSwitchGlyphCommand_ = std::make_unique(faceWidget_, viewModel_.get(), currentIndex.value(), newIndex.value()); } else { pendingSwitchGlyphCommand_->setToIndex(newIndex.value()); } pendingSwitchGlyphCommand_->redo(); } else { faceWidget_->setCurrentGlyphIndex(newIndex); viewModel_->setActiveGlyphIndex(newIndex); } } void MainWindow::resetCurrentGlyph() { f2b::font::glyph currentGlyphState { viewModel_->faceModel()->activeGlyph().value() }; auto glyphIndex = viewModel_->faceModel()->activeGlyphIndex().value(); pushUndoCommand(new Command(tr("Reset Glyph"), [&, currentGlyphState, glyphIndex] { viewModel_->modifyGlyph(glyphIndex, currentGlyphState); viewModel_->updateDocumentTitle(); displayGlyph(viewModel_->faceModel()->activeGlyph().value()); faceWidget_->updateGlyphInfo(glyphIndex, viewModel_->faceModel()->activeGlyph().value()); }, [&, glyphIndex] { viewModel_->resetGlyph(glyphIndex); viewModel_->updateDocumentTitle(); displayGlyph(viewModel_->faceModel()->activeGlyph().value()); faceWidget_->updateGlyphInfo(glyphIndex, viewModel_->faceModel()->activeGlyph().value()); })); } void MainWindow::resetFont() { auto message = tr("Are you sure you want to reset all changes to the font? This operation cannot be undone."); auto result = QMessageBox::warning(this, tr("Reset Font"), message, QMessageBox::Reset, QMessageBox::Cancel); if (result == QMessageBox::Reset) { viewModel_->faceModel()->reset(); viewModel_->updateDocumentTitle(); undoStack_->clear(); updateResetActions(); displayFace(viewModel_->faceModel()->face()); } } void MainWindow::updateResetActions() { if (viewModel_->faceModel() == nullptr) { ui_->actionReset_Glyph->setEnabled(false); ui_->actionReset_Font->setEnabled(false); } else { auto currentIndex = viewModel_->faceModel()->activeGlyphIndex(); if (currentIndex.has_value()) { ui_->actionReset_Glyph->setEnabled(viewModel_->faceModel()->isGlyphModified(currentIndex.value())); } else { ui_->actionReset_Glyph->setEnabled(false); } ui_->actionReset_Font->setEnabled(viewModel_->faceModel()->isModified()); } } void MainWindow::debounceFontNameChanged(const QString &fontName) { if (fontNameDebounceTimer_ == nullptr) { fontNameDebounceTimer_ = std::make_unique(); fontNameDebounceTimer_->setInterval(300); fontNameDebounceTimer_->setSingleShot(true); } else { fontNameDebounceTimer_->stop(); fontNameDebounceTimer_->disconnect(); } connect(fontNameDebounceTimer_.get(), &QTimer::timeout, [&, fontName] { viewModel_->setFontArrayName(fontName); }); fontNameDebounceTimer_->start(); } void MainWindow::displaySourceCode() { ui_->stackedWidget->setCurrentWidget(ui_->sourceCodeContainer); QElapsedTimer timer; timer.start(); ui_->sourceCodeTextBrowser->setPlainText(viewModel_->sourceCode()); qDebug() << "Displaying finished in" << timer.elapsed() << "ms"; } void MainWindow::exportSourceCode() { ui_->statusBar->clearMessage(); QString directoryPath = viewModel_->lastSourceCodeDirectory(); if (directoryPath.isNull()) { directoryPath = defaultDialogDirectory(); } auto dialog = std::make_shared(this, tr("Save Source Code"), directoryPath); dialog->setAcceptMode(QFileDialog::AcceptSave); dialog->setFileMode(QFileDialog::AnyFile); connect(dialog.get(), &QFileDialog::finished, [=](int) { dialog->setParent(nullptr); auto files = dialog->selectedFiles(); if (!files.isEmpty()) { auto filePath = files.first(); if (!filePath.isNull()) { QFile output(filePath); if (output.open(QFile::WriteOnly | QFile::Truncate)) { output.write(ui_->sourceCodeTextBrowser->document()->toPlainText().toUtf8()); output.close(); viewModel_->setLastSourceCodeDirectory(filePath); ui_->statusBar->showMessage(tr("Source code successfully exported."), 5000); } else { displayError(tr("Unable to write to file: ") + filePath); } } } }); dialog->open(); } void MainWindow::pushUndoCommand(QUndoCommand *command) { bool shouldPushSwitchGlyphCommand = pendingSwitchGlyphCommand_ != nullptr; if (shouldPushSwitchGlyphCommand) { undoStack_->beginMacro(command->text()); undoStack_->push(pendingSwitchGlyphCommand_.get()); pendingSwitchGlyphCommand_.release(); } undoStack_->push(command); if (shouldPushSwitchGlyphCommand) { undoStack_->endMacro(); } } ================================================ FILE: app/mainwindow.h ================================================ #ifndef MAINWINDOW_H #define MAINWINDOW_H #include #include #include #include #include "mainwindowmodel.h" #include "updatehelper.h" #include "facewidget.h" #include "glyphwidget.h" #include "batchpixelchange.h" #include "command.h" #include namespace Ui { class MainWindow; } class QLabel; class MainWindow : public QMainWindow { Q_OBJECT public: explicit MainWindow(QWidget *parent = nullptr); virtual ~MainWindow(); protected: virtual void closeEvent(QCloseEvent *event) override; private slots: void displayFace(f2b::font::face& face); private: void connectUpdateHelper(); void connectUIInputs(); void connectViewModelOutputs(); void initUI(); void setupActions(); void showUpdateDialog(std::optional update); void showAboutDialog(); void showFontDialog(); void showOpenDocumentDialog(); void showCloseDocumentDialogIfNeeded(); void showAddGlyphDialog(); void showDeleteGlyphDialog(); void save(); void saveAs(); void resetCurrentGlyph(); void resetFont(); void displayGlyph(const f2b::font::glyph& glyph); void updateUI(UIState uiState); void editGlyph(const BatchPixelChange& change); void switchActiveGlyph(std::optional newIndex); void setGlyphExported(std::size_t index, bool isExported); void updateResetActions(); void updateFaceInfoLabel(const FaceInfo& faceInfo); void updateDefaultFontName(const FaceInfo& faceInfo); void displaySourceCode(); void exportSourceCode(); void closeCurrentDocument(); void displayError(const QString& error); void pushUndoCommand(QUndoCommand *command); void debounceFontNameChanged(const QString& fontName); QString defaultDialogDirectory() const; enum SavePromptButton { Save, DontSave, Cancel }; SavePromptButton promptToSaveDirtyDocument(); Ui::MainWindow *ui_; std::unique_ptr glyphWidget_ {}; FaceWidget *faceWidget_ { nullptr }; QLabel *statusLabel_; std::unique_ptr updateHelper_ { std::make_unique() }; std::unique_ptr viewModel_ { std::make_unique() }; std::unique_ptr faceScene_ { std::make_unique() }; std::unique_ptr undoStack_ { std::make_unique() }; std::unique_ptr fontNameDebounceTimer_ {}; std::unique_ptr pendingSwitchGlyphCommand_ {}; }; #endif // MAINWINDOW_H ================================================ FILE: app/mainwindow.ui ================================================ MainWindow 0 0 724 619 FontEdit :/icon/assets/icon/fontedit256.png:/icon/assets/icon/fontedit256.png QTabWidget::Triangular 0 Edit Font 0 24 24 24 24 24 24 Ctrl+R Qt::Horizontal QSizePolicy::Fixed 8 20 24 24 24 24 24 24 24 24 24 24 24 24 24 24 Qt::Horizontal QSizePolicy::Fixed 8 20 24 24 Qt::Horizontal 40 20 Qt::AlignLeading|Qt::AlignLeft|Qt::AlignTop 240 0 240 16777215 Qt::ScrollBarAlwaysOff Qt::AlignLeading|Qt::AlignLeft|Qt::AlignTop Toggle exported state by pressing Space on a selected glyph Show non-exported Glyphs Qt::Horizontal true 240 16777215 TextLabel Qt::AlignCenter true Qt::Horizontal QSizePolicy::Ignored 237 20 Source Code 0 0 0 0 0 QTextEdit::NoWrap Updating source code... Qt::AlignCenter Output Format Export Selected Glyphs exportMethodButtonGroup Export All Glyphs exportMethodButtonGroup 0 0 Misc Options Invert Bits Reverse Bits (MSB) Include Line Spacing Font Array Name 0 0 font Indentation Qt::Vertical 20 40 Print Export Source Code... 0 0 724 22 File Edit Help View New Ctrl+N :/toolbar/assets/font.svg:/toolbar/assets/font.svg Import Font Ctrl+N :/toolbar/assets/reset.svg:/toolbar/assets/reset.svg Reset Glyph :/toolbar/assets/code.svg:/toolbar/assets/code.svg Export... :/toolbar/assets/save.svg:/toolbar/assets/save.svg Save Ctrl+S :/toolbar/assets/clear.svg:/toolbar/assets/clear.svg Reset Font :/toolbar/assets/print.svg:/toolbar/assets/print.svg Print... Ctrl+P Save As... Ctrl+Shift+S :/toolbar/assets/add-glyph.svg:/toolbar/assets/add-glyph.svg Add Glyph Ctrl+Shift+N Quit Ctrl+Q :/toolbar/assets/copy.svg:/toolbar/assets/copy.svg Copy Glyph :/toolbar/assets/paste.svg:/toolbar/assets/paste.svg Paste Glyph :/toolbar/assets/open.svg:/toolbar/assets/open.svg Open... Ctrl+O Recent files Close About FontEdit :/toolbar/assets/delete-glyph.svg:/toolbar/assets/delete-glyph.svg Delete Glyph true true Show non-exported Glyphs Check for Updates... GlyphGraphicsView QGraphicsView
glyphgraphicsview.h
showNonExportedGlyphsCheckBox toggled(bool) actionShow_non_exported_Glyphs setChecked(bool) 563 508 -1 -1 actionShow_non_exported_Glyphs toggled(bool) showNonExportedGlyphsCheckBox setChecked(bool) -1 -1 563 508
================================================ FILE: app/mainwindowmodel.cpp ================================================ #include "mainwindowmodel.h" #include "sourcecoderunnable.h" #include "f2b_qt_compat.h" #include #include #include #include #include #include #include #include Q_DECLARE_METATYPE(f2b::source_code_options::bit_numbering_type); Q_DECLARE_METATYPE(f2b::source_code_options::export_method_type); namespace SettingsKey { static const QString showNonExportedGlyphs = "main_window/show_non_expoerted_glyphs"; static const QString exportMethod = "source_code_options/export_method"; static const QString bitNumbering = "source_code_options/bit_numbering"; static const QString invertBits = "source_code_options/invert_bits"; static const QString includeLineSpacing = "source_code_options/include_line_spacing"; static const QString format = "source_code_options/format"; static const QString indentation = "source_code_options/indentation"; static const QString documentPath = "source_code_options/document_path"; static const QString lastDocumentDirectory = "source_code_options/last_document_directory"; static const QString lastSourceCodeDirectory = "source_code_options/last_source_code_directory"; } bool operator==(const UIState& lhs, const UIState& rhs) { return lhs.actions == rhs.actions && lhs.statusBarMessage == rhs.statusBarMessage && lhs.lastUserAction == rhs.lastUserAction && lhs.selectedTab == rhs.selectedTab; } bool operator!=(const UIState& lhs, const UIState& rhs) { return !(lhs == rhs); } MainWindowModel::MainWindowModel(QObject *parent) : QObject(parent) { shouldShowNonExportedGlyphs_ = settings_.value(SettingsKey::showNonExportedGlyphs, true).toBool(); sourceCodeOptions_.export_method = qvariant_cast( settings_.value(SettingsKey::exportMethod, f2b::source_code_options::export_selected) ); sourceCodeOptions_.bit_numbering = qvariant_cast( settings_.value(SettingsKey::bitNumbering, f2b::source_code_options::lsb) ); sourceCodeOptions_.invert_bits = settings_.value(SettingsKey::invertBits, false).toBool(); sourceCodeOptions_.include_line_spacing = settings_.value(SettingsKey::includeLineSpacing, false).toBool(); sourceCodeOptions_.indentation = from_qvariant(settings_.value(SettingsKey::indentation, to_qvariant(f2b::source_code::tab {}))); formats_.insert(QString::fromStdString(std::string(f2b::format::c::identifier)), "C/C++"); formats_.insert(QString::fromStdString(std::string(f2b::format::arduino::identifier)), "Arduino"); formats_.insert(QString::fromStdString(std::string(f2b::format::python_list::identifier)), "Python List"); formats_.insert(QString::fromStdString(std::string(f2b::format::python_bytes::identifier)), "Python Bytes"); currentFormat_ = settings_.value(SettingsKey::format, formats_.firstKey()).toString(); indentationStyles_.push_back({ f2b::source_code::tab {}, tr("Tab") }); for (std::size_t i = 1; i <= 8; ++i) { indentationStyles_.push_back({ f2b::source_code::space {i}, tr("%n Space(s)", "", i) }); } connect(this, &MainWindowModel::runnableFinished, this, &MainWindowModel::sourceCodeChanged, Qt::BlockingQueuedConnection); qDebug() << "output format:" << currentFormat_; } void MainWindowModel::restoreSession() { auto path = settings_.value(SettingsKey::documentPath).toString(); if (!path.isNull()) { openDocument(path, true); } else { updateDocumentTitle(); } } void MainWindowModel::registerInputEvent(InputEvent e) { auto state { uiState_ }; if (std::holds_alternative(e)) { auto action = std::get(e); switch (action) { case UIState::ActionTabEdit: state.selectedTab = UIState::TabEdit; break; case UIState::ActionTabCode: state.selectedTab = UIState::TabCode; break; default: break; } } else if (std::holds_alternative(e)) { auto action = std::get(e); state.lastUserAction = action; switch (action) { case UIState::UserIdle: state.statusBarMessage = UIState::MessageIdle; state.actions.reset(); break; case UIState::UserLoadedDocument: state.statusBarMessage = UIState::MessageLoadedFace; state.actions.reset(); state.actions.set(UIState::ActionAddGlyph); state.actions.set(UIState::ActionSave); state.actions.set(UIState::ActionClose); state.actions.set(UIState::ActionPrint); state.actions.set(UIState::ActionExport); state.actions.set(UIState::ActionTabCode); break; case UIState::UserLoadedGlyph: state.statusBarMessage = UIState::MessageLoadedGlyph; state.actions.set(UIState::ActionDeleteGlyph); state.actions.set(UIState::ActionCopy); break; } state.actions.set(UIState::ActionTabEdit); } if (state != uiState_) { uiState_ = state; emit uiStateChanged(uiState_); } } void MainWindowModel::updateDocumentTitle() { QString name; if (documentPath_.has_value()) { QFileInfo fileInfo {documentPath_.value()}; name = fileInfo.fileName(); } else { name = tr("New Document"); } if (auto faceModel = fontFaceViewModel_.get()) { if (faceModel->isModifiedSinceSave()) { name += " - "; name += tr("Edited"); } } if (name != documentTitle_) { documentTitle_ = name; emit documentTitleChanged(documentTitle_); } } void MainWindowModel::importFont(const QFont &font) { fontFaceViewModel_ = std::make_unique(font); registerInputEvent(UIState::UserLoadedDocument); setDocumentPath({}); emit faceLoaded(fontFaceViewModel_->face()); updateDocumentTitle(); } void MainWindowModel::openDocument(const QString &fileName) { openDocument(fileName, false); } void MainWindowModel::openDocument(const QString &fileName, bool failSilently) { try { fontFaceViewModel_ = std::make_unique(fileName); qDebug() << "face loaded from" << fileName; registerInputEvent(UIState::UserLoadedDocument); setDocumentPath(fileName); updateDocumentTitle(); emit faceLoaded(fontFaceViewModel_->face()); } catch (std::runtime_error& e) { setDocumentPath({}); updateDocumentTitle(); qCritical() << e.what(); if (!failSilently) { emit documentError(QString::fromStdString(e.what())); } } } void MainWindowModel::saveDocument(const QString& fileName) { try { fontFaceViewModel_->saveToFile(fileName); qDebug() << "face saved to" << fileName; setDocumentPath(fileName); updateDocumentTitle(); } catch (std::runtime_error& e) { qCritical() << e.what(); emit documentError(QString::fromStdString(e.what())); } } void MainWindowModel::closeCurrentDocument() { fontFaceViewModel_.release(); setDocumentPath({}); updateDocumentTitle(); registerInputEvent(UIState::UserIdle); emit documentClosed(); } void MainWindowModel::setActiveGlyphIndex(std::optional index) { if (fontFaceViewModel_->activeGlyphIndex().has_value() and fontFaceViewModel_->activeGlyphIndex().value() == index) { return; } try { fontFaceViewModel_->setActiveGlyphIndex(index); registerInputEvent(UIState::UserLoadedGlyph); emit activeGlyphChanged(fontFaceViewModel_->activeGlyph()); } catch (const std::exception& e) { qCritical() << e.what(); } } void MainWindowModel::setShouldShowNonExportedGlyphs(bool enabled) { shouldShowNonExportedGlyphs_ = enabled; settings_.setValue(SettingsKey::showNonExportedGlyphs, enabled); } void MainWindowModel::setExportAllEnabled(bool enabled) { auto exportMethod = enabled ? f2b::source_code_options::export_all : f2b::source_code_options::export_selected; sourceCodeOptions_.export_method = exportMethod; settings_.setValue(SettingsKey::exportMethod, exportMethod); reloadSourceCode(); } void MainWindowModel::setInvertBits(bool enabled) { sourceCodeOptions_.invert_bits = enabled; settings_.setValue(SettingsKey::invertBits, enabled); reloadSourceCode(); } void MainWindowModel::setMSBEnabled(bool enabled) { auto bitNumbering = enabled ? f2b::source_code_options::msb : f2b::source_code_options::lsb; sourceCodeOptions_.bit_numbering = bitNumbering; settings_.setValue(SettingsKey::bitNumbering, bitNumbering); reloadSourceCode(); } void MainWindowModel::setIncludeLineSpacing(bool enabled) { sourceCodeOptions_.include_line_spacing = enabled; settings_.setValue(SettingsKey::includeLineSpacing, enabled); reloadSourceCode(); } void MainWindowModel::setOutputFormat(const QString& format) { currentFormat_ = formats_.key(format, formats_.first()); settings_.setValue(SettingsKey::format, currentFormat_); reloadSourceCode(); } void MainWindowModel::setIndentation(const QString &indentationLabel) { auto i = std::find_if(indentationStyles_.cbegin(), indentationStyles_.cend(), [&](const auto& pair) -> bool { return pair.second == indentationLabel; }); if (i != indentationStyles_.end()) { sourceCodeOptions_.indentation = i->first; settings_.setValue(SettingsKey::indentation, to_qvariant(i->first)); reloadSourceCode(); } } QString MainWindowModel::indentationStyleCaption() const { auto i = std::find_if(indentationStyles_.cbegin(), indentationStyles_.cend(), [&](const auto& pair) -> bool { return pair.first == sourceCodeOptions_.indentation; }); if (i != indentationStyles_.end()) { return i->second; } return indentationStyles_.front().second; } void MainWindowModel::setDocumentPath(const std::optional& path) { documentPath_ = path; if (path.has_value()) { settings_.setValue(SettingsKey::documentPath, path.value()); setLastVisitedDirectory(path.value()); } else { settings_.remove(SettingsKey::documentPath); } } QString MainWindowModel::lastVisitedDirectory() const { return settings_.value(SettingsKey::lastDocumentDirectory).toString(); } void MainWindowModel::setLastVisitedDirectory(const QString& path) { settings_.setValue(SettingsKey::lastDocumentDirectory, QFileInfo(path).path()); } QString MainWindowModel::lastSourceCodeDirectory() const { return settings_.value(SettingsKey::lastSourceCodeDirectory).toString(); } void MainWindowModel::setLastSourceCodeDirectory(const QString& path) { settings_.setValue(SettingsKey::lastSourceCodeDirectory, QFileInfo(path).path()); } void MainWindowModel::reloadSourceCode() { /// WIP :) emit sourceCodeUpdating(); auto r = new SourceCodeRunnable { faceModel()->face(), sourceCodeOptions_, currentFormat_, fontArrayName_ }; r->setCompletionHandler([&](const QString& output) { qDebug() << "Source code size:" << output.size() << "bytes"; std::scoped_lock { sourceCodeMutex_ }; sourceCode_ = std::move(output); emit runnableFinished(); }); r->setAutoDelete(true); QThreadPool::globalInstance()->start(r); } void MainWindowModel::resetGlyph(std::size_t index) { fontFaceViewModel_->resetGlyph(index); reloadSourceCode(); } void MainWindowModel::modifyGlyph(std::size_t index, const f2b::font::glyph &new_glyph) { fontFaceViewModel_->modifyGlyph(index, new_glyph); reloadSourceCode(); } void MainWindowModel::modifyGlyph(std::size_t index, const BatchPixelChange &change, BatchPixelChange::ChangeType changeType) { fontFaceViewModel_->modifyGlyph(index, change, changeType); reloadSourceCode(); } void MainWindowModel::appendGlyph(f2b::font::glyph glyph) { fontFaceViewModel_->appendGlyph(std::move(glyph)); reloadSourceCode(); } void MainWindowModel::deleteGlyph(std::size_t index) { fontFaceViewModel_->deleteGlyph(index); if (index == fontFaceViewModel_->face().num_glyphs() - 1) { fontFaceViewModel_->setActiveGlyphIndex(index - 1); emit activeGlyphChanged(fontFaceViewModel_->activeGlyph()); } reloadSourceCode(); } void MainWindowModel::setGlyphExported(std::size_t index, bool isExported) { fontFaceViewModel_->setGlyphExportedState(index, isExported); reloadSourceCode(); } ================================================ FILE: app/mainwindowmodel.h ================================================ #ifndef MAINWINDOWMODEL_H #define MAINWINDOWMODEL_H #include "fontfaceviewmodel.h" #include #include #include #include #include #include #include #include #include #include struct UIState { enum InterfaceAction { ActionAddGlyph = 0, ActionDeleteGlyph, ActionSave, ActionClose, ActionCopy, ActionPaste, ActionPrint, ActionExport, ActionTabEdit, ActionTabCode, ActionCount, ActionFirst = ActionAddGlyph }; enum UserAction { UserIdle = 0, UserLoadedDocument, UserLoadedGlyph, }; enum Message { MessageIdle = 0, MessageLoadedFace, MessageLoadedGlyph }; enum Tab { TabEdit, TabCode }; std::bitset actions; UserAction lastUserAction; Message statusBarMessage; Tab selectedTab; }; class MainWindowModel: public QObject { Q_OBJECT public: using InputEvent = std::variant; explicit MainWindowModel(QObject *parent = nullptr); void restoreSession(); FontFaceViewModel* faceModel() const { return fontFaceViewModel_.get(); } const UIState& uiState() const { return uiState_; } Qt::CheckState shouldShowNonExportedGlyphs() const { return shouldShowNonExportedGlyphs_ ? Qt::Checked : Qt::Unchecked; } bool exportAllEnabled() const { return sourceCodeOptions_.export_method == f2b::source_code_options::export_method_type::export_all ? Qt::Checked : Qt::Unchecked; } Qt::CheckState invertBits() const { return sourceCodeOptions_.invert_bits ? Qt::Checked : Qt::Unchecked; } Qt::CheckState msbEnabled() const { return sourceCodeOptions_.bit_numbering == f2b::source_code_options::bit_numbering_type::msb ? Qt::Checked : Qt::Unchecked; } Qt::CheckState includeLineSpacing() const { return sourceCodeOptions_.include_line_spacing ? Qt::Checked : Qt::Unchecked; } const QMap& outputFormats() const { return formats_; } QString outputFormat() const { return formats_.value(currentFormat_, formats_.first()); } const std::vector>& indentationStyles() const { return indentationStyles_; } QString indentationStyleCaption() const; void registerInputEvent(InputEvent e); const std::optional& currentDocumentPath() const { return documentPath_; } QString documentTitle() const { return documentTitle_; } void updateDocumentTitle(); void setFontArrayName(const QString& fontArrayName) { if (fontArrayName_ != fontArrayName) { fontArrayName_ = fontArrayName; reloadSourceCode(); } } QString lastVisitedDirectory() const; QString lastSourceCodeDirectory() const; void setLastSourceCodeDirectory(const QString& path); void resetGlyph(std::size_t index); void modifyGlyph(std::size_t index, const f2b::font::glyph &new_glyph); void modifyGlyph(std::size_t index, const BatchPixelChange &change, BatchPixelChange::ChangeType changeType); void appendGlyph(f2b::font::glyph glyph); void deleteGlyph(std::size_t index); void setGlyphExported(std::size_t index, bool isExported); const QString& sourceCode() { std::scoped_lock { sourceCodeMutex_ }; return sourceCode_; } public slots: void importFont(const QFont& font); void openDocument(const QString& fileName); void saveDocument(const QString& fileName); void closeCurrentDocument(); void setActiveGlyphIndex(std::optional index); void setShouldShowNonExportedGlyphs(bool enabled); void setExportAllEnabled(bool enabled); void setInvertBits(bool enabled); void setMSBEnabled(bool enabled); void setIncludeLineSpacing(bool enabled); void setOutputFormat(const QString &format); // human-readable void setIndentation(const QString &indentationLabel); // human-readable signals: void uiStateChanged(UIState state) const; void faceLoaded(f2b::font::face& face) const; void activeGlyphChanged(std::optional glyph) const; void sourceCodeUpdating() const; void sourceCodeChanged() const; void runnableFinished() const; void documentTitleChanged(const QString& title); void documentClosed(); void documentError(const QString& error); private: void reloadSourceCode(); void setDocumentPath(const std::optional& path); void setLastVisitedDirectory(const QString& path); void openDocument(const QString& fileName, bool failSilently); UIState uiState_ {}; std::unique_ptr fontFaceViewModel_; std::optional documentPath_; QString documentTitle_; QString fontArrayName_; f2b::source_code_options sourceCodeOptions_; bool shouldShowNonExportedGlyphs_; QString sourceCode_; std::mutex sourceCodeMutex_; QMap formats_; // identifier <-> human-readable QString currentFormat_; // identifier std::vector> indentationStyles_; QSettings settings_; }; #endif // MAINWINDOWMODEL_H ================================================ FILE: app/qfontfacereader.cpp ================================================ #include "qfontfacereader.h" #include "f2b_qt_compat.h" #include #include #include #include #include #include #include "utf8.h" using namespace std::literals::string_view_literals; static constexpr std::string_view ascii_glyphs = " !\"#$%&'()*+,-./0123456789:;<=>?@ABCDEFGHIJKLMNOPQRSTUVWXYZ[\\]^_`abcdefghijklmnopqrstuvwxyz{|}~"sv; QFontFaceReader::QFontFaceReader(const QFont &font, std::string text, std::optional forced_size) : f2b::font::face_reader() { std::string source_text { text.empty() ? ascii_glyphs : std::move(text) }; num_glyphs_ = source_text.length(); auto result = read_font(font, std::move(source_text), forced_size); sz_ = result.first; font_image_ = std::move(result.second); } bool QFontFaceReader::is_pixel_set(std::size_t glyph_id, f2b::font::point p) const { p.y += glyph_id * sz_.height; return font_image_->pixelColor(f2b::font::qpoint_with_point(p)) == Qt::color1; } QString QFontFaceReader::template_text(std::string text) { std::stringstream stream; utf8::iterator i(text.begin(), text.begin(), text.end()); utf8::iterator end(text.end(), text.begin(), text.end()); while (i != end) { auto utf8_begin = i; auto utf8_end = ++i; stream << std::string(utf8_begin.base(), utf8_end.base()) << "\n"; } return QString::fromStdString(stream.str()); } std::pair> QFontFaceReader::read_font( const QFont &font, std::string text, std::optional forced_size) { auto text_length = text.length(); auto template_text = QFontFaceReader::template_text(std::move(text)); QFontMetrics fm(font); qDebug() << font << fm.height() << fm.maxWidth() << fm.leading() << fm.lineSpacing(); int width = [&] { if (forced_size.has_value()) { return static_cast(forced_size.value().width); } return fm.boundingRect(QRect(), Qt::AlignLeft, template_text).width(); }(); int height = [&] { if (forced_size.has_value()) { return static_cast(forced_size.value().height); } return fm.lineSpacing(); }(); QSize img_size(width, height * text_length); // qDebug() << "img size" << img_size; auto image = std::make_unique(img_size, QImage::Format::Format_Mono); QPainter p(image.get()); p.fillRect(QRect(QPoint(), img_size), QColor(Qt::color0)); QTextDocument doc; doc.useDesignMetrics(); doc.documentLayout()->setPaintDevice(image.get()); doc.setDefaultFont(font); doc.setDocumentMargin(0); doc.setPlainText(template_text); doc.setTextWidth(img_size.width()); QTextFrame *rootFrame = doc.rootFrame(); for (auto it = rootFrame->begin(); !(it.atEnd()); ++it) { auto block = it.currentBlock(); QTextCursor cursor(block); auto blockFormat = block.blockFormat(); blockFormat.setLineHeight(fm.lineSpacing(), QTextBlockFormat::FixedHeight); cursor.setBlockFormat(blockFormat); } QAbstractTextDocumentLayout::PaintContext ctx; ctx.palette.setColor(QPalette::Text, Qt::color1); doc.documentLayout()->draw(&p, ctx); p.end(); // image->save("output.bmp"); return { f2b::font::size_with_qsize(QSize(width, fm.lineSpacing())), std::move(image) }; } ================================================ FILE: app/qfontfacereader.h ================================================ #ifndef QFONTFACEREADER_H #define QFONTFACEREADER_H #include "fontdata.h" #include #include #include #include #include class QFontFaceReader : public f2b::font::face_reader { public: explicit QFontFaceReader(const QFont &font, std::string text = {}, std::optional forced_size = {}); virtual ~QFontFaceReader() override = default; virtual f2b::font::glyph_size font_size() const override { return sz_; } virtual std::size_t num_glyphs() const override { return num_glyphs_; } virtual bool is_pixel_set(std::size_t glyph_id, f2b::font::point p) const override; private: static QString template_text(std::string text); static std::pair> read_font( const QFont &font, std::string text, std::optional forcedSize); f2b::font::glyph_size sz_ { 0, 0 }; std::unique_ptr font_image_ { nullptr }; std::size_t num_glyphs_ { 0 }; }; #endif // QFONTFACEREADER_H ================================================ FILE: app/semver.hpp ================================================ // _____ _ _ // / ____| | | (_) // | (___ ___ _ __ ___ __ _ _ __ | |_ _ ___ // \___ \ / _ \ '_ ` _ \ / _` | '_ \| __| |/ __| // ____) | __/ | | | | | (_| | | | | |_| | (__ // |_____/ \___|_| |_| |_|\__,_|_| |_|\__|_|\___| // __ __ _ _ _____ // \ \ / / (_) (_) / ____|_ _ // \ \ / /__ _ __ ___ _ ___ _ __ _ _ __ __ _ | | _| |_ _| |_ // \ \/ / _ \ '__/ __| |/ _ \| '_ \| | '_ \ / _` | | | |_ _|_ _| // \ / __/ | \__ \ | (_) | | | | | | | | (_| | | |____|_| |_| // \/ \___|_| |___/_|\___/|_| |_|_|_| |_|\__, | \_____| // https://github.com/Neargye/semver __/ | // version 0.2.2 |___/ // // Licensed under the MIT License . // SPDX-License-Identifier: MIT // Copyright (c) 2018 - 2020 Daniil Goncharov . // // Permission is hereby granted, free of charge, to any person obtaining a copy // of this software and associated documentation files (the "Software"), to deal // in the Software without restriction, including without limitation the rights // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell // copies of the Software, and to permit persons to whom the Software is // furnished to do so, subject to the following conditions: // // The above copyright notice and this permission notice shall be included in all // copies or substantial portions of the Software. // // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE // SOFTWARE. #ifndef NEARGYE_SEMANTIC_VERSIONING_HPP #define NEARGYE_SEMANTIC_VERSIONING_HPP #include #include #include #include #include #include #include #if __has_include() #include #else #include #endif // Allow to disable exceptions. #if (defined(__cpp_exceptions) || defined(__EXCEPTIONS) || defined(_CPPUNWIND)) && !defined(SEMVER_NOEXCEPTION) # include # define __SEMVER_THROW(exception) throw exception #else # include # define __SEMVER_THROW(exception) std::abort() #endif #if defined(__APPLE_CC__) # pragma clang diagnostic push # pragma clang diagnostic ignored "-Wmissing-braces" // Ignore warning: suggest braces around initialization of subobject 'return {first, std::errc::invalid_argument};'. #endif namespace semver { enum struct prerelease : std::uint8_t { alpha = 0, beta = 1, rc = 2, none = 3 }; // Max version string length = 3() + 1(.) + 3() + 1(.) + 3() + 1(-) + 5() + 1(.) + 3() = 21. inline constexpr std::size_t max_version_string_length = 21; namespace detail { #if __has_include() struct from_chars_result : std::from_chars_result { [[nodiscard]] constexpr operator bool() const noexcept { return ec == std::errc{}; } }; struct to_chars_result : std::to_chars_result { [[nodiscard]] constexpr operator bool() const noexcept { return ec == std::errc{}; } }; #else struct from_chars_result { const char* ptr; std::errc ec; [[nodiscard]] constexpr operator bool() const noexcept { return ec == std::errc{}; } }; struct to_chars_result { char* ptr; std::errc ec; [[nodiscard]] constexpr operator bool() const noexcept { return ec == std::errc{}; } }; #endif inline constexpr std::string_view alpha = {"-alpha", 6}; inline constexpr std::string_view beta = {"-beta", 5}; inline constexpr std::string_view rc = {"-rc", 3}; // Min version string length = 1() + 1(.) + 1() + 1(.) + 1() = 5. inline constexpr auto min_version_string_length = 5; constexpr char to_lower(char c) noexcept { return (c >= 'A' && c <= 'Z') ? c + ('a' - 'A') : c; } constexpr bool is_digit(char c) noexcept { return c >= '0' && c <= '9'; } constexpr std::uint8_t to_digit(char c) noexcept { return c - '0'; } constexpr std::uint8_t length(std::uint8_t x) noexcept { return x < 10 ? 1 : (x < 100 ? 2 : 3); } constexpr std::uint8_t length(prerelease t) noexcept { if (t == prerelease::alpha) { return 5; } else if (t == prerelease::beta) { return 4; } else if(t == prerelease::rc) { return 2; } return 0; } constexpr bool equals(const char* first, const char* last, std::string_view str) noexcept { for (std::size_t i = 0; first != last && i < str.length(); ++i, ++first) { if (to_lower(*first) != to_lower(str[i])) { return false; } } return true; } constexpr char* to_chars(char* str, std::uint8_t x, bool dot = true) noexcept { do { *(--str) = static_cast('0' + (x % 10)); x /= 10; } while (x != 0); if (dot) { *(--str) = '.'; } return str; } constexpr char* to_chars(char* str, prerelease t) noexcept { const auto p = t == prerelease::alpha ? alpha : t == prerelease::beta ? beta : t == prerelease::rc ? rc : std::string_view{}; for (auto it = p.rbegin(); it != p.rend(); ++it) { *(--str) = *it; } return str; } constexpr const char* from_chars(const char* first, const char* last, std::uint8_t& d) noexcept { if (first != last && is_digit(*first)) { std::int32_t t = 0; for (; first != last && is_digit(*first); ++first) { t = t * 10 + to_digit(*first); } if (t <= (std::numeric_limits::max)()) { d = static_cast(t); return first; } } return nullptr; } constexpr const char* from_chars(const char* first, const char* last, prerelease& p) noexcept { if (equals(first, last, alpha)) { p = prerelease::alpha; return first + alpha.length(); } else if (equals(first, last, beta)) { p = prerelease::beta; return first + beta.length(); } else if (equals(first, last, rc)) { p = prerelease::rc; return first + rc.length(); } return nullptr; } constexpr bool check_delimiter(const char* first, const char* last, char d) noexcept { return first != last && first != nullptr && *first == d; } } // namespace semver::detail struct version { std::uint8_t major = 0; std::uint8_t minor = 1; std::uint8_t patch = 0; prerelease prerelease_type = prerelease::none; std::uint8_t prerelease_number = 0; constexpr version(std::uint8_t major, std::uint8_t minor, std::uint8_t patch, prerelease prerelease_type = prerelease::none, std::uint8_t prerelease_number = 0) noexcept : major{major}, minor{minor}, patch{patch}, prerelease_type{prerelease_type}, prerelease_number{prerelease_type == prerelease::none ? static_cast(0) : prerelease_number} { } explicit constexpr version(std::string_view str) : version(0, 0, 0, prerelease::none, 0) { from_string(str); } constexpr version() = default; // https://semver.org/#how-should-i-deal-with-revisions-in-the-0yz-initial-development-phase constexpr version(const version&) = default; constexpr version(version&&) = default; ~version() = default; version& operator=(const version&) = default; version& operator=(version&&) = default; [[nodiscard]] constexpr detail::from_chars_result from_chars(const char* first, const char* last) noexcept { if (first == nullptr || last == nullptr || (last - first) < detail::min_version_string_length) { return {first, std::errc::invalid_argument}; } auto next = first; if (next = detail::from_chars(next, last, major); detail::check_delimiter(next, last, '.')) { if (next = detail::from_chars(++next, last, minor); detail::check_delimiter(next, last, '.')) { if (next = detail::from_chars(++next, last, patch); next == last) { prerelease_type = prerelease::none; prerelease_number = 0; return {next, std::errc{}}; } else if (detail::check_delimiter(next, last, '-')) { if (next = detail::from_chars(next, last, prerelease_type); next == last) { prerelease_number = 0; return {next, std::errc{}}; } else if (detail::check_delimiter(next, last, '.')) { if (next = detail::from_chars(++next, last, prerelease_number); next == last) { return {next, std::errc{}}; } } } } } return {first, std::errc::invalid_argument}; } [[nodiscard]] constexpr detail::to_chars_result to_chars(char* first, char* last) const noexcept { const auto length = chars_length(); if (first == nullptr || last == nullptr || (last - first) < length) { return {last, std::errc::value_too_large}; } auto next = first + length; if (prerelease_type != prerelease::none) { if (prerelease_number != 0) { next = detail::to_chars(next, prerelease_number); } next = detail::to_chars(next, prerelease_type); } next = detail::to_chars(next, patch); next = detail::to_chars(next, minor); next = detail::to_chars(next, major, false); return {first + length, std::errc{}}; } [[nodiscard]] constexpr bool from_string_noexcept(std::string_view str) noexcept { return from_chars(str.data(), str.data() + str.length()); } constexpr version& from_string(std::string_view str) { if (!from_string_noexcept(str)) { __SEMVER_THROW(std::invalid_argument{"semver::version::from_string invalid version."}); } return *this; } [[nodiscard]] std::string to_string() const { auto str = std::string(chars_length(), '\0'); if (!to_chars(str.data(), str.data() + str.length())) { __SEMVER_THROW(std::invalid_argument{"semver::version::to_string invalid version."}); } return str; } [[nodiscard]] constexpr int compare(const version& other) const noexcept { if (major != other.major) { return major - other.major; } if (minor != other.minor) { return minor - other.minor; } if (patch != other.patch) { return patch - other.patch; } if (prerelease_type != other.prerelease_type) { return static_cast(prerelease_type) - static_cast(other.prerelease_type); } if (prerelease_number != other.prerelease_number) { return prerelease_number - other.prerelease_number; } return 0; } private: constexpr std::uint8_t chars_length() const noexcept { // () + 1(.) + () + 1(.) + () std::uint8_t length = detail::length(major) + detail::length(minor) + detail::length(patch) + 2; if (prerelease_type != prerelease::none) { // + 1(-) + () length += detail::length(prerelease_type) + 1; if (prerelease_number != 0) { // + 1(.) + () length += detail::length(prerelease_number) + 1; } } return length; } }; [[nodiscard]] constexpr bool operator==(const version& lhs, const version& rhs) noexcept { return lhs.compare(rhs) == 0; } [[nodiscard]] constexpr bool operator!=(const version& lhs, const version& rhs) noexcept { return lhs.compare(rhs) != 0; } [[nodiscard]] constexpr bool operator>(const version& lhs, const version& rhs) noexcept { return lhs.compare(rhs) > 0; } [[nodiscard]] constexpr bool operator>=(const version& lhs, const version& rhs) noexcept { return lhs.compare(rhs) >= 0; } [[nodiscard]] constexpr bool operator<(const version& lhs, const version& rhs) noexcept { return lhs.compare(rhs) < 0; } [[nodiscard]] constexpr bool operator<=(const version& lhs, const version& rhs) noexcept { return lhs.compare(rhs) <= 0; } [[nodiscard]] constexpr version operator""_version(const char* str, std::size_t length) { return version{std::string_view{str, length}}; } [[nodiscard]] constexpr bool valid(std::string_view str) noexcept { return version{}.from_string_noexcept(str); } [[nodiscard]] constexpr detail::from_chars_result from_chars(const char* first, const char* last, version& v) noexcept { return v.from_chars(first, last); } [[nodiscard]] constexpr detail::to_chars_result to_chars(char* first, char* last, const version& v) noexcept { return v.to_chars(first, last); } [[nodiscard]] constexpr std::optional from_string_noexcept(std::string_view str) noexcept { if (auto v = version{}; v.from_string_noexcept(str)) { return v; } return std::nullopt; } [[nodiscard]] constexpr version from_string(std::string_view str) { return version{str}; } [[nodiscard]] inline std::string to_string(const version& v) { return v.to_string(); } template inline std::basic_ostream& operator<<(std::basic_ostream& os, const version& v) { for (const auto c : v.to_string()) { os.put(c); } return os; } namespace ranges { } // namespace semver::ranges } // namespace semver #if defined(__APPLE_CC__) # pragma clang diagnostic pop #endif #endif // NEARGYE_SEMANTIC_VERSIONING_HPP ================================================ FILE: app/sourcecoderunnable.cpp ================================================ #include "sourcecoderunnable.h" #include #include void SourceCodeRunnable::run() { QString output; QElapsedTimer timer; timer.start(); if (format_ == f2b::format::arduino::identifier) { output = QString::fromStdString(generator_.generate(face_, fontArrayName_)); } else if (format_ == f2b::format::c::identifier) { output = QString::fromStdString(generator_.generate(face_, fontArrayName_)); } else if (format_ == f2b::format::python_list::identifier) { output = QString::fromStdString(generator_.generate(face_, fontArrayName_)); } else if (format_ == f2b::format::python_bytes::identifier) { output = QString::fromStdString(generator_.generate(face_, fontArrayName_)); } qDebug() << "Generation finished in" << timer.elapsed() << "ms"; setFinished(true); if (!isCanceled()) { handler_(output); } } bool SourceCodeRunnable::isFinished() { std::scoped_lock lock { mutex_ }; return m_finished; } bool SourceCodeRunnable::isCanceled() { std::scoped_lock lock { mutex_ }; return m_canceled; } void SourceCodeRunnable::setCanceled(bool canceled) { std::scoped_lock lock { mutex_ }; m_canceled = canceled; } void SourceCodeRunnable::setFinished(bool finished) { std::scoped_lock lock { mutex_ }; m_finished = finished; } ================================================ FILE: app/sourcecoderunnable.h ================================================ #ifndef SOURCECODERUNNABLE_H #define SOURCECODERUNNABLE_H #include #include #include #include #include #include #include class SourceCodeRunnable : public QRunnable { using CompletionHandler = std::function; public: SourceCodeRunnable(f2b::font::face face, f2b::source_code_options options, const QString& format, const QString& fontArrayName) : QRunnable(), face_ { std::move(face) }, generator_ { options }, format_ { format.toStdString() }, fontArrayName_ { fontArrayName.toStdString() } {}; void run() override; void setCompletionHandler(CompletionHandler handler) { handler_ = std::move(handler); } bool isFinished(); bool isCanceled(); void setCanceled(bool canceled); protected: void setFinished(bool finished); private: bool m_finished { false }; bool m_canceled { false }; std::mutex mutex_; f2b::font::face face_; f2b::font_source_code_generator generator_; std::string format_; std::string fontArrayName_; CompletionHandler handler_ {}; }; #endif // SOURCECODERUNNABLE_H ================================================ FILE: app/ui/CMakeLists.txt ================================================ cmake_minimum_required(VERSION 3.9) project(ui LANGUAGES CXX) find_package(Qt5 COMPONENTS Core Gui Widgets REQUIRED) add_library(ui aboutdialog.cpp aboutdialog.h aboutdialog.ui batchpixelchange.h facewidget.cpp facewidget.h focuswidget.cpp focuswidget.h glyphgraphicsview.cpp glyphgraphicsview.h glyphinfowidget.cpp glyphinfowidget.h glyphwidget.cpp glyphwidget.h ) target_link_libraries(ui PRIVATE Qt5::Widgets Qt5::Core common font2bytes) ================================================ FILE: app/ui/aboutdialog.cpp ================================================ #include "aboutdialog.h" #include "./ui_aboutdialog.h" #include #include #include static constexpr auto about = R"(

FontEdit v##version## build ##build##

Copyright ##year## Dominik Kapusta
https://kapusta.cc@ayoy


Get the Source CodeReport a Bug

This program is distributed under the terms of General Public License v3.

Icons from www.flaticon.com made by:
SmashiconsFreepikPixel perfect

)"; AboutDialog::AboutDialog(QWidget *parent) : QDialog(parent), ui(new Ui::AboutDialog) { ui->setupUi(this); QIcon icon(":/icon/assets/icon/fontedit96.png"); ui->iconLabel->setPixmap(icon.pixmap(ui->iconLabel->size())); auto text = QString::fromStdString({about}); text.replace("##version##", QApplication::applicationVersion()); text.replace("##build##", BUILD); text.replace("##year##", YEAR); connect(ui->htmlLabel, &QLabel::linkActivated, [&](const QString& link) { QDesktopServices::openUrl(QUrl(link)); }); ui->htmlLabel->setText(text); } AboutDialog::~AboutDialog() { delete ui; } ================================================ FILE: app/ui/aboutdialog.h ================================================ #ifndef ABOUTDIALOG_H #define ABOUTDIALOG_H #include namespace Ui { class AboutDialog; } class AboutDialog : public QDialog { Q_OBJECT public: explicit AboutDialog(QWidget *parent = nullptr); ~AboutDialog(); private: Ui::AboutDialog *ui; }; #endif // ABOUTDIALOG_H ================================================ FILE: app/ui/aboutdialog.ui ================================================ AboutDialog 0 0 425 300 About FontEdit 0 0 0 96 96 :/icon/assets/icon/fontedit96.png true Qt::Vertical 20 40 0 0 <html><head/><body><p><span style=" font-size:18pt;">FontEdit </span><span style=" font-size:13pt; font-weight:400;">v##version## build ##build##</span></p><p><span style=" font-size:13pt; font-weight:400;">Copyright ##year## Dominik Kapusta</span></p><p align="center"><a href="https://github.com/ayoy/fontedit"><span style=" font-size:13pt; font-weight:400; text-decoration: underline; color:#0068da;">Get Source Code</span></a><span style=" font-size:13pt; font-weight:400;">•</span><a href="https://github.com/ayoy/fontedit/issues"><span style=" font-size:13pt; font-weight:400; text-decoration: underline; color:#0068da;">Report a Bug</span></a></p><p><span style=" font-size:13pt; font-weight:400;">This program is distributed under the terms of </span><a href="https://www.gnu.org/licenses/gpl-3.0.en.html"><span style=" font-size:13pt; font-weight:400; text-decoration: underline; color:#0068da;">General Public License v3</span></a><span style=" font-size:13pt;">.</span></p><p><span style=" font-size:13pt; font-weight:400;"><br/>Icons made by </span><a href="https://www.flaticon.com/authors/smashicons"><span style=" font-size:13pt; font-weight:400; text-decoration: underline; color:#0068da;">Smashicons</span></a><span style=" font-size:13pt; font-weight:400;"> and </span><a href="https://www.flaticon.com/authors/freepik"><span style=" font-size:13pt; font-weight:400; text-decoration: underline; color:#0068da;">Freepik</span></a><span style=" font-size:13pt; font-weight:400;"> from </span><a href="https://www.flaticon.com/"><span style=" font-size:13pt; font-weight:400; text-decoration: underline; color:#0068da;">www.flaticon.com</span></a><span style=" font-size:13pt; font-weight:400;">.</span><a href="https://www.flaticon.com/"><span style=" font-size:13pt; font-weight:400; text-decoration: underline; color:#0068da;"><br/></span></a></p></body></html> Qt::RichText false Qt::AlignBottom|Qt::AlignLeading|Qt::AlignLeft true Qt::Vertical 20 40 Qt::Horizontal QDialogButtonBox::Ok true buttonBox accepted() AboutDialog accept() 248 254 157 274 buttonBox rejected() AboutDialog reject() 316 260 286 274 ================================================ FILE: app/ui/batchpixelchange.h ================================================ #ifndef GLYPHEDITCOMMAND_H #define GLYPHEDITCOMMAND_H #include #include #include #include struct PointHash { size_t operator()(const f2b::font::point &p) const { return std::hash()(p.x) ^ std::hash()(p.y); } }; struct BatchPixelChange { enum class ChangeType { Normal, Reverse }; std::unordered_map changes; void add(const f2b::font::point &pixel, bool value) { auto i = changes.find(pixel); if (i == changes.end()) { changes.insert({ pixel, value }); } else if (i->second != value) { changes.erase(pixel); } } void apply(f2b::font::glyph& glyph, ChangeType type = ChangeType::Normal) const { for (const auto& i : changes) { glyph.set_pixel_set(i.first, type == ChangeType::Normal ? i.second : !i.second); } } }; #endif // GLYPHEDITCOMMAND_H ================================================ FILE: app/ui/facewidget.cpp ================================================ #include "facewidget.h" #include "glyphinfowidget.h" #include "f2b_qt_compat.h" #include #include #include #include #include #include static constexpr auto printable_ascii_offset = ' '; static constexpr auto min_cell_height = 120.0; static constexpr auto min_image_height = min_cell_height - GlyphInfoWidget::descriptionHeight - 3 * GlyphInfoWidget::cellMargin; static constexpr auto max_image_width = FaceWidget::cell_width - 2 * GlyphInfoWidget::cellMargin; FaceWidget::FaceWidget(int columnCount, QGraphicsItem *parent) : QGraphicsWidget(parent), columnCount_ { columnCount } { setFocusPolicy(Qt::ClickFocus); layout_->setSpacing(0); layout_->setContentsMargins(0, 0, 0, 0); setLayout(layout_); } void FaceWidget::reset() { // TODO: Reuse items instead of deleting them all for (auto& item : childItems()) { if (item->zValue() == 0) { delete item; } } resetFocusWidget(); } QSizeF FaceWidget::calculateImageSize(f2b::font::glyph_size glyph_size) { // height: 6 + desc.height + 6 + img.height + 6 // width: 6 + img.width + 6 // max image width: 80 - 2*6 = 68 QSizeF imageSize { f2b::font::qsize_with_size(glyph_size) }; if (imageSize.width() > max_image_width) { imageSize.scale(max_image_width, qInf(), Qt::KeepAspectRatio); } else if (imageSize.height() < min_image_height) { imageSize.scale(qInf(), min_image_height, Qt::KeepAspectRatio); } auto size = imageSize; size.rheight() += GlyphInfoWidget::cellMargin + GlyphInfoWidget::descriptionHeight; size.rwidth() += 2 * GlyphInfoWidget::cellMargin; size.rheight() += 2 * GlyphInfoWidget::cellMargin; size.rheight() = qMax(size.height(), min_cell_height); size.rwidth() = qMax(size.width(), cell_width); itemSize_ = size; qDebug() << "NEW ITEM SIZE:" << itemSize_; return imageSize; } void FaceWidget::load(const f2b::font::face &face, f2b::font::margins margins) { face_ = &face; margins_ = margins; reset(); auto imageSize = calculateImageSize(face.glyphs_size()); auto index = 0; for (const auto& g : face.glyphs()) { auto glyphWidget = new GlyphInfoWidget(g, index, true, printable_ascii_offset + index, imageSize, margins); glyphWidget->setIsExportedAdjustable(false); addGlyphInfoWidget(glyphWidget, index); index++; } } void FaceWidget::load(f2b::font::face &face, f2b::font::margins margins) { face_ = &face; margins_ = margins; reloadFace(); } void FaceWidget::reloadFace() { reset(); if (face_ == nullptr) { return; } auto imageSize = calculateImageSize(face_->glyphs_size()); auto index = 0; auto widgetIndex = 0; auto exportedGlyphIDs = face_->exported_glyph_ids(); for (const auto& g : face_->glyphs()) { auto isExported = exportedGlyphIDs.find(index) != exportedGlyphIDs.end(); if (isExported || showsNonExportedItems_) { auto glyphWidget = new GlyphInfoWidget(g, index, isExported, printable_ascii_offset + index, imageSize, margins_); connect(glyphWidget, &GlyphInfoWidget::isExportedChanged, [&, index] (bool isExported) { emit glyphExportedStateChanged(index, isExported); }); addGlyphInfoWidget(glyphWidget, widgetIndex); widgetIndex++; } index++; } } void FaceWidget::addGlyphInfoWidget(QGraphicsLayoutItem *glyphWidget, std::size_t index) { auto row = index / columnCount_; auto col = index % columnCount_; if (row == 0) { layout_->setColumnFixedWidth(index % columnCount_, itemSize_.width()); } if (col == 0) { layout_->setRowFixedHeight(index / columnCount_, itemSize_.height()); } layout_->addItem(glyphWidget, index / columnCount_, index % columnCount_, 1, 1); } void FaceWidget::setCurrentGlyphIndex(std::optional index) { if (index.has_value()) { auto item = glyphWidgetAtIndex(index.value()); if (item) { setFocusForItem(item, true); } } else { clearFocus(); } } void FaceWidget::updateGlyphInfo(std::size_t index, std::optional glyph, std::optional isExported) { auto item = glyphWidgetAtIndex(index); if (item) { item->updateGlyph(glyph, isExported); } } GlyphInfoWidget* FaceWidget::glyphWidgetAtIndex(std::size_t index) { int itemIndex = index; if (!showsNonExportedItems_) { itemIndex = std::count_if(face_->exported_glyph_ids().begin(), face_->exported_glyph_ids().find(index), [&](std::size_t i) { return i < index; }); } return dynamic_cast(layout_->itemAt(itemIndex / columnCount_, itemIndex % columnCount_)); } QSize FaceWidget::glyphCoordsAtPos(QPointF pos) { qreal leftMargin; qreal topMargin; layout_->getContentsMargins(&leftMargin, &topMargin, nullptr, nullptr); int row = static_cast((pos.y() - topMargin) / itemSize_.height()); int col = static_cast((pos.x() - leftMargin) / itemSize_.width()); return { col, row }; } GlyphInfoWidget* FaceWidget::glyphWidgetAtPos(QPointF pos) { auto coords = glyphCoordsAtPos(pos); return dynamic_cast(layout_->itemAt(coords.height(), coords.width())); } void FaceWidget::setFocusForItem(QGraphicsLayoutItem *item, bool isFocused) { if (focusWidget_ == nullptr) { focusWidget_ = std::make_unique(this); focusWidget_->setZValue(1); focusWidget_->setColor(Qt::blue); } focusedItem_ = item; focusWidget_->setFocus(item, isFocused); if (isFocused) { QGraphicsView *graphicsView = scene()->views().first(); if (graphicsView != nullptr) graphicsView->ensureVisible(focusWidget_->geometry()); } } void FaceWidget::resetFocusWidget() { if (focusWidget_ != nullptr) { focusWidget_->setFocus(nullptr); } focusedItem_ = nullptr; } void FaceWidget::keyPressEvent(QKeyEvent *event) { if (focusWidget_ == nullptr) { return; } auto coords = glyphCoordsAtPos(focusWidget_->pos()); auto item = dynamic_cast(layout_->itemAt(coords.height(), coords.width())); if (item == nullptr) { return; } switch (event->key()) { case Qt::Key_Left: case Qt::Key_H: if (coords.width() > 0) --coords.rwidth(); break; case Qt::Key_Right: case Qt::Key_L: if (coords.width() < columnCount_ - 1) ++coords.rwidth(); break; case Qt::Key_Up: case Qt::Key_K: if (coords.height() > 0) --coords.rheight(); break; case Qt::Key_Down: case Qt::Key_J: { auto rowCount = (layout_->count() + columnCount_) / columnCount_; if (coords.height() < rowCount - 1) ++coords.rheight(); break; } case Qt::Key_Space: { auto isExported = face_->exported_glyph_ids().find(item->glyphIndex()) != face_->exported_glyph_ids().end(); emit glyphExportedStateChanged(item->glyphIndex(), !isExported); } } item = dynamic_cast(layout_->itemAt(coords.height(), coords.width())); if (item) { setFocusForItem(item, true); emit currentGlyphIndexChanged(item->glyphIndex()); } } void FaceWidget::mousePressEvent(QGraphicsSceneMouseEvent *event) { handleMousePress(event); } void FaceWidget::mouseDoubleClickEvent(QGraphicsSceneMouseEvent *event) { handleMousePress(event); } void FaceWidget::handleMousePress(QGraphicsSceneMouseEvent *event) { auto item = glyphWidgetAtPos(event->pos()); if (item != nullptr) { setFocusForItem(item, true); emit currentGlyphIndexChanged(item->glyphIndex()); } else { resetFocusWidget(); } } void FaceWidget::updateGeometry() { QGraphicsWidget::updateGeometry(); if (auto fw = focusWidget_.get()) { fw->setFocus(focusedItem_, true); } } void FaceWidget::setShowsNonExportedItems(bool isEnabled) { if (showsNonExportedItems_ != isEnabled) { showsNonExportedItems_ = isEnabled; if (face_ != nullptr && face_->exported_glyph_ids().size() != face_->num_glyphs()) { reloadFace(); } if (auto fw = focusWidget_.get()) { auto item = glyphWidgetAtPos(fw->pos()); if (item != nullptr) { setFocusForItem(item, true); emit currentGlyphIndexChanged(item->glyphIndex()); } } } } ================================================ FILE: app/ui/facewidget.h ================================================ #ifndef FACEWIDGET_H #define FACEWIDGET_H #include #include #include #include "focuswidget.h" #include #include class GlyphInfoWidget; class FaceWidget : public QGraphicsWidget { Q_OBJECT public: static constexpr auto cell_width = 80.0; explicit FaceWidget(int columnCount = 3, QGraphicsItem *parent = nullptr); void load(const f2b::font::face& face, f2b::font::margins margins); void load(f2b::font::face& face, f2b::font::margins margins); void setCurrentGlyphIndex(std::optional index); void updateGlyphInfo(std::size_t index, std::optional glyph, std::optional isExported = {}); bool showsNonExportedItems() const { return showsNonExportedItems_; } void setShowsNonExportedItems(bool isEnabled); signals: void currentGlyphIndexChanged(std::optional index); void glyphExportedStateChanged(std::size_t index, bool isExported); protected: void mousePressEvent(QGraphicsSceneMouseEvent *event) override; void mouseDoubleClickEvent(QGraphicsSceneMouseEvent *event) override; void keyPressEvent(QKeyEvent *event) override; void updateGeometry() override; private: void handleMousePress(QGraphicsSceneMouseEvent *event); void reloadFace(); void reset(); QSizeF calculateImageSize(f2b::font::glyph_size glyph_size); void addGlyphInfoWidget(QGraphicsLayoutItem* glyphWidget, std::size_t index); void setFocusForItem(QGraphicsLayoutItem *item, bool isFocused); void resetFocusWidget(); GlyphInfoWidget* glyphWidgetAtIndex(std::size_t index); GlyphInfoWidget* glyphWidgetAtPos(QPointF pos); QSize glyphCoordsAtPos(QPointF pos); QGraphicsLayoutItem *focusedItem_ { nullptr }; QGraphicsGridLayout *layout_ { new QGraphicsGridLayout() }; std::unique_ptr focusWidget_ { nullptr }; QSizeF itemSize_; int columnCount_; bool showsNonExportedItems_; const f2b::font::face* face_ { nullptr }; f2b::font::margins margins_; }; #endif // FACEWIDGET_H ================================================ FILE: app/ui/focuswidget.cpp ================================================ #include "focuswidget.h" #include #include #include #include FocusWidget::FocusWidget(QGraphicsItem *parent) : QGraphicsWidget(parent) { setZValue(1); } void FocusWidget::paint(QPainter *painter, const QStyleOptionGraphicsItem *option, QWidget *widget) { Q_UNUSED(option); Q_UNUSED(widget); painter->setPen(QPen(QBrush(color_), 2)); painter->drawRect(rect()); } void FocusWidget::setFocus(QGraphicsLayoutItem *item, bool isFocused) { if (item != nullptr) { if (isFocused) { setVisible(true); setGeometry(item->geometry()); ensureVisible(); } else { setVisible(false); } } else { setVisible(false); } } void FocusWidget::ensureVisible() { QGraphicsView *graphicsView = scene()->views().first(); if (graphicsView != nullptr) graphicsView->ensureVisible(this); } ================================================ FILE: app/ui/focuswidget.h ================================================ #ifndef FOCUSWIDGET_H #define FOCUSWIDGET_H #include class QGraphicsLayoutItem; class FocusWidget : public QGraphicsWidget { public: explicit FocusWidget(QGraphicsItem *parent = nullptr); virtual ~FocusWidget() = default; void setFocus(QGraphicsLayoutItem *item, bool isFocused = true); void paint(QPainter *painter, const QStyleOptionGraphicsItem *option, QWidget *widget = nullptr) override; const QColor& color() const { return color_; } void setColor(const QColor &color) { color_ = color; update(); } private: void ensureVisible(); QColor color_ { Qt::red }; }; #endif // FOCUSWIDGET_H ================================================ FILE: app/ui/glyphgraphicsview.cpp ================================================ #include "glyphgraphicsview.h" #include #include #include #include #include "glyphwidget.h" #include static constexpr auto max_zoom_level = 2.0; static constexpr auto min_zoom_level = 0.1; static constexpr auto zoom_factor = 1.01; GlyphGraphicsView::GlyphGraphicsView(QWidget *parent) : QGraphicsView(parent), scene_ { std::make_unique() } { scene_->setBackgroundBrush(QBrush(Qt::lightGray)); setScene(scene_.get()); } void GlyphGraphicsView::resizeEvent(QResizeEvent *event) { Q_UNUSED(event); if (!scene()->items().isEmpty()) { auto glyphView = scene()->items().constLast(); fitInView(glyphView, Qt::KeepAspectRatio); } } void GlyphGraphicsView::wheelEvent(QWheelEvent *event) { auto isZooming = event->modifiers().testFlag(Qt::ControlModifier); if (isZooming && !event->angleDelta().isNull()) { qreal factor = pow(zoom_factor, event->angleDelta().y()); setScale(factor); } else { QGraphicsView::wheelEvent(event); } } void GlyphGraphicsView::setScale(qreal factor) { auto transform = this->transform(); bool isZoomingInBeyondLimit { factor > 1.0 && transform.m11() > max_zoom_level }; bool isZoomingOutBeyondLimit { factor < 1.0 && transform.m11() < min_zoom_level }; if (isZoomingInBeyondLimit || isZoomingOutBeyondLimit) { return; } auto targetTransform = transform.scale(factor, factor); if (targetTransform.m11() > max_zoom_level) { auto clippedFactor = max_zoom_level / transform.m11(); targetTransform = transform.scale(clippedFactor, clippedFactor); } else if (targetTransform.m11() < min_zoom_level) { auto clippedFactor = min_zoom_level / transform.m11(); targetTransform = transform.scale(clippedFactor, clippedFactor); } setTransform(targetTransform); } ================================================ FILE: app/ui/glyphgraphicsview.h ================================================ #ifndef GLYPHGRAPHICSVIEW_H #define GLYPHGRAPHICSVIEW_H #include #include #include "f2b.h" class GlyphWidget; class GlyphGraphicsView : public QGraphicsView { public: explicit GlyphGraphicsView(QWidget *parent = nullptr); protected: void wheelEvent(QWheelEvent *event) override; void resizeEvent(QResizeEvent *event) override; private: void setScale(qreal factor); std::unique_ptr scene_; }; #endif // GLYPHGRAPHICSVIEW_H ================================================ FILE: app/ui/glyphinfowidget.cpp ================================================ #include "glyphinfowidget.h" #include "f2b_qt_compat.h" #include "common.h" #include #include #include #include #include #include static QString description(unsigned char asciiCode) { std::stringstream stream; stream << "hex: 0x" << std::setw(2) << std::setfill('0') << std::hex << static_cast(asciiCode) << std::endl; stream << "dec: " << std::setw(3) << std::dec << static_cast(asciiCode) << std::endl; if (std::isprint(asciiCode)) { stream << "chr: '" << asciiCode << "'"; } return QString::fromStdString(stream.str()); } GlyphInfoWidget::GlyphInfoWidget(const f2b::font::glyph &glyph, std::size_t index, bool isExported, unsigned char asciiCode, QSizeF imageSize, f2b::font::margins margins, QGraphicsItem *parent) : QGraphicsWidget(parent), description_ { description(asciiCode) }, imageSize_ { imageSize }, isExportedAdjustable_ { true }, isExported_ { isExported }, preview_ { f2b::font::glyph_preview_image(glyph, margins) }, margins_ { margins }, toggleExportedAction_ { QAction(tr("Exported")) }, glyphIndex_ { index } { toggleExportedAction_.setCheckable(true); toggleExportedAction_.setChecked(isExported_); connect(&toggleExportedAction_, &QAction::triggered, this, &GlyphInfoWidget::isExportedChanged); } void GlyphInfoWidget::setIsExportedAdjustable(bool isEnabled) { isExportedAdjustable_ = isEnabled; } void GlyphInfoWidget::updateGlyph(std::optional glyph, std::optional isExported, std::optional margins) { if (isExported.has_value()) { isExported_ = isExported.value(); } if (margins.has_value()) { margins_ = margins.value(); } if (glyph.has_value()) { preview_ = f2b::font::glyph_preview_image(glyph.value(), margins_); } update(); } void GlyphInfoWidget::contextMenuEvent(QGraphicsSceneContextMenuEvent *event) { if (!isExportedAdjustable_) { return; } QMenu menu; menu.addAction(&toggleExportedAction_); menu.exec(event->screenPos()); } void GlyphInfoWidget::paint(QPainter *painter, const QStyleOptionGraphicsItem *option, QWidget *widget) { Q_UNUSED(option); Q_UNUSED(widget); painter->fillRect(rect(), QBrush(Qt::white)); QPen pen(QBrush(Qt::darkGray), 0.5); painter->setPen(pen); painter->drawRect(rect()); QFont f(consoleFontName); f.setStyleHint(QFont::TypeWriter); f.setPixelSize(12); QRectF textRect(rect()); textRect.setTop(cellMargin); textRect.setLeft(cellMargin); textRect.setWidth(textRect.width() - cellMargin); textRect.setHeight(descriptionHeight); if (!isExported_) { painter->setPen(QPen(QBrush(QColor(Color::inactiveText)), 0.5)); } painter->setFont(f); painter->drawText(textRect, Qt::TextWordWrap, description_); // Preview rect (with margins) QRectF imageRect { QPointF(), imageSize_ }; imageRect.moveCenter(rect().center().toPoint()); imageRect.moveBottom(rect().height() - cellMargin); painter->fillRect(imageRect, Color::glyphMargin); // Glyph rect (margins removed) preview_.setColor(0, isExported_ ? Color::activeGlyph : Color::inactiveGlyph); QRectF imageSansMarginsRect = imageRect.marginsRemoved(QMarginsF(0, margins_.top, 0, margins_.bottom)); painter->drawImage(imageSansMarginsRect, preview_, preview_.rect()); // Glyph preview outline painter->setPen(QPen(QBrush(Qt::lightGray), 1)); painter->drawRect(imageRect.marginsAdded(QMarginsF(0.5, 0.5, 0.5, 0.5))); } ================================================ FILE: app/ui/glyphinfowidget.h ================================================ #ifndef GLYPHINFOWIDGET_H #define GLYPHINFOWIDGET_H #include #include #include #include #include class GlyphInfoWidget : public QGraphicsWidget { Q_OBJECT public: static constexpr auto cellMargin = 6.0; static constexpr auto descriptionHeight = 50.0; GlyphInfoWidget(const f2b::font::glyph& glyph, std::size_t index, bool isExported, unsigned char asciiCode, QSizeF imageSize, f2b::font::margins margins = {}, QGraphicsItem *parent = nullptr); std::size_t glyphIndex() const { return glyphIndex_; } void updateGlyph(std::optional glyph, std::optional isExported = {}, std::optional margins = {}); void paint(QPainter *painter, const QStyleOptionGraphicsItem *option, QWidget *widget = nullptr) override; void setIsExportedAdjustable(bool isEnabled); signals: void isExportedChanged(bool isExported); protected: void contextMenuEvent(QGraphicsSceneContextMenuEvent *event) override; private: const QString description_; const QSizeF imageSize_; bool isExportedAdjustable_; bool isExported_; QImage preview_; f2b::font::margins margins_; QAction toggleExportedAction_; std::size_t glyphIndex_; }; #endif // GLYPHINFOWIDGET_H ================================================ FILE: app/ui/glyphwidget.cpp ================================================ #include "glyphwidget.h" #include "common.h" #include #include #include #include #include #include #include static constexpr qreal gridSize = 20; QRectF rectForPoint(const f2b::font::point& point) { return QRectF(QPointF(point.x * gridSize, point.y * gridSize), QSizeF(gridSize, gridSize)); } GlyphWidget::GlyphWidget(const f2b::font::glyph& glyph, f2b::font::margins margins, QGraphicsItem* parent) : QGraphicsWidget(parent), glyph_ { glyph }, margins_ { margins } { setFocusPolicy(Qt::ClickFocus); setPreferredSize({ gridSize * static_cast(glyph.size().width), gridSize * static_cast(glyph.size().height) }); } void GlyphWidget::load(const f2b::font::glyph &glyph, f2b::font::margins margins) { glyph_ = glyph; margins_ = margins; setPreferredSize({ gridSize * static_cast(glyph.size().width), gridSize * static_cast(glyph.size().height) }); update(); } QRectF GlyphWidget::boundingRect() const { return QRectF(-0.25, -0.25, glyph_.size().width * gridSize + 0.25, glyph_.size().height * gridSize + 0.25); } void GlyphWidget::togglePixel(f2b::font::point p) { setPixel(p, !glyph_.is_pixel_set(p)); } void GlyphWidget::setPixel(f2b::font::point p, bool value) { if (glyph_.is_pixel_set(p) != value) { glyph_.set_pixel_set(p, value); affectedPixels_.add(p, value); if (!isDuringMouseMove_) { emit pixelsChanged(affectedPixels_); affectedPixels_.changes.clear(); } } } void GlyphWidget::applyChange(const BatchPixelChange &change, BatchPixelChange::ChangeType changeType) { // // Apply only if there are no affected pixels (no operation in progress) // - this is the initial call to redo() action. // if (affectedPixels_.changes.size() == 0) { change.apply(glyph_, changeType); update(); } } void GlyphWidget::paint(QPainter *painter, const QStyleOptionGraphicsItem *option, QWidget *widget) { Q_UNUSED(option); Q_UNUSED(widget); // QElapsedTimer timer; // timer.start(); QRectF rect = QRectF(0, 0, glyph_.size().width * gridSize, glyph_.size().height * gridSize); QRectF activeAreaRect = QRectF(0, margins_.top * gridSize, boundingRect().width(), (glyph_.size().height - margins_.top - margins_.bottom) * gridSize); painter->fillRect(rect, Color::glyphMargin); painter->fillRect(activeAreaRect, Qt::white); painter->setPen(QPen(QBrush(Qt::darkGray), 0.5)); painter->drawRect(rect); for (std::size_t row = 0; row < glyph_.size().height; ++row) { for (std::size_t col = 0; col < glyph_.size().width; ++col) { f2b::font::point p { col, row }; if (glyph_.is_pixel_set(p)) { painter->fillRect(rectForPoint(p), Qt::black); } } } for (qreal w = 0; w < rect.width(); w += gridSize) { painter->drawLine(QLineF(QPointF(w, rect.top()), QPointF(w, rect.bottom()))); } for (qreal h = 0; h < rect.height(); h += gridSize) { painter->drawLine(QLineF(QPointF(rect.left(), h), QPointF(rect.right(), h))); } if (focusedPixel_.has_value()) { painter->setPen(QPen(QBrush(Qt::red), 1)); painter->drawRect(rectForPoint(focusedPixel_.value())); } // qDebug() << __FUNCTION__ << option->exposedRect << boundingRect() // << option->exposedRect.toRect().contains(boundingRect().toRect()) // << QString("(%1ms)").arg(timer.elapsed()); } void GlyphWidget::keyPressEvent(QKeyEvent *event) { if (!focusedPixel_.has_value()) { return; } auto previousPixel = focusedPixel_; auto updateMode = UpdateMode::UpdateFocus; switch (event->key()) { case Qt::Key_Left: case Qt::Key_H: if (focusedPixel_->x > 0) --focusedPixel_->x; break; case Qt::Key_Right: case Qt::Key_L: if (focusedPixel_->x < glyph_.size().width - 1) ++focusedPixel_->x; break; case Qt::Key_Up: case Qt::Key_K: if (focusedPixel_->y > 0) --focusedPixel_->y; break; case Qt::Key_Down: case Qt::Key_J: if (focusedPixel_->y < glyph_.size().height - 1) ++focusedPixel_->y; break; case Qt::Key_Space: togglePixel(focusedPixel_.value()); updateMode = UpdateMode::UpdateFocusAndPixels; break; case Qt::Key_Alt: case Qt::Key_AltGr: case Qt::Key_Control: penState_ = false; break; } updateIfNeeded(updateMode, previousPixel); } void GlyphWidget::keyReleaseEvent(QKeyEvent *event) { if (!isDuringMouseMove_) { return; } if (event->key() == Qt::Key_Alt || event->key() == Qt::Key_AltGr || event->key() == Qt::Key_Control) { penState_ = true; } } void GlyphWidget::mousePressEvent(QGraphicsSceneMouseEvent *event) { handleMousePress(event); } void GlyphWidget::mouseDoubleClickEvent(QGraphicsSceneMouseEvent *event) { handleMousePress(event); } void GlyphWidget::handleMousePress(QGraphicsSceneMouseEvent *event) { f2b::font::point currentPixel = pointForEvent(event); // qDebug() << event << currentPixel.x << currentPixel.y; penState_ = !event->modifiers().testFlag(Qt::AltModifier) && !event->modifiers().testFlag(Qt::ControlModifier); affectedPixels_.changes.clear(); isDuringMouseMove_ = true; setPixel(currentPixel, penState_); auto previousPixel = focusedPixel_; focusedPixel_ = currentPixel; updateIfNeeded(UpdateMode::UpdateFocusAndPixels, previousPixel); } void GlyphWidget::mouseMoveEvent(QGraphicsSceneMouseEvent *event) { f2b::font::point currentPixel = pointForEvent(event); auto updateMode = UpdateMode::UpdateFocus; // If item not visited or visited with a different state if (affectedPixels_.changes.find(currentPixel) == affectedPixels_.changes.end() || affectedPixels_.changes[currentPixel] != penState_) { // qDebug() << "mouse move to new item" << currentPixel.x << currentPixel.y << penState_; updateMode = UpdateMode::UpdateFocusAndPixels; setPixel(currentPixel, penState_); } auto previousPixel = focusedPixel_; focusedPixel_ = currentPixel; updateIfNeeded(updateMode, previousPixel); } void GlyphWidget::updateIfNeeded(UpdateMode updateMode, std::optional previousFocusedPixel) { QRectF rect; if (focusedPixel_.has_value() && previousFocusedPixel.has_value() && *focusedPixel_ == *previousFocusedPixel && updateMode == UpdateMode::UpdateFocus) { // no change in focus so no redraw return; } if (focusedPixel_.has_value()) { rect = rectForPoint(focusedPixel_.value()); } if (previousFocusedPixel.has_value()) { auto previousRect = rectForPoint(previousFocusedPixel.value()); if (rect.isValid()) { rect = rect.united(previousRect); } else { rect = previousRect; } } if (rect.isValid()) { update(rect.marginsAdded({0.5, 0.5, 0.5, 0.5})); } } void GlyphWidget::mouseReleaseEvent(QGraphicsSceneMouseEvent *event) { Q_UNUSED(event); isDuringMouseMove_ = false; for (const auto& i : affectedPixels_.changes) { std::cout << "((" << i.first.x << ", " << i.first.y << "), " << i.second << ") "; } std::cout << std::endl; emit pixelsChanged(affectedPixels_); affectedPixels_.changes.clear(); } f2b::font::point GlyphWidget::pointForEvent(QGraphicsSceneMouseEvent *event) const { auto row = static_cast(std::max(event->pos().y() / gridSize, 0.0)); auto col = static_cast(std::max(event->pos().x() / gridSize, 0.0)); row = std::min(row, glyph_.size().height-1); col = std::min(col, glyph_.size().width-1); return { col, row }; } ================================================ FILE: app/ui/glyphwidget.h ================================================ #ifndef RAWGLYPHWIDGET_H #define RAWGLYPHWIDGET_H #include #include #include #include #include "batchpixelchange.h" class GlyphWidget : public QGraphicsWidget { Q_OBJECT enum class UpdateMode { UpdateFocus, UpdateFocusAndPixels }; public: explicit GlyphWidget(const f2b::font::glyph& glyph, f2b::font::margins margins = {}, QGraphicsItem* parent = nullptr); virtual ~GlyphWidget() = default; void load(const f2b::font::glyph& glyph, f2b::font::margins margins = {}); QRectF boundingRect() const override; void paint(QPainter *painter, const QStyleOptionGraphicsItem *option, QWidget *widget = nullptr) override; void applyChange(const BatchPixelChange &change, BatchPixelChange::ChangeType changeType = BatchPixelChange::ChangeType::Normal); signals: void pixelsChanged(const BatchPixelChange& changes); protected: void keyPressEvent(QKeyEvent *event) override; void keyReleaseEvent(QKeyEvent *event) override; void mousePressEvent(QGraphicsSceneMouseEvent *event) override; void mouseDoubleClickEvent(QGraphicsSceneMouseEvent *event) override; void mouseMoveEvent(QGraphicsSceneMouseEvent *event) override; void mouseReleaseEvent(QGraphicsSceneMouseEvent *event) override; private: void handleMousePress(QGraphicsSceneMouseEvent *event); void updateIfNeeded(UpdateMode updateMode, std::optional previousFocusedPixel); void togglePixel(f2b::font::point p); void setPixel(f2b::font::point p, bool value); f2b::font::point pointForEvent(QGraphicsSceneMouseEvent *event) const; f2b::font::glyph glyph_; f2b::font::margins margins_; BatchPixelChange affectedPixels_; std::optional focusedPixel_ {}; bool isDuringMouseMove_ { false }; bool penState_ { false }; }; #endif // RAWGLYPHWIDGET_H ================================================ FILE: app/updatehelper.cpp ================================================ #include "updatehelper.h" #include #include #include #include "semver.hpp" namespace SettingsKey { static const QString checkForUpdatesAtStartup = "main_window/check_for_updates_at_startup"; } UpdateHelper::UpdateHelper(QObject *parent) : QObject(parent), manager_ { std::make_unique() } { shouldCheckAtStartup_ = settings_.value(SettingsKey::checkForUpdatesAtStartup, true).toBool(); manager_->setStrictTransportSecurityEnabled(true); // qDebug() << QSslSocket::supportsSsl() << QSslSocket::sslLibraryBuildVersionString() << QSslSocket::sslLibraryVersionString(); connect(manager_.get(), &QNetworkAccessManager::finished, this, &UpdateHelper::handleReply); } void UpdateHelper::setShouldCheckAtStartup(bool check) { shouldCheckAtStartup_ = check; settings_.setValue(SettingsKey::checkForUpdatesAtStartup, check); } void UpdateHelper::checkForUpdatesIfNeeded() { if (shouldCheckAtStartup_) { checkForUpdates(false); } } void UpdateHelper::checkForUpdates(bool verbose) { QNetworkRequest request(QUrl("https://api.github.com/repos/ayoy/fontedit/releases/latest")); request.setRawHeader("Accept", "application/vnd.github.v3+json"); request.setAttribute(QNetworkRequest::User, verbose); manager_->get(request); } void UpdateHelper::handleReply(QNetworkReply *reply) { auto verbose = reply->request().attribute(QNetworkRequest::User).toBool(); if (reply->error() != QNetworkReply::NoError) { qDebug() << "Error fetching latest version:" << reply->error(); reply->deleteLater(); return; } QJsonParseError error; auto replyJSON = QJsonDocument::fromJson(reply->readAll(), &error); if (error.error != QJsonParseError::NoError) { qDebug() << "Error parsing JSON:" << error.errorString(); reply->deleteLater(); return; } auto latestVersionString = replyJSON.object().value("tag_name").toString(); if (latestVersionString.isNull()) { qDebug() << "Latest version unknown"; reply->deleteLater(); return; } else if (latestVersionString.startsWith('v')) { latestVersionString.remove(0, 1); } semver::version latestVersion { latestVersionString.toStdString() }; semver::version currentVersion { QApplication::applicationVersion().toStdString() }; if (latestVersion > currentVersion) { auto releaseNotes = replyJSON.object().value("body").toString(); auto urlString = replyJSON.object().value("html_url").toString(); qDebug() << "An update is available. Check out" << urlString << "for more info"; emit updateAvailable({ QApplication::applicationVersion(), latestVersionString, releaseNotes, QUrl(urlString) }); } else { qDebug() << "No update available"; if (verbose) emit updateNotAvailable(); } reply->deleteLater(); } ================================================ FILE: app/updatehelper.h ================================================ #ifndef UPDATEHELPER_H #define UPDATEHELPER_H #include #include #include class UpdateHelper : public QObject { Q_OBJECT public: struct Update { QString currentVersion; QString latestVersion; QString releaseNotes; QUrl webpageURL; }; explicit UpdateHelper(QObject *parent = nullptr); void checkForUpdatesIfNeeded(); void checkForUpdates(bool verbose = false); bool shouldCheckAtStartup() const { return shouldCheckAtStartup_; } void setShouldCheckAtStartup(bool check); signals: void updateAvailable(const Update& update); void updateNotAvailable(); private: void handleReply(QNetworkReply *reply); std::unique_ptr manager_; bool shouldCheckAtStartup_ { true }; bool isChecking_ { false }; QSettings settings_; }; #endif // UPDATEHELPER_H ================================================ FILE: app/utf8/CMakeLists.txt ================================================ cmake_minimum_required(VERSION 3.9) set(CMAKE_AUTOUIC OFF) set(CMAKE_AUTOMOC OFF) set(CMAKE_AUTORCC OFF) project(utf8 LANGUAGES CXX) add_library(utf8 INTERFACE) target_include_directories(utf8 SYSTEM INTERFACE $) ================================================ FILE: app/utf8/utf8/checked.h ================================================ // Copyright 2006 Nemanja Trifunovic /* Permission is hereby granted, free of charge, to any person or organization obtaining a copy of the software and accompanying documentation covered by this license (the "Software") to use, reproduce, display, distribute, execute, and transmit the Software, and to prepare derivative works of the Software, and to permit third-parties to whom the Software is furnished to do so, all subject to the following: The copyright notices in the Software and this entire statement, including the above license grant, this restriction and the following disclaimer, must be included in all copies of the Software, in whole or in part, and all derivative works of the Software, unless such copies or derivative works are solely in the form of machine-executable object code generated by a source language processor. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE, TITLE AND NON-INFRINGEMENT. IN NO EVENT SHALL THE COPYRIGHT HOLDERS OR ANYONE DISTRIBUTING THE SOFTWARE BE LIABLE FOR ANY DAMAGES OR OTHER LIABILITY, WHETHER IN CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ #ifndef UTF8_FOR_CPP_CHECKED_H_2675DCD0_9480_4c0c_B92A_CC14C027B731 #define UTF8_FOR_CPP_CHECKED_H_2675DCD0_9480_4c0c_B92A_CC14C027B731 #include "core.h" #include namespace utf8 { // Base for the exceptions that may be thrown from the library class exception : public ::std::exception { }; // Exceptions that may be thrown from the library functions. class invalid_code_point : public exception { uint32_t cp; public: invalid_code_point(uint32_t cp) : cp(cp) {} virtual const char* what() const throw() { return "Invalid code point"; } uint32_t code_point() const {return cp;} }; class invalid_utf8 : public exception { uint8_t u8; public: invalid_utf8 (uint8_t u) : u8(u) {} virtual const char* what() const throw() { return "Invalid UTF-8"; } uint8_t utf8_octet() const {return u8;} }; class invalid_utf16 : public exception { uint16_t u16; public: invalid_utf16 (uint16_t u) : u16(u) {} virtual const char* what() const throw() { return "Invalid UTF-16"; } uint16_t utf16_word() const {return u16;} }; class not_enough_room : public exception { public: virtual const char* what() const throw() { return "Not enough space"; } }; /// The library API - functions intended to be called by the users template octet_iterator append(uint32_t cp, octet_iterator result) { if (!utf8::internal::is_code_point_valid(cp)) throw invalid_code_point(cp); if (cp < 0x80) // one octet *(result++) = static_cast(cp); else if (cp < 0x800) { // two octets *(result++) = static_cast((cp >> 6) | 0xc0); *(result++) = static_cast((cp & 0x3f) | 0x80); } else if (cp < 0x10000) { // three octets *(result++) = static_cast((cp >> 12) | 0xe0); *(result++) = static_cast(((cp >> 6) & 0x3f) | 0x80); *(result++) = static_cast((cp & 0x3f) | 0x80); } else { // four octets *(result++) = static_cast((cp >> 18) | 0xf0); *(result++) = static_cast(((cp >> 12) & 0x3f) | 0x80); *(result++) = static_cast(((cp >> 6) & 0x3f) | 0x80); *(result++) = static_cast((cp & 0x3f) | 0x80); } return result; } template output_iterator replace_invalid(octet_iterator start, octet_iterator end, output_iterator out, uint32_t replacement) { while (start != end) { octet_iterator sequence_start = start; internal::utf_error err_code = utf8::internal::validate_next(start, end); switch (err_code) { case internal::UTF8_OK : for (octet_iterator it = sequence_start; it != start; ++it) *out++ = *it; break; case internal::NOT_ENOUGH_ROOM: throw not_enough_room(); case internal::INVALID_LEAD: out = utf8::append (replacement, out); ++start; break; case internal::INCOMPLETE_SEQUENCE: case internal::OVERLONG_SEQUENCE: case internal::INVALID_CODE_POINT: out = utf8::append (replacement, out); ++start; // just one replacement mark for the sequence while (start != end && utf8::internal::is_trail(*start)) ++start; break; } } return out; } template inline output_iterator replace_invalid(octet_iterator start, octet_iterator end, output_iterator out) { static const uint32_t replacement_marker = utf8::internal::mask16(0xfffd); return utf8::replace_invalid(start, end, out, replacement_marker); } template uint32_t next(octet_iterator& it, octet_iterator end) { uint32_t cp = 0; internal::utf_error err_code = utf8::internal::validate_next(it, end, cp); switch (err_code) { case internal::UTF8_OK : break; case internal::NOT_ENOUGH_ROOM : throw not_enough_room(); case internal::INVALID_LEAD : case internal::INCOMPLETE_SEQUENCE : case internal::OVERLONG_SEQUENCE : throw invalid_utf8(*it); case internal::INVALID_CODE_POINT : throw invalid_code_point(cp); } return cp; } template uint32_t peek_next(octet_iterator it, octet_iterator end) { return utf8::next(it, end); } template uint32_t prior(octet_iterator& it, octet_iterator start) { // can't do much if it == start if (it == start) throw not_enough_room(); octet_iterator end = it; // Go back until we hit either a lead octet or start while (utf8::internal::is_trail(*(--it))) if (it == start) throw invalid_utf8(*it); // error - no lead byte in the sequence return utf8::peek_next(it, end); } /// Deprecated in versions that include "prior" template uint32_t previous(octet_iterator& it, octet_iterator pass_start) { octet_iterator end = it; while (utf8::internal::is_trail(*(--it))) if (it == pass_start) throw invalid_utf8(*it); // error - no lead byte in the sequence octet_iterator temp = it; return utf8::next(temp, end); } template void advance (octet_iterator& it, distance_type n, octet_iterator end) { for (distance_type i = 0; i < n; ++i) utf8::next(it, end); } template typename std::iterator_traits::difference_type distance (octet_iterator first, octet_iterator last) { typename std::iterator_traits::difference_type dist; for (dist = 0; first < last; ++dist) utf8::next(first, last); return dist; } template octet_iterator utf16to8 (u16bit_iterator start, u16bit_iterator end, octet_iterator result) { while (start != end) { uint32_t cp = utf8::internal::mask16(*start++); // Take care of surrogate pairs first if (utf8::internal::is_lead_surrogate(cp)) { if (start != end) { uint32_t trail_surrogate = utf8::internal::mask16(*start++); if (utf8::internal::is_trail_surrogate(trail_surrogate)) cp = (cp << 10) + trail_surrogate + internal::SURROGATE_OFFSET; else throw invalid_utf16(static_cast(trail_surrogate)); } else throw invalid_utf16(static_cast(cp)); } // Lone trail surrogate else if (utf8::internal::is_trail_surrogate(cp)) throw invalid_utf16(static_cast(cp)); result = utf8::append(cp, result); } return result; } template u16bit_iterator utf8to16 (octet_iterator start, octet_iterator end, u16bit_iterator result) { while (start != end) { uint32_t cp = utf8::next(start, end); if (cp > 0xffff) { //make a surrogate pair *result++ = static_cast((cp >> 10) + internal::LEAD_OFFSET); *result++ = static_cast((cp & 0x3ff) + internal::TRAIL_SURROGATE_MIN); } else *result++ = static_cast(cp); } return result; } template octet_iterator utf32to8 (u32bit_iterator start, u32bit_iterator end, octet_iterator result) { while (start != end) result = utf8::append(*(start++), result); return result; } template u32bit_iterator utf8to32 (octet_iterator start, octet_iterator end, u32bit_iterator result) { while (start != end) (*result++) = utf8::next(start, end); return result; } // The iterator class template class iterator : public std::iterator { octet_iterator it; octet_iterator range_start; octet_iterator range_end; public: iterator () {} explicit iterator (const octet_iterator& octet_it, const octet_iterator& range_start, const octet_iterator& range_end) : it(octet_it), range_start(range_start), range_end(range_end) { if (it < range_start || it > range_end) throw std::out_of_range("Invalid utf-8 iterator position"); } // the default "big three" are OK octet_iterator base () const { return it; } uint32_t operator * () const { octet_iterator temp = it; return utf8::next(temp, range_end); } bool operator == (const iterator& rhs) const { if (range_start != rhs.range_start || range_end != rhs.range_end) throw std::logic_error("Comparing utf-8 iterators defined with different ranges"); return (it == rhs.it); } bool operator != (const iterator& rhs) const { return !(operator == (rhs)); } iterator& operator ++ () { utf8::next(it, range_end); return *this; } iterator operator ++ (int) { iterator temp = *this; utf8::next(it, range_end); return temp; } iterator& operator -- () { utf8::prior(it, range_start); return *this; } iterator operator -- (int) { iterator temp = *this; utf8::prior(it, range_start); return temp; } }; // class iterator } // namespace utf8 #endif //header guard ================================================ FILE: app/utf8/utf8/core.h ================================================ // Copyright 2006 Nemanja Trifunovic /* Permission is hereby granted, free of charge, to any person or organization obtaining a copy of the software and accompanying documentation covered by this license (the "Software") to use, reproduce, display, distribute, execute, and transmit the Software, and to prepare derivative works of the Software, and to permit third-parties to whom the Software is furnished to do so, all subject to the following: The copyright notices in the Software and this entire statement, including the above license grant, this restriction and the following disclaimer, must be included in all copies of the Software, in whole or in part, and all derivative works of the Software, unless such copies or derivative works are solely in the form of machine-executable object code generated by a source language processor. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE, TITLE AND NON-INFRINGEMENT. IN NO EVENT SHALL THE COPYRIGHT HOLDERS OR ANYONE DISTRIBUTING THE SOFTWARE BE LIABLE FOR ANY DAMAGES OR OTHER LIABILITY, WHETHER IN CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ #ifndef UTF8_FOR_CPP_CORE_H_2675DCD0_9480_4c0c_B92A_CC14C027B731 #define UTF8_FOR_CPP_CORE_H_2675DCD0_9480_4c0c_B92A_CC14C027B731 #include namespace utf8 { // The typedefs for 8-bit, 16-bit and 32-bit unsigned integers // You may need to change them to match your system. // These typedefs have the same names as ones from cstdint, or boost/cstdint typedef unsigned char uint8_t; typedef unsigned short uint16_t; typedef unsigned int uint32_t; // Helper code - not intended to be directly called by the library users. May be changed at any time namespace internal { // Unicode constants // Leading (high) surrogates: 0xd800 - 0xdbff // Trailing (low) surrogates: 0xdc00 - 0xdfff const uint16_t LEAD_SURROGATE_MIN = 0xd800u; const uint16_t LEAD_SURROGATE_MAX = 0xdbffu; const uint16_t TRAIL_SURROGATE_MIN = 0xdc00u; const uint16_t TRAIL_SURROGATE_MAX = 0xdfffu; const uint16_t LEAD_OFFSET = LEAD_SURROGATE_MIN - (0x10000 >> 10); const uint32_t SURROGATE_OFFSET = 0x10000u - (LEAD_SURROGATE_MIN << 10) - TRAIL_SURROGATE_MIN; // Maximum valid value for a Unicode code point const uint32_t CODE_POINT_MAX = 0x0010ffffu; template inline uint8_t mask8(octet_type oc) { return static_cast(0xff & oc); } template inline uint16_t mask16(u16_type oc) { return static_cast(0xffff & oc); } template inline bool is_trail(octet_type oc) { return ((utf8::internal::mask8(oc) >> 6) == 0x2); } template inline bool is_lead_surrogate(u16 cp) { return (cp >= LEAD_SURROGATE_MIN && cp <= LEAD_SURROGATE_MAX); } template inline bool is_trail_surrogate(u16 cp) { return (cp >= TRAIL_SURROGATE_MIN && cp <= TRAIL_SURROGATE_MAX); } template inline bool is_surrogate(u16 cp) { return (cp >= LEAD_SURROGATE_MIN && cp <= TRAIL_SURROGATE_MAX); } template inline bool is_code_point_valid(u32 cp) { return (cp <= CODE_POINT_MAX && !utf8::internal::is_surrogate(cp)); } template inline typename std::iterator_traits::difference_type sequence_length(octet_iterator lead_it) { uint8_t lead = utf8::internal::mask8(*lead_it); if (lead < 0x80) return 1; else if ((lead >> 5) == 0x6) return 2; else if ((lead >> 4) == 0xe) return 3; else if ((lead >> 3) == 0x1e) return 4; else return 0; } template inline bool is_overlong_sequence(uint32_t cp, octet_difference_type length) { if (cp < 0x80) { if (length != 1) return true; } else if (cp < 0x800) { if (length != 2) return true; } else if (cp < 0x10000) { if (length != 3) return true; } return false; } enum utf_error {UTF8_OK, NOT_ENOUGH_ROOM, INVALID_LEAD, INCOMPLETE_SEQUENCE, OVERLONG_SEQUENCE, INVALID_CODE_POINT}; /// Helper for get_sequence_x template utf_error increase_safely(octet_iterator& it, octet_iterator end) { if (++it == end) return NOT_ENOUGH_ROOM; if (!utf8::internal::is_trail(*it)) return INCOMPLETE_SEQUENCE; return UTF8_OK; } #define UTF8_CPP_INCREASE_AND_RETURN_ON_ERROR(IT, END) {utf_error ret = increase_safely(IT, END); if (ret != UTF8_OK) return ret;} /// get_sequence_x functions decode utf-8 sequences of the length x template utf_error get_sequence_1(octet_iterator& it, octet_iterator end, uint32_t& code_point) { if (it == end) return NOT_ENOUGH_ROOM; code_point = utf8::internal::mask8(*it); return UTF8_OK; } template utf_error get_sequence_2(octet_iterator& it, octet_iterator end, uint32_t& code_point) { if (it == end) return NOT_ENOUGH_ROOM; code_point = utf8::internal::mask8(*it); UTF8_CPP_INCREASE_AND_RETURN_ON_ERROR(it, end) code_point = ((code_point << 6) & 0x7ff) + ((*it) & 0x3f); return UTF8_OK; } template utf_error get_sequence_3(octet_iterator& it, octet_iterator end, uint32_t& code_point) { if (it == end) return NOT_ENOUGH_ROOM; code_point = utf8::internal::mask8(*it); UTF8_CPP_INCREASE_AND_RETURN_ON_ERROR(it, end) code_point = ((code_point << 12) & 0xffff) + ((utf8::internal::mask8(*it) << 6) & 0xfff); UTF8_CPP_INCREASE_AND_RETURN_ON_ERROR(it, end) code_point += (*it) & 0x3f; return UTF8_OK; } template utf_error get_sequence_4(octet_iterator& it, octet_iterator end, uint32_t& code_point) { if (it == end) return NOT_ENOUGH_ROOM; code_point = utf8::internal::mask8(*it); UTF8_CPP_INCREASE_AND_RETURN_ON_ERROR(it, end) code_point = ((code_point << 18) & 0x1fffff) + ((utf8::internal::mask8(*it) << 12) & 0x3ffff); UTF8_CPP_INCREASE_AND_RETURN_ON_ERROR(it, end) code_point += (utf8::internal::mask8(*it) << 6) & 0xfff; UTF8_CPP_INCREASE_AND_RETURN_ON_ERROR(it, end) code_point += (*it) & 0x3f; return UTF8_OK; } #undef UTF8_CPP_INCREASE_AND_RETURN_ON_ERROR template utf_error validate_next(octet_iterator& it, octet_iterator end, uint32_t& code_point) { // Save the original value of it so we can go back in case of failure // Of course, it does not make much sense with i.e. stream iterators octet_iterator original_it = it; uint32_t cp = 0; // Determine the sequence length based on the lead octet typedef typename std::iterator_traits::difference_type octet_difference_type; const octet_difference_type length = utf8::internal::sequence_length(it); // Get trail octets and calculate the code point utf_error err = UTF8_OK; switch (length) { case 0: return INVALID_LEAD; case 1: err = utf8::internal::get_sequence_1(it, end, cp); break; case 2: err = utf8::internal::get_sequence_2(it, end, cp); break; case 3: err = utf8::internal::get_sequence_3(it, end, cp); break; case 4: err = utf8::internal::get_sequence_4(it, end, cp); break; } if (err == UTF8_OK) { // Decoding succeeded. Now, security checks... if (utf8::internal::is_code_point_valid(cp)) { if (!utf8::internal::is_overlong_sequence(cp, length)){ // Passed! Return here. code_point = cp; ++it; return UTF8_OK; } else err = OVERLONG_SEQUENCE; } else err = INVALID_CODE_POINT; } // Failure branch - restore the original value of the iterator it = original_it; return err; } template inline utf_error validate_next(octet_iterator& it, octet_iterator end) { uint32_t ignored; return utf8::internal::validate_next(it, end, ignored); } } // namespace internal /// The library API - functions intended to be called by the users // Byte order mark const uint8_t bom[] = {0xef, 0xbb, 0xbf}; template octet_iterator find_invalid(octet_iterator start, octet_iterator end) { octet_iterator result = start; while (result != end) { utf8::internal::utf_error err_code = utf8::internal::validate_next(result, end); if (err_code != internal::UTF8_OK) return result; } return result; } template inline bool is_valid(octet_iterator start, octet_iterator end) { return (utf8::find_invalid(start, end) == end); } template inline bool starts_with_bom (octet_iterator it, octet_iterator end) { return ( ((it != end) && (utf8::internal::mask8(*it++)) == bom[0]) && ((it != end) && (utf8::internal::mask8(*it++)) == bom[1]) && ((it != end) && (utf8::internal::mask8(*it)) == bom[2]) ); } //Deprecated in release 2.3 template inline bool is_bom (octet_iterator it) { return ( (utf8::internal::mask8(*it++)) == bom[0] && (utf8::internal::mask8(*it++)) == bom[1] && (utf8::internal::mask8(*it)) == bom[2] ); } } // namespace utf8 #endif // header guard ================================================ FILE: app/utf8/utf8/unchecked.h ================================================ // Copyright 2006 Nemanja Trifunovic /* Permission is hereby granted, free of charge, to any person or organization obtaining a copy of the software and accompanying documentation covered by this license (the "Software") to use, reproduce, display, distribute, execute, and transmit the Software, and to prepare derivative works of the Software, and to permit third-parties to whom the Software is furnished to do so, all subject to the following: The copyright notices in the Software and this entire statement, including the above license grant, this restriction and the following disclaimer, must be included in all copies of the Software, in whole or in part, and all derivative works of the Software, unless such copies or derivative works are solely in the form of machine-executable object code generated by a source language processor. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE, TITLE AND NON-INFRINGEMENT. IN NO EVENT SHALL THE COPYRIGHT HOLDERS OR ANYONE DISTRIBUTING THE SOFTWARE BE LIABLE FOR ANY DAMAGES OR OTHER LIABILITY, WHETHER IN CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ #ifndef UTF8_FOR_CPP_UNCHECKED_H_2675DCD0_9480_4c0c_B92A_CC14C027B731 #define UTF8_FOR_CPP_UNCHECKED_H_2675DCD0_9480_4c0c_B92A_CC14C027B731 #include "core.h" namespace utf8 { namespace unchecked { template octet_iterator append(uint32_t cp, octet_iterator result) { if (cp < 0x80) // one octet *(result++) = static_cast(cp); else if (cp < 0x800) { // two octets *(result++) = static_cast((cp >> 6) | 0xc0); *(result++) = static_cast((cp & 0x3f) | 0x80); } else if (cp < 0x10000) { // three octets *(result++) = static_cast((cp >> 12) | 0xe0); *(result++) = static_cast(((cp >> 6) & 0x3f) | 0x80); *(result++) = static_cast((cp & 0x3f) | 0x80); } else { // four octets *(result++) = static_cast((cp >> 18) | 0xf0); *(result++) = static_cast(((cp >> 12) & 0x3f)| 0x80); *(result++) = static_cast(((cp >> 6) & 0x3f) | 0x80); *(result++) = static_cast((cp & 0x3f) | 0x80); } return result; } template uint32_t next(octet_iterator& it) { uint32_t cp = utf8::internal::mask8(*it); typename std::iterator_traits::difference_type length = utf8::internal::sequence_length(it); switch (length) { case 1: break; case 2: it++; cp = ((cp << 6) & 0x7ff) + ((*it) & 0x3f); break; case 3: ++it; cp = ((cp << 12) & 0xffff) + ((utf8::internal::mask8(*it) << 6) & 0xfff); ++it; cp += (*it) & 0x3f; break; case 4: ++it; cp = ((cp << 18) & 0x1fffff) + ((utf8::internal::mask8(*it) << 12) & 0x3ffff); ++it; cp += (utf8::internal::mask8(*it) << 6) & 0xfff; ++it; cp += (*it) & 0x3f; break; } ++it; return cp; } template uint32_t peek_next(octet_iterator it) { return utf8::unchecked::next(it); } template uint32_t prior(octet_iterator& it) { while (utf8::internal::is_trail(*(--it))) ; octet_iterator temp = it; return utf8::unchecked::next(temp); } // Deprecated in versions that include prior, but only for the sake of consistency (see utf8::previous) template inline uint32_t previous(octet_iterator& it) { return utf8::unchecked::prior(it); } template void advance (octet_iterator& it, distance_type n) { for (distance_type i = 0; i < n; ++i) utf8::unchecked::next(it); } template typename std::iterator_traits::difference_type distance (octet_iterator first, octet_iterator last) { typename std::iterator_traits::difference_type dist; for (dist = 0; first < last; ++dist) utf8::unchecked::next(first); return dist; } template octet_iterator utf16to8 (u16bit_iterator start, u16bit_iterator end, octet_iterator result) { while (start != end) { uint32_t cp = utf8::internal::mask16(*start++); // Take care of surrogate pairs first if (utf8::internal::is_lead_surrogate(cp)) { uint32_t trail_surrogate = utf8::internal::mask16(*start++); cp = (cp << 10) + trail_surrogate + internal::SURROGATE_OFFSET; } result = utf8::unchecked::append(cp, result); } return result; } template u16bit_iterator utf8to16 (octet_iterator start, octet_iterator end, u16bit_iterator result) { while (start < end) { uint32_t cp = utf8::unchecked::next(start); if (cp > 0xffff) { //make a surrogate pair *result++ = static_cast((cp >> 10) + internal::LEAD_OFFSET); *result++ = static_cast((cp & 0x3ff) + internal::TRAIL_SURROGATE_MIN); } else *result++ = static_cast(cp); } return result; } template octet_iterator utf32to8 (u32bit_iterator start, u32bit_iterator end, octet_iterator result) { while (start != end) result = utf8::unchecked::append(*(start++), result); return result; } template u32bit_iterator utf8to32 (octet_iterator start, octet_iterator end, u32bit_iterator result) { while (start < end) (*result++) = utf8::unchecked::next(start); return result; } // The iterator class template class iterator : public std::iterator { octet_iterator it; public: iterator () {} explicit iterator (const octet_iterator& octet_it): it(octet_it) {} // the default "big three" are OK octet_iterator base () const { return it; } uint32_t operator * () const { octet_iterator temp = it; return utf8::unchecked::next(temp); } bool operator == (const iterator& rhs) const { return (it == rhs.it); } bool operator != (const iterator& rhs) const { return !(operator == (rhs)); } iterator& operator ++ () { ::std::advance(it, utf8::internal::sequence_length(it)); return *this; } iterator operator ++ (int) { iterator temp = *this; ::std::advance(it, utf8::internal::sequence_length(it)); return temp; } iterator& operator -- () { utf8::unchecked::prior(it); return *this; } iterator operator -- (int) { iterator temp = *this; utf8::unchecked::prior(it); return temp; } }; // class iterator } // namespace utf8::unchecked } // namespace utf8 #endif // header guard ================================================ FILE: app/utf8/utf8.h ================================================ // Copyright 2006 Nemanja Trifunovic /* Permission is hereby granted, free of charge, to any person or organization obtaining a copy of the software and accompanying documentation covered by this license (the "Software") to use, reproduce, display, distribute, execute, and transmit the Software, and to prepare derivative works of the Software, and to permit third-parties to whom the Software is furnished to do so, all subject to the following: The copyright notices in the Software and this entire statement, including the above license grant, this restriction and the following disclaimer, must be included in all copies of the Software, in whole or in part, and all derivative works of the Software, unless such copies or derivative works are solely in the form of machine-executable object code generated by a source language processor. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE, TITLE AND NON-INFRINGEMENT. IN NO EVENT SHALL THE COPYRIGHT HOLDERS OR ANYONE DISTRIBUTING THE SOFTWARE BE LIABLE FOR ANY DAMAGES OR OTHER LIABILITY, WHETHER IN CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ #ifndef UTF8_FOR_CPP_2675DCD0_9480_4c0c_B92A_CC14C027B731 #define UTF8_FOR_CPP_2675DCD0_9480_4c0c_B92A_CC14C027B731 #include "utf8/checked.h" #include "utf8/unchecked.h" #endif // header guard ================================================ FILE: app/win/fontedit.rc ================================================ IDI_ICON1 ICON "fontedit.ico" ================================================ FILE: app/x11/fontedit.desktop ================================================ [Desktop Entry] Exec=fontedit Name=FontEdit Icon=fontedit Type=Application Categories=Qt;Development; Comment=Font image to bytes converter GenericName=Font to Source Code Converter Terminal=false ================================================ FILE: fontedit.iss ================================================ ; Script generated by the Inno Setup Script Wizard. ; SEE THE DOCUMENTATION FOR DETAILS ON CREATING INNO SETUP SCRIPT FILES! #define MyAppName "FontEdit" #define MyAppVersion "1.0.0" #define MyAppPublisher "Dominik Kapusta" #define MyAppURL "https://www.kapusta.cc" #define MyAppExeName "FontEdit.exe" [Setup] ; NOTE: The value of AppId uniquely identifies this application. Do not use the same AppId value in installers for other applications. ; (To generate a new GUID, click Tools | Generate GUID inside the IDE.) AppId={{D7FE7A75-E913-4479-8798-E5626C192A37} AppName={#MyAppName} AppVersion={#MyAppVersion} ;AppVerName={#MyAppName} {#MyAppVersion} AppPublisher={#MyAppPublisher} AppPublisherURL={#MyAppURL} AppSupportURL={#MyAppURL} AppUpdatesURL={#MyAppURL} DefaultDirName={autopf}\{#MyAppName} DisableProgramGroupPage=yes LicenseFile=C:\Users\ayoy\fontedit\LICENSE ; Uncomment the following line to run in non administrative install mode (install for current user only.) ;PrivilegesRequired=lowest OutputBaseFilename=FontEdit Compression=lzma SolidCompression=yes WizardStyle=modern [Languages] Name: "english"; MessagesFile: "compiler:Default.isl" [Tasks] Name: "desktopicon"; Description: "{cm:CreateDesktopIcon}"; GroupDescription: "{cm:AdditionalIcons}"; Flags: unchecked [Files] Source: "C:\Users\ayoy\build-fontedit-Desktop_Qt_5_14_1_MinGW_64_bit-Release\FontEdit.exe"; DestDir: "{app}"; Flags: ignoreversion Source: "C:\Users\ayoy\build-fontedit-Desktop_Qt_5_14_1_MinGW_64_bit-Release\win\*"; DestDir: "{app}"; Flags: ignoreversion recursesubdirs createallsubdirs Source: "C:\Qt\Tools\OpenSSL\Win_x64\bin\lib*.dll"; DestDir: "{app}"; Flags: ignoreversion recursesubdirs createallsubdirs ; NOTE: Don't use "Flags: ignoreversion" on any shared system files [Icons] Name: "{autoprograms}\{#MyAppName}"; Filename: "{app}\{#MyAppExeName}" Name: "{autodesktop}\{#MyAppName}"; Filename: "{app}\{#MyAppExeName}"; Tasks: desktopicon [Run] Filename: "{app}\{#MyAppExeName}"; Description: "{cm:LaunchProgram,{#StringChange(MyAppName, '&', '&&')}}"; Flags: nowait postinstall skipifsilent ================================================ FILE: lib/CMakeLists.txt ================================================ cmake_minimum_required(VERSION 3.9) project(libFontEdit) set(LIBFONTEDIT_STANDALONE_PROJECT OFF) if (CMAKE_CURRENT_SOURCE_DIR STREQUAL CMAKE_SOURCE_DIR) set(LIBFONTEDIT_STANDALONE_PROJECT ON) endif () add_subdirectory(src) enable_testing() if(${BUILD_TESTS}) add_subdirectory(test) endif() ================================================ FILE: lib/src/CMakeLists.txt ================================================ cmake_minimum_required(VERSION 3.9) set(CMAKE_CXX_STANDARD 17) set(CMAKE_CXX_STANDARD_REQUIRED ON) project(font2bytes) set(HEADERS f2b.h fontdata.h fontsourcecodegenerator.h format.h sourcecode.h ) set(SOURCES fontdata.cpp fontsourcecodegenerator.cpp ) if (LIBFONTEDIT_STANDALONE_PROJECT) add_library(${PROJECT_NAME} SHARED ${HEADERS} ${SOURCES} ) set_target_properties(${PROJECT_NAME} PROPERTIES SOVERSION 1.1.0 LIBRARY_OUTPUT_DIRECTORY ${CMAKE_BINARY_DIR}/lib) else () add_library(${PROJECT_NAME} ${HEADERS} ${SOURCES}) endif () target_link_libraries(${PROJECT_NAME} GSL) target_include_directories(${PROJECT_NAME} PUBLIC ${CMAKE_CURRENT_SOURCE_DIR} ) if (UNIX AND NOT APPLE) if (LIBFONTEDIT_STANDALONE_PROJECT) INSTALL(FILES ${HEADERS} DESTINATION include/f2b) INSTALL(FILES include/f2b.h DESTINATION include) INSTALL(TARGETS ${PROJECT_NAME} LIBRARY DESTINATION lib ARCHIVE DESTINATION lib) endif () endif () ================================================ FILE: lib/src/f2b.h ================================================ #ifndef F2B_PRIVATE_H #define F2B_PRIVATE_H #include "fontdata.h" #include "fontsourcecodegenerator.h" #include "format.h" #include "sourcecode.h" #endif // F2B_PRIVATE_H ================================================ FILE: lib/src/fontdata.cpp ================================================ #include "fontdata.h" #include namespace f2b { namespace font { glyph::glyph(font::glyph_size sz) : size_ { sz }, pixels_ { std::vector(sz.width * sz.height, false) } {} glyph::glyph(font::glyph_size sz, std::vector pixels) : size_ { sz }, pixels_ { std::move(pixels) } { if (pixels_.size() != sz.width * sz.height) { throw std::logic_error { "pixels size must equal glyph size (width * height)" }; } } void glyph::clear() { pixels_ = std::vector(size_.width * size_.height, false); } std::size_t glyph::top_margin() const { auto first_set_pixel = std::find(pixels_.begin(), pixels_.end(), true); return static_cast(std::distance(pixels_.begin(), first_set_pixel)) / size_.width; } std::size_t glyph::bottom_margin() const { auto last_set_pixel = std::find(pixels_.rbegin(), pixels_.rend(), true); return static_cast(std::distance(pixels_.rbegin(), last_set_pixel)) / size_.width; } face::face(const face_reader &data) : face(data.font_size(), read_glyphs(data)) { for (std::size_t i = 0; i < glyphs_.size(); i++) { exported_glyph_ids_.insert(i); } } face::face(font::glyph_size size, std::vector glyphs, std::set exported_glyph_ids) : sz_ { size }, glyphs_ { std::move(glyphs) }, exported_glyph_ids_ { std::move(exported_glyph_ids) } {} std::vector face::read_glyphs(const face_reader &data) { std::vector glyphs; glyphs.reserve(data.num_glyphs()); const std::vector::size_type pixels_per_glyph { data.font_size().width * data.font_size().height }; for (std::size_t i = 0; i < data.num_glyphs(); i++) { std::vector pixels; pixels.reserve(pixels_per_glyph); for (std::size_t y = 0; y < data.font_size().height; y++) { for (std::size_t x = 0; x < data.font_size().width; x++) { point p { x, y }; pixels.push_back(data.is_pixel_set(i, p)); } } glyph g { data.font_size(), pixels }; glyphs.push_back(g); } return glyphs; } margins face::calculate_margins() const noexcept { margins m {sz_.height, sz_.height}; std::for_each(glyphs_.begin(), glyphs_.end(), [&](const font::glyph& g) { m.top = std::min(m.top, g.top_margin()); m.bottom = std::min(m.bottom, g.bottom_margin()); }); return m; } } // namespace Font } // namespace f2b ================================================ FILE: lib/src/fontdata.h ================================================ #ifndef FONTDATA_H #define FONTDATA_H #include #include #include namespace f2b { namespace font { /** * @brief A struct that describes font top and bottom margins * (lines where no glyphs are drawn). */ struct margins { std::size_t top; std::size_t bottom; }; inline bool operator==(const margins& lhs, const margins& rhs) noexcept { return lhs.top == rhs.top && lhs.bottom == rhs.bottom; } inline bool operator!=(const margins& lhs, const margins& rhs) noexcept { return !(lhs == rhs); } /** * @brief A struct that describes font size (glyph size) in pixels. */ struct glyph_size { std::size_t width; std::size_t height; glyph_size with_margins(const margins& m) { glyph_size size { width, height }; auto m_height = m.top + m.bottom; if (m_height > height) { size.height = 0; } else { size.height -= m_height; } return size; } }; inline bool operator==(const glyph_size& lhs, const glyph_size& rhs) noexcept { return lhs.width == rhs.width && lhs.height == rhs.height; } inline bool operator!=(const glyph_size& lhs, const glyph_size& rhs) noexcept { return !(lhs == rhs); } /** * @brief A struct that describes a point in font glyph coordinates. */ struct point { std::size_t x; std::size_t y; std::size_t offset(glyph_size sz) { return y * sz.width + x; } }; inline bool operator==(const point& lhs, const point& rhs) noexcept { return lhs.x == rhs.x && lhs.y == rhs.y; } inline bool operator!=(const point& lhs, const point& rhs) noexcept { return !(lhs == rhs); } /** * @brief A class that describes a single Font Glyph. * * A Glyph is represented by a std::vector of a fixed size. */ class glyph { public: explicit glyph(glyph_size sz = {}); explicit glyph(glyph_size sz, std::vector pixels); f2b::font::glyph_size size() const noexcept { return size_; } bool is_pixel_set(point p) const { return pixels_[p.offset(size_)]; } void set_pixel_set(point p, bool is_set) { pixels_[p.offset(size_)] = is_set; } void clear(); const std::vector& pixels() const noexcept { return pixels_; } std::size_t top_margin() const; std::size_t bottom_margin() const; private: font::glyph_size size_; std::vector pixels_; }; inline bool operator==(const glyph& lhs, const glyph& rhs) noexcept { return lhs.size() == rhs.size() && lhs.pixels() == rhs.pixels(); } inline bool operator!=(const glyph& lhs, const glyph& rhs) noexcept { return !(lhs == rhs); } /** * @brief An abstract class defining an interface for a font face reader. * * FaceReader instance can be passed as a "data source" for the Face constructor. */ class face_reader { public: virtual glyph_size font_size() const = 0; virtual std::size_t num_glyphs() const = 0; virtual bool is_pixel_set(std::size_t glyph_id, point p) const = 0; virtual ~face_reader() = default; }; /** * @brief A class describing a font face (a set of glyphs for a specific * combination of font family, pixel size and weight). */ class face { public: /// The default constructor intializing an empty face explicit face() = default; /// The constructor reading a face using face reader. By default all glyphs are exported. explicit face(const face_reader &data); /// The constructor initializing a face with a given size, vector of glyphs and exported glyph IDs. explicit face(glyph_size glyphs_size, std::vector glyphs, std::set exported_glyph_ids = {}); f2b::font::glyph_size glyphs_size() const noexcept { return sz_; } std::size_t num_glyphs() const noexcept { return glyphs_.size(); } glyph& glyph_at(std::size_t index) { return glyphs_.at(index); } const glyph& glyph_at(std::size_t index) const { return glyphs_.at(index); } std::set& exported_glyph_ids() { return exported_glyph_ids_; } const std::set& exported_glyph_ids() const { return exported_glyph_ids_; } const std::vector& glyphs() const { return glyphs_; } void set_glyph(glyph g, std::size_t index) { glyphs_[index] = g; } void append_glyph(glyph g) { glyphs_.push_back(std::move(g)); } void delete_last_glyph() { if (glyphs_.size() > 0) glyphs_.pop_back(); } void clear_glyph(std::size_t index) { if (index >= glyphs_.size()) { throw std::out_of_range { "Glyph index out of range" }; } glyphs_[index].clear(); } glyph& operator[](char ascii) { if (ascii < ' ') { throw std::out_of_range { "Glyphs for 0-31 ASCII range are not supported" }; } return glyphs_[ascii-32]; } const glyph& operator[](char ascii) const { if (ascii < ' ') { throw std::out_of_range { "Glyphs for 0-31 ASCII range are not supported" }; } return glyphs_[ascii-32]; } /** * Calculcates margins of a face. * * This method analyzes all glyphs and takes the maximum common empty areas * on the top and bottom. */ margins calculate_margins() const noexcept; private: static std::vector read_glyphs(const face_reader &data); font::glyph_size sz_; std::vector glyphs_; std::set exported_glyph_ids_; }; inline bool operator==(const face& lhs, const face& rhs) noexcept { return lhs.glyphs_size() == rhs.glyphs_size() && lhs.glyphs() == rhs.glyphs(); } inline bool operator!=(const face& lhs, const face& rhs) noexcept { return !(lhs == rhs); } } // namespace font } // namespace f2b inline std::ostream& operator<<(std::ostream& os, const f2b::font::glyph& g) { std::size_t col = 0; for (auto p : g.pixels()) { os << p; if (++col >= g.size().width) { col = 0; os << std::endl; } } os << std::flush; return os; } #endif // FONTDATA_H ================================================ FILE: lib/src/fontsourcecodegenerator.cpp ================================================ #include "fontsourcecodegenerator.h" #include #include namespace f2b { font::margins pixel_margins(font::margins line_margins, font::glyph_size glyph_size) { return { line_margins.top * glyph_size.width, line_margins.bottom * glyph_size.width }; } std::string font_source_code_generator::current_timestamp() { auto t = std::time(nullptr); auto tm = *std::localtime(&t); std::ostringstream s; s << std::put_time(&tm, "%d-%m-%Y %H:%M:%S"); return s.str(); } std::string font_source_code_generator::comment_for_glyph(std::size_t index) { index += 32; std::ostringstream s; s << "Character 0x" << std::hex << std::setfill('0') << std::setw(2) << index << std::dec << " (" << index; if (std::isprint(index)) { s << ": '" << static_cast(index) << "'"; } s << ")"; return s.str(); } } ================================================ FILE: lib/src/fontsourcecodegenerator.h ================================================ #ifndef FONTSOURCECODEGENERATOR_H #define FONTSOURCECODEGENERATOR_H #include "fontdata.h" #include "sourcecode.h" #include "format.h" #include #include #include #include namespace f2b { static constexpr auto byte_size = 8; struct source_code_options { enum bit_numbering_type { lsb, msb }; enum export_method_type { export_selected, export_all }; uint8_t wrap_column = 80; export_method_type export_method { export_selected }; bit_numbering_type bit_numbering { lsb }; bool invert_bits { false }; bool include_line_spacing { false }; source_code::indentation indentation { source_code::tab {} }; }; /** * @brief Converts line margins to pixel margins. * @param line_margins - margins expressed in lines * @param glyph_size * @return Margins expressed in pixel offset for a given glyph size (width). */ font::margins pixel_margins(font::margins line_margins, font::glyph_size glyph_size); class font_source_code_generator_interface { public: virtual std::string current_timestamp() = 0; virtual std::string comment_for_glyph(std::size_t index) = 0; }; /** * @brief A simple converter which converts fixed-width fonts. * * Starting from the top-left 8x8 block, it converts each block from * left to right, from top to down. * * For 'w' width and 'h' height, each w x h block is converted to the total * amount of h*(Int(w/8)+1) bytes, i.e. each row is represented by Int(w/8)+1 bytes, * so: * * width | bytes * --------------- * 5 | 1 * 7 | 1 * 8 | 1 * 12 | 2 * 16 | 2 * 19 | 3 * * The left bit is the highest bit, and the first byte in a row is a leftmost byte. * Unused trailing bits are zeroed. * Example: * 'A' (17 pixels wide) * ...######........ -> 0x1F, 0x80, 0x00 * ...#######....... -> 0x1F, 0xC0, 0x00 * .......###....... -> 0x01, 0xC0, 0x00 * ......##.##...... -> 0x03, 0x60, 0x00 * ......##.##...... -> 0x03, 0x60, 0x00 * .....##...##..... -> 0x06, 0x30, 0x00 * .....##...##..... -> 0x06, 0x30, 0x00 * ....##....##..... -> 0x0C, 0x30, 0x00 * ....#########.... -> 0x0F, 0xF8, 0x00 * ...##########.... -> 0x1F, 0xF8, 0x00 * ...##.......##... -> 0x18, 0x0C, 0x00 * ..##........##... -> 0x30, 0x0C, 0x00 * ######...#######. -> 0xFC, 0x7F, 0x00 * ######...#######. -> 0xFC, 0x7F, 0x00 * ................. -> 0x00, 0x00, 0x00 * * This char will result in the byte sequence: 0x1F, 0x80, 0x00, 0x1F, 0xC0, 0x00, ... * * '9' (8 pixels wide) * ..XXXX.. -> 0x3C * .XX..XX. -> 0x66 * .XX..XX. -> 0x66 * ..XXXXX. -> 0x3E * .....XX. -> 0x06 * .....XX. -> 0x06 * .XX..XX. -> 0x66 * ..XXXX.. -> 0x3C * ........ -> 0x00 * * This char will result in the byte sequence: 0x3c, 0x66, 0x66, ... * */ class font_source_code_generator : private font_source_code_generator_interface { public: font_source_code_generator(source_code_options options): options_ { options } {} /** * A template method that generates source code for a given \c face. * * The source code format is defined as a template parameter, * and the optional \c font_name parameter allows for customising * the font byte array parameter. * * The generator builds the source code out of building blocks - * structs from \c source_code::Idiom namespace. * * @see source_code::Idiom */ template std::string generate(const font::face& face, std::string font_name = "font"); private: template std::string generate_all(const font::face& face, std::string font_name = "font"); template std::string generate_subset(const font::face& face, std::string font_name = "font"); template std::string subset_lut(const std::set& exported_glyph_ids, bool has_dummy_blank_glyph, std::size_t bytes_per_glyph); template void output_glyph(const font::glyph& glyph, font::glyph_size size, font::margins margins, std::ostream& s); std::string current_timestamp() override; std::string comment_for_glyph(std::size_t index) override; source_code_options options_; }; template void font_source_code_generator::output_glyph(const font::glyph& glyph, font::glyph_size size, font::margins margins, std::ostream& s) { using namespace source_code; std::bitset bits; std::size_t bit_pos { 0 }; std::size_t col { 0 }; auto append_byte = [&](std::bitset& bits, std::ios::pos_type& pos) { if (options_.invert_bits) { bits.flip(); } auto byte = static_cast(bits.to_ulong()); s << idiom::value { std::move(byte) }; bits.reset(); if (s.tellp() - pos >= options_.wrap_column) { s << idiom::array_line_break {}; pos = s.tellp(); s << idiom::begin_array_row { options_.indentation }; } }; auto pos = s.tellp(); s << idiom::begin_array_row { options_.indentation }; std::for_each(glyph.pixels().cbegin() + margins.top, glyph.pixels().cend() - margins.bottom, [&](auto pixel) { switch (options_.bit_numbering) { case source_code_options::lsb: bits[bit_pos] = pixel; break; case source_code_options::msb: bits[byte_size-1-bit_pos] = pixel; break; } ++bit_pos; ++col; if (col >= size.width) { append_byte(bits, pos); bit_pos = 0; col = 0; } else if (bit_pos >= byte_size) { append_byte(bits, pos); bit_pos = 0; } }); } template std::string font_source_code_generator::generate_all(const font::face& face, std::string font_name) { using namespace source_code; auto [size, margins] = [&] () -> std::pair { if (options_.include_line_spacing) { return { face.glyphs_size(), {} }; } auto line_margins = face.calculate_margins(); return { face.glyphs_size().with_margins(line_margins), pixel_margins(line_margins, face.glyphs_size()) }; }(); std::ostringstream s; s << idiom::begin { font_name, size, current_timestamp() } << std::endl; s << idiom::comment {} << std::endl; s << idiom::comment { "Pseudocode for retrieving data for a specific character:" } << std::endl; s << idiom::comment {} << std::endl; s << idiom::comment { "bytes_per_char = font_height * (font_width / 8 + ((font_width % 8) ? 1 : 0))" } << std::endl; s << idiom::comment { "offset = (ascii_code(character) - ascii_code(' ')) * bytes_per_char" } << std::endl; s << idiom::comment { "data = " + font_name + "[offset]" } << std::endl; s << idiom::comment {}; s << idiom::begin_array { std::move(font_name) }; std::size_t glyph_id { 0 }; for (const auto& glyph : face.glyphs()) { output_glyph(glyph, size, margins, s); s << idiom::comment { comment_for_glyph(glyph_id) }; s << idiom::array_line_break {}; ++glyph_id; } s << idiom::end_array {}; s << idiom::end {}; return s.str(); } template std::string font_source_code_generator::subset_lut(const std::set& exported_glyph_ids, bool has_dummy_blank_glyph, std::size_t bytes_per_glyph) { using namespace source_code; std::ostringstream s; // If there's a dummy blank glyph, first exported character is at index 1, not 0. V exported_id { static_cast(has_dummy_blank_glyph) }; auto last_exported_glyph = std::prev(exported_glyph_ids.end()); s << idiom::begin_array { "lut" }; // Control line breaks with this flag - add a line break only before an exported glyph bool is_previous_exported = true; for (std::size_t glyph_id = 0; glyph_id <= *last_exported_glyph; ++glyph_id) { if (exported_glyph_ids.find(glyph_id) != exported_glyph_ids.end()) { if (!is_previous_exported) s << idiom::array_line_break {}; s << idiom::begin_array_row { options_.indentation }; s << idiom::value { static_cast(bytes_per_glyph * exported_id) }; s << idiom::comment { comment_for_glyph(glyph_id) }; ++exported_id; s << idiom::array_line_break {}; is_previous_exported = true; } else { if (is_previous_exported) s << idiom::begin_array_row { options_.indentation }; s << idiom::value { 0 }; is_previous_exported = false; } } s << idiom::end_array {}; return s.str(); } template std::string font_source_code_generator::generate_subset(const font::face& face, std::string font_name) { using namespace source_code; auto [size, margins] = [&] () -> std::pair { if (options_.include_line_spacing) { return { face.glyphs_size(), {} }; } auto line_margins = face.calculate_margins(); return { face.glyphs_size().with_margins(line_margins), pixel_margins(line_margins, face.glyphs_size()) }; }(); std::ostringstream s; s << idiom::begin { font_name, size, current_timestamp() } << std::endl; s << idiom::comment {} << std::endl; s << idiom::comment { "Pseudocode for retrieving data for a specific character:" } << std::endl; s << idiom::comment {} << std::endl; s << idiom::comment { "offset = ascii_code(character) - ascii_code(' ')" } << std::endl; s << idiom::comment { "data = " + font_name + "[lut[offset]]" } << std::endl; s << idiom::comment {}; s << idiom::begin_array { std::move(font_name) }; // Not exported characters are replaced with a space character. // If space character (ASCII 32, the first glyph) itself is not exported, // we add a dummy blank character and default all not exported characters to it. bool has_dummy_blank_glyph = false; if (face.exported_glyph_ids().find(0) == face.exported_glyph_ids().end()) { output_glyph(font::glyph(face.glyphs_size()), size, margins, s); s << idiom::comment { "Dummy blank character" }; s << idiom::array_line_break {}; has_dummy_blank_glyph = true; } for (auto glyph_id : face.exported_glyph_ids()) { const auto& glyph = face.glyph_at(glyph_id); output_glyph(glyph, size, margins, s); s << idiom::comment { comment_for_glyph(glyph_id) }; s << idiom::array_line_break {}; } s << idiom::end_array {}; auto bytes_per_line = size.width / byte_size + (size.width % byte_size ? 1 : 0); auto bytes_per_glyph = size.height * bytes_per_line; auto max_offset = (face.exported_glyph_ids().size() - 1) * bytes_per_glyph; if (max_offset < (1<<8)) { s << subset_lut(face.exported_glyph_ids(), has_dummy_blank_glyph, bytes_per_glyph); } else if (max_offset < (1<<16)) { s << subset_lut(face.exported_glyph_ids(), has_dummy_blank_glyph, bytes_per_glyph); } else if (max_offset < (1ull<<32)) { s << subset_lut(face.exported_glyph_ids(), has_dummy_blank_glyph, bytes_per_glyph); } else { s << subset_lut(face.exported_glyph_ids(), has_dummy_blank_glyph, bytes_per_glyph); } s << idiom::end {}; return s.str(); } template std::string font_source_code_generator::generate(const font::face &face, std::string font_name) { switch (options_.export_method) { case source_code_options::export_all: return generate_all(face, font_name); case source_code_options::export_selected: return generate_subset(face, font_name); } } } // namespace f2b #endif // FONTSOURCECODEGENERATOR_H ================================================ FILE: lib/src/format.h ================================================ #ifndef FORMAT_H #define FORMAT_H #include #include #include #include "sourcecode.h" namespace f2b { /// This namespace defines available source code formats namespace format { using namespace source_code; /// A type that identifies C-based language format. struct c_based {}; /// A type that identifies Python language format. struct python {}; /// C-style code (suitable also for C++) struct c { using lang = c_based; static constexpr std::string_view identifier = "c"; }; /// Arduino-flavoured C-style code struct arduino { using lang = c_based; static constexpr std::string_view identifier = "arduino"; }; /// Python code format for List object struct python_list { using lang = python; static constexpr std::string_view identifier = "python-list"; }; /// Python code format for Bytes object struct python_bytes { using lang = python; static constexpr std::string_view identifier = "python-bytes"; }; constexpr auto available_formats = { c::identifier, arduino::identifier, python_list::identifier, python_bytes::identifier }; } // namespace Format /// Type trait that is true for C-based language formats template struct is_c_based : std::false_type {}; template struct is_c_based::value>> : std::true_type {}; /// Type trait that is true for Python language formats template struct is_python : std::false_type {}; template struct is_python::value>> : std::true_type {}; static_assert (is_c_based::value, "***"); static_assert (is_c_based::value, "***"); static_assert (!is_c_based::value, "***"); static_assert (!is_c_based::value, "***"); static_assert (!is_python::value, "***"); static_assert (!is_python::value, "***"); static_assert (is_python::value, "***"); static_assert (is_python::value, "***"); // // The code below defines operator<< for all structs from the // source_code::idiom namespace. These functions use type traits // and compile-time ifs and to improve source code generation // performance. // template struct is_bytearray : std::false_type {}; template struct is_bytearray::value && std::is_same::value>> : std::true_type {}; static_assert (!is_bytearray::value, "***"); static_assert (!is_bytearray::value, "***"); static_assert (!is_bytearray::value, "***"); static_assert (!is_bytearray::value, "***"); static_assert (is_bytearray::value, "***"); // Begin template inline std::ostream& operator<<(std::ostream& s, source_code::idiom::begin b) { if constexpr (is_c_based::value) { s << "//\n// " << b.font_name << "\n" << "// Font Size: " << b.font_size.width << "x" << b.font_size.height << "px\n" << "// Created: " << b.timestamp << "\n//\n"; if constexpr (std::is_same::value) { s << "\n#include \n"; } else { s << "\n#include \n"; } } else if constexpr (is_python::value) { s << "#\n# " << b.font_name << "\n" << "# Font Size: " << b.font_size.width << "x" << b.font_size.height << "px\n" << "# Created: " << b.timestamp << "\n#\n"; } return s; } // Constant template inline std::ostream& operator<<(std::ostream& s, source_code::idiom::constant c) { using namespace source_code::idiom; if constexpr (std::is_same::value) { if constexpr (std::is_same::value) { s << "\n\nconst unsigned char "; } else if constexpr (std::is_same::value) { s << "\n\nconst int8_t "; } else if constexpr (std::is_same::value) { s << "\n\nconst int16_t "; } else if constexpr (std::is_same::value) { s << "\n\nconst int32_t "; } s << c.name << " = " << c.value << ";\n"; } else if constexpr (std::is_same::value) { if constexpr (std::is_same::value) { s << "\n\nconst uint8_t "; } else if constexpr (std::is_same::value) { s << "\n\nconst int8_t "; } else if constexpr (std::is_same::value) { s << "\n\nconst int16_t "; } else if constexpr (std::is_same::value) { s << "\n\nconst int32_t "; } s << c.name << " PROGMEM = " << c.value << ";\n"; } else if constexpr (is_python::value) { s << "\n\n" << c.name << " = " << c.value << "\n"; } return s; } // BeginArray template inline std::ostream& operator<<(std::ostream& s, source_code::idiom::begin_array b) { using namespace source_code::idiom; if constexpr (std::is_same::value) { if constexpr (std::is_same::value) { s << "\n\nconst unsigned char "; } else if constexpr (std::is_same::value) { s << "\n\nconst uint16_t "; } else if constexpr (std::is_same::value) { s << "\n\nconst uint32_t "; } else if constexpr (std::is_same::value) { s << "\n\nconst uint64_t "; } else if constexpr (std::is_same::value) { s << "\n\nconst int8_t "; } else if constexpr (std::is_same::value) { s << "\n\nconst int16_t "; } else if constexpr (std::is_same::value) { s << "\n\nconst int32_t "; } else if constexpr (std::is_same::value) { s << "\n\nconst int64_t "; } s << b.array_name << "[] = {\n"; } else if constexpr (std::is_same::value) { if constexpr (std::is_same::value) { s << "\n\nconst uint8_t "; } else if constexpr (std::is_same::value) { s << "\n\nconst uint16_t "; } else if constexpr (std::is_same::value) { s << "\n\nconst uint32_t "; } else if constexpr (std::is_same::value) { s << "\n\nconst uint64_t "; } else if constexpr (std::is_same::value) { s << "\n\nconst int8_t "; } else if constexpr (std::is_same::value) { s << "\n\nconst int16_t "; } else if constexpr (std::is_same::value) { s << "\n\nconst int32_t "; } else if constexpr (std::is_same::value) { s << "\n\nconst int64_t "; } s << b.array_name << "[] PROGMEM = {\n"; } else if constexpr (std::is_same::value) { s << "\n\n" << b.array_name << " = [\n"; } else if constexpr (std::is_same::value) { if constexpr (std::is_same::value) { s << "\n\n" << b.array_name << " = b'' \\\n"; } else { s << "\n\n" << b.array_name << " = [\n"; } } return s; } // BeginArrayRow template inline std::ostream& operator<<(std::ostream& s, source_code::idiom::begin_array_row b) { using namespace source_code::idiom; if constexpr (std::is_same::value && std::is_same::value) { s << b.tab << "b'"; } else { s << b.tab; } return s; } // Value template inline std::ostream& operator<<(std::ostream& s, source_code::idiom::value v) { if constexpr (std::is_same::value) { if constexpr (std::is_same::value) { s << "\\x" << std::setw(2) << std::setfill('0') << std::uppercase << std::hex << static_cast(v.value); } else { s << "0x" << std::setw(2) << std::setfill('0') << std::uppercase << std::hex << static_cast(v.value) << ","; } } else { s << std::resetiosflags(std::ios_base::basefield) << v.value << ","; } return s; } // Comment template inline std::ostream& operator<<(std::ostream& s, source_code::idiom::comment b) { if constexpr (is_c_based::value) { if constexpr (!std::is_void::value) { // add a space if a comment is inside an array s << " "; } s << "// " << b.comment; } else if constexpr (is_python::value && !is_bytearray::value) { if constexpr (!std::is_void::value) { s << " "; } s << "# " << b.comment; } return s; } // LineBreak template inline std::ostream& operator<<(std::ostream& s, source_code::idiom::array_line_break) { if constexpr (is_bytearray::value) { s << "' \\\n"; } else { s << "\n"; } return s; } // EndArray template inline std::ostream& operator<<(std::ostream& s, source_code::idiom::end_array) { if constexpr (is_c_based::value) { s << "};\n"; } else { if constexpr (std::is_same::value || !is_bytearray::value) { s << "]\n"; } } return s; } // End template inline std::ostream& operator<<(std::ostream& s, source_code::idiom::end) { s << "\n\n"; return s; } } // namespace f2b #endif // FORMAT_H ================================================ FILE: lib/src/include/f2b.h ================================================ #ifndef F2B_H #define F2B_H #include "f2b/f2b.h" #endif // F2B_H ================================================ FILE: lib/src/sourcecode.h ================================================ #ifndef SOURCECODE_H #define SOURCECODE_H #include "fontdata.h" #include #include #include namespace f2b { namespace source_code { /// A struct representing a Tabulation character struct tab {}; /// A struct representing a repeated space character (used instead of tabulator) struct space { std::size_t num_spaces; }; /// Indentation can be either a Tab, or multiple Spaces. using indentation = std::variant; /** * This namespace gathers building blocks for a source code generator: * - begin (source code file) * - begin array * - begin array row * - constant definition * - value (e.g. byte) * - comment * - line break with an array * - end array * - end (source code file). * * All the structs in this namespace are templates taking Source Code Format * as a template parameter. They also take parameters as necessary (e.g. * \c array_name for \c BeginArray). * All the structs define the \c operator<< which outputs the relevant source * code idiom to the given output stream. \c FontSourceCodeGenerator uses them * with an \c std::stringstream to output the resulting source code of the font face. */ namespace idiom { template struct begin { std::string font_name; font::glyph_size font_size; std::string timestamp; }; template struct constant { std::string name; V value; }; template struct begin_array { std::string array_name; }; template struct begin_array_row { indentation tab; }; template struct value { V value; }; template struct comment { std::string comment; }; template struct array_line_break {}; template struct end_array {}; template struct end {}; } // namespace Idiom inline bool operator==(const indentation& lhs, const indentation& rhs) { if (std::holds_alternative(lhs) && std::holds_alternative(rhs)) { return true; } if (std::holds_alternative(lhs) && std::holds_alternative(rhs)) { return std::get(lhs).num_spaces == std::get(rhs).num_spaces; } return false; } inline std::ostream& operator<<(std::ostream& o, const indentation& t) { if (std::holds_alternative(t)) { o << std::string(std::get(t).num_spaces, ' '); } else { o << "\t"; } return o; } } // namespace SourceCode } // namespace f2b #endif // SOURCECODE_H ================================================ FILE: lib/test/CMakeLists.txt ================================================ cmake_minimum_required(VERSION 3.9) set(CMAKE_CXX_STANDARD 17) set(CMAKE_CXX_STANDARD_REQUIRED ON) set(UNIT_TEST_LIST fontface glyph sourcecode) foreach(NAME IN LISTS UNIT_TEST_LIST) list(APPEND UNIT_TEST_SOURCE_LIST ${NAME}_test.cpp) endforeach() set(TARGET_NAME font2bytes_tests) add_executable(${TARGET_NAME} ${UNIT_TEST_SOURCE_LIST}) target_link_libraries(${TARGET_NAME} PUBLIC font2bytes gtest_main) target_include_directories(${TARGET_NAME} PUBLIC ../src) add_test(NAME ${TARGET_NAME} COMMAND ${TARGET_NAME}) ================================================ FILE: lib/test/fontface_test.cpp ================================================ #include "gtest/gtest.h" #include "fontdata.h" using namespace f2b; class TestFaceData : public font::face_reader { public: font::glyph_size font_size() const override { return { 4, 3 }; } std::size_t num_glyphs() const override { return 5; } bool is_pixel_set(std::size_t glyph_id, font::point p) const override { auto glyph_offset = glyph_id * font_size().width * font_size().height; return pixels[glyph_offset + p.offset(font_size())]; } std::vector pixels { 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 1, 0, // 0, 1, 0, 0, 1, 0, 1, 0, 1, 0, 0, 1, // 1, 1, 1, 1, 1, 1, 0, 1, 1, 0, 1, 1, // 1, 0, 0, 1, 1, 0, 1, 0, 0, 0, 1, 1, // 0, 1, 1, 1, 1, 1, 0, 0, 1, 0, 1, 0 }; }; TEST(FaceTest, Initialization) { TestFaceData test_data; font::face face(test_data); EXPECT_EQ(5, face.num_glyphs()); auto g = face.glyph_at(2); EXPECT_EQ(test_data.font_size(), g.size()); for (std::size_t i = 0; i < face.num_glyphs(); i++) { auto g = face.glyph_at(i); auto g_offset = i * test_data.font_size().width * test_data.font_size().height; for (std::size_t y = 0; y < test_data.font_size().height; y++) { for (std::size_t x = 0; x < test_data.font_size().width; x++) { font::point p { x, y }; auto offset = g_offset + p.offset(test_data.font_size()); EXPECT_EQ(bool(test_data.pixels[offset]), g.is_pixel_set(p)) << "glyph: " << i << " x: " << x << " y: " << y; } } } } ================================================ FILE: lib/test/glyph_test.cpp ================================================ #include "gtest/gtest.h" #include "fontdata.h" using namespace f2b; TEST(GlyphTest, API) { font::glyph_size sz { 5, 5 }; EXPECT_EQ(sz, font::glyph(sz).size()); } ================================================ FILE: lib/test/sourcecode_test.cpp ================================================ #include "gtest/gtest.h" TEST(SourceCodeTest, IdiomTraits) { } ================================================ FILE: test/CMakeLists.txt ================================================ cmake_minimum_required(VERSION 3.9) set(CMAKE_CXX_STANDARD 17) set(CMAKE_CXX_STANDARD_REQUIRED ON) find_package(Qt5 COMPONENTS Core Gui Widgets REQUIRED) set(UNIT_TESTS f2b_qt_compat_test.cpp sourcecodegeneration_test.cpp ) set(TARGET_NAME fontedit_app_tests) add_executable(${TARGET_NAME} ${UNIT_TESTS}) target_link_libraries(${TARGET_NAME} PUBLIC Qt5::Widgets Qt5::Core appbundle gtest_main) target_compile_definitions(${TARGET_NAME} PRIVATE ASSETS_DIR=\"${CMAKE_CURRENT_SOURCE_DIR}/assets\") add_test(NAME ${TARGET_NAME} COMMAND ${TARGET_NAME}) ================================================ FILE: test/assets/monaco8-subset.c-test ================================================ // // font // Font Size: 5x8px // Created: // #include // // Pseudocode for retrieving data for a specific character: // // offset = ascii_code(character) - ascii_code(' ') // data = font[lut[offset]] // const unsigned char font[] = { 0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00, // Character 0x20 (32: ' ') 0x04,0x04,0x04,0x04,0x04,0x00,0x04,0x00, // Character 0x21 (33: '!') 0x0A,0x0A,0x00,0x00,0x00,0x00,0x00,0x00, // Character 0x22 (34: '"') 0x00,0x08,0x0E,0x0A,0x0F,0x04,0x00,0x00, // Character 0x23 (35: '#') 0x04,0x0E,0x06,0x04,0x0C,0x0C,0x0E,0x00, // Character 0x24 (36: '$') 0x12,0x09,0x05,0x0C,0x16,0x14,0x01,0x00, // Character 0x25 (37: '%') 0x06,0x05,0x06,0x02,0x0D,0x09,0x06,0x00, // Character 0x26 (38: '&') 0x04,0x04,0x00,0x00,0x00,0x00,0x00,0x00, // Character 0x27 (39: ''') 0x08,0x04,0x02,0x02,0x02,0x02,0x04,0x08, // Character 0x28 (40: '(') 0x02,0x04,0x08,0x08,0x08,0x08,0x04,0x02, // Character 0x29 (41: ')') 0x04,0x0E,0x0E,0x04,0x00,0x00,0x00,0x00, // Character 0x2a (42: '*') 0x00,0x00,0x04,0x04,0x0F,0x04,0x00,0x00, // Character 0x2b (43: '+') 0x00,0x00,0x00,0x00,0x00,0x04,0x04,0x04, // Character 0x2c (44: ',') 0x00,0x00,0x00,0x00,0x0E,0x00,0x00,0x00, // Character 0x2d (45: '-') 0x00,0x00,0x00,0x00,0x00,0x04,0x04,0x00, // Character 0x2e (46: '.') 0x00,0x08,0x00,0x04,0x02,0x02,0x01,0x00, // Character 0x2f (47: '/') 0x06,0x0A,0x09,0x15,0x0B,0x0A,0x04,0x00, // Character 0x30 (48: '0') 0x04,0x06,0x04,0x04,0x04,0x04,0x00,0x00, // Character 0x31 (49: '1') 0x0E,0x08,0x08,0x04,0x02,0x02,0x00,0x00, // Character 0x32 (50: '2') 0x06,0x08,0x0C,0x0C,0x08,0x08,0x06,0x00, // Character 0x33 (51: '3') 0x00,0x0C,0x0A,0x08,0x0F,0x08,0x00,0x00, // Character 0x34 (52: '4') 0x0E,0x02,0x02,0x0C,0x08,0x08,0x06,0x00, // Character 0x35 (53: '5') 0x0C,0x02,0x00,0x0B,0x12,0x0A,0x04,0x00, // Character 0x36 (54: '6') 0x0E,0x08,0x08,0x04,0x00,0x02,0x00,0x00, // Character 0x37 (55: '7') 0x0C,0x0A,0x0E,0x0E,0x0A,0x0A,0x04,0x00, // Character 0x38 (56: '8') 0x06,0x08,0x09,0x0E,0x08,0x08,0x06,0x00, // Character 0x39 (57: '9') 0x00,0x00,0x04,0x00,0x00,0x04,0x04,0x00, // Character 0x3a (58: ':') 0x00,0x00,0x04,0x00,0x00,0x04,0x04,0x04, // Character 0x3b (59: ';') 0x00,0x00,0x00,0x04,0x02,0x0C,0x00,0x00, // Character 0x3c (60: '<') 0x00,0x00,0x00,0x0F,0x00,0x00,0x00,0x00, // Character 0x3d (61: '=') 0x00,0x00,0x01,0x06,0x08,0x02,0x00,0x00, // Character 0x3e (62: '>') 0x06,0x08,0x08,0x04,0x00,0x00,0x00,0x00, // Character 0x3f (63: '?') 0x06,0x00,0x17,0x13,0x0F,0x00,0x04,0x00, // Character 0x40 (64: '@') 0x04,0x04,0x0E,0x0A,0x0E,0x09,0x00,0x00, // Character 0x41 (65: 'A') 0x06,0x0A,0x0A,0x06,0x0A,0x0A,0x00,0x00, // Character 0x42 (66: 'B') 0x0C,0x02,0x00,0x01,0x02,0x02,0x0C,0x00, // Character 0x43 (67: 'C') 0x06,0x08,0x00,0x00,0x08,0x0A,0x00,0x00, // Character 0x44 (68: 'D') 0x0E,0x02,0x02,0x0E,0x02,0x02,0x00,0x00, // Character 0x45 (69: 'E') 0x0E,0x02,0x02,0x0E,0x02,0x02,0x00,0x00, // Character 0x46 (70: 'F') 0x0C,0x02,0x00,0x09,0x0A,0x0A,0x0C,0x00, // Character 0x47 (71: 'G') 0x00,0x09,0x09,0x0F,0x09,0x09,0x00,0x00, // Character 0x48 (72: 'H') 0x0E,0x04,0x04,0x04,0x04,0x04,0x00,0x00, // Character 0x49 (73: 'I') 0x0C,0x08,0x08,0x08,0x08,0x08,0x06,0x00, // Character 0x4a (74: 'J') 0x00,0x08,0x06,0x06,0x04,0x08,0x00,0x00, // Character 0x4b (75: 'K') 0x00,0x02,0x02,0x02,0x02,0x02,0x00,0x00, // Character 0x4c (76: 'L') 0x01,0x19,0x1B,0x1F,0x15,0x11,0x00,0x00, // Character 0x4d (77: 'M') 0x00,0x03,0x03,0x05,0x0D,0x09,0x00,0x00, // Character 0x4e (78: 'N') 0x06,0x0A,0x09,0x11,0x09,0x0A,0x04,0x00, // Character 0x4f (79: 'O') 0x06,0x0A,0x0A,0x0E,0x02,0x02,0x00,0x00, // Character 0x50 (80: 'P') 0x06,0x0A,0x09,0x11,0x09,0x0A,0x04,0x08, // Character 0x51 (81: 'Q') 0x06,0x08,0x08,0x06,0x0C,0x08,0x00,0x00, // Character 0x52 (82: 'R') 0x0C,0x02,0x02,0x0C,0x08,0x08,0x06,0x00, // Character 0x53 (83: 'S') 0x0F,0x04,0x04,0x04,0x04,0x04,0x00,0x00, // Character 0x54 (84: 'T') 0x00,0x01,0x01,0x01,0x09,0x0A,0x04,0x00, // Character 0x55 (85: 'U') 0x01,0x01,0x0A,0x0A,0x06,0x04,0x00,0x00, // Character 0x56 (86: 'V') 0x05,0x15,0x07,0x0F,0x0A,0x0A,0x00,0x00, // Character 0x57 (87: 'W') 0x00,0x0A,0x06,0x04,0x0E,0x0A,0x00,0x00, // Character 0x58 (88: 'X') 0x00,0x0A,0x0E,0x04,0x04,0x04,0x00,0x00, // Character 0x59 (89: 'Y') 0x0E,0x08,0x04,0x04,0x02,0x02,0x00,0x00, // Character 0x5a (90: 'Z') 0x0E,0x02,0x02,0x02,0x02,0x02,0x02,0x0E, // Character 0x5b (91: '[') 0x01,0x02,0x02,0x04,0x00,0x08,0x00,0x00, // Character 0x5c (92: '\') 0x07,0x00,0x00,0x00,0x00,0x00,0x00,0x07, // Character 0x5d (93: ']') 0x00,0x04,0x0A,0x0A,0x00,0x00,0x00,0x00, // Character 0x5e (94: '^') 0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x0E, // Character 0x5f (95: '_') 0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00, // Character 0x60 (96: '`') 0x00,0x00,0x0E,0x0A,0x09,0x0A,0x00,0x00, // Character 0x61 (97: 'a') 0x00,0x00,0x0E,0x0A,0x08,0x08,0x02,0x00, // Character 0x62 (98: 'b') 0x00,0x00,0x0C,0x02,0x02,0x02,0x0C,0x00, // Character 0x63 (99: 'c') 0x00,0x08,0x0E,0x0A,0x09,0x0A,0x00,0x00, // Character 0x64 (100: 'd') 0x00,0x00,0x0E,0x0A,0x02,0x02,0x04,0x00, // Character 0x65 (101: 'e') 0x0C,0x04,0x0E,0x00,0x00,0x00,0x00,0x00, // Character 0x66 (102: 'f') 0x00,0x00,0x0E,0x0A,0x09,0x0A,0x08,0x0A, // Character 0x67 (103: 'g') 0x00,0x00,0x0E,0x0A,0x08,0x08,0x00,0x00, // Character 0x68 (104: 'h') 0x04,0x00,0x06,0x04,0x04,0x04,0x00,0x00, // Character 0x69 (105: 'i') 0x08,0x00,0x0E,0x08,0x08,0x08,0x08,0x04, // Character 0x6a (106: 'j') 0x02,0x02,0x0A,0x06,0x06,0x0A,0x00,0x00, // Character 0x6b (107: 'k') 0x02,0x04,0x04,0x04,0x04,0x04,0x00,0x00, // Character 0x6c (108: 'l') 0x00,0x00,0x0F,0x15,0x15,0x15,0x00,0x00, // Character 0x6d (109: 'm') 0x00,0x00,0x0E,0x0A,0x08,0x08,0x00,0x00, // Character 0x6e (110: 'n') 0x00,0x00,0x0E,0x08,0x01,0x0A,0x04,0x00, // Character 0x6f (111: 'o') 0x00,0x00,0x0E,0x0A,0x08,0x08,0x02,0x00, // Character 0x70 (112: 'p') 0x00,0x00,0x0E,0x0A,0x09,0x0A,0x08,0x08, // Character 0x71 (113: 'q') 0x00,0x00,0x0E,0x02,0x02,0x02,0x00,0x00, // Character 0x72 (114: 'r') 0x00,0x00,0x0E,0x02,0x0C,0x08,0x04,0x00, // Character 0x73 (115: 's') 0x00,0x00,0x06,0x02,0x02,0x02,0x0C,0x00, // Character 0x74 (116: 't') 0x00,0x00,0x08,0x08,0x08,0x0A,0x02,0x00, // Character 0x75 (117: 'u') 0x00,0x00,0x11,0x0A,0x0A,0x04,0x00,0x00, // Character 0x76 (118: 'v') 0x00,0x00,0x15,0x15,0x0B,0x0A,0x00,0x00, // Character 0x77 (119: 'w') 0x00,0x00,0x0A,0x06,0x04,0x0A,0x00,0x00, // Character 0x78 (120: 'x') 0x00,0x00,0x11,0x0A,0x0A,0x04,0x04,0x02, // Character 0x79 (121: 'y') 0x00,0x00,0x0E,0x0C,0x04,0x02,0x00,0x00, // Character 0x7a (122: 'z') 0x0C,0x04,0x04,0x04,0x04,0x04,0x04,0x0C, // Character 0x7b (123: '{') 0x04,0x04,0x04,0x04,0x04,0x04,0x00,0x00, // Character 0x7c (124: '|') 0x06,0x04,0x04,0x04,0x04,0x04,0x04,0x02, // Character 0x7d (125: '}') 0x00,0x00,0x00,0x00,0x07,0x00,0x00,0x00, // Character 0x7e (126: '~') }; const uint16_t lut[] = { 0, // Character 0x20 (32: ' ') 8, // Character 0x21 (33: '!') 16, // Character 0x22 (34: '"') 24, // Character 0x23 (35: '#') 32, // Character 0x24 (36: '$') 40, // Character 0x25 (37: '%') 48, // Character 0x26 (38: '&') 56, // Character 0x27 (39: ''') 64, // Character 0x28 (40: '(') 72, // Character 0x29 (41: ')') 80, // Character 0x2a (42: '*') 88, // Character 0x2b (43: '+') 96, // Character 0x2c (44: ',') 104, // Character 0x2d (45: '-') 112, // Character 0x2e (46: '.') 120, // Character 0x2f (47: '/') 128, // Character 0x30 (48: '0') 136, // Character 0x31 (49: '1') 144, // Character 0x32 (50: '2') 152, // Character 0x33 (51: '3') 160, // Character 0x34 (52: '4') 168, // Character 0x35 (53: '5') 176, // Character 0x36 (54: '6') 184, // Character 0x37 (55: '7') 192, // Character 0x38 (56: '8') 200, // Character 0x39 (57: '9') 208, // Character 0x3a (58: ':') 216, // Character 0x3b (59: ';') 224, // Character 0x3c (60: '<') 232, // Character 0x3d (61: '=') 240, // Character 0x3e (62: '>') 248, // Character 0x3f (63: '?') 256, // Character 0x40 (64: '@') 264, // Character 0x41 (65: 'A') 272, // Character 0x42 (66: 'B') 280, // Character 0x43 (67: 'C') 288, // Character 0x44 (68: 'D') 296, // Character 0x45 (69: 'E') 304, // Character 0x46 (70: 'F') 312, // Character 0x47 (71: 'G') 320, // Character 0x48 (72: 'H') 328, // Character 0x49 (73: 'I') 336, // Character 0x4a (74: 'J') 344, // Character 0x4b (75: 'K') 352, // Character 0x4c (76: 'L') 360, // Character 0x4d (77: 'M') 368, // Character 0x4e (78: 'N') 376, // Character 0x4f (79: 'O') 384, // Character 0x50 (80: 'P') 392, // Character 0x51 (81: 'Q') 400, // Character 0x52 (82: 'R') 408, // Character 0x53 (83: 'S') 416, // Character 0x54 (84: 'T') 424, // Character 0x55 (85: 'U') 432, // Character 0x56 (86: 'V') 440, // Character 0x57 (87: 'W') 448, // Character 0x58 (88: 'X') 456, // Character 0x59 (89: 'Y') 464, // Character 0x5a (90: 'Z') 472, // Character 0x5b (91: '[') 480, // Character 0x5c (92: '\') 488, // Character 0x5d (93: ']') 496, // Character 0x5e (94: '^') 504, // Character 0x5f (95: '_') 512, // Character 0x60 (96: '`') 520, // Character 0x61 (97: 'a') 528, // Character 0x62 (98: 'b') 536, // Character 0x63 (99: 'c') 544, // Character 0x64 (100: 'd') 552, // Character 0x65 (101: 'e') 560, // Character 0x66 (102: 'f') 568, // Character 0x67 (103: 'g') 576, // Character 0x68 (104: 'h') 584, // Character 0x69 (105: 'i') 592, // Character 0x6a (106: 'j') 600, // Character 0x6b (107: 'k') 608, // Character 0x6c (108: 'l') 616, // Character 0x6d (109: 'm') 624, // Character 0x6e (110: 'n') 632, // Character 0x6f (111: 'o') 640, // Character 0x70 (112: 'p') 648, // Character 0x71 (113: 'q') 656, // Character 0x72 (114: 'r') 664, // Character 0x73 (115: 's') 672, // Character 0x74 (116: 't') 680, // Character 0x75 (117: 'u') 688, // Character 0x76 (118: 'v') 696, // Character 0x77 (119: 'w') 704, // Character 0x78 (120: 'x') 712, // Character 0x79 (121: 'y') 720, // Character 0x7a (122: 'z') 728, // Character 0x7b (123: '{') 736, // Character 0x7c (124: '|') 744, // Character 0x7d (125: '}') 752, // Character 0x7e (126: '~') }; ================================================ FILE: test/assets/monaco8.c-test ================================================ // // font // Font Size: 5x8px // Created: // #include // // Pseudocode for retrieving data for a specific character: // // bytes_per_char = font_height * (font_width / 8 + ((font_width % 8) ? 1 : 0)) // offset = (ascii_code(character) - ascii_code(' ')) * bytes_per_char // data = font[offset] // const unsigned char font[] = { 0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00, // Character 0x20 (32: ' ') 0x04,0x04,0x04,0x04,0x04,0x00,0x04,0x00, // Character 0x21 (33: '!') 0x0A,0x0A,0x00,0x00,0x00,0x00,0x00,0x00, // Character 0x22 (34: '"') 0x00,0x08,0x0E,0x0A,0x0F,0x04,0x00,0x00, // Character 0x23 (35: '#') 0x04,0x0E,0x06,0x04,0x0C,0x0C,0x0E,0x00, // Character 0x24 (36: '$') 0x12,0x09,0x05,0x0C,0x16,0x14,0x01,0x00, // Character 0x25 (37: '%') 0x06,0x05,0x06,0x02,0x0D,0x09,0x06,0x00, // Character 0x26 (38: '&') 0x04,0x04,0x00,0x00,0x00,0x00,0x00,0x00, // Character 0x27 (39: ''') 0x08,0x04,0x02,0x02,0x02,0x02,0x04,0x08, // Character 0x28 (40: '(') 0x02,0x04,0x08,0x08,0x08,0x08,0x04,0x02, // Character 0x29 (41: ')') 0x04,0x0E,0x0E,0x04,0x00,0x00,0x00,0x00, // Character 0x2a (42: '*') 0x00,0x00,0x04,0x04,0x0F,0x04,0x00,0x00, // Character 0x2b (43: '+') 0x00,0x00,0x00,0x00,0x00,0x04,0x04,0x04, // Character 0x2c (44: ',') 0x00,0x00,0x00,0x00,0x0E,0x00,0x00,0x00, // Character 0x2d (45: '-') 0x00,0x00,0x00,0x00,0x00,0x04,0x04,0x00, // Character 0x2e (46: '.') 0x00,0x08,0x00,0x04,0x02,0x02,0x01,0x00, // Character 0x2f (47: '/') 0x06,0x0A,0x09,0x15,0x0B,0x0A,0x04,0x00, // Character 0x30 (48: '0') 0x04,0x06,0x04,0x04,0x04,0x04,0x00,0x00, // Character 0x31 (49: '1') 0x0E,0x08,0x08,0x04,0x02,0x02,0x00,0x00, // Character 0x32 (50: '2') 0x06,0x08,0x0C,0x0C,0x08,0x08,0x06,0x00, // Character 0x33 (51: '3') 0x00,0x0C,0x0A,0x08,0x0F,0x08,0x00,0x00, // Character 0x34 (52: '4') 0x0E,0x02,0x02,0x0C,0x08,0x08,0x06,0x00, // Character 0x35 (53: '5') 0x0C,0x02,0x00,0x0B,0x12,0x0A,0x04,0x00, // Character 0x36 (54: '6') 0x0E,0x08,0x08,0x04,0x00,0x02,0x00,0x00, // Character 0x37 (55: '7') 0x0C,0x0A,0x0E,0x0E,0x0A,0x0A,0x04,0x00, // Character 0x38 (56: '8') 0x06,0x08,0x09,0x0E,0x08,0x08,0x06,0x00, // Character 0x39 (57: '9') 0x00,0x00,0x04,0x00,0x00,0x04,0x04,0x00, // Character 0x3a (58: ':') 0x00,0x00,0x04,0x00,0x00,0x04,0x04,0x04, // Character 0x3b (59: ';') 0x00,0x00,0x00,0x04,0x02,0x0C,0x00,0x00, // Character 0x3c (60: '<') 0x00,0x00,0x00,0x0F,0x00,0x00,0x00,0x00, // Character 0x3d (61: '=') 0x00,0x00,0x01,0x06,0x08,0x02,0x00,0x00, // Character 0x3e (62: '>') 0x06,0x08,0x08,0x04,0x00,0x00,0x00,0x00, // Character 0x3f (63: '?') 0x06,0x00,0x17,0x13,0x0F,0x00,0x04,0x00, // Character 0x40 (64: '@') 0x04,0x04,0x0E,0x0A,0x0E,0x09,0x00,0x00, // Character 0x41 (65: 'A') 0x06,0x0A,0x0A,0x06,0x0A,0x0A,0x00,0x00, // Character 0x42 (66: 'B') 0x0C,0x02,0x00,0x01,0x02,0x02,0x0C,0x00, // Character 0x43 (67: 'C') 0x06,0x08,0x00,0x00,0x08,0x0A,0x00,0x00, // Character 0x44 (68: 'D') 0x0E,0x02,0x02,0x0E,0x02,0x02,0x00,0x00, // Character 0x45 (69: 'E') 0x0E,0x02,0x02,0x0E,0x02,0x02,0x00,0x00, // Character 0x46 (70: 'F') 0x0C,0x02,0x00,0x09,0x0A,0x0A,0x0C,0x00, // Character 0x47 (71: 'G') 0x00,0x09,0x09,0x0F,0x09,0x09,0x00,0x00, // Character 0x48 (72: 'H') 0x0E,0x04,0x04,0x04,0x04,0x04,0x00,0x00, // Character 0x49 (73: 'I') 0x0C,0x08,0x08,0x08,0x08,0x08,0x06,0x00, // Character 0x4a (74: 'J') 0x00,0x08,0x06,0x06,0x04,0x08,0x00,0x00, // Character 0x4b (75: 'K') 0x00,0x02,0x02,0x02,0x02,0x02,0x00,0x00, // Character 0x4c (76: 'L') 0x01,0x19,0x1B,0x1F,0x15,0x11,0x00,0x00, // Character 0x4d (77: 'M') 0x00,0x03,0x03,0x05,0x0D,0x09,0x00,0x00, // Character 0x4e (78: 'N') 0x06,0x0A,0x09,0x11,0x09,0x0A,0x04,0x00, // Character 0x4f (79: 'O') 0x06,0x0A,0x0A,0x0E,0x02,0x02,0x00,0x00, // Character 0x50 (80: 'P') 0x06,0x0A,0x09,0x11,0x09,0x0A,0x04,0x08, // Character 0x51 (81: 'Q') 0x06,0x08,0x08,0x06,0x0C,0x08,0x00,0x00, // Character 0x52 (82: 'R') 0x0C,0x02,0x02,0x0C,0x08,0x08,0x06,0x00, // Character 0x53 (83: 'S') 0x0F,0x04,0x04,0x04,0x04,0x04,0x00,0x00, // Character 0x54 (84: 'T') 0x00,0x01,0x01,0x01,0x09,0x0A,0x04,0x00, // Character 0x55 (85: 'U') 0x01,0x01,0x0A,0x0A,0x06,0x04,0x00,0x00, // Character 0x56 (86: 'V') 0x05,0x15,0x07,0x0F,0x0A,0x0A,0x00,0x00, // Character 0x57 (87: 'W') 0x00,0x0A,0x06,0x04,0x0E,0x0A,0x00,0x00, // Character 0x58 (88: 'X') 0x00,0x0A,0x0E,0x04,0x04,0x04,0x00,0x00, // Character 0x59 (89: 'Y') 0x0E,0x08,0x04,0x04,0x02,0x02,0x00,0x00, // Character 0x5a (90: 'Z') 0x0E,0x02,0x02,0x02,0x02,0x02,0x02,0x0E, // Character 0x5b (91: '[') 0x01,0x02,0x02,0x04,0x00,0x08,0x00,0x00, // Character 0x5c (92: '\') 0x07,0x00,0x00,0x00,0x00,0x00,0x00,0x07, // Character 0x5d (93: ']') 0x00,0x04,0x0A,0x0A,0x00,0x00,0x00,0x00, // Character 0x5e (94: '^') 0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x0E, // Character 0x5f (95: '_') 0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00, // Character 0x60 (96: '`') 0x00,0x00,0x0E,0x0A,0x09,0x0A,0x00,0x00, // Character 0x61 (97: 'a') 0x00,0x00,0x0E,0x0A,0x08,0x08,0x02,0x00, // Character 0x62 (98: 'b') 0x00,0x00,0x0C,0x02,0x02,0x02,0x0C,0x00, // Character 0x63 (99: 'c') 0x00,0x08,0x0E,0x0A,0x09,0x0A,0x00,0x00, // Character 0x64 (100: 'd') 0x00,0x00,0x0E,0x0A,0x02,0x02,0x04,0x00, // Character 0x65 (101: 'e') 0x0C,0x04,0x0E,0x00,0x00,0x00,0x00,0x00, // Character 0x66 (102: 'f') 0x00,0x00,0x0E,0x0A,0x09,0x0A,0x08,0x0A, // Character 0x67 (103: 'g') 0x00,0x00,0x0E,0x0A,0x08,0x08,0x00,0x00, // Character 0x68 (104: 'h') 0x04,0x00,0x06,0x04,0x04,0x04,0x00,0x00, // Character 0x69 (105: 'i') 0x08,0x00,0x0E,0x08,0x08,0x08,0x08,0x04, // Character 0x6a (106: 'j') 0x02,0x02,0x0A,0x06,0x06,0x0A,0x00,0x00, // Character 0x6b (107: 'k') 0x02,0x04,0x04,0x04,0x04,0x04,0x00,0x00, // Character 0x6c (108: 'l') 0x00,0x00,0x0F,0x15,0x15,0x15,0x00,0x00, // Character 0x6d (109: 'm') 0x00,0x00,0x0E,0x0A,0x08,0x08,0x00,0x00, // Character 0x6e (110: 'n') 0x00,0x00,0x0E,0x08,0x01,0x0A,0x04,0x00, // Character 0x6f (111: 'o') 0x00,0x00,0x0E,0x0A,0x08,0x08,0x02,0x00, // Character 0x70 (112: 'p') 0x00,0x00,0x0E,0x0A,0x09,0x0A,0x08,0x08, // Character 0x71 (113: 'q') 0x00,0x00,0x0E,0x02,0x02,0x02,0x00,0x00, // Character 0x72 (114: 'r') 0x00,0x00,0x0E,0x02,0x0C,0x08,0x04,0x00, // Character 0x73 (115: 's') 0x00,0x00,0x06,0x02,0x02,0x02,0x0C,0x00, // Character 0x74 (116: 't') 0x00,0x00,0x08,0x08,0x08,0x0A,0x02,0x00, // Character 0x75 (117: 'u') 0x00,0x00,0x11,0x0A,0x0A,0x04,0x00,0x00, // Character 0x76 (118: 'v') 0x00,0x00,0x15,0x15,0x0B,0x0A,0x00,0x00, // Character 0x77 (119: 'w') 0x00,0x00,0x0A,0x06,0x04,0x0A,0x00,0x00, // Character 0x78 (120: 'x') 0x00,0x00,0x11,0x0A,0x0A,0x04,0x04,0x02, // Character 0x79 (121: 'y') 0x00,0x00,0x0E,0x0C,0x04,0x02,0x00,0x00, // Character 0x7a (122: 'z') 0x0C,0x04,0x04,0x04,0x04,0x04,0x04,0x0C, // Character 0x7b (123: '{') 0x04,0x04,0x04,0x04,0x04,0x04,0x00,0x00, // Character 0x7c (124: '|') 0x06,0x04,0x04,0x04,0x04,0x04,0x04,0x02, // Character 0x7d (125: '}') 0x00,0x00,0x00,0x00,0x07,0x00,0x00,0x00, // Character 0x7e (126: '~') }; ================================================ FILE: test/f2b_qt_compat_test.cpp ================================================ #include "gtest/gtest.h" #include "f2b_qt_compat.h" #include #include #include #include #include #include #include #include using namespace f2b; template void serialize_and_deserialize(const T& input, T& output) { QByteArray b; QBuffer buf(&b); QDataStream s(&buf); buf.open(QIODevice::WriteOnly); s << input; buf.close(); buf.open(QIODevice::ReadOnly); s >> output; buf.close(); } TEST(SerializationTest, optional_int) { std::optional data {5}; std::optional deserialized; serialize_and_deserialize(data, deserialized); EXPECT_TRUE(deserialized.has_value()); EXPECT_EQ(deserialized.value(), data.value()); data = {}; serialize_and_deserialize(data, deserialized); EXPECT_FALSE(deserialized.has_value()); } TEST(SerializationTest, optional_qstring) { std::optional data { "A quick brown fox jumps over a lazy dog." }; std::optional deserialized; serialize_and_deserialize(data, deserialized); EXPECT_TRUE(deserialized.has_value()); EXPECT_EQ(deserialized.value(), data.value()); } TEST(SerializationTest, vector_bool) { std::vector data; std::vector deserialized = { true, true, false }; serialize_and_deserialize(data, deserialized); EXPECT_TRUE(deserialized.empty()); EXPECT_EQ(data, deserialized); data = { false, false, false, true, true, false, true, false, true, true, true }; serialize_and_deserialize(data, deserialized); EXPECT_EQ(data, deserialized); } TEST(SerializationTest, set_size_t) { std::set data; std::set deserialized = { 1, 2, 3 }; serialize_and_deserialize(data, deserialized); EXPECT_TRUE(deserialized.empty()); EXPECT_EQ(data, deserialized); data = { 100, 152, 3, 6, 17, 0x541882, 1500, 4, 2, 8 }; serialize_and_deserialize(data, deserialized); EXPECT_EQ(data, deserialized); } TEST(SerializationTest, unordered_map_int_qstring) { std::unordered_map data; std::unordered_map deserialized = { { 5, "boo" } }; serialize_and_deserialize(data, deserialized); EXPECT_TRUE(deserialized.empty()); EXPECT_EQ(data, deserialized); data = { { 0, "foo" }, { 100, "bar" }, { -50, "baz" } }; serialize_and_deserialize(data, deserialized); EXPECT_EQ(data, deserialized); } TEST(SerializationTest, font_glyph) { font::glyph data({2, 3}, { false, true, false, false, true, true }); font::glyph deserialized; serialize_and_deserialize(data, deserialized); EXPECT_EQ(data, deserialized); } TEST(SerializationTest, font_face) { font::glyph glyph({2, 3}, { false, true, false, false, true, true }); font::face data({2, 3}, { glyph, glyph, glyph, glyph }); font::face deserialized; serialize_and_deserialize(data, deserialized); EXPECT_EQ(data, deserialized); } ================================================ FILE: test/sourcecodegeneration_test.cpp ================================================ #include "gtest/gtest.h" #include "fontsourcecodegenerator.h" #include "fontfaceviewmodel.h" #include #include #include #include #include #include #include using namespace f2b; template QString asset(const T& fileName) { return QString("%1/%2").arg(ASSETS_DIR, fileName); } class test_source_code_generator : public font_source_code_generator { public: test_source_code_generator(source_code_options options): font_source_code_generator(options) {}; std::string current_timestamp() override { return ""; } }; TEST(SourceCodeGeneratorTest, GeneratorAll) { auto faceFileName = asset("monaco8.fontedit"); auto faceVM = std::make_unique(faceFileName); font::glyph_size expectedSize {5, 11}; font::margins expectedMargins {1, 2}; EXPECT_EQ(faceVM->face().num_glyphs(), 95); EXPECT_EQ(faceVM->face().glyphs_size(), expectedSize); EXPECT_EQ(faceVM->face().calculate_margins(), expectedMargins); source_code_options options; options.export_method = f2b::source_code_options::export_all; test_source_code_generator g(options); auto sourceCode = g.generate(faceVM->face()); auto sourceCodeFileName = asset("monaco8.c-test"); QFile f(sourceCodeFileName); f.open(QFileDevice::ReadOnly); auto referenceSourceCode = f.readAll().toStdString(); f.close(); ASSERT_EQ(sourceCode, referenceSourceCode); } TEST(SourceCodeGeneratorTest, GeneratorSubset) { auto faceFileName = asset("monaco8.fontedit"); auto faceVM = std::make_unique(faceFileName); font::glyph_size expectedSize {5, 11}; font::margins expectedMargins {1, 2}; EXPECT_EQ(faceVM->face().num_glyphs(), 95); EXPECT_EQ(faceVM->face().glyphs_size(), expectedSize); EXPECT_EQ(faceVM->face().calculate_margins(), expectedMargins); source_code_options options; options.export_method = f2b::source_code_options::export_selected; options.indentation = f2b::source_code::space { 4 }; test_source_code_generator g(options); auto sourceCode = g.generate(faceVM->face()); auto sourceCodeFileName = asset("monaco8-subset.c-test"); QFile f(sourceCodeFileName); f.open(QFileDevice::ReadOnly); auto referenceSourceCode = f.readAll().toStdString(); f.close(); ASSERT_EQ(sourceCode, referenceSourceCode); } TEST(SourceCodeGeneratorTest, GeneratorPerformance) { auto faceFileName = asset("jetbrains260.fontedit"); auto faceVM = std::make_unique(faceFileName); f2b::source_code_options::export_method_type export_methods[] = { f2b::source_code_options::export_selected, f2b::source_code_options::export_all }; for (auto export_method : gsl::span(export_methods)) { source_code_options options; options.export_method = export_method; font_source_code_generator g(std::move(options)); std::vector durations; for (int i = 0; i < 5; ++i) { auto start = std::chrono::high_resolution_clock::now(); g.generate(faceVM->face()); auto end = std::chrono::high_resolution_clock::now(); durations.push_back(std::chrono::duration_cast(end-start).count()); } std::cout << "Export Method " << export_method << ": "; for (auto& d : durations) { std::cout << d << " "; } std::cout << "(mean: " << std::accumulate(durations.begin(), durations.end(), 0) / durations.size() << ")"; std::cout << std::endl; } }