├── .gitignore ├── LICENSE ├── README.md ├── kage ├── __init__.py ├── components.py ├── font │ ├── __init__.py │ ├── font.py │ ├── round │ │ ├── __init__.py │ │ ├── round.py │ │ └── round_stroke_drawer.py │ ├── sans │ │ ├── __init__.py │ │ ├── sans.py │ │ └── sans_stroke_drawer.py │ └── serif │ │ ├── __init__.py │ │ ├── serif.py │ │ ├── serif_stroke.py │ │ └── serif_stroke_drawer.py ├── kage.py ├── stroke.py ├── util.py └── vec2.py └── output ├── u5f71.svg └── u5f71_serif.svg /.gitignore: -------------------------------------------------------------------------------- 1 | .vscode 2 | __pycache__ 3 | **/__pycache__ 4 | output*/** 5 | !output/u5f71.svg 6 | !output/u5f71_serif.svg 7 | dump_*.txt -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | GNU GENERAL PUBLIC LICENSE 2 | Version 3, 29 June 2007 3 | 4 | Copyright (C) 2007 Free Software Foundation, Inc. 5 | Everyone is permitted to copy and distribute verbatim copies 6 | of this license document, but changing it is not allowed. 7 | 8 | Preamble 9 | 10 | The GNU General Public License is a free, copyleft license for 11 | software and other kinds of works. 12 | 13 | The licenses for most software and other practical works are designed 14 | to take away your freedom to share and change the works. By contrast, 15 | the GNU General Public License is intended to guarantee your freedom to 16 | share and change all versions of a program--to make sure it remains free 17 | software for all its users. We, the Free Software Foundation, use the 18 | GNU General Public License for most of our software; it applies also to 19 | any other work released this way by its authors. You can apply it to 20 | your programs, too. 21 | 22 | When we speak of free software, we are referring to freedom, not 23 | price. Our General Public Licenses are designed to make sure that you 24 | have the freedom to distribute copies of free software (and charge for 25 | them if you wish), that you receive source code or can get it if you 26 | want it, that you can change the software or use pieces of it in new 27 | free programs, and that you know you can do these things. 28 | 29 | To protect your rights, we need to prevent others from denying you 30 | these rights or asking you to surrender the rights. Therefore, you have 31 | certain responsibilities if you distribute copies of the software, or if 32 | you modify it: responsibilities to respect the freedom of others. 33 | 34 | For example, if you distribute copies of such a program, whether 35 | gratis or for a fee, you must pass on to the recipients the same 36 | freedoms that you received. You must make sure that they, too, receive 37 | or can get the source code. And you must show them these terms so they 38 | know their rights. 39 | 40 | Developers that use the GNU GPL protect your rights with two steps: 41 | (1) assert copyright on the software, and (2) offer you this License 42 | giving you legal permission to copy, distribute and/or modify it. 43 | 44 | For the developers' and authors' protection, the GPL clearly explains 45 | that there is no warranty for this free software. For both users' and 46 | authors' sake, the GPL requires that modified versions be marked as 47 | changed, so that their problems will not be attributed erroneously to 48 | authors of previous versions. 49 | 50 | Some devices are designed to deny users access to install or run 51 | modified versions of the software inside them, although the manufacturer 52 | can do so. This is fundamentally incompatible with the aim of 53 | protecting users' freedom to change the software. The systematic 54 | pattern of such abuse occurs in the area of products for individuals to 55 | use, which is precisely where it is most unacceptable. Therefore, we 56 | have designed this version of the GPL to prohibit the practice for those 57 | products. If such problems arise substantially in other domains, we 58 | stand ready to extend this provision to those domains in future versions 59 | of the GPL, as needed to protect the freedom of users. 60 | 61 | Finally, every program is threatened constantly by software patents. 62 | States should not allow patents to restrict development and use of 63 | software on general-purpose computers, but in those that do, we wish to 64 | avoid the special danger that patents applied to a free program could 65 | make it effectively proprietary. To prevent this, the GPL assures that 66 | patents cannot be used to render the program non-free. 67 | 68 | The precise terms and conditions for copying, distribution and 69 | modification follow. 70 | 71 | TERMS AND CONDITIONS 72 | 73 | 0. Definitions. 74 | 75 | "This License" refers to version 3 of the GNU General Public License. 76 | 77 | "Copyright" also means copyright-like laws that apply to other kinds of 78 | works, such as semiconductor masks. 79 | 80 | "The Program" refers to any copyrightable work licensed under this 81 | License. Each licensee is addressed as "you". "Licensees" and 82 | "recipients" may be individuals or organizations. 83 | 84 | To "modify" a work means to copy from or adapt all or part of the work 85 | in a fashion requiring copyright permission, other than the making of an 86 | exact copy. The resulting work is called a "modified version" of the 87 | earlier work or a work "based on" the earlier work. 88 | 89 | A "covered work" means either the unmodified Program or a work based 90 | on the Program. 91 | 92 | To "propagate" a work means to do anything with it that, without 93 | permission, would make you directly or secondarily liable for 94 | infringement under applicable copyright law, except executing it on a 95 | computer or modifying a private copy. Propagation includes copying, 96 | distribution (with or without modification), making available to the 97 | public, and in some countries other activities as well. 98 | 99 | To "convey" a work means any kind of propagation that enables other 100 | parties to make or receive copies. Mere interaction with a user through 101 | a computer network, with no transfer of a copy, is not conveying. 102 | 103 | An interactive user interface displays "Appropriate Legal Notices" 104 | to the extent that it includes a convenient and prominently visible 105 | feature that (1) displays an appropriate copyright notice, and (2) 106 | tells the user that there is no warranty for the work (except to the 107 | extent that warranties are provided), that licensees may convey the 108 | work under this License, and how to view a copy of this License. If 109 | the interface presents a list of user commands or options, such as a 110 | menu, a prominent item in the list meets this criterion. 111 | 112 | 1. Source Code. 113 | 114 | The "source code" for a work means the preferred form of the work 115 | for making modifications to it. "Object code" means any non-source 116 | form of a work. 117 | 118 | A "Standard Interface" means an interface that either is an official 119 | standard defined by a recognized standards body, or, in the case of 120 | interfaces specified for a particular programming language, one that 121 | is widely used among developers working in that language. 122 | 123 | The "System Libraries" of an executable work include anything, other 124 | than the work as a whole, that (a) is included in the normal form of 125 | packaging a Major Component, but which is not part of that Major 126 | Component, and (b) serves only to enable use of the work with that 127 | Major Component, or to implement a Standard Interface for which an 128 | implementation is available to the public in source code form. A 129 | "Major Component", in this context, means a major essential component 130 | (kernel, window system, and so on) of the specific operating system 131 | (if any) on which the executable work runs, or a compiler used to 132 | produce the work, or an object code interpreter used to run it. 133 | 134 | The "Corresponding Source" for a work in object code form means all 135 | the source code needed to generate, install, and (for an executable 136 | work) run the object code and to modify the work, including scripts to 137 | control those activities. However, it does not include the work's 138 | System Libraries, or general-purpose tools or generally available free 139 | programs which are used unmodified in performing those activities but 140 | which are not part of the work. For example, Corresponding Source 141 | includes interface definition files associated with source files for 142 | the work, and the source code for shared libraries and dynamically 143 | linked subprograms that the work is specifically designed to require, 144 | such as by intimate data communication or control flow between those 145 | subprograms and other parts of the work. 146 | 147 | The Corresponding Source need not include anything that users 148 | can regenerate automatically from other parts of the Corresponding 149 | Source. 150 | 151 | The Corresponding Source for a work in source code form is that 152 | same work. 153 | 154 | 2. Basic Permissions. 155 | 156 | All rights granted under this License are granted for the term of 157 | copyright on the Program, and are irrevocable provided the stated 158 | conditions are met. This License explicitly affirms your unlimited 159 | permission to run the unmodified Program. The output from running a 160 | covered work is covered by this License only if the output, given its 161 | content, constitutes a covered work. This License acknowledges your 162 | rights of fair use or other equivalent, as provided by copyright law. 163 | 164 | You may make, run and propagate covered works that you do not 165 | convey, without conditions so long as your license otherwise remains 166 | in force. You may convey covered works to others for the sole purpose 167 | of having them make modifications exclusively for you, or provide you 168 | with facilities for running those works, provided that you comply with 169 | the terms of this License in conveying all material for which you do 170 | not control copyright. Those thus making or running the covered works 171 | for you must do so exclusively on your behalf, under your direction 172 | and control, on terms that prohibit them from making any copies of 173 | your copyrighted material outside their relationship with you. 174 | 175 | Conveying under any other circumstances is permitted solely under 176 | the conditions stated below. Sublicensing is not allowed; section 10 177 | makes it unnecessary. 178 | 179 | 3. Protecting Users' Legal Rights From Anti-Circumvention Law. 180 | 181 | No covered work shall be deemed part of an effective technological 182 | measure under any applicable law fulfilling obligations under article 183 | 11 of the WIPO copyright treaty adopted on 20 December 1996, or 184 | similar laws prohibiting or restricting circumvention of such 185 | measures. 186 | 187 | When you convey a covered work, you waive any legal power to forbid 188 | circumvention of technological measures to the extent such circumvention 189 | is effected by exercising rights under this License with respect to 190 | the covered work, and you disclaim any intention to limit operation or 191 | modification of the work as a means of enforcing, against the work's 192 | users, your or third parties' legal rights to forbid circumvention of 193 | technological measures. 194 | 195 | 4. Conveying Verbatim Copies. 196 | 197 | You may convey verbatim copies of the Program's source code as you 198 | receive it, in any medium, provided that you conspicuously and 199 | appropriately publish on each copy an appropriate copyright notice; 200 | keep intact all notices stating that this License and any 201 | non-permissive terms added in accord with section 7 apply to the code; 202 | keep intact all notices of the absence of any warranty; and give all 203 | recipients a copy of this License along with the Program. 204 | 205 | You may charge any price or no price for each copy that you convey, 206 | and you may offer support or warranty protection for a fee. 207 | 208 | 5. Conveying Modified Source Versions. 209 | 210 | You may convey a work based on the Program, or the modifications to 211 | produce it from the Program, in the form of source code under the 212 | terms of section 4, provided that you also meet all of these conditions: 213 | 214 | a) The work must carry prominent notices stating that you modified 215 | it, and giving a relevant date. 216 | 217 | b) The work must carry prominent notices stating that it is 218 | released under this License and any conditions added under section 219 | 7. This requirement modifies the requirement in section 4 to 220 | "keep intact all notices". 221 | 222 | c) You must license the entire work, as a whole, under this 223 | License to anyone who comes into possession of a copy. This 224 | License will therefore apply, along with any applicable section 7 225 | additional terms, to the whole of the work, and all its parts, 226 | regardless of how they are packaged. This License gives no 227 | permission to license the work in any other way, but it does not 228 | invalidate such permission if you have separately received it. 229 | 230 | d) If the work has interactive user interfaces, each must display 231 | Appropriate Legal Notices; however, if the Program has interactive 232 | interfaces that do not display Appropriate Legal Notices, your 233 | work need not make them do so. 234 | 235 | A compilation of a covered work with other separate and independent 236 | works, which are not by their nature extensions of the covered work, 237 | and which are not combined with it such as to form a larger program, 238 | in or on a volume of a storage or distribution medium, is called an 239 | "aggregate" if the compilation and its resulting copyright are not 240 | used to limit the access or legal rights of the compilation's users 241 | beyond what the individual works permit. Inclusion of a covered work 242 | in an aggregate does not cause this License to apply to the other 243 | parts of the aggregate. 244 | 245 | 6. Conveying Non-Source Forms. 246 | 247 | You may convey a covered work in object code form under the terms 248 | of sections 4 and 5, provided that you also convey the 249 | machine-readable Corresponding Source under the terms of this License, 250 | in one of these ways: 251 | 252 | a) Convey the object code in, or embodied in, a physical product 253 | (including a physical distribution medium), accompanied by the 254 | Corresponding Source fixed on a durable physical medium 255 | customarily used for software interchange. 256 | 257 | b) Convey the object code in, or embodied in, a physical product 258 | (including a physical distribution medium), accompanied by a 259 | written offer, valid for at least three years and valid for as 260 | long as you offer spare parts or customer support for that product 261 | model, to give anyone who possesses the object code either (1) a 262 | copy of the Corresponding Source for all the software in the 263 | product that is covered by this License, on a durable physical 264 | medium customarily used for software interchange, for a price no 265 | more than your reasonable cost of physically performing this 266 | conveying of source, or (2) access to copy the 267 | Corresponding Source from a network server at no charge. 268 | 269 | c) Convey individual copies of the object code with a copy of the 270 | written offer to provide the Corresponding Source. This 271 | alternative is allowed only occasionally and noncommercially, and 272 | only if you received the object code with such an offer, in accord 273 | with subsection 6b. 274 | 275 | d) Convey the object code by offering access from a designated 276 | place (gratis or for a charge), and offer equivalent access to the 277 | Corresponding Source in the same way through the same place at no 278 | further charge. You need not require recipients to copy the 279 | Corresponding Source along with the object code. If the place to 280 | copy the object code is a network server, the Corresponding Source 281 | may be on a different server (operated by you or a third party) 282 | that supports equivalent copying facilities, provided you maintain 283 | clear directions next to the object code saying where to find the 284 | Corresponding Source. Regardless of what server hosts the 285 | Corresponding Source, you remain obligated to ensure that it is 286 | available for as long as needed to satisfy these requirements. 287 | 288 | e) Convey the object code using peer-to-peer transmission, provided 289 | you inform other peers where the object code and Corresponding 290 | Source of the work are being offered to the general public at no 291 | charge under subsection 6d. 292 | 293 | A separable portion of the object code, whose source code is excluded 294 | from the Corresponding Source as a System Library, need not be 295 | included in conveying the object code work. 296 | 297 | A "User Product" is either (1) a "consumer product", which means any 298 | tangible personal property which is normally used for personal, family, 299 | or household purposes, or (2) anything designed or sold for incorporation 300 | into a dwelling. In determining whether a product is a consumer product, 301 | doubtful cases shall be resolved in favor of coverage. For a particular 302 | product received by a particular user, "normally used" refers to a 303 | typical or common use of that class of product, regardless of the status 304 | of the particular user or of the way in which the particular user 305 | actually uses, or expects or is expected to use, the product. A product 306 | is a consumer product regardless of whether the product has substantial 307 | commercial, industrial or non-consumer uses, unless such uses represent 308 | the only significant mode of use of the product. 309 | 310 | "Installation Information" for a User Product means any methods, 311 | procedures, authorization keys, or other information required to install 312 | and execute modified versions of a covered work in that User Product from 313 | a modified version of its Corresponding Source. The information must 314 | suffice to ensure that the continued functioning of the modified object 315 | code is in no case prevented or interfered with solely because 316 | modification has been made. 317 | 318 | If you convey an object code work under this section in, or with, or 319 | specifically for use in, a User Product, and the conveying occurs as 320 | part of a transaction in which the right of possession and use of the 321 | User Product is transferred to the recipient in perpetuity or for a 322 | fixed term (regardless of how the transaction is characterized), the 323 | Corresponding Source conveyed under this section must be accompanied 324 | by the Installation Information. But this requirement does not apply 325 | if neither you nor any third party retains the ability to install 326 | modified object code on the User Product (for example, the work has 327 | been installed in ROM). 328 | 329 | The requirement to provide Installation Information does not include a 330 | requirement to continue to provide support service, warranty, or updates 331 | for a work that has been modified or installed by the recipient, or for 332 | the User Product in which it has been modified or installed. Access to a 333 | network may be denied when the modification itself materially and 334 | adversely affects the operation of the network or violates the rules and 335 | protocols for communication across the network. 336 | 337 | Corresponding Source conveyed, and Installation Information provided, 338 | in accord with this section must be in a format that is publicly 339 | documented (and with an implementation available to the public in 340 | source code form), and must require no special password or key for 341 | unpacking, reading or copying. 342 | 343 | 7. Additional Terms. 344 | 345 | "Additional permissions" are terms that supplement the terms of this 346 | License by making exceptions from one or more of its conditions. 347 | Additional permissions that are applicable to the entire Program shall 348 | be treated as though they were included in this License, to the extent 349 | that they are valid under applicable law. If additional permissions 350 | apply only to part of the Program, that part may be used separately 351 | under those permissions, but the entire Program remains governed by 352 | this License without regard to the additional permissions. 353 | 354 | When you convey a copy of a covered work, you may at your option 355 | remove any additional permissions from that copy, or from any part of 356 | it. (Additional permissions may be written to require their own 357 | removal in certain cases when you modify the work.) You may place 358 | additional permissions on material, added by you to a covered work, 359 | for which you have or can give appropriate copyright permission. 360 | 361 | Notwithstanding any other provision of this License, for material you 362 | add to a covered work, you may (if authorized by the copyright holders of 363 | that material) supplement the terms of this License with terms: 364 | 365 | a) Disclaiming warranty or limiting liability differently from the 366 | terms of sections 15 and 16 of this License; or 367 | 368 | b) Requiring preservation of specified reasonable legal notices or 369 | author attributions in that material or in the Appropriate Legal 370 | Notices displayed by works containing it; or 371 | 372 | c) Prohibiting misrepresentation of the origin of that material, or 373 | requiring that modified versions of such material be marked in 374 | reasonable ways as different from the original version; or 375 | 376 | d) Limiting the use for publicity purposes of names of licensors or 377 | authors of the material; or 378 | 379 | e) Declining to grant rights under trademark law for use of some 380 | trade names, trademarks, or service marks; or 381 | 382 | f) Requiring indemnification of licensors and authors of that 383 | material by anyone who conveys the material (or modified versions of 384 | it) with contractual assumptions of liability to the recipient, for 385 | any liability that these contractual assumptions directly impose on 386 | those licensors and authors. 387 | 388 | All other non-permissive additional terms are considered "further 389 | restrictions" within the meaning of section 10. If the Program as you 390 | received it, or any part of it, contains a notice stating that it is 391 | governed by this License along with a term that is a further 392 | restriction, you may remove that term. If a license document contains 393 | a further restriction but permits relicensing or conveying under this 394 | License, you may add to a covered work material governed by the terms 395 | of that license document, provided that the further restriction does 396 | not survive such relicensing or conveying. 397 | 398 | If you add terms to a covered work in accord with this section, you 399 | must place, in the relevant source files, a statement of the 400 | additional terms that apply to those files, or a notice indicating 401 | where to find the applicable terms. 402 | 403 | Additional terms, permissive or non-permissive, may be stated in the 404 | form of a separately written license, or stated as exceptions; 405 | the above requirements apply either way. 406 | 407 | 8. Termination. 408 | 409 | You may not propagate or modify a covered work except as expressly 410 | provided under this License. Any attempt otherwise to propagate or 411 | modify it is void, and will automatically terminate your rights under 412 | this License (including any patent licenses granted under the third 413 | paragraph of section 11). 414 | 415 | However, if you cease all violation of this License, then your 416 | license from a particular copyright holder is reinstated (a) 417 | provisionally, unless and until the copyright holder explicitly and 418 | finally terminates your license, and (b) permanently, if the copyright 419 | holder fails to notify you of the violation by some reasonable means 420 | prior to 60 days after the cessation. 421 | 422 | Moreover, your license from a particular copyright holder is 423 | reinstated permanently if the copyright holder notifies you of the 424 | violation by some reasonable means, this is the first time you have 425 | received notice of violation of this License (for any work) from that 426 | copyright holder, and you cure the violation prior to 30 days after 427 | your receipt of the notice. 428 | 429 | Termination of your rights under this section does not terminate the 430 | licenses of parties who have received copies or rights from you under 431 | this License. If your rights have been terminated and not permanently 432 | reinstated, you do not qualify to receive new licenses for the same 433 | material under section 10. 434 | 435 | 9. Acceptance Not Required for Having Copies. 436 | 437 | You are not required to accept this License in order to receive or 438 | run a copy of the Program. Ancillary propagation of a covered work 439 | occurring solely as a consequence of using peer-to-peer transmission 440 | to receive a copy likewise does not require acceptance. However, 441 | nothing other than this License grants you permission to propagate or 442 | modify any covered work. These actions infringe copyright if you do 443 | not accept this License. Therefore, by modifying or propagating a 444 | covered work, you indicate your acceptance of this License to do so. 445 | 446 | 10. Automatic Licensing of Downstream Recipients. 447 | 448 | Each time you convey a covered work, the recipient automatically 449 | receives a license from the original licensors, to run, modify and 450 | propagate that work, subject to this License. You are not responsible 451 | for enforcing compliance by third parties with this License. 452 | 453 | An "entity transaction" is a transaction transferring control of an 454 | organization, or substantially all assets of one, or subdividing an 455 | organization, or merging organizations. If propagation of a covered 456 | work results from an entity transaction, each party to that 457 | transaction who receives a copy of the work also receives whatever 458 | licenses to the work the party's predecessor in interest had or could 459 | give under the previous paragraph, plus a right to possession of the 460 | Corresponding Source of the work from the predecessor in interest, if 461 | the predecessor has it or can get it with reasonable efforts. 462 | 463 | You may not impose any further restrictions on the exercise of the 464 | rights granted or affirmed under this License. For example, you may 465 | not impose a license fee, royalty, or other charge for exercise of 466 | rights granted under this License, and you may not initiate litigation 467 | (including a cross-claim or counterclaim in a lawsuit) alleging that 468 | any patent claim is infringed by making, using, selling, offering for 469 | sale, or importing the Program or any portion of it. 470 | 471 | 11. Patents. 472 | 473 | A "contributor" is a copyright holder who authorizes use under this 474 | License of the Program or a work on which the Program is based. The 475 | work thus licensed is called the contributor's "contributor version". 476 | 477 | A contributor's "essential patent claims" are all patent claims 478 | owned or controlled by the contributor, whether already acquired or 479 | hereafter acquired, that would be infringed by some manner, permitted 480 | by this License, of making, using, or selling its contributor version, 481 | but do not include claims that would be infringed only as a 482 | consequence of further modification of the contributor version. For 483 | purposes of this definition, "control" includes the right to grant 484 | patent sublicenses in a manner consistent with the requirements of 485 | this License. 486 | 487 | Each contributor grants you a non-exclusive, worldwide, royalty-free 488 | patent license under the contributor's essential patent claims, to 489 | make, use, sell, offer for sale, import and otherwise run, modify and 490 | propagate the contents of its contributor version. 491 | 492 | In the following three paragraphs, a "patent license" is any express 493 | agreement or commitment, however denominated, not to enforce a patent 494 | (such as an express permission to practice a patent or covenant not to 495 | sue for patent infringement). To "grant" such a patent license to a 496 | party means to make such an agreement or commitment not to enforce a 497 | patent against the party. 498 | 499 | If you convey a covered work, knowingly relying on a patent license, 500 | and the Corresponding Source of the work is not available for anyone 501 | to copy, free of charge and under the terms of this License, through a 502 | publicly available network server or other readily accessible means, 503 | then you must either (1) cause the Corresponding Source to be so 504 | available, or (2) arrange to deprive yourself of the benefit of the 505 | patent license for this particular work, or (3) arrange, in a manner 506 | consistent with the requirements of this License, to extend the patent 507 | license to downstream recipients. "Knowingly relying" means you have 508 | actual knowledge that, but for the patent license, your conveying the 509 | covered work in a country, or your recipient's use of the covered work 510 | in a country, would infringe one or more identifiable patents in that 511 | country that you have reason to believe are valid. 512 | 513 | If, pursuant to or in connection with a single transaction or 514 | arrangement, you convey, or propagate by procuring conveyance of, a 515 | covered work, and grant a patent license to some of the parties 516 | receiving the covered work authorizing them to use, propagate, modify 517 | or convey a specific copy of the covered work, then the patent license 518 | you grant is automatically extended to all recipients of the covered 519 | work and works based on it. 520 | 521 | A patent license is "discriminatory" if it does not include within 522 | the scope of its coverage, prohibits the exercise of, or is 523 | conditioned on the non-exercise of one or more of the rights that are 524 | specifically granted under this License. You may not convey a covered 525 | work if you are a party to an arrangement with a third party that is 526 | in the business of distributing software, under which you make payment 527 | to the third party based on the extent of your activity of conveying 528 | the work, and under which the third party grants, to any of the 529 | parties who would receive the covered work from you, a discriminatory 530 | patent license (a) in connection with copies of the covered work 531 | conveyed by you (or copies made from those copies), or (b) primarily 532 | for and in connection with specific products or compilations that 533 | contain the covered work, unless you entered into that arrangement, 534 | or that patent license was granted, prior to 28 March 2007. 535 | 536 | Nothing in this License shall be construed as excluding or limiting 537 | any implied license or other defenses to infringement that may 538 | otherwise be available to you under applicable patent law. 539 | 540 | 12. No Surrender of Others' Freedom. 541 | 542 | If conditions are imposed on you (whether by court order, agreement or 543 | otherwise) that contradict the conditions of this License, they do not 544 | excuse you from the conditions of this License. If you cannot convey a 545 | covered work so as to satisfy simultaneously your obligations under this 546 | License and any other pertinent obligations, then as a consequence you may 547 | not convey it at all. For example, if you agree to terms that obligate you 548 | to collect a royalty for further conveying from those to whom you convey 549 | the Program, the only way you could satisfy both those terms and this 550 | License would be to refrain entirely from conveying the Program. 551 | 552 | 13. Use with the GNU Affero General Public License. 553 | 554 | Notwithstanding any other provision of this License, you have 555 | permission to link or combine any covered work with a work licensed 556 | under version 3 of the GNU Affero General Public License into a single 557 | combined work, and to convey the resulting work. The terms of this 558 | License will continue to apply to the part which is the covered work, 559 | but the special requirements of the GNU Affero General Public License, 560 | section 13, concerning interaction through a network will apply to the 561 | combination as such. 562 | 563 | 14. Revised Versions of this License. 564 | 565 | The Free Software Foundation may publish revised and/or new versions of 566 | the GNU General Public License from time to time. Such new versions will 567 | be similar in spirit to the present version, but may differ in detail to 568 | address new problems or concerns. 569 | 570 | Each version is given a distinguishing version number. If the 571 | Program specifies that a certain numbered version of the GNU General 572 | Public License "or any later version" applies to it, you have the 573 | option of following the terms and conditions either of that numbered 574 | version or of any later version published by the Free Software 575 | Foundation. If the Program does not specify a version number of the 576 | GNU General Public License, you may choose any version ever published 577 | by the Free Software Foundation. 578 | 579 | If the Program specifies that a proxy can decide which future 580 | versions of the GNU General Public License can be used, that proxy's 581 | public statement of acceptance of a version permanently authorizes you 582 | to choose that version for the Program. 583 | 584 | Later license versions may give you additional or different 585 | permissions. However, no additional obligations are imposed on any 586 | author or copyright holder as a result of your choosing to follow a 587 | later version. 588 | 589 | 15. Disclaimer of Warranty. 590 | 591 | THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY 592 | APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT 593 | HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY 594 | OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, 595 | THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR 596 | PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM 597 | IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF 598 | ALL NECESSARY SERVICING, REPAIR OR CORRECTION. 599 | 600 | 16. Limitation of Liability. 601 | 602 | IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING 603 | WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS 604 | THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY 605 | GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE 606 | USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF 607 | DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD 608 | PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS), 609 | EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF 610 | SUCH DAMAGES. 611 | 612 | 17. Interpretation of Sections 15 and 16. 613 | 614 | If the disclaimer of warranty and limitation of liability provided 615 | above cannot be given local legal effect according to their terms, 616 | reviewing courts shall apply local law that most closely approximates 617 | an absolute waiver of all civil liability in connection with the 618 | Program, unless a warranty or assumption of liability accompanies a 619 | copy of the Program in return for a fee. 620 | 621 | END OF TERMS AND CONDITIONS 622 | 623 | How to Apply These Terms to Your New Programs 624 | 625 | If you develop a new program, and you want it to be of the greatest 626 | possible use to the public, the best way to achieve this is to make it 627 | free software which everyone can redistribute and change under these terms. 628 | 629 | To do so, attach the following notices to the program. It is safest 630 | to attach them to the start of each source file to most effectively 631 | state the exclusion of warranty; and each file should have at least 632 | the "copyright" line and a pointer to where the full notice is found. 633 | 634 | 635 | Copyright (C) 636 | 637 | This program is free software: you can redistribute it and/or modify 638 | it under the terms of the GNU General Public License as published by 639 | the Free Software Foundation, either version 3 of the License, or 640 | (at your option) any later version. 641 | 642 | This program is distributed in the hope that it will be useful, 643 | but WITHOUT ANY WARRANTY; without even the implied warranty of 644 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 645 | GNU General Public License for more details. 646 | 647 | You should have received a copy of the GNU General Public License 648 | along with this program. If not, see . 649 | 650 | Also add information on how to contact you by electronic and paper mail. 651 | 652 | If the program does terminal interaction, make it output a short 653 | notice like this when it starts in an interactive mode: 654 | 655 | Copyright (C) 656 | This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'. 657 | This is free software, and you are welcome to redistribute it 658 | under certain conditions; type `show c' for details. 659 | 660 | The hypothetical commands `show w' and `show c' should show the appropriate 661 | parts of the General Public License. Of course, your program's commands 662 | might be different; for a GUI interface, you would use an "about box". 663 | 664 | You should also get your employer (if you work as a programmer) or school, 665 | if any, to sign a "copyright disclaimer" for the program, if necessary. 666 | For more information on this, and how to apply and follow the GNU GPL, see 667 | . 668 | 669 | The GNU General Public License does not permit incorporating your program 670 | into proprietary programs. If your program is a subroutine library, you 671 | may consider it more useful to permit linking proprietary applications with 672 | the library. If this is what you want to do, use the GNU Lesser General 673 | Public License instead of this License. But first, please read 674 | . 675 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # `kage-python`: A Python Implementation of Kage Engine 2 | 3 | Kage Engine is a glyph generation engine for Chinese Characters (漢字、汉字), which is mainly developed by [@kamichikoichi](https://github.com/kamichikoichi/kage-engine) (上地宏一) and [@kurgm](https://github.com/kurgm/kage-engine). 4 | 5 | Based on @kurgm's nodejs implementation, this repository focuses on drawing Chinese character glyphs entirely with Bézier curves instead of the previous polygons. 6 | 7 | # Example Usage 8 | 9 | Firstly, You should download `dump_newest_only.txt` or `dump_all_versions.txt` from [GlyphWiki](https://glyphwiki.org/wiki/GlyphWiki:%e9%ab%98%e5%ba%a6%e3%81%aa%e6%b4%bb%e7%94%a8%e6%96%b9%e6%b3%95). 10 | 11 | ```python 12 | from kage import Kage 13 | from kage.font.sans import Sans 14 | from kage.font.serif import Serif 15 | import csv 16 | import os 17 | import multiprocessing 18 | 19 | # Set the flag `ignore_component_version` if you want to use the glyph data in `dump_newest_only.txt`. 20 | # This is because `dump_newest_only.txt` only contains the latest version of components. 21 | # However, glyphs in `dump_newest_only.txt` may reference older versions of multiple components. 22 | k = Kage(ignore_component_version=True) 23 | # You can use `Serif()` as well! 24 | k.font = Sans() 25 | 26 | # generate a glyph 27 | def gen(i: int): 28 | key = f'u{i:x}' 29 | canvas = k.make_glyph(name=key) 30 | canvas.saveas(os.path.join('./output', f'{key}.svg')) 31 | 32 | # read the glyph data 33 | with open('dump_newest_only.txt', 'r', encoding='utf-8') as f: 34 | lines = f.readlines() 35 | 36 | lines = csv.reader(lines, delimiter='|') 37 | for i, line in enumerate(lines): 38 | if i <= 1 or len(line) < 3: 39 | continue 40 | line = [i.strip() for i in line] 41 | 42 | k.components.push(line[0], line[2]) 43 | 44 | # parallel generation 45 | if __name__ == '__main__': 46 | with multiprocessing.Pool(16) as pool: 47 | pool.map(gen, list([0x6708, 0x6c23, 0x6728, 0x9ed1, 0x6230])) 48 | # or maybe you wanna generate the basic CJK Unified Ideographs: 49 | # range(0x4E00, 0x9FA5 + 1) 50 | ``` 51 | 52 | # Sample 53 | 54 | 55 | 56 | 57 | 58 | u+5f71,“影” 59 | 60 | # TODO 61 | 62 | - Serif: Algorithms for drawing offset curves with variable displacement have not been designed. 63 | 64 | - doc: Lack of Documentation. 65 | 66 | # Scholarship Information 67 | 68 | [Kamichi Koichi](https://github.com/kamichikoichi) wrote a paper about his Kage Engine: 69 | 70 | - Koichi KAMICHI (上地 宏一), KAGE - An Automatic Glyph Generating Engine For Large Character Code Set, 「書体・組版ワークショップ報告書」, pp.85-92, Glyph and Typesetting Workshop(書体・組版ワークショップ 京都大學21世紀COE 東アジア世界の人文情報學研究教育據點), 2003年11月28-29日, 京都大学人文科学研究所. -------------------------------------------------------------------------------- /kage/__init__.py: -------------------------------------------------------------------------------- 1 | from . kage import Kage 2 | from . import components 3 | from . import font 4 | from . import stroke 5 | from . import util 6 | from . import vec2 7 | -------------------------------------------------------------------------------- /kage/components.py: -------------------------------------------------------------------------------- 1 | 2 | class Components: 3 | ''' 4 | class `Component` refers to Buhin(部品) in the original implementation. 5 | ''' 6 | def __init__(self, ignore_version = False) -> None: 7 | self.hash = dict() 8 | self.ignore_version = ignore_version 9 | 10 | def search(self, name: str) -> str: 11 | if name in self.hash: 12 | return self.hash[name] 13 | elif self.ignore_version: 14 | if '@' in name: 15 | name = name[0:name.find('@')] 16 | if name in self.hash: 17 | return self.hash[name] 18 | else: 19 | return "" 20 | else: 21 | return "" 22 | 23 | def push(self, name: str, data: str): 24 | self.hash[name] = data 25 | 26 | set = push 27 | -------------------------------------------------------------------------------- /kage/font/__init__.py: -------------------------------------------------------------------------------- 1 | from . font import Font 2 | -------------------------------------------------------------------------------- /kage/font/font.py: -------------------------------------------------------------------------------- 1 | import svgwrite 2 | from .. stroke import Stroke 3 | 4 | class Font: 5 | def __init__(self) -> None: 6 | pass 7 | 8 | def drawer(self, canvas: svgwrite.Drawing, strokes_list: list[Stroke]): 9 | raise NotImplementedError() 10 | -------------------------------------------------------------------------------- /kage/font/round/__init__.py: -------------------------------------------------------------------------------- 1 | from . round import Round 2 | from . round_stroke_drawer import RoundStrokeDrawer -------------------------------------------------------------------------------- /kage/font/round/round.py: -------------------------------------------------------------------------------- 1 | from ...vec2 import Vec2, normalize 2 | from ..serif import Serif 3 | import svgwrite 4 | import numpy as np 5 | 6 | class Round(Serif): 7 | def __init__(self, size=2) -> None: 8 | super().__init__(size) 9 | self.kWidth = 6 10 | 11 | def draw_strokes(self, canvas: svgwrite.Drawing): 12 | from .round_stroke_drawer import RoundStrokeDrawer 13 | stroke_drawer = RoundStrokeDrawer(self, canvas) 14 | for serif_stroke in self.serif_strokes: 15 | stroke = serif_stroke.stroke 16 | if stroke.a1_100 == 0: # None 17 | pass 18 | elif stroke.a1_100 == 1: # Linear stroke, 直線 19 | if stroke.a3_100 == 4 and stroke.a3_opt_2 == 0: # and serif_stroke.hane_adjustment == 0 # left hook, 左撥ね上げ 20 | vec_d = Vec2(0, self.kMage) if all(stroke.vec_1 == stroke.vec_2) else normalize(stroke.vec_1 - stroke.vec_2, self.kMage) 21 | vec_t = stroke.vec_2 + vec_d 22 | stroke_drawer.draw_line(stroke.vec_1, vec_t, stroke.a2_100, 1) 23 | stroke_drawer.draw_curve(vec_t, stroke.vec_2, stroke.vec_2 - Vec2(self.kMage * 2, self.kMage * 0.5), 1, 0, False, True) 24 | else: # other shapes 25 | stroke_drawer.draw_line(stroke.vec_1, stroke.vec_2, stroke.a2_100, stroke.a3_100) 26 | elif stroke.a1_100 in [2, 12]: # 曲線(3 座標:始点, 制御点, 終点), 二次ベジェ曲線, second order bezier curve 27 | if stroke.a3_100 == 4 and stroke.a3_opt_2 == 0: # and serif_stroke.hane_adjustment == 0 # left hook, 左撥ね上げ 28 | vec_d = Vec2(0, -self.kMage) if stroke.vec_2.x == stroke.vec_3.x else normalize(stroke.vec_2 - stroke.vec_3, self.kMage) 29 | vec_t = stroke.vec_3 + vec_d 30 | stroke_drawer.draw_curve(stroke.vec_1, stroke.vec_2, vec_t, stroke.a2_100, 1) 31 | stroke_drawer.draw_curve(vec_t, stroke.vec_3, stroke.vec_3 - Vec2(self.kMage * 2, self.kMage * 0.5), 1, 0, False, True) 32 | elif stroke.a3_100 == 5 and stroke.a3_opt == 0: # right hook, 右撥ね上げ 33 | vec_t1 = stroke.vec_3 + Vec2(self.kMage, 0) 34 | vec_t2 = Vec2(vec_t1.x, stroke.vec_3.y) + Vec2(self.kMage * 0.5, - self.kMage * 2) 35 | stroke_drawer.draw_curve(stroke.vec_1, stroke.vec_2, stroke.vec_3, stroke.a2_100, 1) 36 | stroke_drawer.draw_curve(stroke.vec_3, vec_t1, vec_t2, 1, 0, False, True) 37 | # elif stroke.a2_100 == 7 and stroke.a3_100 == 8: # 點, Dot; consider to move to preprocessor 38 | # stroke_drawer.DrawLine(stroke.vec_1, stroke.vec_3, 1, 0) 39 | else: # other shapes 40 | stroke_drawer.draw_curve(stroke.vec_1, stroke.vec_2, stroke.vec_3, stroke.a2_100, stroke.a3_100) 41 | elif stroke.a1_100 == 3: # 曲げ(3 座標:始点, 経由点, 終点), curve 42 | vec_d1 = Vec2(0, self.kMage) if all(stroke.vec_1 == stroke.vec_2) else normalize(stroke.vec_1 - stroke.vec_2, self.kMage) 43 | vec_t1 = stroke.vec_2 + vec_d1 44 | vec_d2 = Vec2(0, -self.kMage) if all(stroke.vec_2 == stroke.vec_3) else normalize(stroke.vec_3 - stroke.vec_2, self.kMage) 45 | vec_t2 = stroke.vec_2 + vec_d2 46 | 47 | stroke_drawer.draw_line(stroke.vec_1, vec_t1, stroke.a2_100, 1) 48 | stroke_drawer.draw_curve(vec_t1, stroke.vec_2, vec_t2, 1, 1, False, True) 49 | 50 | if stroke.a3_100 == 5 and stroke.a3_opt_1 == 0 and serif_stroke.mage_adjustment == 0: # right hook, 右撥ね上げ 51 | vec_t3 = stroke.vec_3 + Vec2(-self.kMage, 0) 52 | vec_t4 = stroke.vec_3 + Vec2(self.kMage * 0.5, -self.kMage * 2) 53 | 54 | stroke_drawer.draw_line(vec_t2, vec_t3, 1, 1, True) 55 | stroke_drawer.draw_curve(vec_t3, stroke.vec_3, vec_t4, 1, 0, False, True) 56 | else: # other shapes 57 | stroke_drawer.draw_line(vec_t2, stroke.vec_3, 1, stroke.a3_100, True) 58 | elif stroke.a1_100 == 4: # 乙線, OTSU curve 59 | rate = np.hypot(*(stroke.vec_3 - stroke.vec_2)) / 120 * 6 60 | if (rate > 6): 61 | rate = 6 62 | vec_d1 = Vec2(0, self.kMage * rate) if all(stroke.vec_1 == stroke.vec_2) else normalize(stroke.vec_1 - stroke.vec_2, self.kMage * rate) 63 | vec_t1 = stroke.vec_2 + vec_d1 64 | vec_d2 = Vec2(0, -self.kMage * rate) if all(stroke.vec_2 == stroke.vec_3) else normalize(stroke.vec_3 - stroke.vec_2, self.kMage * rate) 65 | vec_t2 = stroke.vec_2 + vec_d2 66 | vec_t3 = stroke.vec_3 + Vec2(-self.kMage, 0) 67 | vec_t4 = stroke.vec_3 + Vec2(self.kMage * 0.5, -self.kMage * 2) 68 | stroke_drawer.draw_line(stroke.vec_1, vec_t1, stroke.a2_100, 1) 69 | stroke_drawer.draw_curve(vec_t1, stroke.vec_2, vec_t2, 1, 1, False, True) 70 | 71 | if stroke.a3_100 == 5 and stroke.a3_opt == 0: # right hook 72 | stroke_drawer.draw_line(vec_t2, vec_t3, 1, 1, True) 73 | stroke_drawer.draw_curve(vec_t3, stroke.vec_3, vec_t4, 1, 0, False, True) 74 | else: 75 | stroke_drawer.draw_line(vec_t2, stroke.vec_3, 1, stroke.a3_100, True) 76 | elif stroke.a1_100 == 6: # 4 点曲線(4 座標:始点, 制御点1, 2, 終点), triple ordered 77 | if stroke.a3_100 == 4: # left hook 78 | vec_d = Vec2(0, -self.kMage) if stroke.vec_3.x == stroke.vec_4.x else normalize(stroke.vec_3 - stroke.vec_4, self.kMage) 79 | vec_t = stroke.vec_4 + vec_d 80 | stroke_drawer.draw_bezier(stroke.vec_1, stroke.vec_2, stroke.vec_3, vec_t, stroke.a2_100, 1) 81 | stroke_drawer.draw_curve(vec_t, stroke.vec_4, stroke.vec_4 - Vec2(self.kMage * 2, self.kMage * 0.5), 1, 0, False, True) 82 | elif stroke.a3_100 == 5 and stroke.a3_opt == 0: # right hook 83 | vec_t1 = stroke.vec_4 + Vec2(-self.kMage, 0) # bug: 戰 84 | vec_t2 = stroke.vec_4 + Vec2(self.kMage * 0.5, -self.kMage * 2) 85 | 86 | stroke_drawer.draw_bezier(stroke.vec_1, stroke.vec_2, stroke.vec_3, vec_t1, stroke.a2_100, 1) 87 | stroke_drawer.draw_curve(vec_t1, stroke.vec_4, vec_t2, 1, 0, False, True) 88 | else: # others 89 | stroke_drawer.draw_bezier(stroke.vec_1, stroke.vec_2, stroke.vec_3, stroke.vec_4, stroke.a2_100, stroke.a3_100) 90 | elif stroke.a1_100 == 7: # 縦払い(3 座標:始点, 経由点, 制御点, 終点), vertical slash 91 | stroke_drawer.draw_line(stroke.vec_1, stroke.vec_2, stroke.a2_100, 1) 92 | stroke_drawer.draw_curve(stroke.vec_2, stroke.vec_3, stroke.vec_4, 1, stroke.a3_100, False, True) 93 | else: 94 | pass -------------------------------------------------------------------------------- /kage/font/round/round_stroke_drawer.py: -------------------------------------------------------------------------------- 1 | from ...vec2 import Vec2, normalize 2 | from ..sans import Sans 3 | import svgwrite 4 | import svgwrite.path 5 | import numpy as np 6 | 7 | def if_in_merge_range(vec_1: Vec2, vec_2: Vec2, merge_range: float) -> bool: 8 | return np.hypot(*(vec_1 - vec_2)) < merge_range 9 | 10 | def generate_d(vec_1: Vec2, vec_s1: Vec2, vec_s2: Vec2, vec_2: Vec2, is_quadratic: bool = False, append_last: bool = False, is_smooth: bool = False) -> str: 11 | ret = str() 12 | if not append_last: 13 | ret += f"M{vec_1} " 14 | 15 | if is_quadratic: 16 | if is_smooth: 17 | ret += f"T{vec_2}" 18 | else: 19 | ret += f"Q{vec_s1} {vec_2}" 20 | else: 21 | if is_smooth: 22 | ret += f"S{vec_s2} {vec_2}" 23 | else: 24 | ret += f"C{vec_s1} {vec_s2} {vec_2}" 25 | return ret 26 | 27 | class RoundStrokeDrawer: 28 | def __init__(self, font: Sans, canvas: svgwrite.Drawing) -> None: 29 | self.font = font 30 | self.canvas = canvas 31 | self.last_point = Vec2(np.inf, np.inf) 32 | 33 | def __draw_curve_universal(self, vec_1: Vec2, vec_s1: Vec2, vec_s2: Vec2, vec_2: Vec2, a1: int, a2: int, is_quadratic: bool = False, is_smooth: bool = False, append_last: bool = False): 34 | delta1 = 0 35 | if (temp := a1 % 10) == 0: 36 | pass 37 | # elif temp == 2: 38 | # delta1 = font.kWidth 39 | # elif temp == 3: 40 | # delta1 = font.kWidth * font.kKakato 41 | # elif temp == 7: # New 42 | # delta1 = -self.font.kWidth 43 | if delta1 != 0: 44 | vec_d1 = Vec2(0, delta1) if all(vec_1 == vec_s1) else normalize(vec_1 - vec_s1, delta1) 45 | vec_1 += vec_d1 46 | 47 | delta2 = 0 48 | if (temp := a2 % 10) == 0: 49 | pass 50 | # elif temp == 2: 51 | # delta2 = self.font.kWidth 52 | # elif temp == 3: 53 | # delta2 = self.font.kWidth * self.font.kKakato 54 | # elif temp == 7: # New 55 | # delta2 = -self.font.kWidth * self.font.kKakato 56 | if delta2 != 0: 57 | vec_d2 = Vec2(0, delta2) if all(vec_2 == vec_s2) else normalize(vec_2 - vec_s2, delta2) 58 | vec_2 += vec_d2 59 | 60 | if not append_last: 61 | append_last = if_in_merge_range(vec_1, self.last_point, self.font.kWidth) 62 | 63 | if not append_last: 64 | self.canvas.add(svgwrite.path.Path(d = generate_d(vec_1, vec_s1, vec_s2, vec_2, is_quadratic, append_last, is_smooth), stroke = 'black', stroke_width = self.font.kWidth * 2, fill = 'none', stroke_linejoin="round", stroke_linecap="round")) 65 | else: 66 | self.canvas.elements[-1].push(generate_d(vec_1, vec_s1, vec_s2, vec_2, is_quadratic, append_last, is_smooth)) 67 | 68 | self.last_point = vec_2 69 | 70 | def draw_bezier(self, vec_1: Vec2, vec_s1: Vec2, vec_s2: Vec2, vec_2: Vec2, a1: int, a2: int, is_smooth: bool = False, append_last: bool = False): 71 | RoundStrokeDrawer.__draw_curve_universal(self, vec_1, vec_s1, vec_s2, vec_2, a1, a2, False, is_smooth, append_last) 72 | 73 | def draw_curve(self, vec_1: Vec2, vec_s: Vec2, vec_2: Vec2, a1: int, a2: int, is_smooth: bool = False, append_last: bool = False): 74 | RoundStrokeDrawer.__draw_curve_universal(self, vec_1, vec_s, vec_s, vec_2, a1, a2, True, is_smooth, append_last) 75 | 76 | def draw_line(self, vec_1: Vec2, vec_2: Vec2, a1: int, a2: int, append_last: bool = False): 77 | if not append_last: 78 | append_last = if_in_merge_range(vec_1, self.last_point, self.font.kWidth) 79 | 80 | if not append_last: 81 | self.canvas.add(svgwrite.path.Path(d = f'M{vec_1.x},{vec_1.y} L{vec_2.x},{vec_2.y}', stroke = 'black', stroke_width = self.font.kWidth * 2, fill = 'none', stroke_linejoin="round", stroke_linecap="round")) 82 | else: 83 | self.canvas.elements[-1].push(f'L{vec_2.x},{vec_2.y}') 84 | 85 | self.last_point = vec_2 86 | -------------------------------------------------------------------------------- /kage/font/sans/__init__.py: -------------------------------------------------------------------------------- 1 | from . sans import Sans 2 | from . sans_stroke_drawer import SansStrokeDrawer -------------------------------------------------------------------------------- /kage/font/sans/sans.py: -------------------------------------------------------------------------------- 1 | from ...vec2 import Vec2, normalize 2 | from ..serif import Serif 3 | import svgwrite 4 | import numpy as np 5 | 6 | class Sans(Serif): 7 | def __init__(self, size=2) -> None: 8 | super().__init__(size) 9 | self.kKakato = 1.5 10 | self.kWidth = 6 11 | 12 | def draw_strokes(self, canvas: svgwrite.Drawing): 13 | from .sans_stroke_drawer import SansStrokeDrawer 14 | stroke_drawer = SansStrokeDrawer(self, canvas) 15 | for serif_stroke in self.serif_strokes: 16 | stroke = serif_stroke.stroke 17 | if stroke.a1_100 == 0: # TODO:Transforms 18 | pass 19 | elif stroke.a1_100 == 1: # Linear stroke, 直線 20 | if stroke.a3_100 == 4 and stroke.a3_opt_2 == 0: # and serif_stroke.hane_adjustment == 0 # left hook, 左撥ね上げ 21 | vec_d = Vec2(0, self.kMage) if all(stroke.vec_1 == stroke.vec_2) else normalize(stroke.vec_1 - stroke.vec_2, self.kMage) 22 | vec_t = stroke.vec_2 + vec_d 23 | stroke_drawer.draw_line(stroke.vec_1, vec_t, stroke.a2_100, 1) 24 | stroke_drawer.draw_curve(vec_t, stroke.vec_2, stroke.vec_2 - Vec2(self.kMage * 2, self.kMage * 0.5), 1, 0, False, True) 25 | else: # other shapes 26 | stroke_drawer.draw_line(stroke.vec_1, stroke.vec_2, stroke.a2_100, stroke.a3_100) 27 | elif stroke.a1_100 in [2, 12]: # 曲線(3 座標:始点, 制御点, 終点), 二次ベジェ曲線, second order bezier curve 28 | if stroke.a3_100 == 4 and stroke.a3_opt_2 == 0: # and serif_stroke.hane_adjustment == 0 # left hook, 左撥ね上げ 29 | vec_d = Vec2(0, -self.kMage) if stroke.vec_2.x == stroke.vec_3.x else normalize(stroke.vec_2 - stroke.vec_3, self.kMage) 30 | vec_t = stroke.vec_3 + vec_d 31 | stroke_drawer.draw_curve(stroke.vec_1, stroke.vec_2, vec_t, stroke.a2_100, 1) 32 | stroke_drawer.draw_curve(vec_t, stroke.vec_3, stroke.vec_3 - Vec2(self.kMage * 2, self.kMage * 0.5), 1, 0, False, True) 33 | elif stroke.a3_100 == 5 and stroke.a3_opt == 0: # right hook, 右撥ね上げ 34 | vec_t1 = stroke.vec_3 + Vec2(self.kMage, 0) 35 | vec_t2 = Vec2(vec_t1.x, stroke.vec_3.y) + Vec2(self.kMage * 0.5, - self.kMage * 2) 36 | stroke_drawer.draw_curve(stroke.vec_1, stroke.vec_2, stroke.vec_3, stroke.a2_100, 1) 37 | stroke_drawer.draw_curve(stroke.vec_3, vec_t1, vec_t2, 1, 0, False, True) 38 | # elif stroke.a2_100 == 7 and stroke.a3_100 == 8: # 點, Dot; consider to move to preprocessor 39 | # stroke_drawer.DrawLine(stroke.vec_1, stroke.vec_3, 1, 0) 40 | else: # other shapes 41 | stroke_drawer.draw_curve(stroke.vec_1, stroke.vec_2, stroke.vec_3, stroke.a2_100, stroke.a3_100) 42 | elif stroke.a1_100 == 3: # 曲げ(3 座標:始点, 経由点, 終点), curve 43 | vec_d1 = Vec2(0, self.kMage) if all(stroke.vec_1 == stroke.vec_2) else normalize(stroke.vec_1 - stroke.vec_2, self.kMage) 44 | vec_t1 = stroke.vec_2 + vec_d1 45 | vec_d2 = Vec2(0, -self.kMage) if all(stroke.vec_2 == stroke.vec_3) else normalize(stroke.vec_3 - stroke.vec_2, self.kMage) 46 | vec_t2 = stroke.vec_2 + vec_d2 47 | 48 | stroke_drawer.draw_line(stroke.vec_1, vec_t1, stroke.a2_100, 1) 49 | stroke_drawer.draw_curve(vec_t1, stroke.vec_2, vec_t2, 1, 1, False, True) 50 | 51 | if stroke.a3_100 == 5 and stroke.a3_opt_1 == 0 and serif_stroke.mage_adjustment == 0: # right hook, 右撥ね上げ 52 | vec_t3 = stroke.vec_3 + Vec2(-self.kMage, 0) 53 | vec_t4 = stroke.vec_3 + Vec2(self.kMage * 0.5, -self.kMage * 2) 54 | 55 | stroke_drawer.draw_line(vec_t2, vec_t3, 1, 1, True) 56 | stroke_drawer.draw_curve(vec_t3, stroke.vec_3, vec_t4, 1, 0, False, True) 57 | else: # other shapes 58 | stroke_drawer.draw_line(vec_t2, stroke.vec_3, 1, stroke.a3_100, True) 59 | elif stroke.a1_100 == 4: # 乙線, OTSU curve 60 | rate = np.hypot(*(stroke.vec_3 - stroke.vec_2)) / 120 * 6 61 | if (rate > 6): 62 | rate = 6 63 | vec_d1 = Vec2(0, self.kMage * rate) if all(stroke.vec_1 == stroke.vec_2) else normalize(stroke.vec_1 - stroke.vec_2, self.kMage * rate) 64 | vec_t1 = stroke.vec_2 + vec_d1 65 | vec_d2 = Vec2(0, -self.kMage * rate) if all(stroke.vec_2 == stroke.vec_3) else normalize(stroke.vec_3 - stroke.vec_2, self.kMage * rate) 66 | vec_t2 = stroke.vec_2 + vec_d2 67 | vec_t3 = stroke.vec_3 + Vec2(-self.kMage, 0) 68 | vec_t4 = stroke.vec_3 + Vec2(self.kMage * 0.5, -self.kMage * 2) 69 | stroke_drawer.draw_line(stroke.vec_1, vec_t1, stroke.a2_100, 1) 70 | stroke_drawer.draw_curve(vec_t1, stroke.vec_2, vec_t2, 1, 1, False, True) 71 | 72 | if stroke.a3_100 == 5 and stroke.a3_opt == 0: # right hook 73 | stroke_drawer.draw_line(vec_t2, vec_t3, 1, 1, True) 74 | stroke_drawer.draw_curve(vec_t3, stroke.vec_3, vec_t4, 1, 0, False, True) 75 | else: 76 | stroke_drawer.draw_line(vec_t2, stroke.vec_3, 1, stroke.a3_100, True) 77 | elif stroke.a1_100 == 6: # 4 点曲線(4 座標:始点, 制御点1, 2, 終点), triple ordered 78 | if stroke.a3_100 == 4: # left hook 79 | vec_d = Vec2(0, -self.kMage) if stroke.vec_3.x == stroke.vec_4.x else normalize(stroke.vec_3 - stroke.vec_4, self.kMage) 80 | vec_t = stroke.vec_4 + vec_d 81 | stroke_drawer.draw_bezier(stroke.vec_1, stroke.vec_2, stroke.vec_3, vec_t, stroke.a2_100, 1) 82 | stroke_drawer.draw_curve(vec_t, stroke.vec_4, stroke.vec_4 - Vec2(self.kMage * 2, self.kMage * 0.5), 1, 0, False, True) 83 | elif stroke.a3_100 == 5 and stroke.a3_opt == 0: # right hook 84 | vec_t1 = stroke.vec_4 + Vec2(-self.kMage, 0) if stroke.vec_4.x - self.kMage > stroke.vec_3.x else Vec2(stroke.vec_3.x, stroke.vec_4.y) 85 | # if else: for '戰' 86 | vec_t2 = stroke.vec_4 + Vec2(self.kMage * 0.5, -self.kMage * 2) 87 | 88 | stroke_drawer.draw_bezier(stroke.vec_1, stroke.vec_2, stroke.vec_3, vec_t1, stroke.a2_100, 1) 89 | stroke_drawer.draw_curve(vec_t1, stroke.vec_4, vec_t2, 1, 0, False, True) 90 | else: # others 91 | stroke_drawer.draw_bezier(stroke.vec_1, stroke.vec_2, stroke.vec_3, stroke.vec_4, stroke.a2_100, stroke.a3_100) 92 | elif stroke.a1_100 == 7: # 縦払い(3 座標:始点, 経由点, 制御点, 終点), vertical slash 93 | stroke_drawer.draw_line(stroke.vec_1, stroke.vec_2, stroke.a2_100, 1) 94 | stroke_drawer.draw_curve(stroke.vec_2, stroke.vec_3, stroke.vec_4, 1, stroke.a3_100, False, True) 95 | else: 96 | pass -------------------------------------------------------------------------------- /kage/font/sans/sans_stroke_drawer.py: -------------------------------------------------------------------------------- 1 | from ...vec2 import Vec2, normalize 2 | from ..sans import Sans 3 | import svgwrite 4 | import svgwrite.path 5 | import numpy as np 6 | 7 | def if_in_merge_range(vec_1: Vec2, vec_2: Vec2, merge_range: float) -> bool: 8 | return np.hypot(*(vec_1 - vec_2)) < merge_range 9 | 10 | def generate_d(vec_1: Vec2, vec_s1: Vec2, vec_s2: Vec2, vec_2: Vec2, is_quadratic: bool = False, append_last: bool = False, is_smooth: bool = False) -> str: 11 | ret = str() 12 | if not append_last: 13 | ret += f"M{vec_1} " 14 | 15 | if is_quadratic: 16 | if is_smooth: 17 | ret += f"T{vec_2}" 18 | else: 19 | ret += f"Q{vec_s1} {vec_2}" 20 | else: 21 | if is_smooth: 22 | ret += f"S{vec_s2} {vec_2}" 23 | else: 24 | ret += f"C{vec_s1} {vec_s2} {vec_2}" 25 | return ret 26 | 27 | class SansStrokeDrawer: 28 | def __init__(self, font: Sans, canvas: svgwrite.Drawing) -> None: 29 | self.font = font 30 | self.canvas = canvas 31 | self.last_point = Vec2(np.inf, np.inf) 32 | 33 | def __draw_curve_universal(self, vec_1: Vec2, vec_s1: Vec2, vec_s2: Vec2, vec_2: Vec2, a1: int, a2: int, is_quadratic: bool = False, is_smooth: bool = False, append_last: bool = False): 34 | delta1 = 0 35 | if a1 % 10 == 0: 36 | pass 37 | # elif a1 % 10 == 2: 38 | # delta1 = font.kWidth 39 | # elif a1 % 10 == 3: 40 | # delta1 = font.kWidth * font.kKakato 41 | # elif a1 % 10 == 7: # New 42 | # delta1 = -self.font.kWidth 43 | if delta1 != 0: 44 | vec_d1 = Vec2(0, delta1) if all(vec_1 == vec_s1) else normalize(vec_1 - vec_s1, delta1) 45 | vec_1 += vec_d1 46 | delta2 = 0 47 | 48 | if a2 % 10 == 0: 49 | pass 50 | # elif a2 % 10 == 2: 51 | # delta2 = self.font.kWidth 52 | # elif a2 % 10 == 3: 53 | # delta2 = self.font.kWidth * self.font.kKakato 54 | # elif a2 % 10 == 7: # New 55 | # delta2 = -self.font.kWidth * self.font.kKakato 56 | if delta2 != 0: 57 | vec_d2 = Vec2(0, delta2) if all(vec_2 == vec_s2) else normalize(vec_2 - vec_s2, delta2) 58 | vec_2 += vec_d2 59 | 60 | if not append_last: 61 | append_last = if_in_merge_range(vec_1, self.last_point, self.font.kWidth) 62 | 63 | if not append_last: 64 | self.canvas.add(svgwrite.path.Path(d = generate_d(vec_1, vec_s1, vec_s2, vec_2, is_quadratic, append_last, is_smooth), stroke = 'black', stroke_width = self.font.kWidth * 2, fill = 'none', stroke_linejoin="bevel")) 65 | else: 66 | self.canvas.elements[-1].push(generate_d(vec_1, vec_s1, vec_s2, vec_2, is_quadratic, append_last, is_smooth)) 67 | 68 | self.last_point = vec_2 69 | 70 | def draw_bezier(self, vec_1: Vec2, vec_s1: Vec2, vec_s2: Vec2, vec_2: Vec2, a1: int, a2: int, is_smooth: bool = False, append_last: bool = False): 71 | SansStrokeDrawer.__draw_curve_universal(self, vec_1, vec_s1, vec_s2, vec_2, a1, a2, False, is_smooth, append_last) 72 | 73 | def draw_curve(self, vec_1: Vec2, vec_s: Vec2, vec_2: Vec2, a1: int, a2: int, is_smooth: bool = False, append_last: bool = False): 74 | SansStrokeDrawer.__draw_curve_universal(self, vec_1, vec_s, vec_s, vec_2, a1, a2, True, is_smooth, append_last) 75 | 76 | def draw_line(self, vec_1: Vec2, vec_2: Vec2, a1: int, a2: int, append_last: bool = False): 77 | if vec_1.x == vec_2.x and vec_1.y > vec_2.y or vec_1.x > vec_2.x: 78 | vec_1, vec_2 = vec_2, vec_1 79 | a1, a2 = a2, a1 80 | exchanged = True 81 | else: 82 | exchanged = False 83 | 84 | norm = normalize(vec_1 - vec_2, self.font.kWidth) 85 | 86 | if a1 % 10 == 2: 87 | vec_1 += norm 88 | elif a1 % 10 == 3: 89 | vec_1 += norm * self.font.kKakato 90 | 91 | if a2 % 10 == 2: 92 | vec_2 -= norm 93 | elif a2 % 10 == 3: 94 | vec_2 -= norm * self.font.kKakato 95 | 96 | if exchanged: 97 | vec_1, vec_2 = vec_2, vec_1 98 | a1, a2 = a2, a1 99 | 100 | if not append_last: 101 | append_last = if_in_merge_range(vec_1, self.last_point, self.font.kWidth) 102 | 103 | if not append_last: 104 | self.canvas.add(svgwrite.path.Path(d = f'M{vec_1} L{vec_2}', stroke = 'black', stroke_width = self.font.kWidth * 2, fill = 'none', stroke_linejoin="bevel")) 105 | else: 106 | self.canvas.elements[-1].push(f'L{vec_2}') 107 | 108 | self.last_point = vec_2 109 | -------------------------------------------------------------------------------- /kage/font/serif/__init__.py: -------------------------------------------------------------------------------- 1 | from . serif import Serif 2 | from . serif_stroke import SerifStroke 3 | from . serif_stroke_drawer import BezierSerifStrokeDrawer -------------------------------------------------------------------------------- /kage/font/serif/serif.py: -------------------------------------------------------------------------------- 1 | from math import floor 2 | from ...vec2 import Vec2, normalize 3 | from ...stroke import Stroke 4 | from ..font import Font 5 | from . serif_stroke import SerifStroke 6 | 7 | import svgwrite 8 | import numpy as np 9 | from argparse import Namespace 10 | 11 | class Serif(Font): 12 | def __init__(self, size = 2) -> None: 13 | self.kRate = 100 14 | if size == 1: 15 | self.kMinWidthY = 1.2 16 | self.kMinWidthU = 2 # 17 | self.kMinWidthT = 3.6 18 | self.kWidth = 3 19 | self.kKakato = 1.8 20 | self.kL2RDfatten = 1.1 21 | self.kMage = 6 22 | self.kUseCurve = False 23 | self.kAdjustKakatoL = [8, 5, 3, 1, 0] 24 | self.kAdjustKakatoR = [4, 3, 2, 1] 25 | self.kAdjustKakatoRangeX = 12 26 | self.kAdjustKakatoRangeY = [1, 11, 14, 18] 27 | self.kAdjustKakatoStep = 3 28 | self.kAdjustUrokoX = [14, 12, 9, 7] 29 | self.kAdjustUrokoY = [7, 6, 5, 4] 30 | self.kAdjustUrokoLength = [13, 21, 30] 31 | self.kAdjustUrokoLengthStep = 3 32 | self.kAdjustUrokoLine = [13, 15, 18] 33 | self.kAdjustUroko2Step = 3 # 34 | self.kAdjustUroko2Length = 40 # 35 | self.kAdjustTateStep = 4 # 36 | self.kAdjustMageStep = 5 # 37 | else: 38 | self.kMinWidthY = 2 39 | self.kMinWidthU = 2 # 40 | self.kMinWidthT = 6 41 | self.kWidth = 5 42 | self.kKakato = 3 43 | self.kL2RDfatten = 1.1 44 | self.kMage = 10 45 | self.kUseCurve = False 46 | self.kAdjustKakatoL = [14, 9, 5, 2, 0] 47 | self.kAdjustKakatoR = [8, 6, 4, 2] 48 | self.kAdjustKakatoRangeX = 20 49 | self.kAdjustKakatoRangeY = [1, 19, 24, 30] 50 | self.kAdjustKakatoStep = 3 51 | self.kAdjustUrokoX = [24, 20, 16, 12] 52 | self.kAdjustUrokoY = [12, 11, 9, 8] 53 | self.kAdjustUrokoLength = [22, 36, 50] 54 | self.kAdjustUrokoLengthStep = 3 55 | self.kAdjustUrokoLine = [22, 26, 30] 56 | self.kAdjustUroko2Step = 3 # 57 | self.kAdjustUroko2Length = 40 # 58 | self.kAdjustTateStep = 4 # 59 | self.kAdjustMageStep = 5 # 60 | 61 | def drawer(self, canvas: svgwrite.Drawing, strokes_list: list[Stroke]): 62 | self.serif_strokes = [SerifStroke(i) for i in strokes_list] 63 | self.adjust_stroke() 64 | self.draw_strokes(canvas) 65 | return canvas 66 | 67 | def adjust_stroke(self): 68 | self.adjust_hane() # ハネ 69 | self.adjust_mage() # 折れのカーブ, 曲げ 70 | self.adjust_tate() # 縦画 71 | self.adjust_kakato() # カカト 72 | self.adjust_uroko() # ウロコ, horizontal strokes' terminal (triangle) 73 | self.adjust_uroko2() # ウロコ 74 | self.adjust_kirikuchi() # 払い, 75 | 76 | def adjust_hane(self): 77 | vert_segments = [ 78 | Namespace( 79 | **{ 80 | 'stroke': stroke.stroke, 81 | 'x': stroke.stroke.vec_1.x, 82 | 'y1': stroke.stroke.vec_1.y, 83 | 'y2': stroke.stroke.vec_2.y, 84 | } 85 | ) 86 | for stroke in self.serif_strokes 87 | if stroke.stroke.a1_100 == 1 and stroke.stroke.a1_opt == 0 and stroke.stroke.vec_1.x == stroke.stroke.vec_2.x 88 | ] 89 | 90 | for serif_stroke in self.serif_strokes: 91 | stroke = serif_stroke.stroke 92 | if (stroke.a1_100 == 1 or stroke.a1_100 == 2 or stroke.a1_100 == 6) and stroke.a1_opt == 0 and stroke.a3_100 == 4 and stroke.a3_opt == 0: 93 | lp = Vec2(np.nan, np.nan) 94 | if stroke.a1_100 == 1: 95 | lp = stroke.vec_2 96 | elif stroke.a1_100 == 2: 97 | lp = stroke.vec_3 98 | else: 99 | lp = stroke.vec_4 100 | mn = np.inf 101 | if lp.x + 18 < 100: 102 | mn = lp.x + 18 103 | for c in vert_segments: 104 | x = c.x 105 | y1 = c.y1 106 | y2 = c.y2 107 | if (stroke != c.stroke 108 | and lp.x - x < 100 and x < lp.x 109 | and y1 <= lp.y and y2 >= lp.y): 110 | mn = np.min([mn, lp.x - x]) 111 | if not np.isinf(mn): 112 | serif_stroke.hane_adjustment += 7 - np.floor(mn / 15) 113 | 114 | def adjust_mage(self): 115 | hori_segments = [] 116 | for serif_stroke in self.serif_strokes: 117 | stroke = serif_stroke.stroke 118 | if stroke.a1_100 == 1 and stroke.a1_opt == 0 and stroke.vec_1.y == stroke.vec_2.y: 119 | hori_segments.append( 120 | Namespace(**{ 121 | 'stroke': stroke, 122 | 'serif_stroke': serif_stroke, 123 | 'is_target': False, 124 | 'y': stroke.vec_2.y, 125 | 'x1': stroke.vec_1.x, 126 | 'x2': stroke.vec_2.x, 127 | }) 128 | ) 129 | elif stroke.a1_100 == 3 and stroke.a1_opt == 0 and stroke.vec_2.y == stroke.vec_3.y: 130 | hori_segments.append( 131 | Namespace(**{ 132 | 'stroke': stroke, 133 | 'serif_stroke': serif_stroke, 134 | 'is_target': True, 135 | 'y': stroke.vec_2.y, 136 | 'x1': stroke.vec_2.x, 137 | 'x2': stroke.vec_3.x, 138 | }) 139 | ) 140 | 141 | for hori_segment in hori_segments: 142 | stroke = hori_segment.stroke 143 | serif_stroke = hori_segment.serif_stroke 144 | is_target = hori_segment.is_target 145 | y = hori_segment.y 146 | x1 = hori_segment.x1 147 | x2 = hori_segment.x2 148 | 149 | if is_target: 150 | for another_hori_segment in hori_segments: 151 | stroke2 = another_hori_segment.stroke 152 | other_y = another_hori_segment.y 153 | other_x1 = another_hori_segment.x1 154 | other_x2 = another_hori_segment.x2 155 | if stroke != stroke2 and not (x1 + 1 > other_x2 or x2 - 1 < other_x1) \ 156 | and np.round(np.abs(y - other_y), 5) < self.kMinWidthT * self.kAdjustMageStep: 157 | serif_stroke.mage_adjustment += self.kAdjustMageStep - np.floor(np.abs(y- other_y) / self.kMinWidthT) 158 | if serif_stroke.mage_adjustment > self.kAdjustMageStep: 159 | serif_stroke.mage_adjustment = self.kAdjustMageStep 160 | 161 | def adjust_tate(self): 162 | vert_segments = [ 163 | Namespace( 164 | **{ 165 | 'stroke': stroke.stroke, 166 | 'serif_stroke': stroke, 167 | 'x': stroke.stroke.vec_1.x, 168 | 'y1': stroke.stroke.vec_1.y, 169 | 'y2': stroke.stroke.vec_2.y, 170 | } 171 | ) 172 | for stroke in self.serif_strokes 173 | if (stroke.stroke.a1_100 == 1 or stroke.stroke.a1_100 == 3 or stroke.stroke.a1_100 == 7) and stroke.stroke.a1_opt == 0 and stroke.stroke.vec_1.x == stroke.stroke.vec_2.x 174 | ] 175 | 176 | for vert_segment in vert_segments: 177 | serif_stroke = vert_segment.serif_stroke 178 | stroke = vert_segment.stroke 179 | x = vert_segment.x 180 | y1 = vert_segment.y1 181 | y2 = vert_segment.y2 182 | for another_vert_segment in vert_segments: 183 | stroke2 = another_vert_segment.stroke 184 | other_x = another_vert_segment.x 185 | other_y1 = another_vert_segment.y1 186 | other_y2 = another_vert_segment.y2 187 | if stroke != stroke2 and not (y1 + 1 > other_y2 or y2 - 1 < other_y1) \ 188 | and np.round(np.abs(x - other_x), 5) < self.kMinWidthT * self.kAdjustTateStep: 189 | serif_stroke.tate_adjustment += self.kAdjustTateStep - np.floor(np.abs(x - other_x) / self.kMinWidthT) 190 | if serif_stroke.tate_adjustment > self.kAdjustTateStep or serif_stroke.tate_adjustment == self.kAdjustTateStep and (stroke.a2_opt_1 != 0 or stroke.a2_100 != 0): 191 | serif_stroke.tate_adjustment = self.kAdjustTateStep 192 | 193 | def adjust_kakato(self): 194 | def loop1(serif_stroke: SerifStroke): 195 | stroke = serif_stroke.stroke 196 | if stroke.a1_100 == 1 and stroke.a1_opt == 0 \ 197 | and (stroke.a3_100 == 13 or stroke.a3_100 == 23) and stroke.a3_opt == 0: 198 | def loop2(k: int): 199 | if any([ 200 | stroke != serif_stroke.stroke and serif_stroke.stroke.is_cross_box(Vec2(stroke.vec_2.x - self.kAdjustKakatoRangeX / 2, stroke.vec_2.y + self.kAdjustKakatoRangeY[k]), Vec2(stroke.vec_2.x + self.kAdjustKakatoRangeX / 2, stroke.vec_2.y + self.kAdjustKakatoRangeY[k + 1])) 201 | for serif_stroke in self.serif_strokes 202 | ])\ 203 | or np.round(stroke.vec_2.y + self.kAdjustKakatoRangeY[k + 1], 5) > 200 \ 204 | or np.round(stroke.vec_2.y - stroke.vec_1.y) < self.kAdjustKakatoRangeY[k + 1]: # for thin box 205 | serif_stroke.kakato_adjustment = 3 - k 206 | return 'break' 207 | 208 | for k_ in range(self.kAdjustKakatoStep): 209 | state = loop2(k_) 210 | if state == 'break': 211 | break 212 | 213 | for serif_stroke in self.serif_strokes: 214 | loop1(serif_stroke) 215 | 216 | def adjust_uroko(self): 217 | def loop3(serif_stroke: SerifStroke): 218 | stroke = serif_stroke.stroke 219 | if stroke.a1_100 == 1 and stroke.a1_opt == 0 and stroke.a3_100 == 0 and stroke.a3_opt == 0: # no operation for TATE 220 | def loop4(k: int): 221 | a = Vec2(1,0) \ 222 | if stroke.vec_1.y == stroke.vec_2.y \ 223 | else \ 224 | normalize(Vec2(stroke.vec_1.x - stroke.vec_2.x, stroke.vec_1.y - stroke.vec_2.y)) \ 225 | if stroke.vec_2.x - stroke.vec_1.x < 0 \ 226 | else \ 227 | normalize(Vec2(stroke.vec_2.x - stroke.vec_1.x, stroke.vec_2.y - stroke.vec_1.y)) 228 | cosrad = a[0] 229 | sinrad = a[1] 230 | tx = stroke.vec_2.x - self.kAdjustUrokoLine[k] * cosrad - 0.5 * sinrad # typo? (sinrad should be -sinrad ?) 231 | ty = stroke.vec_2.y - self.kAdjustUrokoLine[k] * sinrad - 0.5 * cosrad 232 | tlen = stroke.vec_2.x - stroke.vec_1.x if (stroke.vec_1.y == stroke.vec_2.y) else np.hypot(stroke.vec_2.y - stroke.vec_1.y, stroke.vec_2.x - stroke.vec_1.x) 233 | if np.round(tlen, 5) < self.kAdjustUrokoLength[k] or any([ 234 | stroke != serif_stroke.stroke and serif_stroke.stroke.is_cross(Vec2(tx, ty), stroke.vec_2) 235 | for serif_stroke in self.serif_strokes 236 | ]): 237 | serif_stroke.uroko_adjustment = self.kAdjustUrokoLengthStep - k 238 | return 'break' 239 | for k_ in range(self.kAdjustUrokoLengthStep): 240 | state = loop4(k_) 241 | if state == 'break': 242 | break 243 | 244 | for serif_stroke in self.serif_strokes: 245 | loop3(serif_stroke) 246 | 247 | def adjust_uroko2(self): 248 | hori_segments = [] 249 | for serif_stroke in self.serif_strokes: 250 | stroke = serif_stroke.stroke 251 | if stroke.a1_100 == 1 and stroke.a1_opt == 0 and stroke.vec_1.y == stroke.vec_2.y: 252 | hori_segments.append( 253 | Namespace(**{ 254 | 'stroke': stroke, 255 | 'serif_stroke': serif_stroke, 256 | 'is_target': stroke.a3_100 == 0 and stroke.a3_opt == 0 and serif_stroke.uroko_adjustment == 0, 257 | 'y': stroke.vec_1.y, 258 | 'x1': stroke.vec_1.x, 259 | 'x2': stroke.vec_2.x, 260 | }) 261 | ) 262 | elif stroke.a1_100 == 3 and stroke.a1_100 == 0 and stroke.vec_2.y == stroke.vec_3.y: 263 | hori_segments.append( 264 | Namespace(**{ 265 | 'stroke': stroke, 266 | 'serif_stroke': serif_stroke, 267 | 'is_target': False, 268 | 'y': stroke.vec_2.y, 269 | 'x1': stroke.vec_2.x, 270 | 'x2': stroke.vec_3.x, 271 | }) 272 | ) 273 | 274 | for hori_segment in hori_segments: 275 | serif_stroke = hori_segment.serif_stroke 276 | stroke = hori_segment.stroke 277 | is_target = hori_segment.is_target 278 | y = hori_segment.y 279 | x1 = hori_segment.x1 280 | x2 = hori_segment.x2 281 | if is_target: 282 | pressure = 0 283 | for another_hori_segment in hori_segments: 284 | stroke2 = another_hori_segment.stroke 285 | other_y = another_hori_segment.y 286 | other_x1 = another_hori_segment.x1 287 | other_x2 = another_hori_segment.x2 288 | if stroke != stroke2 and not (x1 + 1 > other_x2 or x2 - 1 < other_x1) \ 289 | and np.round(np.abs(y - other_y)) < self.kAdjustUroko2Length: 290 | pressure += np.power((self.kAdjustUroko2Length - np.abs(y - other_y)), 1.1) 291 | serif_stroke.uroko_adjustment = int(np.min([np.floor(pressure / self.kAdjustUroko2Length), self.kAdjustUroko2Step])) 292 | 293 | def adjust_kirikuchi(self): 294 | hori_segments = [] 295 | for serif_stroke in self.serif_strokes: 296 | stroke = serif_stroke.stroke 297 | if stroke.a1_100 == 1 and stroke.a1_opt == 0 and stroke.vec_1.y == stroke.vec_2.y: 298 | hori_segments.append( 299 | Namespace(**{ 300 | 'y': stroke.vec_1.y, 301 | 'x1': stroke.vec_1.x, 302 | 'x2': stroke.vec_2.x, 303 | }) 304 | ) 305 | 306 | def loop5(serif_stroke: SerifStroke): 307 | stroke = serif_stroke.stroke 308 | if stroke.a1_100 == 2 and stroke.a1_opt == 0 \ 309 | and stroke.a2_100 == 32 and stroke.a2_opt == 0 \ 310 | and stroke.vec_1.x > stroke.vec_2.x and stroke.vec_1.y < stroke.vec_2.y and any([ 311 | hori_segment.x1 < stroke.vec_1.x and hori_segment.x2 > stroke.vec_1.x and hori_segment.y == stroke.vec_1.y 312 | for hori_segment in hori_segments 313 | ]): 314 | serif_stroke.kirikuchi_adjustment = 1 315 | 316 | for serif_stroke in self.serif_strokes: 317 | loop5(serif_stroke) 318 | 319 | def draw_strokes(self, canvas: svgwrite.Drawing): 320 | from . serif_stroke_drawer import LegacySerifStrokeDrawer as SerifStrokeDrawer 321 | stroke_drawer = SerifStrokeDrawer(self, canvas) 322 | for serif_stroke in self.serif_strokes: 323 | stroke = serif_stroke.stroke 324 | if stroke.a1_100 == 0: # TODO: Transforms 325 | pass 326 | elif stroke.a1_100 == 1: 327 | if stroke.a3_100 == 4: 328 | m = Vec2(0, self.kMage) if all(stroke.vec_1 == stroke.vec_2) else normalize(stroke.vec_1 - stroke.vec_2, self.kMage) 329 | t1 = stroke.vec_2 + m 330 | stroke_drawer.draw_line(stroke.vec_1, t1, stroke.a2_100 + stroke.a2_opt * 100, 1, serif_stroke.tate_adjustment, 0, 0) 331 | stroke_drawer.draw_curve(t1, stroke.vec_2, 332 | Vec2(stroke.vec_2.x - self.kMage * (((self.kAdjustTateStep + 4) - serif_stroke.tate_adjustment) / (self.kAdjustTateStep + 4)), stroke.vec_2.y), 333 | 1, 14, 334 | serif_stroke.tate_adjustment % 10, 335 | serif_stroke.hane_adjustment, 336 | np.floor(serif_stroke.tate_adjustment / 10), 337 | stroke.a3_opt_2 338 | ) 339 | else: 340 | stroke_drawer.draw_line(stroke.vec_1, stroke.vec_2, stroke.a2_100 + stroke.a2_opt * 100, stroke.a3_100, serif_stroke.tate_adjustment, serif_stroke.uroko_adjustment, serif_stroke.kakato_adjustment) 341 | elif stroke.a1_100 == 2: 342 | if stroke.a3_100 == 4: 343 | vec_d = Vec2(0, -self.kMage) if stroke.vec_2.x == stroke.vec_3.x else Vec2(-self.kMage, 0) if stroke.vec_2.y == stroke.vec_3.y else normalize(stroke.vec_2 - stroke.vec_3, self.kMage) 344 | vec_t1 = stroke.vec_3 + vec_d 345 | stroke_drawer.draw_curve(stroke.vec_1, stroke.vec_2, vec_t1, stroke.a2_100 + serif_stroke.kirikuchi_adjustment * 100, 0, stroke.a2_opt_2, 0, stroke.a2_opt_3, 0) 346 | stroke_drawer.draw_curve(vec_t1, stroke.vec_3, stroke.vec_3 - Vec2(self.kMage, 0), 2, 14, stroke.a2_opt_2, serif_stroke.hane_adjustment, 0, stroke.a3_opt_2) 347 | else: 348 | stroke_drawer.draw_curve(stroke.vec_1, stroke.vec_2, stroke.vec_3, stroke.a2_100 + serif_stroke.kirikuchi_adjustment * 100, 15 if (stroke.a3_100 == 5 and stroke.a3_opt == 0) else stroke.a3_100, 349 | stroke.a2_opt_2, stroke.a3_opt_1, stroke.a2_opt_3, stroke.a3_opt_2) 350 | elif stroke.a1_100 == 3: 351 | vec_d1 = Vec2(0, self.kMage) if all(stroke.vec_1 == stroke.vec_2) else normalize(stroke.vec_1 - stroke.vec_2, self.kMage) 352 | vec_d2 = Vec2(0, -self.kMage) if all(stroke.vec_2 == stroke.vec_3) else normalize(stroke.vec_3 - stroke.vec_2, self.kMage) 353 | vec_t1 = stroke.vec_2 + vec_d1 354 | vec_t2 = stroke.vec_2 + vec_d2 355 | stroke_drawer.draw_line(stroke.vec_1, vec_t1, stroke.a2_100 + stroke.a2_opt * 100, 1, serif_stroke.tate_adjustment, 0, 0) 356 | stroke_drawer.draw_curve(vec_t1, stroke.vec_2, vec_t2, 1, 1, 0, 0, serif_stroke.tate_adjustment, serif_stroke.mage_adjustment) 357 | 358 | if (not(stroke.a3_100 == 5 and stroke.a3_opt_1 == 0 and not ((stroke.vec_2.x < stroke.vec_3.x and stroke.vec_3.x - vec_t2.x > 0) or (stroke.vec_2.x > stroke.vec_3.x and vec_t2.x - stroke.vec_3.x > 0)))): 359 | opt2 = 0 if (stroke.a3_100 == 5 and stroke.a3_opt_1 == 0) else stroke.a3_opt_1 + serif_stroke.mage_adjustment * 10 360 | stroke_drawer.draw_line(vec_t2, stroke.vec_3, 6, stroke.a3_100, serif_stroke.mage_adjustment, opt2, opt2) 361 | elif stroke.a1_100 == 12: 362 | stroke_drawer.draw_curve(stroke.vec_1, stroke.vec_2, stroke.vec_3, 363 | stroke.a2_100 + stroke.a2_opt_1 * 100, 1, stroke.a2_opt_2, 0, stroke.a2_opt_3, 0) 364 | stroke_drawer.draw_line(stroke.vec_3, stroke.vec_4, 6, stroke.a3_100, 0, stroke.a3_opt, stroke.a3_opt) 365 | elif stroke.a1_100 == 4: 366 | rate = np.hypot(*(stroke.vec_3 - stroke.vec_2)) / 120 * 6 367 | if (rate > 6): 368 | rate = 6 369 | vec_d1 = Vec2(0, self.kMage * rate) if all(stroke.vec_1 == stroke.vec_2) else normalize(stroke.vec_1 - stroke.vec_2, self.kMage * rate) 370 | vec_t1 = stroke.vec_2 + vec_d1 371 | vec_d2 = Vec2(0, -self.kMage * rate) if all(stroke.vec_2 == stroke.vec_3) else normalize(stroke.vec_3 - stroke.vec_2, self.kMage * rate) 372 | vec_t2 = stroke.vec_2 + vec_d2 373 | 374 | stroke_drawer.draw_line(stroke.vec_1, vec_t1, stroke.a2_100 + stroke.a2_opt * 100, 1, stroke.a2_opt_2 + stroke.a2_opt_3 * 10, 0, 0) 375 | stroke_drawer.draw_curve(vec_t1, stroke.vec_2, vec_t2, 1, 1, 0, 0, 0, 0) 376 | 377 | if (not(stroke.a3_100 == 5 and stroke.a3_opt == 0 and stroke.vec_3.x - vec_t2.x <= 0)): 378 | stroke_drawer.draw_line(vec_t2, stroke.vec_3, 6, stroke.a3_100, 0, stroke.a3_opt, stroke.a3_opt) 379 | elif stroke.a1_100 == 6: 380 | if stroke.a3_100 == 4: 381 | vec_d = Vec2(0, -self.kMage) if stroke.vec_3.x == stroke.vec_4.x else Vec2(-self.kMage, 0) if stroke.vec_3.y == stroke.vec_4.y else normalize(stroke.vec_3 - stroke.vec_4, self.kMage) 382 | vec_t1 = stroke.vec_4 + vec_d 383 | stroke_drawer.draw_bezier(stroke.vec_1, stroke.vec_2, stroke.vec_3, vec_t1, stroke.a2_100 + stroke.a2_opt * 100, 0, stroke.a2_opt_2, 0, stroke.a2_opt_3, 0) 384 | stroke_drawer.draw_curve(vec_t1, stroke.vec_4, stroke.vec_4 - Vec2(self.kMage, 0), 1, 14, 0, serif_stroke.hane_adjustment, 0, stroke.a3_opt_2) 385 | else: 386 | stroke_drawer.draw_bezier(stroke.vec_1, stroke.vec_2, stroke.vec_3, stroke.vec_4, stroke.a2_100 + stroke.a2_opt * 100, 15 if stroke.a3_100 == 5 and stroke.a3_opt == 0 else stroke.a3_100, stroke.a2_opt_2, stroke.a3_opt_1, stroke.a2_opt_3, stroke.a3_opt_2) 387 | elif stroke.a1_100 == 7: 388 | stroke_drawer.draw_line(stroke.vec_1, stroke.vec_2, stroke.a2_100 + stroke.a2_opt * 100, 1, serif_stroke.tate_adjustment, 0, 0) 389 | stroke_drawer.draw_curve(stroke.vec_2, stroke.vec_3, stroke.vec_4, 1, stroke.a3_100, serif_stroke.tate_adjustment % 10, stroke.a3_opt_1, np.floor(serif_stroke.tate_adjustment / 10), stroke.a3_opt_2) 390 | elif stroke.a1_100 == 9: 391 | # may not be exist ... no need 392 | pass -------------------------------------------------------------------------------- /kage/font/serif/serif_stroke.py: -------------------------------------------------------------------------------- 1 | from ...stroke import Stroke 2 | 3 | class SerifStroke: 4 | def __init__(self, stroke: Stroke) -> None: 5 | self.stroke = stroke 6 | self.kirikuchi_adjustment = self.stroke.a2_opt_1 7 | self.tate_adjustment = self.stroke.a2_opt_2 + self.stroke.a2_opt_3 * 10 8 | self.hane_adjustment = self.stroke.a3_opt_1 9 | self.uroko_adjustment = int(self.stroke.a3_opt) 10 | self.kakato_adjustment = int(self.stroke.a3_opt) 11 | self.mage_adjustment = self.stroke.a3_opt_2 12 | 13 | def __repr__(self) -> str: 14 | return '[' + repr(self.stroke) + ',' + ','.join([str(int(i)) for i in [self.kirikuchi_adjustment, self.tate_adjustment, self.hane_adjustment, self.uroko_adjustment, self.kakato_adjustment, self.mage_adjustment]]) + ']\n' 15 | -------------------------------------------------------------------------------- /kage/font/serif/serif_stroke_drawer.py: -------------------------------------------------------------------------------- 1 | from ...vec2 import Vec2, normalize 2 | from ...util import generate_flatten_curve 3 | from ..serif import Serif 4 | import svgwrite 5 | import svgwrite.path 6 | import numpy as np 7 | 8 | class LegacySerifStrokeDrawer: 9 | def __init__(self, font: Serif, canvas: svgwrite.Drawing) -> None: 10 | self.font = font 11 | self.canvas = canvas 12 | 13 | def __draw_curve_universal(self, vec_1: Vec2, vec_s1: Vec2, vec_s2: Vec2, vec_2: Vec2, a1: int, a2: int, opt1, hane_adjustment, opt3, opt4) -> None: 14 | kMinWidthT = self.font.kMinWidthT - opt1 / 2 15 | 16 | if (temp := a1 % 100) in [0,7,27]: 17 | delta1 = -1 * self.font.kMinWidthY * 0.5 18 | elif temp in [1, 2, 6, 22, 32]: 19 | delta1 = 0 20 | elif temp == 12: 21 | delta1 = self.font.kMinWidthY 22 | else: 23 | return 24 | 25 | if delta1 != 0: 26 | vec_d = Vec2(0, delta1) if all(vec_1 == vec_s1) else normalize(vec_1 - vec_s1, delta1) 27 | vec_1 += vec_d 28 | 29 | corner_offset = 0 30 | if ((a1 == 22 or a1 == 27) and a2 == 7 and kMinWidthT > 6): 31 | contourLength = np.hypot(*(vec_s1 - vec_1)) + np.hypot(*(vec_s2 - vec_1)) + np.hypot(*(vec_2 - vec_s2)) 32 | if (contourLength < 100): 33 | corner_offset = (kMinWidthT - 6) * ((100 - contourLength) / 100) 34 | vec_1.x += corner_offset 35 | 36 | if (temp := a2 % 100) in [0,1,7,9,15,14,17,5]: 37 | delta2 = 0 38 | elif temp == 8: 39 | delta2 = -1 * kMinWidthT * 0.5 40 | else: 41 | delta2 = delta1 42 | 43 | if delta2 != 0: 44 | vec_d = Vec2(0, -delta2) if all(vec_2 == vec_s2) else normalize(vec_2 - vec_s2, delta2) 45 | vec_2 += vec_d 46 | 47 | self.__draw_curve_body(vec_1, vec_s1, vec_s2, vec_2, a1, a2, kMinWidthT, opt3, opt4) 48 | self.__draw_curve_head(vec_1, vec_s1, a1, kMinWidthT, vec_1.y <= vec_2.y, corner_offset) # XXX: should check NaN or inf? 49 | self.__draw_curve_tail(vec_s2, vec_2, a1, a2, kMinWidthT, hane_adjustment, opt4, vec_2.y <= vec_1.y) 50 | 51 | def __draw_curve_body(self, vec_1: Vec2, vec_s1: Vec2, vec_s2: Vec2, vec_2: Vec2, a1, a2, kMinWidthT, opt3, opt4) -> None: 52 | is_quadratic = all(vec_s1 == vec_s2) 53 | 54 | hosomi = 0.5 55 | 56 | if np.hypot(*(vec_2 - vec_1)) < 50: 57 | hosomi += 0.4 * (1 - np.hypot(*(vec_2 - vec_1)) / 50) 58 | 59 | def delta_d(t: float) -> float: 60 | if (a1 == 7 or a1 == 27) and a2 == 0: # L2RD: fatten 61 | return t ** hosomi * self.font.kL2RDfatten 62 | if a1 == 7 or a1 == 27: 63 | if is_quadratic: 64 | return t ** hosomi 65 | else: 66 | return (t ** hosomi) ** 0.7 # make fatten 67 | if a2 == 7: 68 | return (1 - t) ** hosomi 69 | if is_quadratic and (opt3 > 0 or opt4 > 0): 70 | return ((self.font.kMinWidthT - opt3 / 2) - (opt4 - opt3) / 2 * t) / self.font.kMinWidthT 71 | else: 72 | return 1 73 | 74 | left, right = generate_flatten_curve(vec_1, vec_s1, vec_s2, vec_2, self.font.kRate, lambda t: ((temp if (temp := delta_d(t)) > 0.15 else 0.15) * kMinWidthT)) 75 | 76 | # horizontal joint, 水平線に接続 77 | if a1 == 132 or a1 == 22 and (vec_1.y > vec_2.y) if is_quadratic else (vec_1.x > vec_s1.x): 78 | for index in range(len(right) - 1): 79 | point1 = right[index] 80 | point2 = right[index + 1] 81 | if (point1.y <= vec_1.y and vec_1.y <= point2.y): 82 | new1 = Vec2( 83 | point2.x + (point1.x - point2.x) * (vec_1.y - point2.y) / (point1.y - point2.y) , 84 | vec_1.y 85 | ) 86 | point3 = left[0] 87 | point4 = left[1] 88 | new2 = Vec2( 89 | point3.x + (point4.x - point3.x) * (vec_1.y - point3.y) / (point4.y - point3.y) \ 90 | if a1 == 132 else \ 91 | point3.x + (point4.x - point3.x + 1) * (vec_1.y - point3.y) / (point4.y - point3.y), 92 | vec_1.y \ 93 | if a1 == 132 else \ 94 | vec_1.y + 1 95 | ) 96 | for i in range(index): 97 | if len(right) > 0: 98 | right = right[1:] 99 | right[0] = new1 100 | left.insert(0, new2) 101 | break 102 | 103 | right.reverse() 104 | dots = left + right 105 | dots = [str(dot) for dot in dots] 106 | # draw 107 | path = svgwrite.path.Path(d = "M" + (" L".join(dots)), stroke = 'black', stroke_width = 0, fill = 'black') 108 | self.canvas.add(path) 109 | 110 | def __draw_curve_head(self, vec_1: Vec2, vec_s1: Vec2, a1: int, kMinWidthT, is_up_to_bottom: bool, corner_offset) -> None: 111 | """ 112 | process for head of stroke 113 | """ 114 | if a1 == 12: 115 | degree = np.arctan2(vec_1.x - vec_s1.x, vec_s1.y - vec_1.y) / (np.pi * 2) * 360 116 | polygon = [ 117 | Vec2(-kMinWidthT, 0), 118 | Vec2(+kMinWidthT, 0), 119 | Vec2(-kMinWidthT, -kMinWidthT) 120 | ] 121 | # draw 122 | polygon = [str(i) for i in polygon] 123 | path = svgwrite.path.Path(d = "M" + (" L".join(polygon)), stroke = 'black', stroke_width = 0, fill = 'black', 124 | transform = f'translate({vec_1}) rotate({degree},0,0)') 125 | self.canvas.add(path) 126 | elif a1 == 0: 127 | if is_up_to_bottom: 128 | # from up to bottom 129 | degree = np.arctan2(vec_1.x - vec_s1.x, vec_s1.y - vec_1.y) / (np.pi * 2) * 360 130 | head_type = np.arctan2(np.abs(vec_1.y - vec_s1.y), np.abs(vec_1.x - vec_s1.x)) / np.pi * 2 - 0.4 131 | head_type *= 2 if head_type > 0 else 16 132 | pm = -1 if head_type < 0 else 1 133 | polygon = [ 134 | Vec2(-kMinWidthT, 1), 135 | Vec2(+kMinWidthT, 0), 136 | Vec2(-pm * kMinWidthT, -self.font.kMinWidthY * np.abs(head_type)) 137 | ] 138 | # draw 139 | polygon = [str(i) for i in polygon] 140 | path = svgwrite.path.Path(d = "M" + (" L".join(polygon)), stroke = 'black', stroke_width = 0, fill = 'black', 141 | transform = f'translate({vec_1}) rotate({degree},0,0)') 142 | self.canvas.add(path) 143 | 144 | # beginning of the stroke 145 | move = - head_type * self.font.kMinWidthY if head_type < 0 else 0 146 | polygon2 = [vec_1] + [ 147 | Vec2(kMinWidthT, -move), 148 | Vec2(kMinWidthT * 1.5, self.font.kMinWidthY - move), 149 | Vec2(kMinWidthT - 2, self.font.kMinWidthY * 2 + 1), 150 | ] \ 151 | if all(vec_1 == vec_s1) else [ 152 | Vec2(kMinWidthT, -move), 153 | Vec2(kMinWidthT * 1.5, self.font.kMinWidthY - move * 1.2), 154 | Vec2(kMinWidthT - 2, self.font.kMinWidthY * 2 - move * 0.8 + 1), 155 | ] 156 | # draw 157 | polygon2 = [str(i) for i in polygon2] 158 | path = svgwrite.path.Path(d = "M" + (" L".join(polygon2)), stroke = 'black', stroke_width = 0, fill = 'black', 159 | transform = f'translate({vec_1}) rotate({degree},0,0)') 160 | self.canvas.add(path) 161 | else: 162 | # bottom to up 163 | degree = np.arctan2(vec_s1.y - vec_1.y, vec_s1.x - vec_1.x) / (np.pi * 2) * 360 164 | polygon = [ 165 | Vec2(0, +kMinWidthT), 166 | Vec2(0, -kMinWidthT), 167 | Vec2(-self.font.kMinWidthY, -kMinWidthT) 168 | ] 169 | # draw 170 | polygon = [str(i) for i in polygon] 171 | path = svgwrite.path.Path(d = "M" + (" L".join(polygon)), stroke = 'black', stroke_width = 0, fill = 'black', 172 | transform = f'translate({vec_1}) rotate({degree},0,0)') 173 | self.canvas.add(path) 174 | 175 | polygon2 = [ 176 | Vec2(0, +kMinWidthT), 177 | Vec2(+self.font.kMinWidthY, +kMinWidthT * 1.5), 178 | Vec2(+self.font.kMinWidthY * 3, +kMinWidthT * 0.5) 179 | ] 180 | # draw 181 | polygon2 = [str(i) for i in polygon2] 182 | path = svgwrite.path.Path(d = "M" + (" L".join(polygon2)), stroke = 'black', stroke_width = 0, fill = 'black', 183 | transform = f'translate({vec_1}) rotate({degree},0,0)') 184 | self.canvas.add(path) 185 | 186 | elif a1 in [22, 27]: # box's right top corner 187 | # 四角右上鱗斜めでもまっすぐ向き 188 | # 箱形右上三角形装饰 189 | polygon = [ 190 | Vec2(-kMinWidthT, -self.font.kMinWidthY), 191 | Vec2(0, -self.font.kMinWidthY - self.font.kWidth), 192 | Vec2(+kMinWidthT + self.font.kWidth, +self.font.kMinWidthY), 193 | Vec2(+kMinWidthT, +kMinWidthT - 1) 194 | ] 195 | 196 | polygon += [ 197 | Vec2(0, +kMinWidthT + 2), 198 | Vec2(0, 0) 199 | ] if a1 == 27 else [ 200 | Vec2(-kMinWidthT, +kMinWidthT + 4) 201 | ] 202 | # draw 203 | polygon = [str(i) for i in polygon] 204 | path = svgwrite.path.Path(d = "M" + (" L".join(polygon)), stroke = 'black', stroke_width = 0, fill = 'black', 205 | transform = f'translate({vec_1 - Vec2(corner_offset, 0)})') 206 | self.canvas.add(path) 207 | 208 | 209 | def __draw_curve_tail(self, vec_s2: Vec2, vec_2: Vec2, a1: int, a2: int, kMinWidthT, hane_adjustment, opt4, is_bottom_to_up): 210 | """ 211 | process for tail of stroke 212 | """ 213 | if a2 in [1,8,15]: 214 | degree = np.arctan2(vec_2.y - vec_s2.y, vec_2.x - vec_s2.x) / (np.pi * 2) * 360 215 | kMinWidthT2 = self.font.kMinWidthT - opt4 / 2 216 | path = [ 217 | "M", 218 | Vec2(0, -kMinWidthT2), 219 | "Q", 220 | Vec2(+kMinWidthT2 * 0.9, -kMinWidthT2 * 0.9), 221 | Vec2(+kMinWidthT2, 0), 222 | "Q", 223 | Vec2(+kMinWidthT2 * 0.9, +kMinWidthT2 * 0.9), 224 | Vec2(0, +kMinWidthT2) 225 | ] 226 | path = [str(i) for i in path] 227 | path = svgwrite.path.Path(d = " ".join(path), stroke = 'black', stroke_width = 0, fill = 'black', 228 | transform = f'translate({vec_2}) rotate({degree},0,0)') 229 | self.canvas.add(path) 230 | 231 | if a2 == 15: 232 | degree = 0 233 | if is_bottom_to_up: 234 | degree = 180 235 | polygon = [ 236 | Vec2(0, -kMinWidthT + 1), 237 | Vec2(+2, -kMinWidthT - self.font.kWidth * 5 ), 238 | Vec2(0, -kMinWidthT - self.font.kWidth * 5), 239 | Vec2(-kMinWidthT, -kMinWidthT + 1), 240 | ] 241 | polygon = [str(i) for i in polygon] 242 | path = svgwrite.path.Path(d = "M" + (" L".join(polygon)), stroke = 'black', stroke_width = 0, fill = 'black', 243 | transform = f'translate({vec_2}) rotate({degree},0,0)') 244 | self.canvas.add(path) 245 | 246 | elif a2 in [0,9]: 247 | if a2 == 0 and not (a1 == 7 or a1 == 27): 248 | return 249 | degree = np.arctan2(vec_2.y - vec_s2.y, vec_2.x - vec_s2.x) / (np.pi * 2) * 360 250 | tail_type = np.arctan2(np.abs(vec_2.y - vec_s2.y), np.abs(vec_2.x - vec_s2.x)) / np.pi * 2 - 0.6 251 | tail_type *= 8 if tail_type > 0 else 3 252 | pm = -1 if tail_type < 0 else 1 253 | polygon = [ 254 | Vec2(0, +kMinWidthT * self.font.kL2RDfatten), 255 | Vec2(0, -kMinWidthT * self.font.kL2RDfatten), 256 | Vec2(np.abs(tail_type) * kMinWidthT * self.font.kL2RDfatten, pm * kMinWidthT * self.font.kL2RDfatten) 257 | ] 258 | polygon = [str(i) for i in polygon] 259 | path = svgwrite.path.Path(d = "M" + (" L".join(polygon)), stroke = 'black', stroke_width = 0, fill = 'black', 260 | transform = f'translate({vec_2}) rotate({degree},0,0)') 261 | self.canvas.add(path) 262 | elif a2 == 14: 263 | jump_factor = (6.0 / kMinWidthT) if kMinWidthT > 6 else 1.0 264 | hane_length = self.font.kWidth * 4 * np.min([1 - hane_adjustment / 10, (kMinWidthT / self.font.kMinWidthT) ** 3]) * jump_factor 265 | polygon = [ 266 | Vec2(0, 0), 267 | Vec2(0, -kMinWidthT), 268 | Vec2(-hane_length, -kMinWidthT), 269 | Vec2(-hane_length, -kMinWidthT * 0.5), 270 | ] 271 | polygon = [str(i) for i in polygon] 272 | path = svgwrite.path.Path(d = "M" + (" L".join(polygon)), stroke = 'black', stroke_width = 0, fill = 'black', 273 | transform = f'translate({vec_2})') 274 | self.canvas.add(path) 275 | 276 | def draw_bezier(self, vec_1: Vec2, vec_s1: Vec2, vec_s2: Vec2, vec_2: Vec2, a1: int, a2: int, opt1, hane_adjustment, opt3, opt4): 277 | self.__draw_curve_universal(vec_1, vec_s1, vec_s2, vec_2, a1, a2, opt1, hane_adjustment, opt3, opt4) 278 | 279 | def draw_curve(self, vec_1: Vec2, vec_s: Vec2, vec_2: Vec2, a1: int, a2: int, opt1, hane_adjustment, opt3, opt4): 280 | self.__draw_curve_universal(vec_1, vec_s, vec_s, vec_2, a1, a2, opt1, hane_adjustment, opt3, opt4) 281 | 282 | def draw_line(self, vec_1: Vec2, vec_2: Vec2, a1: int, a2: int, opt1, uroko_adjustment: int, kakato_adjustment: int): 283 | kMinWidthT = self.font.kMinWidthT - opt1 / 2 284 | 285 | if (vec_1.x == vec_2.x or vec_1.y != vec_2.y and (vec_1.x > vec_2.x or np.abs(vec_2.y - vec_1.y) >= np.abs(vec_2.x - vec_1.x) or a1 == 6 or a2 == 6)): 286 | # 縦, 竖, vertical storke: use y-axis 287 | # 角度が深い / 鈎(かぎ)の横 288 | 289 | cosrad, sinrad = Vec2(0, 1) if (vec_1.x == vec_2.x) else normalize(vec_2 - vec_1) 290 | 291 | rotation_matrix = np.array([ 292 | [ sinrad, cosrad], 293 | [-cosrad, sinrad] 294 | ]) 295 | 296 | poly0 = [Vec2(0, 0)] * 4 297 | 298 | # head 299 | if a1 == 0: 300 | poly0[0] = rotation_matrix @ Vec2(kMinWidthT, self.font.kMinWidthY / 2) 301 | poly0[3] = rotation_matrix @ Vec2(-kMinWidthT, -self.font.kMinWidthY / 2) 302 | elif a1 in [1,6]: 303 | poly0[0] = rotation_matrix @ Vec2(kMinWidthT, 0) 304 | poly0[3] = rotation_matrix @ Vec2(-kMinWidthT, 0) 305 | elif a1 == 12: # 箱型左上角 306 | poly0[0] = rotation_matrix @ Vec2(kMinWidthT, -self.font.kMinWidthY) 307 | poly0[3] = rotation_matrix @ Vec2(-kMinWidthT, -self.font.kMinWidthY - kMinWidthT) 308 | elif a1 == 22: # 箱型右上角 309 | v = -1 if vec_1.x > vec_2.x else 1 310 | if vec_1.x == vec_2.x: 311 | poly0[0] = Vec2(+ kMinWidthT, 0) 312 | poly0[3] = Vec2(- kMinWidthT, 0) 313 | else: 314 | poly0[0] = Vec2(+ (kMinWidthT + v) / sinrad, +1) 315 | poly0[3] = Vec2(- kMinWidthT / sinrad, 0) 316 | elif a1 == 32: # ? 317 | if vec_1.x == vec_2.x: 318 | poly0[0] = Vec2(+ kMinWidthT, - self.font.kMinWidthY) 319 | poly0[3] = Vec2(- kMinWidthT, - self.font.kMinWidthY) 320 | else: 321 | poly0[0] = Vec2(+ kMinWidthT / sinrad, 0) 322 | poly0[3] = Vec2(- kMinWidthT / sinrad, 0) 323 | 324 | # head dots translate with vec_1 325 | poly0[0] += vec_1 326 | poly0[3] += vec_1 327 | 328 | # tail 329 | if a2 == 0: 330 | if a1 == 6: 331 | poly0[1] = rotation_matrix @ Vec2(kMinWidthT, 0) 332 | poly0[2] = rotation_matrix @ Vec2(-kMinWidthT, 0) 333 | else: 334 | poly0[1] = rotation_matrix @ Vec2(kMinWidthT, -kMinWidthT / 2) 335 | poly0[2] = rotation_matrix @ Vec2(-kMinWidthT, kMinWidthT / 2) 336 | elif a2 in [1,5]: 337 | if a2 == 5 and vec_1.x == vec_2.x: 338 | pass 339 | else: 340 | poly0[1] = rotation_matrix @ Vec2(kMinWidthT, 0) 341 | poly0[2] = rotation_matrix @ Vec2(-kMinWidthT, 0) 342 | elif a2 == 13: 343 | poly0[1] = rotation_matrix @ Vec2(kMinWidthT, self.font.kAdjustKakatoL[kakato_adjustment]) 344 | poly0[2] = rotation_matrix @ Vec2(-kMinWidthT, self.font.kAdjustKakatoL[kakato_adjustment] + kMinWidthT) 345 | elif a2 == 23: 346 | poly0[1] = rotation_matrix @ Vec2(kMinWidthT, self.font.kAdjustKakatoR[kakato_adjustment]) 347 | poly0[2] = rotation_matrix @ Vec2(-kMinWidthT, self.font.kAdjustKakatoR[kakato_adjustment] + kMinWidthT) 348 | elif a2 in [24,32]: 349 | if vec_1.x == vec_2.x: 350 | poly0[1] = Vec2(+ kMinWidthT, self.font.kMinWidthY) 351 | poly0[2] = Vec2(- kMinWidthT, self.font.kMinWidthY) 352 | else: 353 | poly0[1] = Vec2(+ kMinWidthT / sinrad, 0) 354 | poly0[2] = Vec2(- kMinWidthT / sinrad, 0) 355 | 356 | # tail dots translate with vec_2 357 | poly0[1] += vec_2 358 | poly0[2] += vec_2 359 | 360 | # draw body 361 | poly0 = [str(i) for i in poly0] 362 | path = svgwrite.path.Path(d = "M" + (" L".join(poly0)), stroke = 'black', stroke_width = 0, fill = 'black') 363 | self.canvas.add(path) 364 | 365 | if a2 == 24: # for T design 366 | polygon = [ 367 | Vec2(0, self.font.kMinWidthY), 368 | Vec2(+kMinWidthT, -self.font.kMinWidthY * 3) if vec_1.x == vec_2.x else Vec2(+kMinWidthT * 0.5, -self.font.kMinWidthY * 4), 369 | Vec2(+kMinWidthT * 2, -self.font.kMinWidthY), 370 | Vec2(+kMinWidthT * 2, +self.font.kMinWidthY) 371 | ] 372 | polygon = [str(i) for i in polygon] 373 | path = svgwrite.path.Path(d = "M" + (" L".join(polygon)), stroke = 'black', stroke_width = 0, fill = 'black', 374 | transform = f'translate({vec_2})') 375 | self.canvas.add(path) 376 | elif a2 == 13: 377 | if kakato_adjustment == 4: # for new GTH box's left bottom corner 378 | if vec_1.x == vec_2.x: 379 | polygon = [ 380 | Vec2(-kMinWidthT, -self.font.kMinWidthY*3), 381 | Vec2(-kMinWidthT * 2, 0), 382 | Vec2(-self.font.kMinWidthY, +self.font.kMinWidthY * 5), 383 | Vec2(+kMinWidthT, +self.font.kMinWidthY) 384 | ] 385 | polygon = [str(i) for i in polygon] 386 | path = svgwrite.path.Path(d = "M" + (" L".join(polygon)), stroke = 'black', stroke_width = 0, fill = 'black', 387 | transform = f'translate({vec_2})') 388 | self.canvas.add(path) 389 | else: # direction unrelated,向き関係なし 390 | polygon = [ 391 | Vec2(0, -self.font.kMinWidthY*5), 392 | Vec2(-kMinWidthT * 2, 0), 393 | Vec2(-self.font.kMinWidthY, +self.font.kMinWidthY * 5), 394 | Vec2(+kMinWidthT, +self.font.kMinWidthY), 395 | Vec2(0,0) 396 | ] 397 | polygon = [str(i) for i in polygon] 398 | path = svgwrite.path.Path(d = "M" + (" L".join(polygon)), stroke = 'black', stroke_width = 0, fill = 'black', 399 | transform = f'translate({vec_2 + Vec2((vec_1.x - vec_2.x) / (vec_2.y - vec_1.y) * 3 if (vec_1.x > vec_2.x and vec_1.y != vec_2.y) else 0, 0)})') 400 | self.canvas.add(path) 401 | 402 | if a1 in [22,27]: # box's right top corner 403 | # 四角右上鱗斜めでもまっすぐ向き 404 | # 箱形右上三角形装饰 405 | poly = [ 406 | Vec2(-kMinWidthT, -self.font.kMinWidthY), 407 | Vec2(0, -self.font.kMinWidthY - self.font.kWidth), 408 | Vec2(+kMinWidthT + self.font.kWidth, +self.font.kMinWidthY) 409 | ] 410 | poly += [ 411 | Vec2(+kMinWidthT, +kMinWidthT), 412 | Vec2(-kMinWidthT, 0) 413 | ] if vec_1.x == vec_2.x else [ 414 | Vec2(+kMinWidthT, +kMinWidthT - 1), 415 | Vec2(0, +kMinWidthT + 2), 416 | Vec2(0, 0), 417 | ] if a1 == 27 else [ 418 | Vec2(+kMinWidthT, +kMinWidthT - 1), 419 | Vec2(-kMinWidthT, +kMinWidthT + 4) 420 | ] 421 | poly = [str(i) for i in poly] 422 | path = svgwrite.path.Path(d = "M" + (" L".join(poly)), stroke = 'black', stroke_width = 0, fill = 'black', 423 | transform = f'translate({vec_1})') 424 | self.canvas.add(path) 425 | elif a1 == 0: # beginning of the stroke 426 | poly = [ 427 | vec_1 + rotation_matrix @ Vec2(kMinWidthT, self.font.kMinWidthY * 0.5), 428 | vec_1 + rotation_matrix @ Vec2(kMinWidthT + kMinWidthT * 0.5, self.font.kMinWidthY * 0.5 + self.font.kMinWidthY), 429 | vec_1 + rotation_matrix @ Vec2(kMinWidthT - 2, self.font.kMinWidthY * 0.5 + self.font.kMinWidthY * 2 + 1), 430 | ] 431 | if vec_1.x != vec_2.x: 432 | poly[2] = Vec2(vec_1.x + (kMinWidthT - 2) * sinrad + (self.font.kMinWidthY * 0.5 + self.font.kMinWidthY * 2) * cosrad, 433 | vec_1.y + (kMinWidthT + 1) * -cosrad + (self.font.kMinWidthY * 0.5 + self.font.kMinWidthY * 2) * sinrad) 434 | 435 | poly = [str(i) for i in poly] 436 | path = svgwrite.path.Path(d = "M" + (" L".join(poly)), stroke = 'black', stroke_width = 0, fill = 'black') 437 | self.canvas.add(path) 438 | 439 | if (vec_1.x == vec_2.x and a2 == 1 or a1 == 6 and (a2 == 0 or vec_1.x != vec_2.x and a2 == 5)): 440 | # 鈎の横棒の最後の丸 441 | # no need only used at 1st=yoko 442 | poly = [ 443 | "M", 444 | vec_2 + rotation_matrix @ Vec2(kMinWidthT, 0), 445 | "Q", 446 | Vec2(vec_2.x - cosrad * kMinWidthT * 0.9 + -sinrad * -kMinWidthT * 0.9, # typo? (- cosrad should be + cosrad) 447 | vec_2.y + sinrad * kMinWidthT * 0.9 + cosrad * -kMinWidthT * 0.9), 448 | vec_2 + rotation_matrix @ Vec2(0, kMinWidthT), 449 | "Q", 450 | vec_2 + rotation_matrix @ Vec2(-kMinWidthT * 0.9, kMinWidthT * 0.9), 451 | vec_2 + rotation_matrix @ Vec2(-kMinWidthT, 0) 452 | ] 453 | poly = [str(i) for i in poly] 454 | path = svgwrite.path.Path(d = " ".join(poly), stroke = 'black', stroke_width = 0, fill = 'black') 455 | self.canvas.add(path) 456 | 457 | if (vec_1.x != vec_2.x and a1 == 6 and a2 == 5): 458 | # 鈎の横棒のハネ 459 | hane_length = self.font.kWidth * 5 460 | rv = 1 if vec_1.x < vec_2.x else -1 461 | poly = [ 462 | Vec2(rv * (kMinWidthT - 1), 0), 463 | Vec2(rv * (kMinWidthT + hane_length), 2), 464 | Vec2(rv * (kMinWidthT + hane_length), 0), 465 | Vec2(kMinWidthT - 1, -kMinWidthT), 466 | ] 467 | poly = [str(vec_2 + rotation_matrix @ i) for i in poly] 468 | path = svgwrite.path.Path(d = "M" + " L".join(poly), stroke = 'black', stroke_width = 0, fill = 'black') 469 | self.canvas.add(path) 470 | 471 | elif (vec_1.y == vec_2.y and a1 == 6): 472 | # 横(よこ), 横划, horizontal stroke: use x-axis 473 | # 鈎の横, 钩的横划, horizontal stroke of hook: get bold 474 | 475 | # body 476 | poly0 = [ 477 | vec_1 + Vec2(0, -kMinWidthT), 478 | vec_2 + Vec2(0, -kMinWidthT), 479 | vec_2 + Vec2(0, +kMinWidthT), 480 | vec_1 + Vec2(0, +kMinWidthT), 481 | ] 482 | poly0 = [str(i) for i in poly0] 483 | path = svgwrite.path.Path(d = "M" + (" L".join(poly0)), stroke = 'black', stroke_width = 0, fill = 'black') 484 | self.canvas.add(path) 485 | 486 | if a2 in [1,0,5]: 487 | # 鍵の横棒に最後の丸 488 | degree = 180 if vec_1.x > vec_2.x else 0 489 | 490 | poly = [ 491 | "M", 492 | Vec2(0, -kMinWidthT), 493 | "Q", 494 | Vec2(+kMinWidthT * 0.9, -kMinWidthT * 0.9), 495 | Vec2(+kMinWidthT, 0), 496 | "Q", 497 | Vec2(+kMinWidthT * 0.9, +kMinWidthT * 0.9), 498 | Vec2(0, +kMinWidthT), 499 | ] 500 | poly = [str(i) for i in poly] 501 | path = svgwrite.path.Path(d = " ".join(poly), stroke = 'black', stroke_width = 0, fill = 'black', transform = f'translate({vec_2}) rotate({degree},0,0)') 502 | self.canvas.add(path) 503 | 504 | if a2 == 5: 505 | # 鈎の横棒のハネ 506 | hane_length = self.font.kWidth * (4 * (1 - opt1 / self.font.kAdjustMageStep) + 1) 507 | rv = 1 if vec_1.x < vec_2.x else -1 508 | poly = [ 509 | Vec2(0, rv * -kMinWidthT), 510 | Vec2(2, rv * (-kMinWidthT - hane_length)), 511 | Vec2(0, rv * (-kMinWidthT - hane_length)), 512 | Vec2(-kMinWidthT, rv * -kMinWidthT) 513 | ] 514 | poly = [str(i) for i in poly] 515 | path = svgwrite.path.Path(d = "M" + " L".join(poly), stroke = 'black', stroke_width = 0, fill = 'black', transform = f'translate({vec_2}) rotate({degree},0,0)') 516 | self.canvas.add(path) 517 | 518 | else: 519 | # for others, use x-axis 520 | # 浅い角度 521 | cosrad, sinrad = Vec2(1, 0) if vec_1.y == vec_2.y else normalize(vec_2 - vec_1) 522 | 523 | rotation_matrix = np.array([ 524 | [cosrad, -sinrad], 525 | [sinrad, cosrad] 526 | ]) 527 | 528 | # body 529 | poly = [ 530 | vec_1 + rotation_matrix @ Vec2(0, -self.font.kMinWidthY), 531 | vec_2 + rotation_matrix @ Vec2(0, -self.font.kMinWidthY), 532 | vec_2 + rotation_matrix @ Vec2(0, self.font.kMinWidthY), 533 | vec_1 + rotation_matrix @ Vec2(0, self.font.kMinWidthY), 534 | ] 535 | 536 | # draw body 537 | poly = [str(i) for i in poly] 538 | path = svgwrite.path.Path(d = "M" + (" L".join(poly)), stroke = 'black', stroke_width = 0, fill = 'black') 539 | self.canvas.add(path) 540 | 541 | # tail 542 | if a2 == 0: # 鱗 543 | uroko_scale = (self.font.kMinWidthU / self.font.kMinWidthY - 1.0) / 4.0 + 1 544 | poly2 = [ 545 | vec_2 + rotation_matrix @ Vec2(0, -self.font.kMinWidthY), 546 | vec_2 + rotation_matrix @ Vec2(-self.font.kAdjustUrokoX[uroko_adjustment] * uroko_scale, 0), 547 | vec_2 - (rotation_matrix[:,0] + rotation_matrix[:,1]) * uroko_scale * Vec2(0.5, 1) * Vec2(self.font.kAdjustUrokoX[uroko_adjustment], self.font.kAdjustUrokoY[uroko_adjustment]) 548 | ] 549 | poly2 = [str(i) for i in poly2] 550 | path = svgwrite.path.Path(d = "M" + (" L".join(poly2)), stroke = 'black', stroke_width = 0, fill = 'black') 551 | self.canvas.add(path) 552 | 553 | class BezierSerifStrokeDrawer(LegacySerifStrokeDrawer): 554 | pass 555 | -------------------------------------------------------------------------------- /kage/kage.py: -------------------------------------------------------------------------------- 1 | from . components import Components 2 | from . stroke import Stroke 3 | from . vec2 import Vec2 4 | from . font.serif import Serif 5 | from argparse import Namespace 6 | import svgwrite 7 | import numpy as np 8 | 9 | class Kage: 10 | def __init__(self, ignore_component_version = False) -> None: 11 | self.components = Components(ignore_component_version) 12 | self.font = Serif() # TODO: フォントを選択できるようにする 13 | 14 | @property 15 | def type(self): 16 | return self.font 17 | 18 | @type.setter 19 | def type(self, another): 20 | self.font = another 21 | 22 | def make_glyph(self, name: str) -> svgwrite.Drawing: 23 | data = self.components.search(name) 24 | canvas = svgwrite.Drawing(size=('200', '200')) 25 | return self.make_glyph_with_data(canvas, data) 26 | 27 | def make_glyph_with_name(self, canvas: svgwrite.Drawing, name: str) -> svgwrite.Drawing: 28 | data = self.components.search(name) 29 | return self.make_glyph_with_data(canvas, data) 30 | 31 | def make_glyph_with_data(self, canvas: svgwrite.Drawing, data: str) -> svgwrite.Drawing: 32 | if data != '': 33 | strokes_list = self.get_each_strokes(data) 34 | return self.font.drawer(canvas, strokes_list) 35 | 36 | def get_each_strokes(self, data: str) -> list[Stroke]: 37 | strokes_list = [] 38 | strokes = data.split('$') 39 | for stroke in strokes: 40 | columns = stroke.split(':') 41 | columns += [np.nan] * (11 - len(columns)) 42 | if columns[0] != '99': 43 | strokes_list.append(Stroke(columns)) 44 | else: 45 | component_data = self.components.search(columns[7]) 46 | if component_data != '': 47 | strokes_list.extend( 48 | self.get_each_strokes_of_component(component_data, 49 | float(columns[3]), 50 | float(columns[4]), 51 | float(columns[5]), 52 | float(columns[6]), 53 | float(columns[1]), 54 | float(columns[2]), 55 | float(columns[9]), 56 | float(columns[10]) 57 | ) 58 | ) 59 | 60 | return strokes_list 61 | 62 | def get_each_strokes_of_component(self, component_data, x1, y1, x2, y2, sx, sy, sx2, sy2) -> list[Stroke]: 63 | strokes = self.get_each_strokes(component_data) 64 | box = self.get_box(strokes) 65 | if sx != 0 or sy != 0: 66 | if sx > 100: 67 | sx -= 200 68 | else: 69 | sx2 = 0 70 | sy2 = 0 71 | for stroke in strokes: 72 | if (sx != 0 or sy != 0): 73 | stroke.stretch(sx, sx2, sy, sy2, box.minX, box.maxX, box.minY, box.maxY) 74 | 75 | vec_1 = Vec2(x1, y1) 76 | vec_2 = Vec2(x2, y2) 77 | stroke.vec_1 = vec_1 + stroke.vec_1 * (vec_2 - vec_1) / 200 78 | stroke.vec_2 = vec_1 + stroke.vec_2 * (vec_2 - vec_1) / 200 79 | stroke.vec_3 = vec_1 + stroke.vec_3 * (vec_2 - vec_1) / 200 80 | stroke.vec_4 = vec_1 + stroke.vec_4 * (vec_2 - vec_1) / 200 81 | 82 | return strokes 83 | 84 | def get_box(self, strokes: list[Stroke]) -> Namespace: 85 | minX = 200 86 | minY = 200 87 | maxX = 0 88 | maxY = 0 89 | 90 | for stroke_ in strokes: 91 | s_box = stroke_.get_box() 92 | minX = np.min([minX, s_box.minX]) 93 | maxX = np.max([maxX, s_box.maxX]) 94 | minY = np.min([minY, s_box.minY]) 95 | maxY = np.max([maxY, s_box.maxY]) 96 | 97 | return Namespace(**{'minX': minX, 'maxX': maxX, 'minY': minY, 'maxY': maxY}) 98 | -------------------------------------------------------------------------------- /kage/stroke.py: -------------------------------------------------------------------------------- 1 | from argparse import Namespace 2 | from . vec2 import Vec2, is_cross, is_cross_box 3 | import numpy as np 4 | 5 | def stretch(dp, sp, p, min_, max_): 6 | if (p < sp + 100): 7 | p1 = min_ 8 | p3 = min_ 9 | p2 = sp + 100 10 | p4 = dp + 100 11 | else: 12 | p1 = sp + 100 13 | p3 = dp + 100 14 | p2 = max_ 15 | p4 = max_ 16 | return np.floor(((p - p1) / (p2 - p1)) * (p4 - p3) + p3) 17 | 18 | class Stroke: 19 | def __init__(self, data: list) -> None: 20 | self.a1_100 = int(data[0]) 21 | self.a2_100 = int(data[1]) 22 | self.a3_100 = int(data[2]) 23 | self.vec_1 = Vec2(data[3], data[4]) 24 | self.vec_2 = Vec2(data[5], data[6]) 25 | self.vec_3 = Vec2(data[7], data[8]) 26 | self.vec_4 = Vec2(data[9], data[10]) 27 | self.a1_opt = np.floor(self.a1_100 / 100) 28 | self.a1_100 %= 100 29 | self.a2_opt = np.floor(self.a2_100 / 100) 30 | self.a2_100 %= 100 31 | self.a2_opt_1 = self.a2_opt % 10 32 | self.a2_opt_2 = np.floor(self.a2_opt / 10) % 10 33 | self.a2_opt_3 = np.floor(self.a2_opt / 100) 34 | self.a3_opt = np.floor(self.a3_100 / 100) 35 | self.a3_100 %= 100 36 | self.a3_opt_1 = self.a3_opt % 10 37 | self.a3_opt_2 = np.floor(self.a3_opt / 10) 38 | 39 | def get_control_segments(self): 40 | res = [] 41 | a1 = self.a1_100 if self.a1_opt == 0 else 1 # XXX: ??? 42 | if a1 in [0,8,9]: 43 | pass 44 | if a1 in [6,7]: 45 | res.insert(0, [self.vec_3, self.vec_4]) 46 | if a1 in [2,12,3,4] or a1 in [6,7]: 47 | res.insert(0, [self.vec_2, self.vec_3]) 48 | if a1 not in [0,8,9] or a1 in [6,7,2,12,3,4]: 49 | res.insert(0, [self.vec_1, self.vec_2]) 50 | return res 51 | 52 | def is_cross(self, vec_b1: Vec2, vec_b2: Vec2): 53 | return any(is_cross(vec2s[0], vec2s[1], vec_b1, vec_b2) for vec2s in self.get_control_segments()) 54 | 55 | def is_cross_box(self, vec_b1: Vec2, vec_b2: Vec2): 56 | return any(is_cross_box(vec2s[0], vec2s[1], vec_b1, vec_b2) for vec2s in self.get_control_segments()) 57 | 58 | def stretch(self, sx, sx2, sy, sy2, bminX, bmaxX, bminY, bmaxY): 59 | self.vec_1 = Vec2( 60 | stretch(sx, sx2, self.vec_1.x, bminX, bmaxX), 61 | stretch(sy, sy2, self.vec_1.y, bminY, bmaxY), 62 | ) 63 | self.vec_2 = Vec2( 64 | stretch(sx, sx2, self.vec_2.x, bminX, bmaxX), 65 | stretch(sy, sy2, self.vec_2.y, bminY, bmaxY), 66 | ) 67 | if not (self.a1_100 == 99 and self.a1_opt == 0): # always true 68 | self.vec_3 = Vec2( 69 | stretch(sx, sx2, self.vec_3.x, bminX, bmaxX), 70 | stretch(sy, sy2, self.vec_3.y, bminY, bmaxY), 71 | ) 72 | self.vec_4 = Vec2( 73 | stretch(sx, sx2, self.vec_4.x, bminX, bmaxX), 74 | stretch(sy, sy2, self.vec_4.y, bminY, bmaxY), 75 | ) 76 | 77 | def get_box(self): 78 | minX = np.inf 79 | minY = np.inf 80 | maxX = -np.inf 81 | maxY = -np.inf 82 | a1 = self.a1_100 if self.a1_opt == 0 else 6 # XXX ????? 83 | if a1 not in [2,3,4,1,99,0]: 84 | minX = np.nanmin([minX, self.vec_4.x]) 85 | maxX = np.nanmax([maxX, self.vec_4.x]) 86 | minY = np.nanmin([minY, self.vec_4.y]) 87 | maxY = np.nanmax([maxY, self.vec_4.y]) 88 | if a1 in [2,3,4] or a1 not in [2,3,4,1,99,0]: 89 | minX = np.nanmin([minX, self.vec_3.x]) 90 | maxX = np.nanmax([maxX, self.vec_3.x]) 91 | minY = np.nanmin([minY, self.vec_3.y]) 92 | maxY = np.nanmax([maxY, self.vec_3.y]) 93 | if a1 in [1,99] or a1 in [2,3,4] or a1 not in [2,3,4,1,99,0]: 94 | minX = np.nanmin([minX, self.vec_1.x, self.vec_2.x]) 95 | maxX = np.nanmax([maxX, self.vec_1.x, self.vec_2.x]) 96 | minY = np.nanmin([minY, self.vec_1.y, self.vec_2.y]) 97 | maxY = np.nanmax([maxY, self.vec_1.y, self.vec_2.y]) 98 | if a1 == 0: 99 | pass 100 | 101 | return Namespace(**{'minX': minX, 'maxX': maxX, 'minY': minY, 'maxY': maxY}) 102 | 103 | def __repr__(self) -> str: 104 | return f'{self.a1_100}:{self.a2_100}:{self.a3_100}:{self.vec_1.x}:{self.vec_1.y}:{self.vec_2.x}:{self.vec_2.y}:{self.vec_3.x}:{self.vec_3.y}:{self.vec_4.x}:{self.vec_4.y}' 105 | 106 | def _get_data(self) -> list: 107 | return [ 108 | self.a1_100, 109 | self.a2_100, 110 | self.a3_100, 111 | self.vec_1.x, 112 | self.vec_1.y, 113 | self.vec_2.x, 114 | self.vec_2.y, 115 | self.vec_3.x, 116 | self.vec_3.y, 117 | self.vec_4.x, 118 | self.vec_4.y, 119 | ] -------------------------------------------------------------------------------- /kage/util.py: -------------------------------------------------------------------------------- 1 | from . vec2 import Vec2, normalize 2 | 3 | from typing import Callable, Tuple, List 4 | from math import floor 5 | 6 | 7 | def ternary_search_max(f: Callable[[float], float], left: float, right: float, 8 | absolute_precision=1E-5) -> float: 9 | """ 10 | Find maximum of unimodal function f() within [left, right]. 11 | To find the minimum, reverse the if/else statement or reverse the comparison. 12 | """ 13 | while abs(right - left) >= absolute_precision: 14 | left_third = left + (right - left) / 3 15 | right_third = right - (right - left) / 3 16 | 17 | if f(left_third) < f(right_third): 18 | left = left_third 19 | else: 20 | right = right_third 21 | 22 | # Left and right are the current bounds; the maximum is between them 23 | return (left + right) / 2 24 | 25 | 26 | def ternary_search_min(f: Callable[[float], float], left: float, right: float, 27 | absolute_precision=1E-5) -> float: 28 | return ternary_search_max( 29 | lambda x: -f(x), 30 | left, right, absolute_precision 31 | ) 32 | 33 | 34 | def quadratic_bezier(vec_1: Vec2, vec_s1: Vec2, vec_2: Vec2, t: float) -> Vec2: 35 | s = 1 - t 36 | return s ** 2 * vec_1 + 2 * s * t * vec_s1 + t ** 2 * vec_2 37 | 38 | 39 | def cubic_bezier(vec_1: Vec2, vec_s1: Vec2, vec_s2: Vec2, vec_2: Vec2, t: float) -> Vec2: 40 | s = 1 - t 41 | return s ** 3 * vec_1 + 3 * s ** 2 * t * vec_s1 + 3 * s * t ** 2 * vec_s2 + t ** 3 * vec_2 42 | 43 | 44 | def quadratic_bezier_deriv(vec_1: Vec2, vec_s1: Vec2, vec_2: Vec2, t: float) -> Vec2: 45 | return 2 * (t * (vec_1 - 2 * vec_s1 + vec_2) - vec_1 + vec_s1) 46 | 47 | 48 | def cubic_bezier_deriv(vec_1: Vec2, vec_s1: Vec2, vec_s2: Vec2, vec_2: Vec2, t: float) -> Vec2: 49 | return 3 * 3 * (t * (t * (-vec_1 + 3 * vec_s1 - 3 * vec_s2 + vec_2) + 2 * (vec_1 - 2 * vec_s1 + vec_s2)) - vec_1 + vec_s1) 50 | 51 | 52 | def divide_curve(dot_1: Vec2, dot_s1: Vec2, dot_2: Vec2, curve: List[Vec2]) \ 53 | -> Tuple[int, Tuple[Vec2, Vec2, Vec2], Tuple[Vec2, Vec2, Vec2]]: 54 | rate = 0.5 55 | cut = floor(len(curve) * rate) 56 | cut_rate = cut / len(curve) 57 | dot_t1 = dot_1 + (dot_s1 - dot_1) * cut_rate 58 | dot_t2 = dot_s1 + (dot_2 - dot_s1) * cut_rate 59 | dot_t3 = dot_t1 + (dot_t2 - dot_t1) * cut_rate 60 | 61 | return cut, (dot_1, dot_t1, dot_t3), (dot_t3, dot_t2, dot_2) 62 | 63 | 64 | def find_offcurve(curve: List[Vec2], dot_s: Vec2) -> Tuple[Vec2, Vec2, Vec2]: 65 | dot_n1 = curve[0] 66 | dot_n2 = curve[-1] 67 | 68 | area = 8 69 | 70 | minx = ternary_search_min( 71 | lambda tx: 72 | sum([(p[0] - quadratic_bezier(dot_n1.x, tx, dot_n2.x, i / (len(curve) - 1))) ** 2 73 | for i, p in enumerate(curve)]), 74 | dot_s.x - area, dot_s.x + area) 75 | miny = ternary_search_min( 76 | lambda ty: 77 | sum([(p[0] - quadratic_bezier(dot_n1.y, ty, dot_n2.y, i / (len(curve) - 1))) ** 2 78 | for i, p in enumerate(curve)]), 79 | dot_s.y - area, dot_s.y + area) 80 | 81 | return dot_n1, Vec2(minx, miny), dot_n2 82 | 83 | 84 | def generate_flatten_curve( 85 | vec_1: Vec2, vec_s1: Vec2, 86 | vec_s2: Vec2, vec_2: Vec2, 87 | k_rate: float, width_func: Callable[[float], float] 88 | ) -> Tuple[List[Vec2], List[Vec2]]: 89 | left = [] 90 | right = [] 91 | 92 | is_quadratic = all(vec_s1 == vec_s2) 93 | 94 | dot_func, i_dot_func = \ 95 | (lambda t: quadratic_bezier(vec_1, vec_s1, vec_2, t), 96 | lambda t: quadratic_bezier_deriv(vec_1, vec_s1, vec_2, t)) \ 97 | if is_quadratic else \ 98 | (lambda t: cubic_bezier(vec_1, vec_s1, vec_s2, vec_2, t), 99 | lambda t: cubic_bezier_deriv(vec_1, vec_s1, vec_s2, vec_2, t)) 100 | 101 | for tt in range(0, 1001, k_rate): 102 | t = tt / 1000 103 | dot = dot_func(t) 104 | i_dot = i_dot_func(t) 105 | width = width_func(t) 106 | i_dot = Vec2(-width, 0) if all(i_dot == Vec2(0, 0)) else \ 107 | normalize(Vec2(-i_dot.y, i_dot.x), width) # XXX ??? 108 | 109 | left.append(dot - i_dot) 110 | right.append(dot + i_dot) 111 | 112 | return left, right 113 | -------------------------------------------------------------------------------- /kage/vec2.py: -------------------------------------------------------------------------------- 1 | import numpy as np 2 | 3 | class Vec2(np.ndarray): 4 | def __new__(cls, x, y): 5 | return super(Vec2, cls).__new__( 6 | cls, shape=(2,), dtype=np.float64, 7 | buffer=np.array([x,y], dtype=np.float64) 8 | ) 9 | 10 | def __str__(self) -> str: 11 | return f"{self.x},{self.y}" 12 | 13 | x = property( 14 | lambda self : self.data.__getitem__(0), 15 | lambda self, new_x : self.data.__setitem__(0, new_x) 16 | ) 17 | 18 | y = property( 19 | lambda self : self.data.__getitem__(1), 20 | lambda self, new_y : self.data.__setitem__(1, new_y) 21 | ) 22 | 23 | def cross_product(self, another) -> 'Vec2': 24 | return self.x * another.y - another.x * self.y 25 | 26 | def normalize(self, magnitude = 1) -> None: 27 | """ 28 | normalize this vector with the same angle and a new magnitude. 29 | """ 30 | assert magnitude > 0 31 | # self in polar coordinate system form 32 | self.x, self.y = magnitude, np.arctan2(self.y, self.x) 33 | self.x, self.y = self.x * np.cos(self.y), self.x * np.sin(self.y) 34 | 35 | 36 | def normalize(array: Vec2, magnitude = 1) -> Vec2: 37 | """ 38 | calculates a new vector with the same angle and a new magnitude. 39 | """ 40 | # ret vector in polar coordinate system form 41 | assert magnitude != 0 42 | ret = Vec2(magnitude, np.arctan2(array.y, array.x)) 43 | ret = Vec2(ret[0] * np.cos(ret[1]), ret[0] * np.sin(ret[1])) 44 | return ret 45 | 46 | 47 | def is_cross(vec11: Vec2, vec12: Vec2, vec21: Vec2, vec22: Vec2) -> bool: 48 | cross_1112_2122 = (vec12 - vec11).cross_product(vec21 - vec22) 49 | if np.isnan(cross_1112_2122): 50 | return True # for backward compatibility... 51 | if cross_1112_2122 == 0: 52 | # parallel 53 | return False # XXX should check if segments overlap? 54 | cross_1112_1121 = (vec11 - vec12).cross_product(vec11 - vec21) 55 | cross_1112_1122 = (vec11 - vec12).cross_product(vec11 - vec22) 56 | cross_2122_2111 = (vec21 - vec22).cross_product(vec21 - vec11) 57 | cross_2122_2112 = (vec21 - vec22).cross_product(vec21 - vec12) 58 | return cross_1112_1121 * cross_1112_1122 <= 0 and cross_2122_2111 * cross_2122_2112 <= 0 # XXX round 59 | 60 | 61 | def is_cross_box(vec_1: Vec2, vec_2: Vec2, vec_b1: Vec2, vec_b2: Vec2) -> bool: 62 | if is_cross(vec_1, vec_2, vec_b1, Vec2(vec_b2.x, vec_b1.y)): 63 | return True 64 | elif is_cross(vec_1, vec_2, Vec2(vec_b2.x, vec_b1.y), vec_b2): 65 | return True 66 | elif is_cross(vec_1, vec_2, Vec2(vec_b1.x, vec_b2.y), vec_b2): 67 | return True 68 | elif is_cross(vec_1, vec_2, vec_b1, Vec2(vec_b1.x, vec_b2.y)): 69 | return True 70 | else: 71 | return False 72 | -------------------------------------------------------------------------------- /output/u5f71.svg: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /output/u5f71_serif.svg: -------------------------------------------------------------------------------- 1 | 2 | --------------------------------------------------------------------------------