├── .clang-format ├── .clangd ├── .editorconfig ├── .gitignore ├── CMakeLists.txt ├── Doxyfile ├── LICENSE ├── README.md ├── app ├── chr2png │ ├── CMakeLists.txt │ ├── app.hpp │ ├── app.hpp.cfg │ ├── main.cpp │ └── setup.hpp ├── palview │ ├── CMakeLists.txt │ ├── app.hpp │ ├── app.hpp.cfg │ ├── main.cpp │ └── setup.hpp ├── png2chr │ ├── CMakeLists.txt │ ├── app.hpp │ ├── app.hpp.cfg │ ├── main.cpp │ └── setup.hpp └── shared │ ├── CMakeLists.txt │ ├── blob.hpp │ ├── cfgload.cpp │ ├── cfgload.hpp │ ├── filesys.hpp │ ├── gfxdef_builder.hpp │ ├── gfxdefman.hpp │ ├── lineread.hpp │ ├── shared.cpp │ ├── shared.hpp │ ├── strutil.hpp │ ├── usage.cpp │ ├── usage.hpp │ ├── xdgdirs.cpp │ └── xdgdirs.hpp ├── graphics.md ├── lib └── chrgfx │ ├── CMakeLists.txt │ ├── app.hpp │ ├── app.hpp.cfg │ ├── builtin_defs.cpp │ ├── builtin_defs.hpp │ ├── chrconv.cpp │ ├── chrconv.hpp │ ├── chrdef.cpp │ ├── chrdef.hpp │ ├── chrgfx.hpp │ ├── colconv.cpp │ ├── colconv.hpp │ ├── coldef.cpp │ ├── coldef.hpp │ ├── custom.cpp │ ├── custom.hpp │ ├── filesys.hpp │ ├── gfxdef.cpp │ ├── gfxdef.hpp │ ├── image.hpp │ ├── image_types.hpp │ ├── imageformat_png.cpp │ ├── imageformat_png.hpp │ ├── imaging.cpp │ ├── imaging.hpp │ ├── palconv.cpp │ ├── palconv.hpp │ ├── paldef.cpp │ ├── paldef.hpp │ ├── rgb_layout.cpp │ ├── rgb_layout.hpp │ ├── strutil.hpp │ ├── types.hpp │ ├── utils.cpp │ └── utils.hpp └── share ├── README.md └── gfxdefs /.clang-format: -------------------------------------------------------------------------------- 1 | Language: Cpp 2 | Standard: c++17 3 | BasedOnStyle: LLVM 4 | 5 | AlignAfterOpenBracket: DontAlign 6 | AlignEscapedNewlines: DontAlign 7 | AlignOperands: Align 8 | AlignTrailingComments: true 9 | AllowAllArgumentsOnNextLine: true 10 | AllowAllConstructorInitializersOnNextLine: true 11 | AlignArrayOfStructures: None 12 | AllowShortBlocksOnASingleLine: false 13 | AllowShortCaseLabelsOnASingleLine: false 14 | AllowShortFunctionsOnASingleLine: Empty 15 | AllowShortLoopsOnASingleLine: false 16 | AllowShortIfStatementsOnASingleLine: Never 17 | AlwaysBreakBeforeMultilineStrings: true 18 | AlwaysBreakTemplateDeclarations: Yes 19 | BreakBeforeBraces: Allman 20 | BreakBeforeTernaryOperators: true 21 | BreakConstructorInitializers: AfterColon 22 | BinPackArguments: false 23 | BinPackParameters: false 24 | ColumnLimit: 120 25 | ContinuationIndentWidth: 2 26 | Cpp11BracedListStyle: true 27 | MaxEmptyLinesToKeep: 1 28 | IndentCaseLabels: true 29 | PackConstructorInitializers: Never 30 | PointerAlignment: Middle 31 | ReferenceAlignment: Pointer 32 | SortIncludes: true 33 | SpaceAfterCStyleCast: true 34 | SpaceAfterLogicalNot: true 35 | SpaceAroundPointerQualifiers: Both 36 | SpaceBeforeAssignmentOperators: true 37 | SpaceBeforeCpp11BracedList: true 38 | SpaceBeforeCtorInitializerColon: true 39 | SpaceBeforeInheritanceColon: true 40 | SpaceBeforeRangeBasedForLoopColon: true 41 | SpaceBeforeParens: ControlStatements 42 | SpacesInAngles: false 43 | SpacesInCStyleCastParentheses: false 44 | SpacesInParentheses: false 45 | SpacesInSquareBrackets: false 46 | TabWidth: 2 47 | UseTab: Always 48 | -------------------------------------------------------------------------------- /.clangd: -------------------------------------------------------------------------------- 1 | CompileFlags: 2 | Add: [-xc++, -std=c++17, -Wall] 3 | Diagnostics: 4 | ClangTidy: 5 | Add: [performance*, modernize*] 6 | Remove: [modernize-use-trailing-return-type, modernize-avoid-c-arrays] 7 | CheckOptions: 8 | readability-identifier-naming.VariableCase: SnakeCase 9 | 10 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | [*] 2 | end_of_line = lf 3 | insert_final_newline = true 4 | charset = utf-8 5 | trim_trailing_whitespace = true 6 | 7 | [*.{c,h,cpp,hpp}] 8 | indent_style = tab 9 | indent_size = 2 10 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .cache/ 2 | .vscode/ 3 | build/ 4 | work 5 | docs/ 6 | **/src/app.hpp 7 | -------------------------------------------------------------------------------- /CMakeLists.txt: -------------------------------------------------------------------------------- 1 | cmake_minimum_required(VERSION 3.23) 2 | project(chrgfx VERSION 3.0.2) 3 | 4 | set(PROJECT_CONTACT "Damian R (damian@motoi.pro)") 5 | set(PROJECT_WEBSITE "https://github.com/drojaazu") 6 | set(PROJECT_COPYRIGHT "©2017 Motoi Productions / Released under MIT License") 7 | 8 | # Include CCache for compilation caching if available 9 | find_program(CCACHE_PROGRAM ccache) 10 | if(CCACHE_PROGRAM) 11 | message("using ccache") 12 | set(CMAKE_CXX_COMPILER_LAUNCHER "${CCACHE_PROGRAM}") 13 | else() 14 | message("ccache not available") 15 | endif() 16 | 17 | # Interprocedural Optimisation if available 18 | include(CheckIPOSupported) 19 | check_ipo_supported(RESULT result OUTPUT output) 20 | if(result) 21 | message("using interprocedural optimisation") 22 | set(CMAKE_INTERPROCEDURAL_OPTIMIZATION TRUE) 23 | endif() 24 | 25 | enable_language(CXX) 26 | set(CMAKE_CXX_STANDARD 17) 27 | set(CMAKE_CXX_STANDARD_REQUIRED True) 28 | set(CMAKE_CXX_EXTENSIONS OFF) 29 | 30 | set(CMAKE_BUILD_PARALLEL_LEVEL 16) 31 | set(CMAKE_CXX_FLAGS_DEBUG "-g -fstandalone-debug -DDEBUG" CACHE STRING "" FORCE) 32 | 33 | include(GNUInstallDirs) 34 | 35 | if(NOT EXISTS ${CMAKE_BINARY_DIR}/CMakeCache.txt) 36 | if(NOT CMAKE_BUILD_TYPE) 37 | set(CMAKE_BUILD_TYPE "Release" CACHE STRING "" FORCE) 38 | endif() 39 | endif() 40 | 41 | add_subdirectory(lib/chrgfx) 42 | 43 | if(NOT NO_UTILS) 44 | add_subdirectory(app/shared) 45 | add_subdirectory(app/chr2png) 46 | add_subdirectory(app/png2chr) 47 | add_subdirectory(app/palview) 48 | endif() 49 | 50 | install(FILES share/gfxdefs 51 | DESTINATION ${CMAKE_INSTALL_DATADIR}/chrgfx) 52 | 53 | #set(CMAKE_VERBOSE_MAKEFILE true) 54 | -------------------------------------------------------------------------------- /Doxyfile: -------------------------------------------------------------------------------- 1 | # Doxyfile 1.8.13 2 | DOXYFILE_ENCODING = UTF-8 3 | PROJECT_NAME = chrgfx 4 | PROJECT_NUMBER = 1 5 | PROJECT_BRIEF = "A library with CLI utilities for converting tile (aka CHR) based graphics used in retro video games to and from PNG, with support for a wide range of hardware via external graphics definitions." 6 | OUTPUT_DIRECTORY = docs 7 | TAB_SIZE = 2 8 | USE_MDFILE_AS_MAINPAGE = ./README.md 9 | GENERATE_LATEX = NO 10 | GENERATE_HTML = YES 11 | RECURSIVE = YES 12 | 13 | INPUT = README.md chrgfx/inc 14 | FILE_PATTERNS = *.hpp *.cpp *.md 15 | ALIASES += "break=@warning \b BREAK: " 16 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2018 Damian Rogers 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # chrgfx 2 | A library with CLI utilities for converting tile (aka CHR) based graphics used in retro video games to and from bitmap images, with support for a wide range of hardware via external graphics definitions. It is useful for viewing data as well as converting modern graphics into native formats for retro game development. 3 | 4 | ## Building 5 | 6 | chrgfx requires libpng and the [png++](https://www.nongnu.org/pngpp/) wrapper for the support utilities. These are available in most distro repositories. Installing the wrapper will include libpng as a dependency. In Arch Linux, the wrapper package is `png++`; in Debian, it is `libpng++-dev`. 7 | 8 | If you wish to build only libchrgfx without the utilities (and thus without the need for the png packages), pass `-DNO_UTILS=1` when running cmake. 9 | 10 | CMake is used for the build process. From the root of the project directory: 11 | 12 | mkdir build && cd build 13 | cmake .. 14 | make 15 | sudo make install 16 | 17 | Note that cmake installs library content to `/usr/local` by default, which is not included in the ld search path in some distros. If libchrgfx.so cannot be found, you may want to add `/usr/local/lib` to the search path using `ldconfig`, or manually specify the installation prefix, e.g.: 18 | 19 | cmake -DCMAKE_INSTALL_PREFIX:PATH=/usr .. 20 | 21 | Manual uninstallation can be done using the install_manifest.txt file within the cmake build directory: 22 | 23 | sudo xargs rm < install_manifest.txt 24 | 25 | # Utilities 26 | There are three support utilities included: `chr2png`, `png2chr`, and `palview`. 27 | 28 | ## chr2png 29 | 30 | This will convert encoded tile/palette data to a PNG image. 31 | 32 | ## png2chr 33 | 34 | This will take a sufficiently compatible PNG and output encoded tile/palette data. 35 | 36 | Compatible in this case means a PNG in indexed color mode with a 256 color palette. 37 | 38 | ## palview 39 | 40 | This will take a color palette and generate an image of color swatches corresponding to the palette. 41 | 42 | ## Graphics Definitions (gfxdefs) 43 | 44 | The conversion routines rely on graphics definitions (gfxdef), which describe the format of the data for encoding and decoding. There are three kinds of definitions: tile (chrdef), palette (paldef) and color (coldef). 45 | 46 | Graphics definitions can be mixed and modified at run time. With this system, chrgfx is extensible and can support practically any tile-based hardware. 47 | 48 | Graphics definitions are loaded from three possible sources. 49 | 50 | ### Internal Definitions 51 | 52 | The chrgfx library has a number of common, generic definitions included. Please see the [builtin_defs.hpp source file](src/chrgfx/builtin_defs.hpp) for a list. 53 | 54 | ### gfxdefs File 55 | 56 | External graphics definitions are stored in the `gfxdefs` file. The project comes with a number of definitions for many common hardware systems already created in this file. 57 | 58 | Please [see the readme in the gfxdef directory](share/README.md) and [the gfxdefs file itself](share/gfxdefs) for more details on the format. 59 | 60 | ### CLI Definitions 61 | 62 | Graphics definitions can also be defined on the command line. This is useful for testing or dealing with dynamic formats (for example, perhaps tile sizes are not consistent across multiple files when running a batch conversion). 63 | 64 | Definitions specified on the command line override definitions loaded from internal/external sources in a piecemeal fashion. For example, you may load the `col_bgr_222_packed` coldef and then specify `--col-big-endian` on the command line to change the endianness aspect only. 65 | 66 | ### Hardware Profiles 67 | 68 | For convenience, there are also profiles, which are groupings of a single chrdef, paldef and coldef to represent the graphics subsystem of a certain piece of hardware. In this way, it is simple for the user to specify which decoders/encoders to use for certain hardware. Moreover, we reduce redundancies for hardware that may share one type of definition, but not others. 69 | 70 | For example, the Sega Master System and Game Gear have the same tile format, but the palette and color encodings are different. We can make a different profile for each system, with both using the same tile encoding but seperate palette and color encodings. Another example is the original Gameboy and the later Gameboy Pocket. They are exactly the same in hardware, but the original has a more green tint while the Pocket is more gray. We can create profiles for both, with the same tile and palette formats, but with a different color list to simulate their different perceived colors. 71 | 72 | The entries within the profile can be override by specifying a chrdef, paldef or coldef ID seperately on the command line. 73 | 74 | ### gfxdef Loading Order 75 | 76 | Because definitions can be loaded from multiple sources, there is an ordering for processing to determine what definition is finally used: 77 | 78 | 1. If a profile is specified, the external gfxdefs file is checked first to get the target chrdef/paldef/coldef from that profile. 79 | 2. The internal list is checked for the target gfxdef. 80 | 3. If not present in the internal list, the gfxdef file is checked for the target gfxdef. If a chrdef/paldef/coldef ID was specified and was not found at this point, an error occurs. 81 | 4. If a tile or color definition option is specified on the command line, that modifes the definition 82 | 83 | NOTE: If a chrdef/paldef/coldef or profile ID was NOT specified, steps 1 to 3 are skipped and it is assumed all required gfxdefs are fully defined on the command line. 84 | 85 | ## Usage 86 | 87 | ### Shared Options 88 | 89 | The following options are available for all three utilitiese (`chr2png`, `png2chr`, `palview`) 90 | 91 | `--help`, `-h` 92 | 93 | Display built in program usage 94 | 95 | `--gfx-def `, `-G ` 96 | 97 | Path to gfxdef file. If not specified, it checks for: 98 | 99 | - `${XDG_DATA_HOME}/chrgfx/gfxdefs` 100 | - `${XDG_DATA_DIRS}/chrgfx/gfxdefs` 101 | 102 | `--profile `, `-H ` 103 | 104 | Specify hardware profile to use 105 | 106 | `--chr-def `, `-T ` 107 | 108 | `--col-def `, `-C ` 109 | 110 | `--pal-def `, `-P ` 111 | 112 | These arguments specify the tile, color and palette encoding, respectively. They are only required if a graphics profile was not specified. If they are used in conjunction with a graphics profile, they will override that particular encoding. (For example, using `--chr-def` will override the tile encoding that was specified in the profile.) 113 | 114 | Please [see the readme in the gfxdef directory](share/README.md) for more information about values represent in a gfxdef. 115 | 116 | The following options are used to build or modify a gfxdef: 117 | 118 | `--chr-width ` 119 | 120 | Specify the width of a tile 121 | 122 | `--chr-height ` 123 | 124 | Specify the height of a tile 125 | 126 | `--chr-bpp ` 127 | 128 | Specify the bits per pixel (BPP) of a tile 129 | 130 | `--chr-plane-offsets ` 131 | 132 | Specify the offset (in bits) to the start of each plane within a pixel. 133 | 134 | This is written as either a comma-delimted list of values, or as a range in the format of [start:count:step]. Please [see the readme in the gfxdef directory](share/README.md) for more about this option and list/range formatting. 135 | 136 | The number of entries should match the bits per pixel value for the tile. 137 | 138 | `--chr-pixel-offsets ` 139 | 140 | Specify the start (in bits) to the start of each pixel within a row. 141 | 142 | Value is specified in the same format as `--chr-plane-offsets`. 143 | 144 | The number of entries should match the width of the tile. 145 | 146 | `--chr-row-offsets ` 147 | 148 | Specify the offset (in bits) to the start of each pixel row within a tile. 149 | 150 | Value is specified in the same format as `--chr-plane-offsets`. 151 | 152 | The number of entries should match the height of the tile. 153 | 154 | `--pal-datasize ` 155 | 156 | Specify the size (in bits) of one palette. 157 | 158 | This is optional when defining a palette and should rarely be needed. It is only necessary when the size of palette differs entry datasize * palette length. For example, if a color entry is 2 bits with a palette length of 4, but the palette is stored in a 16 bit value. In such a case, the `--pal-datasize` should be 16. 159 | 160 | `--pal-entry-datasize ` 161 | 162 | Specify the size (in bits) of a color entry. 163 | 164 | `--pal-length ` 165 | 166 | Specify the number of entries in a single palette. 167 | 168 | `--col-bitdepth ` 169 | 170 | Specify the bitdepth of a color. 171 | 172 | `--col-layout ` 173 | 174 | Specify the groupings of red, green and blue color channels within a color value. This should be a comma delimited list of exactly 6 entries, in this format: red_offset, red_size, green_offset, green_size, blue_offset, blue_size 175 | 176 | Note that, for simplicity's sake, only one layout pass can be done via the command line. If you need to work with a complex color format that requires multiple layout passes, please use an external file (gfxdefs). 177 | 178 | `--col-big-endian <1|0|true|false>` 179 | 180 | Specify that the color data is big-endian. Data is processed as little-endian by default. 181 | 182 | ### chr2png - Additional Options 183 | 184 | `--chr-data `, `-c ` 185 | 186 | Required; path to the encoded tile data 187 | 188 | `--pal-data `, `-p ` 189 | 190 | Path to the encoded palette data; if not specified, a palette of random colors will be generated 191 | 192 | `--output `, `-o ` 193 | 194 | Path to output PNG image; if not specifed, will output to stdout 195 | 196 | `--pal-line `, `-l ` 197 | 198 | Specify the palette line (also called the subpalette) to use for rendering when passing in palette data that contains more tha one palette 199 | 200 | `--row-size `, `-r ` 201 | 202 | Specify the number of tiles in a row in the output PNG 203 | 204 | `--trns-index `, `-i ` 205 | 206 | Specify a palette index to use for transparency. This is often index 0. If not specified, the output image will not have transparency. 207 | 208 | ### Example Usage 209 | chr2png --profile sega_md --chr-data sonic1_sprite.chr --pal-data sonic1.cram --trns --row-size 32 > sonic1_sprite.png 210 | 211 | ## png2chr - Additional Options 212 | 213 | `--png-data `, `-b ` 214 | 215 | Path to the input PNG image; if not specified, expects PNG data piped from stdin 216 | 217 | `--chr-output `, `-c ` 218 | 219 | `--pal-output `, `-p ` 220 | 221 | Path to the output tile and palette data, respectively. One or both must be specified. 222 | 223 | ### Example 224 | png2chr --profile nintendo_sfc --chr-output crono.chr --pal-output crono.pal < crono_sprite.png 225 | 226 | ### palview - Additional Options 227 | 228 | Images are generated with indexed color, unless `--full-pal` is specified, in which case direct color is used. 229 | 230 | `--pal-data `, `-p ` 231 | 232 | Path to the encoded palette data; if not specified, a palette of random colors will be generated 233 | 234 | `--pal-line `, `-l ` 235 | 236 | Specify the palette line (also called the subpalette) to use for rendering when passing in palette data that contains more tha one palette. 237 | 238 | Ignored when `--full-pal` is specified. 239 | 240 | `--full-pal`, `-f` 241 | 242 | Renders an image with multiple palettes. It will draw as many palettes as the data provided contains. 243 | 244 | `--output `, `-o ` 245 | 246 | Path to output PNG image; if not specifed, will output to stdout 247 | 248 | ### Example Usage 249 | 250 | ## Special Thanks 251 | 252 | Special thanks to: 253 | 254 | - Klarth, for his venerable `consolegfx.txt`, which was my introduction to the data side of tiled graphics and was instrumental in my early development work on Dumpster and now chrgfx. 255 | 256 | - The MAME Team, for their work in documenting hardware through source code and for inspiring some solutions that are essential to chrgfx. 257 | 258 | - UCC BLACK canned coffee. 259 | 260 | - Greetz to dosdemon, mdl, sebmal, lord, eri, freem and the rest of the internet graybeards from IRC and the old scene. 261 | -------------------------------------------------------------------------------- /app/chr2png/CMakeLists.txt: -------------------------------------------------------------------------------- 1 | project(chr2png 2 | DESCRIPTION "Convert encoded tile graphics to PNG image" 3 | VERSION 2.0.0 4 | LANGUAGES CXX) 5 | 6 | add_executable(chr2png) 7 | 8 | target_include_directories(chr2png 9 | PRIVATE "${PROJECT_SOURCE_DIR}/../shared" 10 | PRIVATE "${PROJECT_SOURCE_DIR}/../../lib" 11 | ) 12 | 13 | configure_file("${CMAKE_CURRENT_SOURCE_DIR}/app.hpp.cfg" "${CMAKE_CURRENT_SOURCE_DIR}/app.hpp" ESCAPE_QUOTES) 14 | 15 | target_sources(chr2png 16 | PRIVATE 17 | main.cpp 18 | ${PROJECT_SOURCE_DIR}/../shared/cfgload.cpp 19 | ${PROJECT_SOURCE_DIR}/../shared/shared.cpp 20 | ${PROJECT_SOURCE_DIR}/../shared/usage.cpp 21 | ${PROJECT_SOURCE_DIR}/../shared/xdgdirs.cpp 22 | ) 23 | 24 | target_link_libraries(chr2png PRIVATE chrgfx) 25 | 26 | 27 | 28 | install(TARGETS chr2png RUNTIME DESTINATION ${CMAKE_INSTALL_PREFIX}/bin) 29 | -------------------------------------------------------------------------------- /app/chr2png/app.hpp: -------------------------------------------------------------------------------- 1 | /** 2 | * @author Damian R (damian@motoi.pro) 3 | * @brief Convert encoded tile graphics to PNG image 4 | * @version 2.0.0 5 | * 6 | * @copyright ©2017 Motoi Productions / Released under MIT License 7 | * 8 | */ 9 | 10 | #ifndef __MOTOI__APP_HPP 11 | #define __MOTOI__APP_HPP 12 | 13 | #include 14 | #include 15 | 16 | /* 17 | These values should be set within CMakeLists.txt 18 | */ 19 | namespace APP 20 | { 21 | static unsigned int const VERSION_MAJOR {2}; 22 | static unsigned int const VERSION_MINOR {0}; 23 | static unsigned int const VERSION_PATCH {0}; 24 | static char const * VERSION {"2.0.0"}; 25 | 26 | static char const * NAME {"chr2png"}; 27 | static char const * COPYRIGHT {"©2017 Motoi Productions / Released under MIT License"}; 28 | static char const * CONTACT {"Damian R (damian@motoi.pro)"}; 29 | static char const * WEBSITE {"https://github.com/drojaazu"}; 30 | static char const * BRIEF {"Convert encoded tile graphics to PNG image"}; 31 | 32 | std::string app_info() 33 | { 34 | std::stringstream ss; 35 | ss << APP::NAME << ' ' << APP::VERSION << '\n'; 36 | ss << APP::COPYRIGHT << '\n'; 37 | ss << APP::CONTACT << " / " << APP::WEBSITE << '\n'; 38 | 39 | return ss.str(); 40 | } 41 | 42 | } // namespace APP 43 | #endif 44 | -------------------------------------------------------------------------------- /app/chr2png/app.hpp.cfg: -------------------------------------------------------------------------------- 1 | /** 2 | * @author @PROJECT_CONTACT@ 3 | * @brief @PROJECT_DESCRIPTION@ 4 | * @version @PROJECT_VERSION@ 5 | * 6 | * @copyright @PROJECT_COPYRIGHT@ 7 | * 8 | */ 9 | 10 | #ifndef __MOTOI__APP_HPP 11 | #define __MOTOI__APP_HPP 12 | 13 | #include 14 | #include 15 | 16 | /* 17 | These values should be set within CMakeLists.txt 18 | */ 19 | namespace APP 20 | { 21 | static unsigned int const VERSION_MAJOR {@PROJECT_VERSION_MAJOR@}; 22 | static unsigned int const VERSION_MINOR {@PROJECT_VERSION_MINOR@}; 23 | static unsigned int const VERSION_PATCH {@PROJECT_VERSION_PATCH@}; 24 | static char const * VERSION {"@PROJECT_VERSION@"}; 25 | 26 | static char const * NAME {"@PROJECT_NAME@"}; 27 | static char const * COPYRIGHT {"@PROJECT_COPYRIGHT@"}; 28 | static char const * CONTACT {"@PROJECT_CONTACT@"}; 29 | static char const * WEBSITE {"@PROJECT_WEBSITE@"}; 30 | static char const * BRIEF {"@PROJECT_DESCRIPTION@"}; 31 | 32 | std::string app_info() 33 | { 34 | std::stringstream ss; 35 | ss << APP::NAME << ' ' << APP::VERSION << '\n'; 36 | ss << APP::COPYRIGHT << '\n'; 37 | ss << APP::CONTACT << " / " << APP::WEBSITE << '\n'; 38 | 39 | return ss.str(); 40 | } 41 | 42 | } // namespace APP 43 | #endif 44 | -------------------------------------------------------------------------------- /app/chr2png/main.cpp: -------------------------------------------------------------------------------- 1 | #include "blob.hpp" 2 | #include "filesys.hpp" 3 | #include "gfxdefman.hpp" 4 | #include "imageformat_png.hpp" 5 | #include "setup.hpp" 6 | #include 7 | 8 | #include 9 | #include 10 | 11 | #ifdef DEBUG 12 | #include 13 | #endif 14 | 15 | using namespace std; 16 | using namespace chrgfx; 17 | using namespace motoi; 18 | 19 | void process_args(int argc, char ** argv); 20 | 21 | int main(int argc, char ** argv) 22 | { 23 | #ifdef DEBUG 24 | chrono::high_resolution_clock::time_point t1, t2; 25 | #endif 26 | 27 | try 28 | { 29 | /******************************************************* 30 | * SETUP & SANITY CHECKING 31 | *******************************************************/ 32 | 33 | #ifdef DEBUG 34 | t1 = chrono::high_resolution_clock::now(); 35 | #endif 36 | process_args(argc, argv); 37 | 38 | gfxdef_manager defs(cfg); 39 | istream * chr_data; 40 | ifstream ifs_chr_data; 41 | if (cfg.chrdata_path.empty()) 42 | { 43 | chr_data = &cin; 44 | } 45 | else 46 | { 47 | ifs_chr_data = ifstream_checked(cfg.chrdata_path); 48 | chr_data = &ifs_chr_data; 49 | } 50 | 51 | #ifdef DEBUG 52 | t2 = chrono::high_resolution_clock::now(); 53 | auto duration = chrono::duration_cast(t2 - t1).count(); 54 | cerr << "SETUP: " << duration << "ms\n"; 55 | #endif 56 | 57 | /******************************************************* 58 | * PALETTE CONVERSION 59 | *******************************************************/ 60 | #ifdef DEBUG 61 | t1 = chrono::high_resolution_clock::now(); 62 | #endif 63 | 64 | palette workpal; 65 | if (! cfg.paldata_path.empty()) 66 | { 67 | if (defs.paldef() == nullptr) 68 | throw runtime_error("no paldef loaded"); 69 | if (defs.coldef() == nullptr) 70 | throw runtime_error("no coldef loaded"); 71 | 72 | ifstream paldata {ifstream_checked(cfg.paldata_path)}; 73 | size_t pal_size {defs.paldef()->datasize_bytes()}; 74 | auto palbuffer {unique_ptr(new byte_t[pal_size])}; 75 | 76 | paldata.seekg(cfg.pal_line * pal_size, ios::beg); 77 | paldata.read(reinterpret_cast(palbuffer.get()), pal_size); 78 | if (! paldata.good()) 79 | throw runtime_error("Cannot read specified palette line index"); 80 | 81 | decode_pal(*defs.paldef(), *defs.coldef(), palbuffer.get(), &workpal); 82 | } 83 | else 84 | { 85 | workpal = make_pal_random(); 86 | } 87 | 88 | #ifdef DEBUG 89 | t2 = chrono::high_resolution_clock::now(); 90 | duration = chrono::duration_cast(t2 - t1).count(); 91 | 92 | cerr << "PALETTE GENERATION: " << duration << "ms\n"; 93 | #endif 94 | 95 | /******************************************************* 96 | * TILE CONVERSION 97 | *******************************************************/ 98 | 99 | #ifdef DEBUG 100 | t1 = chrono::high_resolution_clock::now(); 101 | #endif 102 | 103 | if (defs.chrdef() == nullptr) 104 | throw runtime_error("no chrdef loaded"); 105 | 106 | blob out_buffer; 107 | // vector out_buffer; 108 | { 109 | #ifdef DEBUG 110 | t1 = chrono::high_resolution_clock::now(); 111 | #endif 112 | size_t 113 | // byte size of one encoded tile 114 | in_chunksize {(uint) (defs.chrdef()->datasize_bytes())}, 115 | // byte size of one basic (decoded) tile 116 | out_chunksize {(size_t) (defs.chrdef()->width() * defs.chrdef()->height())}; 117 | 118 | // buffer for a single encoded tile, read from the stream 119 | auto in_tile {unique_ptr(new byte_t[in_chunksize])}, 120 | out_tile {unique_ptr(new pixel[out_chunksize])}; 121 | 122 | /* 123 | Some speed testing was done and, somewhat surprisingly, calling append 124 | on the buffer repeatedly was a bit faster than creating a large 125 | temporary buffer and resizing 126 | */ 127 | while (true) 128 | { 129 | chr_data->read(reinterpret_cast(in_tile.get()), in_chunksize); 130 | if (! chr_data->good()) 131 | break; 132 | 133 | decode_chr(*defs.chrdef(), in_tile.get(), out_tile.get()); 134 | out_buffer.append(out_tile.get(), out_chunksize); 135 | // out_buffer.insert(out_buffer.end(), out_tile.get(), out_tile.get() + out_chunksize); 136 | } 137 | 138 | #ifdef DEBUG 139 | t2 = chrono::high_resolution_clock::now(); 140 | duration = chrono::duration_cast(t2 - t1).count(); 141 | 142 | cerr << "TILE CONVERSION: " << to_string(duration) << "ms\n"; 143 | #endif 144 | 145 | /******************************************************* 146 | * IMAGE RENDER 147 | *******************************************************/ 148 | 149 | #ifdef DEBUG 150 | t1 = chrono::high_resolution_clock::now(); 151 | #endif 152 | 153 | auto rendered_tiles = render_tileset(*defs.chrdef(), out_buffer, out_buffer.size(), cfg.render_cfg); 154 | // auto rendered_tiles = render_tileset(*defs.chrdef(), out_buffer.data(), out_buffer.size(), 155 | // cfg.render_cfg); 156 | rendered_tiles.set_color_map(workpal); 157 | png::image outimg {to_png(rendered_tiles, cfg.render_cfg.trns_index)}; 158 | 159 | #ifdef DEBUG 160 | t2 = chrono::high_resolution_clock::now(); 161 | duration = chrono::duration_cast(t2 - t1).count(); 162 | cerr << "PNG RENDER: " << duration << "ms\n"; 163 | #endif 164 | 165 | /******************************************************* 166 | * STREAM OUTPUT 167 | *******************************************************/ 168 | 169 | #ifdef DEBUG 170 | t1 = chrono::high_resolution_clock::now(); 171 | #endif 172 | 173 | if (cfg.out_png_path.empty()) 174 | outimg.write_stream(cout); 175 | else 176 | outimg.write(cfg.out_png_path); 177 | 178 | #ifdef DEBUG 179 | t2 = chrono::high_resolution_clock::now(); 180 | duration = chrono::duration_cast(t2 - t1).count(); 181 | cerr << "OUTPUT TO STREAM: " << duration << "ms\n"; 182 | #endif 183 | } 184 | 185 | return 0; 186 | } 187 | catch (exception const & e) 188 | { 189 | cerr << "Error: " << e.what() << '\n'; 190 | return -1; 191 | } 192 | } 193 | -------------------------------------------------------------------------------- /app/chr2png/setup.hpp: -------------------------------------------------------------------------------- 1 | #include "imaging.hpp" 2 | #include "shared.hpp" 3 | #include 4 | #include 5 | #include 6 | 7 | struct runtime_config_chr2png : runtime_config 8 | { 9 | std::string chrdata_path; 10 | std::string paldata_path; 11 | chrgfx::render_config render_cfg; 12 | std::string out_png_path; 13 | uint pal_line {0}; 14 | } cfg; 15 | 16 | void process_args(int argc, char ** argv) 17 | { 18 | // add chr2png specific options 19 | long_opts.push_back({"chr-data", required_argument, nullptr, 'c'}); 20 | long_opts.push_back({"pal-data", required_argument, nullptr, 'p'}); 21 | long_opts.push_back({"pal-line", required_argument, nullptr, 'l'}); 22 | long_opts.push_back({"trns-index", required_argument, nullptr, 'i'}); 23 | long_opts.push_back({"row-size", required_argument, nullptr, 'r'}); 24 | long_opts.push_back({"output", required_argument, nullptr, 'o'}); 25 | long_opts.push_back({nullptr, 0, nullptr, 0}); 26 | short_opts.append("c:p:l:i:r:o:"); 27 | 28 | opt_details.push_back({false, "Path to input encoded tiles", nullptr}); 29 | opt_details.push_back({false, "Path to input encoded palette", nullptr}); 30 | opt_details.push_back({false, "Palette line to use for PNG output", nullptr}); 31 | opt_details.push_back({false, "Palette index to use for transparency", nullptr}); 32 | opt_details.push_back({false, "Number of tiles per row in output image", nullptr}); 33 | opt_details.push_back({false, "Path to output PNG image", nullptr}); 34 | 35 | // read/parse arguments 36 | while (true) 37 | { 38 | const auto this_opt = getopt_long(argc, argv, short_opts.data(), long_opts.data(), nullptr); 39 | if (this_opt == -1) 40 | break; 41 | 42 | // handle shared arguments 43 | if (shared_args(this_opt, cfg)) 44 | continue; 45 | 46 | // handle chr2png specific arguments 47 | switch (this_opt) 48 | { 49 | // input tile data path 50 | case 'c': 51 | cfg.chrdata_path = optarg; 52 | break; 53 | 54 | // input palette data path 55 | case 'p': 56 | cfg.paldata_path = optarg; 57 | break; 58 | 59 | // palette line 60 | case 'l': 61 | try 62 | { 63 | auto pal_line {std::stoi(optarg)}; 64 | if (pal_line < 1) 65 | throw std::invalid_argument("Invalid palette line value"); 66 | // user input is indexed from 1, convert to 0 index here 67 | cfg.pal_line = pal_line - 1; 68 | } 69 | catch (const std::invalid_argument & e) 70 | { 71 | throw std::invalid_argument("Invalid palette line value"); 72 | } 73 | break; 74 | 75 | // palette entry index for transparency 76 | case 'i': 77 | try 78 | { 79 | std::string s = optarg; 80 | auto trns_index {stoi(s)}; 81 | if (trns_index < 0) 82 | throw std::invalid_argument("Invalid transparency index value"); 83 | cfg.render_cfg.trns_index = trns_index; 84 | } 85 | catch (const std::invalid_argument & e) 86 | { 87 | throw std::invalid_argument("Invalid transparency index value"); 88 | } 89 | break; 90 | 91 | // row size 92 | case 'r': 93 | try 94 | { 95 | cfg.render_cfg.row_size = std::stoi(optarg); 96 | } 97 | catch (const std::invalid_argument & e) 98 | { 99 | throw std::invalid_argument("Invalid columns value"); 100 | } 101 | break; 102 | 103 | // png output path 104 | case 'o': 105 | cfg.out_png_path = optarg; 106 | break; 107 | } 108 | } 109 | } -------------------------------------------------------------------------------- /app/palview/CMakeLists.txt: -------------------------------------------------------------------------------- 1 | project(palview 2 | DESCRIPTION "Visualize an encoded color palette" 3 | VERSION 1.0.0 4 | LANGUAGES CXX) 5 | 6 | add_executable(palview) 7 | 8 | target_include_directories(palview 9 | PRIVATE "${PROJECT_SOURCE_DIR}/../shared" 10 | PRIVATE "${PROJECT_SOURCE_DIR}/../../lib" 11 | ) 12 | 13 | configure_file("${CMAKE_CURRENT_SOURCE_DIR}/app.hpp.cfg" "${CMAKE_CURRENT_SOURCE_DIR}/app.hpp" ESCAPE_QUOTES) 14 | 15 | target_sources(palview 16 | PRIVATE 17 | app.hpp 18 | main.cpp 19 | ${PROJECT_SOURCE_DIR}/../shared/cfgload.cpp 20 | ${PROJECT_SOURCE_DIR}/../shared/filesys.hpp 21 | ${PROJECT_SOURCE_DIR}/../shared/gfxdef_builder.hpp 22 | ${PROJECT_SOURCE_DIR}/../shared/gfxdefman.hpp 23 | ${PROJECT_SOURCE_DIR}/../shared/shared.cpp 24 | ${PROJECT_SOURCE_DIR}/../shared/strutil.hpp 25 | ${PROJECT_SOURCE_DIR}/../shared/xdgdirs.cpp 26 | ${PROJECT_SOURCE_DIR}/../shared/usage.cpp 27 | ) 28 | 29 | target_link_libraries(palview PRIVATE chrgfx) 30 | 31 | install(TARGETS palview RUNTIME DESTINATION ${CMAKE_INSTALL_PREFIX}/bin) 32 | -------------------------------------------------------------------------------- /app/palview/app.hpp: -------------------------------------------------------------------------------- 1 | /** 2 | * @author Damian R (damian@motoi.pro) 3 | * @brief Visualize an encoded color palette 4 | * @version 1.0.0 5 | * 6 | * @copyright ©2017 Motoi Productions / Released under MIT License 7 | * 8 | */ 9 | 10 | #ifndef __MOTOI__APP_HPP 11 | #define __MOTOI__APP_HPP 12 | 13 | #include 14 | #include 15 | 16 | /* 17 | These values should be set within CMakeLists.txt 18 | */ 19 | namespace APP 20 | { 21 | static unsigned int const VERSION_MAJOR {1}; 22 | static unsigned int const VERSION_MINOR {0}; 23 | static unsigned int const VERSION_PATCH {0}; 24 | static char const * VERSION {"1.0.0"}; 25 | 26 | static char const * NAME {"palview"}; 27 | static char const * COPYRIGHT {"©2017 Motoi Productions / Released under MIT License"}; 28 | static char const * CONTACT {"Damian R (damian@motoi.pro)"}; 29 | static char const * WEBSITE {"https://github.com/drojaazu"}; 30 | static char const * BRIEF {"Visualize an encoded color palette"}; 31 | 32 | std::string app_info() 33 | { 34 | std::stringstream ss; 35 | ss << APP::NAME << ' ' << APP::VERSION << '\n'; 36 | ss << APP::COPYRIGHT << '\n'; 37 | ss << APP::CONTACT << " / " << APP::WEBSITE << '\n'; 38 | 39 | return ss.str(); 40 | } 41 | 42 | } // namespace APP 43 | #endif 44 | -------------------------------------------------------------------------------- /app/palview/app.hpp.cfg: -------------------------------------------------------------------------------- 1 | /** 2 | * @author @PROJECT_CONTACT@ 3 | * @brief @PROJECT_DESCRIPTION@ 4 | * @version @PROJECT_VERSION@ 5 | * 6 | * @copyright @PROJECT_COPYRIGHT@ 7 | * 8 | */ 9 | 10 | #ifndef __MOTOI__APP_HPP 11 | #define __MOTOI__APP_HPP 12 | 13 | #include 14 | #include 15 | 16 | /* 17 | These values should be set within CMakeLists.txt 18 | */ 19 | namespace APP 20 | { 21 | static unsigned int const VERSION_MAJOR {@PROJECT_VERSION_MAJOR@}; 22 | static unsigned int const VERSION_MINOR {@PROJECT_VERSION_MINOR@}; 23 | static unsigned int const VERSION_PATCH {@PROJECT_VERSION_PATCH@}; 24 | static char const * VERSION {"@PROJECT_VERSION@"}; 25 | 26 | static char const * NAME {"@PROJECT_NAME@"}; 27 | static char const * COPYRIGHT {"@PROJECT_COPYRIGHT@"}; 28 | static char const * CONTACT {"@PROJECT_CONTACT@"}; 29 | static char const * WEBSITE {"@PROJECT_WEBSITE@"}; 30 | static char const * BRIEF {"@PROJECT_DESCRIPTION@"}; 31 | 32 | std::string app_info() 33 | { 34 | std::stringstream ss; 35 | ss << APP::NAME << ' ' << APP::VERSION << '\n'; 36 | ss << APP::COPYRIGHT << '\n'; 37 | ss << APP::CONTACT << " / " << APP::WEBSITE << '\n'; 38 | 39 | return ss.str(); 40 | } 41 | 42 | } // namespace APP 43 | #endif 44 | -------------------------------------------------------------------------------- /app/palview/main.cpp: -------------------------------------------------------------------------------- 1 | #include "blob.hpp" 2 | #include "filesys.hpp" 3 | #include "gfxdefman.hpp" 4 | #include "image.hpp" 5 | #include "imageformat_png.hpp" 6 | #include "setup.hpp" 7 | #include 8 | 9 | #include 10 | #include 11 | 12 | using namespace std; 13 | using namespace chrgfx; 14 | using namespace motoi; 15 | 16 | // uint const swatch_size {32}; 17 | 18 | chrgfx::coldef const * work_coldef {&chrgfx::gfxdefs::col_bgr_333_packed}; 19 | chrgfx::paldef const * work_paldef {&chrgfx::gfxdefs::pal_16bit_256color}; 20 | 21 | int main(int argc, char ** argv) 22 | { 23 | try 24 | { 25 | 26 | process_args(argc, argv); 27 | 28 | gfxdef_manager defs(cfg); 29 | work_coldef = defs.coldef(); 30 | work_paldef = defs.paldef(); 31 | ifstream is_paldata {ifstream_checked(cfg.paldata_name)}; 32 | 33 | if (cfg.full_pal) 34 | { 35 | blob paldata {is_paldata}; 36 | auto image = render_palette_full(*work_paldef, *work_coldef, paldata, paldata.size()); 37 | png::image outimg(image.width(), image.height()); 38 | for (auto i {0}; i < image.height(); ++i) 39 | { 40 | auto p = (png::basic_rgb_pixel *) image.pixel_map_row(i); 41 | auto c = vector>(p, p + image.width()); 42 | outimg.get_pixbuf().put_row(i, c); 43 | } 44 | 45 | if (cfg.out_path.empty()) 46 | outimg.write_stream(cout); 47 | else 48 | outimg.write(cfg.out_path); 49 | } 50 | else 51 | { 52 | size_t pal_size {work_paldef->datasize_bytes()}; 53 | auto palbuffer {unique_ptr(new byte_t[pal_size])}; 54 | is_paldata.seekg(cfg.pal_line * pal_size, ios::beg); 55 | is_paldata.read(reinterpret_cast(palbuffer.get()), pal_size); 56 | if (! is_paldata.good()) 57 | throw runtime_error("Cannot read specified palette line index"); 58 | 59 | auto image = render_palette(*work_paldef, *work_coldef, palbuffer.get()); 60 | auto outimg {to_png(image)}; 61 | if (cfg.out_path.empty()) 62 | outimg.write_stream(cout); 63 | else 64 | outimg.write(cfg.out_path); 65 | } 66 | 67 | return 0; 68 | } 69 | catch (exception const & e) 70 | { 71 | cerr << "Error: " << e.what() << '\n'; 72 | return -1; 73 | } 74 | } 75 | -------------------------------------------------------------------------------- /app/palview/setup.hpp: -------------------------------------------------------------------------------- 1 | #ifndef __MOTOI__SETUP_HPP 2 | #define __MOTOI__SETUP_HPP 3 | 4 | #include "shared.hpp" 5 | #include 6 | #include 7 | 8 | using namespace std; 9 | 10 | struct runtime_config_palview : runtime_config 11 | { 12 | string paldata_name; 13 | string out_path; 14 | uint pal_line {0}; 15 | bool full_pal {false}; 16 | } cfg; 17 | 18 | void process_args(int argc, char ** argv) 19 | { 20 | // add palview specific options 21 | long_opts.push_back({"pal-data", required_argument, nullptr, 'p'}); 22 | long_opts.push_back({"pal-line", required_argument, nullptr, 'l'}); 23 | long_opts.push_back({"full-pal", no_argument, nullptr, 'f'}); 24 | long_opts.push_back({"output", required_argument, nullptr, 'o'}); 25 | long_opts.push_back({nullptr, 0, nullptr, 0}); 26 | short_opts.append("p:l:fr:o:"); 27 | 28 | opt_details.push_back({false, "Path to input encoded palette", nullptr}); 29 | opt_details.push_back({false, "Palette line to render", nullptr}); 30 | opt_details.push_back({false, "Render all palette data instead of a single palette line", nullptr}); 31 | opt_details.push_back({false, "Path to output image", nullptr}); 32 | 33 | // read/parse arguments 34 | while (true) 35 | { 36 | const auto this_opt = getopt_long(argc, argv, short_opts.data(), long_opts.data(), nullptr); 37 | if (this_opt == -1) 38 | break; 39 | 40 | // handle shared arguments 41 | if (shared_args(this_opt, cfg)) 42 | continue; 43 | 44 | // handle chr2png specific arguments 45 | switch (this_opt) 46 | { 47 | // input palette data path 48 | case 'p': 49 | cfg.paldata_name = optarg; 50 | break; 51 | 52 | // palette line 53 | case 'l': 54 | try 55 | { 56 | auto pal_line {stoi(optarg)}; 57 | if (pal_line < 1) 58 | throw invalid_argument("Invalid palette line value"); 59 | // user input is indexed from 1, convert to 0 index here 60 | cfg.pal_line = pal_line - 1; 61 | } 62 | catch (const invalid_argument & e) 63 | { 64 | throw invalid_argument("Invalid palette line value"); 65 | } 66 | break; 67 | 68 | case 'f': 69 | cfg.full_pal = true; 70 | break; 71 | 72 | // png output path 73 | case 'o': 74 | cfg.out_path = optarg; 75 | break; 76 | } 77 | } 78 | } 79 | 80 | #endif -------------------------------------------------------------------------------- /app/png2chr/CMakeLists.txt: -------------------------------------------------------------------------------- 1 | project(png2chr 2 | DESCRIPTION "Convert PNG image input to encoded tile graphics" 3 | VERSION 2.0.0 4 | LANGUAGES CXX) 5 | 6 | add_executable(png2chr) 7 | 8 | target_include_directories(png2chr 9 | PRIVATE "${PROJECT_SOURCE_DIR}/../shared" 10 | PRIVATE "${PROJECT_SOURCE_DIR}/../../lib" 11 | ) 12 | 13 | configure_file("${CMAKE_CURRENT_SOURCE_DIR}/app.hpp.cfg" "${CMAKE_CURRENT_SOURCE_DIR}/app.hpp" ESCAPE_QUOTES) 14 | 15 | target_sources(png2chr 16 | PRIVATE 17 | main.cpp 18 | ${PROJECT_SOURCE_DIR}/../shared/cfgload.cpp 19 | ${PROJECT_SOURCE_DIR}/../shared/shared.cpp 20 | ${PROJECT_SOURCE_DIR}/../shared/usage.cpp 21 | ${PROJECT_SOURCE_DIR}/../shared/xdgdirs.cpp 22 | ) 23 | 24 | target_link_libraries(png2chr PRIVATE chrgfx) 25 | 26 | install(TARGETS png2chr RUNTIME DESTINATION ${CMAKE_INSTALL_PREFIX}/bin) 27 | -------------------------------------------------------------------------------- /app/png2chr/app.hpp: -------------------------------------------------------------------------------- 1 | /** 2 | * @author Damian R (damian@motoi.pro) 3 | * @brief Convert PNG image input to encoded tile graphics 4 | * @version 2.0.0 5 | * 6 | * @copyright ©2017 Motoi Productions / Released under MIT License 7 | * 8 | */ 9 | 10 | #ifndef __MOTOI__APP_HPP 11 | #define __MOTOI__APP_HPP 12 | 13 | #include 14 | #include 15 | 16 | /* 17 | These values should be set within CMakeLists.txt 18 | */ 19 | namespace APP 20 | { 21 | static unsigned int const VERSION_MAJOR {2}; 22 | static unsigned int const VERSION_MINOR {0}; 23 | static unsigned int const VERSION_PATCH {0}; 24 | static char const * VERSION {"2.0.0"}; 25 | 26 | static char const * NAME {"png2chr"}; 27 | static char const * COPYRIGHT {"©2017 Motoi Productions / Released under MIT License"}; 28 | static char const * CONTACT {"Damian R (damian@motoi.pro)"}; 29 | static char const * WEBSITE {"https://github.com/drojaazu"}; 30 | static char const * BRIEF {"Convert PNG image input to encoded tile graphics"}; 31 | 32 | std::string app_info() 33 | { 34 | std::stringstream ss; 35 | ss << APP::NAME << ' ' << APP::VERSION << '\n'; 36 | ss << APP::COPYRIGHT << '\n'; 37 | ss << APP::CONTACT << " / " << APP::WEBSITE << '\n'; 38 | 39 | return ss.str(); 40 | } 41 | 42 | } // namespace APP 43 | #endif 44 | -------------------------------------------------------------------------------- /app/png2chr/app.hpp.cfg: -------------------------------------------------------------------------------- 1 | /** 2 | * @author @PROJECT_CONTACT@ 3 | * @brief @PROJECT_DESCRIPTION@ 4 | * @version @PROJECT_VERSION@ 5 | * 6 | * @copyright @PROJECT_COPYRIGHT@ 7 | * 8 | */ 9 | 10 | #ifndef __MOTOI__APP_HPP 11 | #define __MOTOI__APP_HPP 12 | 13 | #include 14 | #include 15 | 16 | /* 17 | These values should be set within CMakeLists.txt 18 | */ 19 | namespace APP 20 | { 21 | static unsigned int const VERSION_MAJOR {@PROJECT_VERSION_MAJOR@}; 22 | static unsigned int const VERSION_MINOR {@PROJECT_VERSION_MINOR@}; 23 | static unsigned int const VERSION_PATCH {@PROJECT_VERSION_PATCH@}; 24 | static char const * VERSION {"@PROJECT_VERSION@"}; 25 | 26 | static char const * NAME {"@PROJECT_NAME@"}; 27 | static char const * COPYRIGHT {"@PROJECT_COPYRIGHT@"}; 28 | static char const * CONTACT {"@PROJECT_CONTACT@"}; 29 | static char const * WEBSITE {"@PROJECT_WEBSITE@"}; 30 | static char const * BRIEF {"@PROJECT_DESCRIPTION@"}; 31 | 32 | std::string app_info() 33 | { 34 | std::stringstream ss; 35 | ss << APP::NAME << ' ' << APP::VERSION << '\n'; 36 | ss << APP::COPYRIGHT << '\n'; 37 | ss << APP::CONTACT << " / " << APP::WEBSITE << '\n'; 38 | 39 | return ss.str(); 40 | } 41 | 42 | } // namespace APP 43 | #endif 44 | -------------------------------------------------------------------------------- /app/png2chr/main.cpp: -------------------------------------------------------------------------------- 1 | #include "filesys.hpp" 2 | #include "gfxdefman.hpp" 3 | #include "setup.hpp" 4 | #include 5 | #include 6 | #include 7 | #include 8 | 9 | #ifdef DEBUG 10 | #include 11 | #endif 12 | 13 | using namespace std; 14 | using namespace chrgfx; 15 | using namespace motoi; 16 | 17 | int main(int argc, char ** argv) 18 | { 19 | #ifdef DEBUG 20 | chrono::high_resolution_clock::time_point t1, t2; 21 | #endif 22 | 23 | try 24 | { 25 | /******************************************************* 26 | * SETUP & SANITY CHECKING 27 | *******************************************************/ 28 | 29 | #ifdef DEBUG 30 | t1 = chrono::high_resolution_clock::now(); 31 | #endif 32 | process_args(argc, argv); 33 | 34 | gfxdef_manager defs(cfg); 35 | istream * png_data; 36 | ifstream ifs_png_data; 37 | if (cfg.pngdata_path.empty()) 38 | { 39 | png_data = &cin; 40 | } 41 | else 42 | { 43 | ifs_png_data = ifstream_checked(cfg.pngdata_path); 44 | png_data = &ifs_png_data; 45 | } 46 | 47 | #ifdef DEBUG 48 | t2 = chrono::high_resolution_clock::now(); 49 | auto duration = chrono::duration_cast(t2 - t1).count(); 50 | cerr << "SETUP: " << duration << "ms\n"; 51 | #endif 52 | 53 | /******************************************************* 54 | * LOAD IMAGE 55 | *******************************************************/ 56 | 57 | #ifdef DEBUG 58 | t1 = chrono::high_resolution_clock::now(); 59 | #endif 60 | 61 | auto image_data {from_png({*png_data, png::require_color_space()})}; 62 | 63 | #ifdef DEBUG 64 | t2 = chrono::high_resolution_clock::now(); 65 | duration = chrono::duration_cast(t2 - t1).count(); 66 | 67 | cerr << "LOAD PNG: " << to_string(duration) << "ms\n"; 68 | #endif 69 | 70 | /******************************************************* 71 | * TILE SEGMENTATION 72 | *******************************************************/ 73 | 74 | if (! cfg.out_chrdata_path.empty()) 75 | { 76 | if (defs.chrdef() == nullptr) 77 | throw runtime_error("no chrdef loaded"); 78 | 79 | #ifdef DEBUG 80 | t1 = chrono::high_resolution_clock::now(); 81 | #endif 82 | 83 | auto tile_columns {image_data.width() / defs.chrdef()->width()}, 84 | tile_rows {image_data.height() / defs.chrdef()->height()}, 85 | chr_datasize {defs.chrdef()->width() * defs.chrdef()->height()}, 86 | tileset_datasize {tile_columns * tile_rows * chr_datasize}; 87 | vector tileset_data(tileset_datasize); 88 | make_tileset(*defs.chrdef(), image_data, tileset_data.data()); 89 | 90 | #ifdef DEBUG 91 | t2 = chrono::high_resolution_clock::now(); 92 | duration = chrono::duration_cast(t2 - t1).count(); 93 | 94 | cerr << "TILE SEGMENTATION: " << to_string(duration) << "ms\n"; 95 | cerr << "TILE COUNT: " << (tileset_data.size() / (defs.chrdef()->width() * defs.chrdef()->height())) << '\n'; 96 | #endif 97 | 98 | /******************************************************* 99 | * TILE CONVERSION & OUTPUT 100 | *******************************************************/ 101 | 102 | #ifdef DEBUG 103 | t1 = chrono::high_resolution_clock::now(); 104 | #endif 105 | auto chr_outfile {ofstream_checked(cfg.out_chrdata_path)}; 106 | 107 | size_t in_chunksize {(size_t) (defs.chrdef()->width() * defs.chrdef()->height())}, 108 | out_chunksize {(uint) (defs.chrdef()->datasize_bytes())}; 109 | 110 | auto ptr_in_tile = tileset_data.data(); 111 | byte_t * ptr_imgdata_end = ptr_in_tile + tileset_data.size(); 112 | 113 | auto out_tile {unique_ptr(new pixel[in_chunksize])}; 114 | while (ptr_in_tile != ptr_imgdata_end) 115 | { 116 | // TODO create a cache and use the out pointer on encode_chr 117 | encode_chr(*defs.chrdef(), ptr_in_tile, out_tile.get()); 118 | copy(out_tile.get(), out_tile.get() + out_chunksize, ostream_iterator(chr_outfile)); 119 | ptr_in_tile += in_chunksize; 120 | } 121 | 122 | #ifdef DEBUG 123 | t2 = chrono::high_resolution_clock::now(); 124 | duration = chrono::duration_cast(t2 - t1).count(); 125 | 126 | cerr << "TILE ENCODE/OUTPUT: " << to_string(duration) << "ms\n"; 127 | #endif 128 | } 129 | 130 | /******************************************************* 131 | * PALETTE CONVERSION 132 | *******************************************************/ 133 | if (! cfg.out_paldata_path.empty()) 134 | { 135 | if (defs.paldef() == nullptr) 136 | throw runtime_error("no paldef loaded"); 137 | if (defs.coldef() == nullptr) 138 | throw runtime_error("no coldef loaded"); 139 | 140 | #ifdef DEBUG 141 | t1 = chrono::high_resolution_clock::now(); 142 | #endif 143 | 144 | auto paldef_palette_data {unique_ptr(new byte_t[defs.paldef()->datasize() >> 3])}; 145 | encode_pal(*defs.paldef(), *defs.coldef(), image_data.color_map(), paldef_palette_data.get()); 146 | 147 | #ifdef DEBUG 148 | t2 = chrono::high_resolution_clock::now(); 149 | duration = chrono::duration_cast(t2 - t1).count(); 150 | 151 | cerr << "CONVERT PALETTE: " << to_string(duration) << "ms\n"; 152 | #endif 153 | 154 | #ifdef DEBUG 155 | t1 = chrono::high_resolution_clock::now(); 156 | #endif 157 | 158 | ofstream pal_outfile {ofstream_checked(cfg.out_paldata_path)}; 159 | pal_outfile.write(reinterpret_cast(paldef_palette_data.get()), defs.paldef()->datasize_bytes()); 160 | 161 | #ifdef DEBUG 162 | t2 = chrono::high_resolution_clock::now(); 163 | duration = chrono::duration_cast(t2 - t1).count(); 164 | 165 | cerr << "PAL OUTPUT TO STREAM: " << to_string(duration) << "ms\n"; 166 | #endif 167 | } 168 | 169 | // everything's good, we're outta here 170 | return 0; 171 | } 172 | catch (exception const & e) 173 | { 174 | cerr << "Error: " << e.what() << '\n'; 175 | return -1; 176 | } 177 | } 178 | -------------------------------------------------------------------------------- /app/png2chr/setup.hpp: -------------------------------------------------------------------------------- 1 | #ifndef __MOTOI__SETUP_HPP 2 | #define __MOTOI__SETUP_HPP 3 | 4 | #include "shared.hpp" 5 | #include 6 | 7 | struct runtime_config_png2chr : runtime_config 8 | { 9 | std::string pngdata_path; 10 | std::string out_chrdata_path; 11 | std::string out_paldata_path; 12 | } cfg; 13 | 14 | void process_args(int argc, char ** argv) 15 | { 16 | // add png2chr specific options 17 | long_opts.push_back({"chr-output", required_argument, nullptr, 'c'}); 18 | long_opts.push_back({"pal-output", required_argument, nullptr, 'p'}); 19 | long_opts.push_back({"png-data", required_argument, nullptr, 'b'}); 20 | long_opts.push_back({nullptr, 0, nullptr, 0}); 21 | short_opts.append("c:p:b:"); 22 | 23 | opt_details.push_back({true, "Path to output encoded tiles", nullptr}); 24 | opt_details.push_back({true, "Path to output encoded palette", nullptr}); 25 | opt_details.push_back({true, "Path to input PNG image", nullptr}); 26 | 27 | // read/parse arguments 28 | while (true) 29 | { 30 | const auto this_opt = getopt_long(argc, argv, short_opts.data(), long_opts.data(), nullptr); 31 | if (this_opt == -1) 32 | break; 33 | 34 | // handle shared arguments 35 | if (shared_args(this_opt, cfg)) 36 | continue; 37 | 38 | // handle png2chr specific arguments 39 | switch (this_opt) 40 | { 41 | // chr-output 42 | case 'c': 43 | cfg.out_chrdata_path = optarg; 44 | break; 45 | 46 | // pal-output 47 | case 'p': 48 | cfg.out_paldata_path = optarg; 49 | break; 50 | 51 | // png-data 52 | case 'b': 53 | cfg.pngdata_path = optarg; 54 | break; 55 | } 56 | } 57 | } 58 | 59 | #endif -------------------------------------------------------------------------------- /app/shared/CMakeLists.txt: -------------------------------------------------------------------------------- 1 | find_library(PNG_LIB png) 2 | 3 | if(NOT PNG_LIB) 4 | message(FATAL_ERROR "libpng not found") 5 | endif() 6 | 7 | include(CheckIncludeFileCXX) 8 | check_include_file_cxx("png++/png.hpp" PNGPP_H) 9 | if(NOT PNGPP_H) 10 | message(FATAL_ERROR "png++ not found") 11 | endif() 12 | -------------------------------------------------------------------------------- /app/shared/cfgload.cpp: -------------------------------------------------------------------------------- 1 | #include "cfgload.hpp" 2 | #include "lineread.hpp" 3 | #include "strutil.hpp" 4 | #include 5 | #include 6 | 7 | namespace motoi 8 | { 9 | using namespace std; 10 | 11 | file_parse_error::file_parse_error(char const * message, size_t line_number) : 12 | std::runtime_error(std::string(message) + " at line " + std::to_string(line_number)) 13 | { 14 | } 15 | 16 | config_loader::config_loader(string const & filepath) : 17 | m_file(filepath) 18 | { 19 | m_file.reset(); 20 | m_parse_state = parse_state::search_header; 21 | string block_header; 22 | block_map * block_content {nullptr}; 23 | while (m_file.next()) 24 | { 25 | if (m_file.line().empty()) 26 | continue; 27 | 28 | auto line {trim_view(m_file.line())}; 29 | if (line.empty() || line[0] == COMMENT_MARKER) 30 | continue; 31 | 32 | switch (m_parse_state) 33 | { 34 | case parse_state::search_header: 35 | { 36 | if (line[0] == BLOCK_OPENER || line[0] == BLOCK_CLOSER) 37 | { 38 | throw file_parse_error("Found block opener/closer with no header", m_file.line_number()); 39 | break; 40 | } 41 | 42 | // at this point, we should have found some text which represents a block header 43 | // (because blank lines and comments are dealt with above) 44 | // we want to verify there is a block opener before actyually allocating a new map, however 45 | block_header = line; 46 | m_parse_state = parse_state::search_opener; 47 | break; 48 | } 49 | 50 | case parse_state::search_opener: 51 | { 52 | if (line[0] == BLOCK_OPENER) 53 | { 54 | block_content = &this->emplace(block_header, block_map())->second; 55 | m_parse_state = parse_state::in_block; 56 | break; 57 | } 58 | 59 | if (line[0] == BLOCK_CLOSER) 60 | { 61 | throw file_parse_error("Block closer found without matching opener", m_file.line_number()); 62 | break; 63 | } 64 | 65 | throw file_parse_error("Found text outside of config block", m_file.line_number()); 66 | break; 67 | } 68 | 69 | case parse_state::search_closer: 70 | { 71 | if (line[0] == BLOCK_CLOSER) 72 | { 73 | m_parse_state = parse_state::search_header; 74 | block_content = nullptr; 75 | block_header.clear(); 76 | break; 77 | } 78 | 79 | if (line[0] == BLOCK_OPENER) 80 | { 81 | throw file_parse_error("Block opener found without matching closer", m_file.line_number()); 82 | break; 83 | } 84 | break; 85 | } 86 | 87 | case parse_state::in_block: 88 | { 89 | if (line[0] == BLOCK_CLOSER) 90 | { 91 | m_parse_state = parse_state::search_header; 92 | block_content = nullptr; 93 | block_header.clear(); 94 | break; 95 | } 96 | 97 | // kvsplit does check for presence of delimiter, throws if not found 98 | try 99 | { 100 | block_content->emplace(kvsplit(basic_string(line), "\t ")); 101 | } 102 | catch (invalid_argument const & e) 103 | { 104 | throw file_parse_error("Could not find entry delimiter", m_file.line_number()); 105 | } 106 | break; 107 | } 108 | } 109 | } 110 | } 111 | } // namespace motoi -------------------------------------------------------------------------------- /app/shared/cfgload.hpp: -------------------------------------------------------------------------------- 1 | /** 2 | * @file cfgload2.hpp 3 | * @author Damian Rogers / damian@motoi.pro 4 | * @brief Reads and parses a text file containing configuration data in a simple format of a header with 5 | * key/value pairs listed in a brace block. 6 | * @copyright ©2024 Motoi Productions / Released under MIT License 7 | * 8 | * Updates: 9 | * 20241205 Initial 10 | */ 11 | 12 | #ifndef __MOTOI__CFGLOAD_HPP 13 | #define __MOTOI__CFGLOAD_HPP 14 | 15 | #include "lineread.hpp" 16 | #include 17 | #include 18 | #include 19 | #include 20 | 21 | /* 22 | Example config block: 23 | 24 | # This is a comment 25 | config_block 26 | { 27 | some_value 1234 28 | some_filepath /usr/local/whatever 29 | } 30 | 31 | This is config block which is composed of config entries. Each entry is a simple key/value pair 32 | seperated by a space OR tab. This means that keys cannot have spaces/tabs. 33 | (Block headers may have spaces, but they should probably be avoided for the sake of consistency with entries.) 34 | 35 | a block header should appear first, following by open bracket { on the next line, a number of config entries, 36 | and then a closing bracket } on the line after that. 37 | 38 | Comments are prefixed with a hash # 39 | 40 | Whitespace (matching "\t\n\v\f\r ") is trimmed before processing, so entries or blocks can be indented 41 | as preferred by the user. 42 | 43 | Nested blocks are not supported. 44 | 45 | No type or data checking is done. Each entry is stored as an unordered multimap of , while the 46 | whole config is stored as an unordered multimap of where each string is a block header. 47 | */ 48 | 49 | namespace motoi 50 | { 51 | class file_parse_error : public std::runtime_error 52 | { 53 | private: 54 | std::string m_err_msg; 55 | size_t m_line_number; 56 | 57 | public: 58 | file_parse_error() = delete; 59 | file_parse_error(char const * message, size_t line_number); 60 | }; 61 | 62 | using block_map = std::multimap; 63 | using config_map = std::multimap; 64 | 65 | class config_loader : public config_map 66 | { 67 | private: 68 | static auto constexpr COMMENT_MARKER {'#'}; 69 | static auto constexpr BLOCK_OPENER {'{'}; 70 | static auto constexpr BLOCK_CLOSER {'}'}; 71 | 72 | motoi::linereader m_file; 73 | 74 | enum class parse_state : uint8_t 75 | { 76 | search_header, 77 | search_opener, 78 | search_closer, 79 | in_block 80 | }; 81 | 82 | parse_state m_parse_state; 83 | 84 | public: 85 | config_loader(std::string const & filepath); 86 | config_loader() = delete; 87 | }; 88 | } // namespace motoi 89 | 90 | #endif -------------------------------------------------------------------------------- /app/shared/filesys.hpp: -------------------------------------------------------------------------------- 1 | /** 2 | * @file filesys.hpp 3 | * @author Damian Rogers / damian@motoi.pro 4 | * @brief File system/path utilities 5 | * @copyright ©2021 Motoi Productions / Released under MIT License 6 | * 7 | * Updates: 8 | * 20211214 Initial 9 | * 20220420 Converted to basic_string, added path parsing functions 10 | * 20220720 Change stringstream to ostringstream; added file_size & concat_paths 11 | * 20240109 Consolidated checked filestream methods into this header; fixes to thrown exception types 12 | */ 13 | 14 | #ifndef __MOTOI__FILESYS_HPP 15 | #define __MOTOI__FILESYS_HPP 16 | 17 | #include 18 | #include 19 | #include 20 | #include 21 | #include 22 | 23 | namespace motoi 24 | { 25 | template ...>>> 28 | std::basic_string concat_paths(StringT const & base_path, StringTs const &... paths) 29 | { 30 | // enable_if + conjunction_v + is_same ensures all variadic parameters are the same type 31 | std::basic_ostringstream oss; 32 | oss << base_path; 33 | if (base_path.back() != '/') 34 | oss << '/'; 35 | 36 | size_t path_count = sizeof...(paths); 37 | size_t u = 0; 38 | for (auto const & filepath : {paths...}) 39 | { 40 | ++u; 41 | if (filepath.empty()) 42 | continue; 43 | if (filepath.front() == '/') 44 | oss.seekp(-1, std::ios::cur); 45 | oss << filepath; 46 | if (filepath.back() != '/' && u < path_count) 47 | oss << '/'; 48 | } 49 | 50 | return oss.str(); 51 | } 52 | 53 | template 54 | struct stat file_stat(std::basic_string const & filepath) 55 | { 56 | static struct stat status; 57 | if (::stat(filepath.c_str(), &status) != 0) 58 | { 59 | std::basic_ostringstream oss; 60 | oss << "Could not open \"" << filepath << "\": " << strerror(errno); 61 | throw std::system_error(oss.str()); 62 | } 63 | return status; 64 | } 65 | 66 | template 67 | inline bool file_exists(std::basic_string const & filepath) 68 | { 69 | static struct stat status; 70 | return (::stat(filepath.c_str(), &status) == 0); 71 | } 72 | 73 | template 74 | inline size_t file_size(std::basic_string const & filepath) 75 | { 76 | return file_stat(filepath).st_size; 77 | } 78 | 79 | template 80 | std::basic_string strip_extension(std::basic_string const & filepath) 81 | { 82 | auto pos {filepath.find_last_of('.')}; 83 | if (pos == std::string::npos) 84 | return filepath; 85 | return filepath.substr(0, pos); 86 | } 87 | 88 | template 89 | std::basic_string path(std::basic_string const & filepath) 90 | { 91 | auto pos {filepath.find_last_of('/')}; 92 | if (pos == std::string::npos) 93 | return filepath; 94 | return filepath.substr(0, pos); 95 | } 96 | 97 | template 98 | std::basic_string filename(std::basic_string const & filepath) 99 | { 100 | auto pos {filepath.find_last_of('/')}; 101 | if (pos == std::string::npos) 102 | return filepath; 103 | return filepath.substr(pos + 1, filepath.size() - pos); 104 | } 105 | 106 | template 107 | std::ifstream ifstream_checked( 108 | std::basic_string const & filepath, std::basic_string const & purpose = "read") 109 | { 110 | std::ifstream ifs(filepath); 111 | if (! ifs.good()) 112 | { 113 | std::basic_ostringstream oss; 114 | oss << "Could not open \"" << filepath << "\" for " << purpose; 115 | throw std::system_error(errno, std::generic_category(), oss.str()); 116 | } 117 | return ifs; 118 | } 119 | 120 | template 121 | std::ofstream ofstream_checked( 122 | std::basic_string const & filepath, std::basic_string const & purpose = "write") 123 | { 124 | std::ofstream ofs(filepath); 125 | if (! ofs.good()) 126 | { 127 | std::basic_ostringstream oss; 128 | oss << "Could not open \"" << filepath << "\" for " << purpose; 129 | throw std::system_error(errno, std::generic_category(), oss.str()); 130 | } 131 | return ofs; 132 | } 133 | 134 | template 135 | std::fstream fstream_checked( 136 | std::basic_string const & filepath, std::basic_string const & purpose = "read/write") 137 | { 138 | std::fstream fs(filepath); 139 | if (! fs.good()) 140 | { 141 | std::basic_ostringstream oss; 142 | oss << "Could not open \"" << filepath << "\" for " << purpose; 143 | throw std::system_error(errno, std::generic_category(), oss.str()); 144 | } 145 | return fs; 146 | } 147 | } // namespace motoi 148 | 149 | #endif 150 | -------------------------------------------------------------------------------- /app/shared/gfxdef_builder.hpp: -------------------------------------------------------------------------------- 1 | /** 2 | * @file gfxdef_builder.hpp 3 | * @author Motoi Productions (Damian Rogers damian@motoi.pro) 4 | * @brief Creates a gfxdef on the fly 5 | */ 6 | 7 | #ifndef __CHRGFX__GFXDEF_BUILDER_HPP 8 | #define __CHRGFX__GFXDEF_BUILDER_HPP 9 | 10 | #include 11 | #include 12 | #include 13 | 14 | #include "cfgload.hpp" 15 | #include "chrdef.hpp" 16 | #include "coldef.hpp" 17 | #include "image_types.hpp" 18 | #include "paldef.hpp" 19 | #include "strutil.hpp" 20 | 21 | using namespace std; 22 | using namespace chrgfx; 23 | using namespace motoi; 24 | 25 | #define SET_FIELD(_field) \ 26 | if (entry.first == #_field) \ 27 | { \ 28 | set_##_field(entry.second); \ 29 | continue; \ 30 | } 31 | 32 | /* 33 | we may add builders for paldef/coldef sometime in the future, so we have an abstract class 34 | 35 | however, coldefs in particular are a bit more complicated than chrdefs when it comes to command line definitions 36 | 37 | for example, for rgbcoldefs, if we override an existing loaded def (e.g. from a profile) with a color passes value that 38 | does not match the overridden def, we have a problem 39 | 40 | moreover, trying to specify a refcoldef on the command line sounds like a nightmare 41 | 42 | all that aside, I can't think of a use case where col/pal defs would need to be modified "on the fly" like chrdefs may 43 | need to be (e.g. large "tiles" with non-uniform sizes) 44 | */ 45 | 46 | class gfxdef_builder 47 | { 48 | protected: 49 | string m_id; 50 | string m_desc; 51 | gfxdef_builder() {} 52 | 53 | public: 54 | void set_id(string const & id) 55 | { 56 | if (id.empty()) 57 | throw runtime_error("gfxdef id cannot be empty"); 58 | m_id = id; 59 | } 60 | 61 | void set_desc(string const & comment) 62 | { 63 | m_desc = comment; 64 | } 65 | }; 66 | 67 | class rgbcoldef_builder : public gfxdef_builder 68 | { 69 | private: 70 | uint m_bitdepth {0}; 71 | std::vector m_layout; 72 | bool m_big_endian {false}; 73 | 74 | public: 75 | rgbcoldef_builder() = default; 76 | rgbcoldef_builder(rgbcoldef const & coldef) 77 | { 78 | from_def(coldef); 79 | } 80 | 81 | rgbcoldef_builder(block_map const & map) 82 | { 83 | from_map(map); 84 | } 85 | 86 | void from_def(rgbcoldef const & coldef) 87 | { 88 | set_id(coldef.id()); 89 | set_desc(coldef.desc()); 90 | m_bitdepth = coldef.bitdepth(); 91 | m_layout = coldef.layout(); 92 | } 93 | 94 | void from_map(block_map const & map) 95 | { 96 | for (auto const & entry : map) 97 | { 98 | SET_FIELD(id); 99 | SET_FIELD(desc); 100 | SET_FIELD(bitdepth); 101 | SET_FIELD(layout); 102 | SET_FIELD(big_endian); 103 | } 104 | } 105 | 106 | void set_bitdepth(string const & bitdepth) 107 | { 108 | m_bitdepth = sto(trim_view(bitdepth)); 109 | } 110 | 111 | void clear_layout() 112 | { 113 | m_layout.clear(); 114 | } 115 | 116 | void set_layout(string const & layout) 117 | { 118 | auto layout_raw = sto_container>(layout); 119 | if (layout_raw.size() != 6) 120 | throw runtime_error("invalid rgb layout for coldef, must have exactly 6 entries"); 121 | rgb_layout rgblayout { 122 | {layout_raw[0], layout_raw[1]}, {layout_raw[2], layout_raw[3]}, {layout_raw[4], layout_raw[5]}}; 123 | m_layout.emplace_back(rgblayout); 124 | } 125 | 126 | void set_big_endian(string const & big_endian) 127 | { 128 | m_big_endian = sto_bool(big_endian); 129 | } 130 | 131 | [[nodiscard]] rgbcoldef * build() const 132 | { 133 | // check the validity of the definition 134 | if (m_bitdepth == 0) 135 | throw runtime_error("Bitdepth must be greater than zero"); 136 | if (m_layout.size() == 0) 137 | throw runtime_error("rgb_layout list cannot be empty"); 138 | for (auto const & layout : m_layout) 139 | { 140 | if (layout.red_size() > 8 || layout.green_size() > 8 || layout.blue_size() > 8) 141 | throw runtime_error("datasize in a bit layout cannot be greater than 8 (maximum of 8 bits per color channel)"); 142 | if (layout.red_offset() > 32 || layout.green_offset() > 32 || layout.blue_offset() > 32) 143 | throw runtime_error("color channel bit offsets cannot be greater than 32"); 144 | } 145 | return new rgbcoldef {m_id, m_bitdepth, m_layout, m_big_endian, m_desc}; 146 | } 147 | }; 148 | 149 | class refcoldef_builder : public gfxdef_builder 150 | { 151 | private: 152 | palette m_refpal; 153 | bool m_big_endian; 154 | 155 | public: 156 | refcoldef_builder() = default; 157 | 158 | refcoldef_builder(refcoldef const & coldef) 159 | { 160 | from_def(coldef); 161 | } 162 | 163 | refcoldef_builder(block_map const & map) 164 | { 165 | from_map(map); 166 | } 167 | 168 | void from_def(refcoldef const & coldef) 169 | { 170 | set_id(coldef.id()); 171 | set_desc(coldef.desc()); 172 | m_refpal = coldef.refpal(); 173 | } 174 | 175 | void from_map(block_map const & map) 176 | { 177 | for (auto const & entry : map) 178 | { 179 | SET_FIELD(id); 180 | SET_FIELD(desc); 181 | SET_FIELD(big_endian); 182 | SET_FIELD(refpal); 183 | } 184 | } 185 | 186 | void set_refpal(string const & refpal) 187 | { 188 | m_refpal = split_array(refpal); 189 | } 190 | 191 | void set_big_endian(string const & big_endian) 192 | { 193 | m_big_endian = sto_bool(trim_view(big_endian)); 194 | } 195 | 196 | [[nodiscard]] refcoldef * build() const 197 | { 198 | return new refcoldef {m_id, m_refpal, m_big_endian, m_desc}; 199 | } 200 | }; 201 | 202 | class paldef_builder : public gfxdef_builder 203 | { 204 | private: 205 | uint m_entry_datasize {0}; 206 | uint m_length {0}; 207 | std::optional m_datasize {std::nullopt}; 208 | 209 | public: 210 | paldef_builder() = default; 211 | paldef_builder(paldef const & paldef) 212 | { 213 | from_def(paldef); 214 | } 215 | 216 | paldef_builder(block_map const & map) 217 | { 218 | from_map(map); 219 | } 220 | 221 | void from_def(paldef const & paldef) 222 | { 223 | set_id(paldef.id()); 224 | set_desc(paldef.desc()); 225 | m_entry_datasize = paldef.entry_datasize(); 226 | m_length = paldef.length(); 227 | m_datasize = paldef.datasize(); 228 | } 229 | 230 | void from_map(block_map const & map) 231 | { 232 | for (auto const & entry : map) 233 | { 234 | SET_FIELD(id); 235 | SET_FIELD(desc); 236 | SET_FIELD(length); 237 | SET_FIELD(entry_datasize); 238 | SET_FIELD(datasize); 239 | } 240 | } 241 | 242 | void set_length(string const & length) 243 | { 244 | m_length = sto(length); 245 | } 246 | 247 | void set_entry_datasize(string const & entry_datasize) 248 | { 249 | m_entry_datasize = sto(trim_view(entry_datasize)); 250 | } 251 | 252 | void set_datasize(string const & datasize) 253 | { 254 | m_datasize = sto(trim_view(datasize)); 255 | } 256 | 257 | [[nodiscard]] paldef * build() const 258 | { 259 | // check the validity of the definition 260 | if (m_length == 0) 261 | throw runtime_error("palette length must be greater than zero"); 262 | if (m_entry_datasize == 0) 263 | throw runtime_error("palette entry data size must be greater than zero"); 264 | if (m_datasize == 0) 265 | throw runtime_error("palette data size must be greater than zero"); 266 | 267 | return new paldef {m_id, m_entry_datasize, m_length, m_datasize, m_desc}; 268 | } 269 | }; 270 | 271 | class chrdef_builder : public gfxdef_builder 272 | { 273 | private: 274 | uint m_width {0}; 275 | uint m_height {0}; 276 | uint m_bpp {0}; 277 | vector m_plane_offsets; 278 | vector m_pixel_offsets; 279 | vector m_row_offsets; 280 | 281 | public: 282 | chrdef_builder() = default; 283 | chrdef_builder(chrdef const & chrdef) 284 | { 285 | from_def(chrdef); 286 | } 287 | 288 | chrdef_builder(block_map const & map) 289 | { 290 | from_map(map); 291 | } 292 | 293 | void from_def(chrdef const & chrdef) 294 | { 295 | set_id(chrdef.id()); 296 | set_desc(chrdef.desc()); 297 | m_width = chrdef.width(); 298 | m_height = chrdef.height(); 299 | m_bpp = chrdef.bpp(); 300 | m_plane_offsets = chrdef.plane_offsets(); 301 | m_pixel_offsets = chrdef.pixel_offsets(); 302 | m_row_offsets = chrdef.row_offsets(); 303 | } 304 | 305 | void from_map(block_map const & map) 306 | { 307 | for (auto const & entry : map) 308 | { 309 | SET_FIELD(id); 310 | SET_FIELD(desc); 311 | SET_FIELD(width); 312 | SET_FIELD(height); 313 | SET_FIELD(bpp); 314 | SET_FIELD(plane_offsets); 315 | SET_FIELD(pixel_offsets); 316 | SET_FIELD(row_offsets); 317 | } 318 | } 319 | 320 | void set_width(string const & width) 321 | { 322 | m_width = sto(trim_view(width)); 323 | } 324 | 325 | void set_height(string const & height) 326 | { 327 | m_height = sto(trim_view(height)); 328 | } 329 | 330 | void set_bpp(string const & bpp) 331 | { 332 | m_bpp = sto(trim_view(bpp)); 333 | } 334 | 335 | void set_plane_offsets(string const & plane_offsets) 336 | { 337 | if (plane_offsets[0] == '[') 338 | m_plane_offsets = sto_range>(plane_offsets); 339 | else 340 | m_plane_offsets = sto_container>(plane_offsets); 341 | } 342 | 343 | void set_pixel_offsets(string const & pixel_offsets) 344 | { 345 | if (pixel_offsets[0] == '[') 346 | m_pixel_offsets = sto_range>(pixel_offsets); 347 | else 348 | m_pixel_offsets = sto_container>(pixel_offsets); 349 | } 350 | 351 | void set_row_offsets(string const & row_offsets) 352 | { 353 | if (row_offsets[0] == '[') 354 | m_row_offsets = sto_range>(row_offsets); 355 | else 356 | m_row_offsets = sto_container>(row_offsets); 357 | } 358 | 359 | [[nodiscard]] chrdef * build() const 360 | { 361 | // check the validity of the definition 362 | if (m_bpp == 0) 363 | throw runtime_error("Bitdepth must be greater than zero"); 364 | if (m_width > m_pixel_offsets.size()) 365 | throw runtime_error("CHR width must be equal to number of pixel offset entries"); 366 | if (m_height > m_row_offsets.size()) 367 | throw runtime_error("CHR height must be equal to number of row offset entries"); 368 | if (m_bpp > m_plane_offsets.size()) 369 | throw runtime_error("CHR bitdepth must be equal to number of plane offset entries"); 370 | return new chrdef {m_id, m_width, m_height, m_bpp, m_pixel_offsets, m_row_offsets, m_plane_offsets, m_desc}; 371 | } 372 | }; 373 | 374 | #endif 375 | -------------------------------------------------------------------------------- /app/shared/gfxdefman.hpp: -------------------------------------------------------------------------------- 1 | #ifndef __MOTOI__GFXDEFMAN_HPP 2 | #define __MOTOI__GFXDEFMAN_HPP 3 | 4 | #include "builtin_defs.hpp" 5 | #include "cfgload.hpp" 6 | #include "chrdef.hpp" 7 | #include "coldef.hpp" 8 | #include "gfxdef_builder.hpp" 9 | #include "paldef.hpp" 10 | #include "shared.hpp" 11 | #include "xdgdirs.hpp" 12 | #include 13 | #include 14 | #ifdef DEBUG 15 | #include 16 | #endif 17 | 18 | static auto constexpr GFXDEF_SUBDIR {"chrgfx/gfxdefs"}; 19 | 20 | class gfxdef_manager 21 | { 22 | private: 23 | runtime_config const & m_cfg; 24 | 25 | chrgfx::chrdef const * m_chrdef {nullptr}; 26 | chrgfx::paldef const * m_paldef {nullptr}; 27 | chrgfx::coldef const * m_coldef {nullptr}; 28 | 29 | std::string m_target_profile; 30 | std::string m_target_chrdef; 31 | std::string m_target_paldef; 32 | std::string m_target_coldef; 33 | 34 | bool profile_found {false}; 35 | 36 | std::string get_gfxdefs_xdg_paths() 37 | { 38 | auto xdg_locations {data_filepaths(GFXDEF_SUBDIR)}; 39 | if (xdg_locations.empty()) 40 | throw runtime_error("Could not find gfxdef file in any default location"); 41 | return xdg_locations.front(); 42 | } 43 | 44 | void load_from_file(std::string const & path) 45 | { 46 | // don't bother loading anything if we already have our defs loaded 47 | if (m_chrdef != nullptr && m_paldef != nullptr && m_coldef != nullptr) 48 | { 49 | #ifdef DEBUG 50 | std::cerr << "gfxdef pointers alreadt set, not loading file\n"; 51 | #endif 52 | return; 53 | } 54 | 55 | motoi::config_loader config(path); 56 | std::string_view block_header; 57 | 58 | // get gfxdef IDs from profile first, if specified 59 | if (! m_target_profile.empty()) 60 | { 61 | #ifdef DEBUG 62 | std::cerr << "Using profile: " << m_target_profile << '\n'; 63 | #endif 64 | for (auto const & block : config) 65 | { 66 | if (block.first != "profile") 67 | continue; 68 | 69 | auto kv = block.second.find("id"); 70 | if (kv == block.second.end()) 71 | continue; 72 | if (kv->second != m_target_profile) 73 | continue; 74 | 75 | // we have our requested profile 76 | // only set gfxdef IDs if they are not set by the user 77 | // (i.e. allow them to override profile settings) 78 | profile_found = true; 79 | if (m_target_chrdef.empty()) 80 | { 81 | auto chrdef_kv = block.second.find("chrdef"); 82 | if (chrdef_kv != block.second.end()) 83 | m_target_chrdef = chrdef_kv->second; 84 | #ifdef DEBUG 85 | std::cerr << "Set target chrdef from profile: " << m_target_chrdef << '\n'; 86 | #endif 87 | } 88 | 89 | if (m_target_paldef.empty()) 90 | { 91 | auto paldef_kv = block.second.find("paldef"); 92 | if (paldef_kv != block.second.end()) 93 | m_target_paldef = paldef_kv->second; 94 | #ifdef DEBUG 95 | std::cerr << "Set target paldef from profile: " << m_target_paldef << '\n'; 96 | #endif 97 | } 98 | 99 | if (m_target_coldef.empty()) 100 | { 101 | auto coldef_kv = block.second.find("coldef"); 102 | if (coldef_kv != block.second.end()) 103 | m_target_coldef = coldef_kv->second; 104 | #ifdef DEBUG 105 | std::cerr << "Set target coldef from profile: " << m_target_coldef << '\n'; 106 | #endif 107 | } 108 | break; 109 | } 110 | 111 | if (! profile_found) 112 | throw runtime_error("could not find specified profile " + m_target_profile); 113 | } 114 | 115 | // load gfxdefs as specified 116 | for (auto const & block : config) 117 | { 118 | block_header = block.first; 119 | 120 | if (m_chrdef == nullptr && ! m_target_chrdef.empty() && block_header == "chrdef") 121 | { 122 | auto kv = block.second.find("id"); 123 | if (kv == block.second.end()) 124 | continue; 125 | 126 | if (kv->second != m_target_chrdef) 127 | continue; 128 | 129 | // matched the block, now load it as a gfxdef 130 | chrdef_builder builder(block.second); 131 | m_chrdef = builder.build(); 132 | continue; 133 | } 134 | 135 | if (m_paldef == nullptr && ! m_target_paldef.empty() && block_header == "paldef") 136 | { 137 | auto kv = block.second.find("id"); 138 | if (kv == block.second.end()) 139 | continue; 140 | 141 | if (kv->second != m_target_paldef) 142 | continue; 143 | 144 | // matched the block, now load it as a gfxdef 145 | paldef_builder builder(block.second); 146 | m_paldef = builder.build(); 147 | continue; 148 | } 149 | 150 | if (m_coldef == nullptr && ! m_target_coldef.empty() && block_header == "rgbcoldef") 151 | { 152 | auto kv = block.second.find("id"); 153 | if (kv == block.second.end()) 154 | continue; 155 | 156 | if (kv->second != m_target_coldef) 157 | continue; 158 | 159 | // matched the block, now load it as a gfxdef 160 | rgbcoldef_builder builder(block.second); 161 | m_coldef = builder.build(); 162 | continue; 163 | } 164 | 165 | if (m_coldef == nullptr && ! m_target_coldef.empty() && block_header == "refcoldef") 166 | { 167 | auto kv = block.second.find("id"); 168 | if (kv == block.second.end()) 169 | continue; 170 | 171 | if (kv->second != m_target_coldef) 172 | continue; 173 | 174 | // matched the block, now load it as a gfxdef 175 | refcoldef_builder builder(block.second); 176 | m_coldef = builder.build(); 177 | continue; 178 | } 179 | } 180 | } 181 | 182 | void load_from_internal() 183 | { 184 | if (m_chrdef == nullptr && ! m_target_chrdef.empty()) 185 | { 186 | auto def = chrgfx::gfxdefs::chrdefs.find(m_target_chrdef); 187 | if (def != chrgfx::gfxdefs::chrdefs.end()) 188 | m_chrdef = new class chrdef(def->second); 189 | } 190 | 191 | if (m_paldef == nullptr && ! m_target_paldef.empty()) 192 | { 193 | auto def = chrgfx::gfxdefs::paldefs.find(m_target_paldef); 194 | if (def != chrgfx::gfxdefs::paldefs.end()) 195 | m_paldef = new class paldef(def->second); 196 | } 197 | 198 | if (m_coldef == nullptr && ! m_target_coldef.empty()) 199 | { 200 | auto def = chrgfx::gfxdefs::rgbcoldefs.find(m_target_coldef); 201 | if (def != chrgfx::gfxdefs::rgbcoldefs.end()) 202 | m_coldef = new class rgbcoldef(def->second); 203 | } 204 | } 205 | 206 | void load_from_cli() 207 | { 208 | // build chrdef from cli 209 | if (m_cfg.chrdef_cli_defined()) 210 | { 211 | chrdef_builder builder; 212 | if (m_chrdef != nullptr) 213 | { 214 | builder.from_def(*m_chrdef); 215 | delete m_chrdef; 216 | } 217 | if (! m_cfg.chrdef_bpp.empty()) 218 | builder.set_bpp(m_cfg.chrdef_bpp); 219 | if (! m_cfg.chrdef_width.empty()) 220 | builder.set_width(m_cfg.chrdef_width); 221 | if (! m_cfg.chrdef_height.empty()) 222 | builder.set_height(m_cfg.chrdef_height); 223 | if (! m_cfg.chrdef_pixel_offsets.empty()) 224 | builder.set_pixel_offsets(m_cfg.chrdef_pixel_offsets); 225 | if (! m_cfg.chrdef_plane_offsets.empty()) 226 | builder.set_plane_offsets(m_cfg.chrdef_plane_offsets); 227 | if (! m_cfg.chrdef_row_offsets.empty()) 228 | builder.set_row_offsets(m_cfg.chrdef_row_offsets); 229 | 230 | m_chrdef = builder.build(); 231 | } 232 | 233 | // build paldef from cli 234 | if (m_cfg.paldef_cli_defined()) 235 | { 236 | paldef_builder builder; 237 | if (m_paldef != nullptr) 238 | { 239 | builder.from_def(*m_paldef); 240 | delete m_paldef; 241 | } 242 | if (! m_cfg.paldef_datasize.empty()) 243 | builder.set_datasize(m_cfg.paldef_datasize); 244 | if (! m_cfg.paldef_entry_datasize.empty()) 245 | builder.set_datasize(m_cfg.paldef_entry_datasize); 246 | if (! m_cfg.paldef_length.empty()) 247 | builder.set_length(m_cfg.paldef_length); 248 | 249 | m_paldef = builder.build(); 250 | } 251 | 252 | // build coldef from cli 253 | if (m_cfg.coldef_cli_defined()) 254 | { 255 | rgbcoldef_builder builder; 256 | if (m_coldef != nullptr) 257 | { 258 | if (m_coldef->type() == coldef_type::rgb) 259 | builder.from_def(*static_cast(m_coldef)); 260 | delete m_coldef; 261 | } 262 | if (! m_cfg.rgbcoldef_bitdepth.empty()) 263 | builder.set_bitdepth(m_cfg.rgbcoldef_bitdepth); 264 | if (! m_cfg.rgbcoldef_big_endian.empty()) 265 | builder.set_big_endian(m_cfg.rgbcoldef_big_endian); 266 | if (! m_cfg.rgbcoldef_rgblayout.empty()) 267 | builder.set_layout(m_cfg.rgbcoldef_rgblayout); 268 | 269 | m_coldef = builder.build(); 270 | } 271 | } 272 | 273 | public: 274 | gfxdef_manager(runtime_config & cfg) : 275 | m_cfg(cfg), 276 | m_target_profile(m_cfg.profile_id), 277 | m_target_chrdef(m_cfg.chrdef_id), 278 | m_target_paldef(m_cfg.paldef_id), 279 | m_target_coldef(m_cfg.coldef_id) 280 | { 281 | // if no IDs are specified (i.e. building entirely from command line), skip these steps 282 | if (! (m_target_profile.empty() && m_target_chrdef.empty() && m_target_paldef.empty() && m_target_coldef.empty())) 283 | { 284 | // we load from file first to get the profile, if specified 285 | if (! m_cfg.gfxdefs_path.empty()) 286 | { 287 | // if a gfxdefs file was specified, use that one only 288 | load_from_file(m_cfg.gfxdefs_path); 289 | #ifdef DEBUG 290 | std::cerr << "Considering gfxdefs file: " << m_cfg.gfxdefs_path << '\n'; 291 | #endif 292 | } 293 | else 294 | { 295 | // otherwise read from XDG location(s) 296 | auto xdg_locations {motoi::data_filepaths(GFXDEF_SUBDIR)}; 297 | if (xdg_locations.empty()) 298 | throw runtime_error("could not find a gfxdefs file in any default location"); 299 | for (auto const & path : xdg_locations) 300 | { 301 | #ifdef DEBUG 302 | std::cerr << "Considering gfxdefs file: " << path << '\n'; 303 | #endif 304 | load_from_file(path); 305 | } 306 | } 307 | 308 | if (! m_target_profile.empty() && ! profile_found) 309 | { 310 | throw runtime_error("could not find specified profile " + m_target_profile); 311 | } 312 | 313 | load_from_internal(); 314 | if (! m_target_chrdef.empty() && m_chrdef == nullptr) 315 | throw runtime_error("could not find specified chrdef " + m_target_chrdef); 316 | if (! m_target_paldef.empty() && m_paldef == nullptr) 317 | throw runtime_error("could not find specified paldef " + m_target_paldef); 318 | if (! m_target_coldef.empty() && m_coldef == nullptr) 319 | throw runtime_error("could not find specified coldef " + m_target_coldef); 320 | } 321 | load_from_cli(); 322 | 323 | if (m_chrdef == nullptr && m_paldef == nullptr && m_coldef == nullptr) 324 | throw runtime_error("no gfxdefs loaded"); 325 | } 326 | 327 | ~gfxdef_manager() 328 | { 329 | delete m_chrdef; 330 | delete m_paldef; 331 | delete m_coldef; 332 | } 333 | 334 | auto chrdef() 335 | { 336 | return m_chrdef; 337 | } 338 | 339 | auto paldef() 340 | { 341 | return m_paldef; 342 | } 343 | 344 | auto coldef() 345 | { 346 | return m_coldef; 347 | } 348 | }; 349 | 350 | #endif -------------------------------------------------------------------------------- /app/shared/lineread.hpp: -------------------------------------------------------------------------------- 1 | /** 2 | * @file lineread.hpp 3 | * @author Damian Rogers / damian@motoi.pro 4 | * @brief Simple class for reading lines from a file and maintaining the state 5 | * of the read pointer 6 | * @copyright ©2024 Motoi Productions / Released under MIT License 7 | * 8 | * Updates: 9 | * 20241201 Initial 10 | */ 11 | 12 | #ifndef __MOTOI__LINEREAD_HPP 13 | #define __MOTOI__LINEREAD_HPP 14 | 15 | #include "filesys.hpp" 16 | #include 17 | 18 | namespace motoi 19 | { 20 | 21 | template 22 | class linereader 23 | { 24 | 25 | protected: 26 | std::basic_string m_filepath; 27 | std::basic_ifstream m_ifstream; 28 | std::basic_string m_line; 29 | size_t m_line_num {0}; 30 | 31 | public: 32 | linereader() = delete; 33 | linereader(std::string const filepath) : 34 | m_filepath {filepath}, 35 | m_ifstream {motoi::ifstream_checked(m_filepath)} {}; 36 | 37 | /** 38 | * @return size_t The current line number. 39 | */ 40 | size_t line_number() 41 | { 42 | return m_line_num; 43 | } 44 | 45 | /** 46 | * @return std::string_view The current line. 47 | */ 48 | std::basic_string_view line() 49 | { 50 | return m_line; 51 | } 52 | 53 | /** 54 | * @brief Moves the read head to the next line. 55 | * 56 | * @return false EOF or other stream error 57 | */ 58 | bool next() 59 | { 60 | if (m_ifstream.good()) 61 | { 62 | getline(m_ifstream, m_line); 63 | ++m_line_num; 64 | return true; 65 | } 66 | return false; 67 | } 68 | 69 | /** 70 | * @return std::string_view Path of the currently open file 71 | */ 72 | std::basic_string_view filepath() 73 | { 74 | return m_filepath; 75 | } 76 | 77 | /** 78 | * @brief Reset line read head back to the start of the file 79 | * 80 | */ 81 | void reset() 82 | { 83 | m_ifstream.clear(); 84 | m_ifstream.seekg(0, std::ios::beg); 85 | m_line_num = 0; 86 | } 87 | }; 88 | 89 | } // namespace motoi 90 | #endif -------------------------------------------------------------------------------- /app/shared/shared.cpp: -------------------------------------------------------------------------------- 1 | #include "shared.hpp" 2 | #include 3 | 4 | using namespace std; 5 | 6 | // command line argument processing 7 | string short_opts {":G:H:T:C:P:h"}; 8 | 9 | int longopt_idx {0}; 10 | vector