├── .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 |
--------------------------------------------------------------------------------